diff --git a/.env.example b/.env.example index 057a521b..9923e3b7 100644 --- a/.env.example +++ b/.env.example @@ -28,9 +28,18 @@ AUTH_GITHUB_SECRET= AUTH_GOOGLE_ID= AUTH_GOOGLE_SECRET= -# Email (magic links) +# Outbound workspace invitations. EMAIL_SERVER is the compact SMTP URL form; +# alternatively set SMTP_HOST/SMTP_PORT/SMTP_SECURE/SMTP_USER/SMTP_PASSWORD, +# or use RESEND_API_KEY. Production invite attempts fail closed when no +# transport is configured. EMAIL_SERVER=smtp://user:pass@smtp.example.com:587 -EMAIL_FROM=noreply@forge.local +EMAIL_FROM="Forge " +SMTP_HOST= +SMTP_PORT=587 +SMTP_SECURE=false +SMTP_USER= +SMTP_PASSWORD= +RESEND_API_KEY= # Plugin / agent authentication PLUGIN_JWT_ISSUER=forge diff --git a/DEVLOG.md b/DEVLOG.md index 12052be9..b6ea3344 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1,12913 +1 @@ -# Forge DEVLOG - -> Append-only session log. Read at session start. Update at session end. - -## 2026-07-14 — Manual GitHub retry persistence and release context hygiene - -Addressed the post-merge Codex review on PR #29 before production deployment. -Manual and MCP-triggered GitHub refreshes now catch provider failures before -returning them, replace the temporary collision lease with the later of the -workspace exponential backoff or GitHub retry/reset header, persist the failure -count and diagnostic, and open the same mapping-wide circuit used by scheduled -reconciliation for rate limits, permissions, and timeouts. Scheduled sweeps -retain their existing single-owner retry accounting. Resource existence is -also checked before the manual lease so missing links still return NOT_FOUND. -The follow-up Codex review on PR #30 identified that a long provider request -could consume a short backoff before persistence; manual retry timing is now -anchored after the provider call finishes. -The second review pass found that a shorter new mapping-wide delay could -replace a longer existing provider reset on sibling resources. Mapping circuits -are now monotonic: only missing or earlier retry gates are extended. -The final review pass identified the non-throwing partial-checks path: PR reads -can succeed while Checks API calls return a settled partial snapshot. Manual -and MCP syncs now promote that metadata into the same persisted mapping-wide -failure circuit and return the updated diagnostic/backoff state immediately. -Generic partial snapshots (pagination limits or transient 5xx responses) are -now distinguished from permission failures and back off only the affected PR; -only auth, rate-limit, and timeout partials open a mapping-wide circuit in both -manual and scheduled reconciliation. -The last concurrency review found that overlapping refreshes could still -shorten the failing resource's own gate. Primary-row retry updates now use the -same atomic extend-only predicate as sibling rows, and failure counts increment -atomically so concurrent errors cannot lose accounting. -Partial-check diagnostics are already counted by the snapshot upsert, so the -subsequent mapping-circuit extension now preserves that count instead of -incrementing it a second time and skipping an exponential-backoff step. -Failure persistence now recognizes the exact collision lease acquired by the -current manual refresh, allowing it to be replaced by a shorter configured -backoff while still preserving any different, later provider gate written by -an overlapping refresh. The Docker ignore file also explicitly re-includes -`next-env.d.ts` for the worker stage's `COPY` contract. - -The first release image attempt also exposed that `.dockerignore` covered -`.next` and `.next-e2e` but not `.next-lifecycle`. All named `.next-*` outputs -are now excluded so local verification caches cannot inflate release contexts -or exhaust Docker storage. - -Verification: focused GitHub reconciliation and completion policy suite (23/23), -lint (existing warnings only), TypeScript typecheck, and `git diff --check`. - -## 2026-07-14 — Resilient linked-PR status reconciliation - -Added a webhook-first repair loop for native GitHub `IMPLEMENTS` links. The -maintenance worker now scans only stale, non-terminal PR snapshots in bounded -per-workspace batches, claims each row with an expiring database lease, and -refreshes PR lifecycle plus both GitHub check suites and legacy combined commit -statuses. Workspace admins can configure enablement, stale age, batch size, and -initial/maximum backoff from Connections settings. - -Persisted provider attempt, retry, failure, diagnostic, and terminal state on -`ExternalResource`. GitHub rate-limit headers override exponential retry; -missing/paused mappings are re-resolved by repository; inaccessible resources -back off without being unlinked; closed PRs enter a slow configurable re-probe -cadence so a missed reopen event self-heals; and confirmed-green merged PRs stop -polling. Partial Checks/status permissions no longer prevent a readable PR from -linking, but partial evidence cannot certify completion. Head changes clear -cached checks and late check webhooks for an older head are ignored. Repeated -unchanged refreshes produce no issue activity or duplicate completion cards. - -The safety review removed webhook conclusions as aggregate completion evidence: -check-suite and check-run deliveries now mark a resource dirty, while the repair -loop paginates every check suite and combines legacy commit status before it can -trust success. Completed suites without conclusions, malformed aggregates, and -partially readable endpoints stay unresolved. Configurable request timeouts, -shared-worker wall budgets, atomic manual-refresh cooldowns, exact stale-row -claims, and persistent mapping-wide circuits contain permission failures, -timeouts, and rate limits without blocking later workspaces. - -Verification: Prisma migrations 0102 and 0103 applied locally; lint passed with -existing repository warnings only; typecheck passed; focused GitHub -reconciliation, client, check aggregation, completion, timeout, circuit-breaker, -dormant-reopen, cooldown, and lease-race coverage passed (**22/22**); the full -Vitest suite passed **1,259 tests** with one intentional live-connector skip; a -fresh production build passed; and the full serial Playwright suite passed -**38/38**. The browser gate also exposed a stale dashboard assertion that -hard-coded two rail columns despite the existing work-count threshold; the -assertion now follows the rendered layout mode and passed both alone and in the -full suite. - -## 2026-07-13 — AXI-102 patient-wait spam + expandable Command Center cards - -Closed the AXI-102 stall-comment loop introduced by approval-expiry recovery. -The runs dispatcher was polling every provider-backed `WAITING` row even though -ordinary `runs.setWaiting` turns are expected to finish provider-side after the -agent parks for an operator reply. That normal completion was consequently -reclassified as a missing `runs.complete` contract, marked STALLED, and surfaced -as a synthetic `[dispatch · run stalled]` issue comment. Polling now covers -ACTIVE runs plus only the WAITING rows that hold a real pending runtime -approval; patient waits remain parked for the existing reply-based resume path. -The generic recovery queue also excludes WAITING work so Command Center does not -duplicate a legitimate question as a stalled run with an incorrect Abandon -action. - -Added a shared compact expandable-text treatment to Command Center asks, -runtime approvals, review prompts and summaries, recoverable-run details, and -agent-attention items. Cards remain a consistent two-line scan by default and -offer keyboard-accessible Show full / Show less controls only when their content -actually overflows. - -Verification: focused dispatcher and operator-attention coverage passed -(**13/13**); lint passed with existing repository warnings only; typecheck -passed; the full Vitest suite passed (**1,211 passed; 1 skipped**); a fresh -production build and the responsive Command Center Playwright contract passed -(**1/1** across desktop, tablet, and mobile); and `git diff --check` passed. - -## 2026-07-13 — v0.11.0 live agent operations release candidate - -Integrated the issue Workstream and durable realtime stream with the global -Mission Control operations overview and workspace Operations Shelf, rebased the -combined release onto v0.10.3, and prepared the additive v0.11.0 release. Updated -the browser contracts to address the shelf's real tab semantics and Live / Queue -/ Agents / Chat information architecture, and made the global read-only assertion -target the top-bar badge exactly now that the same policy is also explained in -the page body. - -Verification: lint passed with existing repository warnings only; typecheck -passed; the full Vitest suite passed (**1,209 passed; 1 skipped**); a fresh -Next.js production build completed; the two corrected browser contract files -passed **8/8** together; and the complete single-worker Playwright run reached -**36/37** before the Chromium headless-shell process itself segfaulted while -creating the last test context. That unaffected connection-affordance test then -passed **3/3** in repeated isolated runs. CI also exposed a pre-existing -Command Center layout assertion that depended on mutable stalled-run seed data; -the contract now accepts both its valid empty and populated attention states -and passed **3/3** repeated local runs. `git diff --check` passed. - -## 2026-07-13 — Mission Control operator hierarchy - -Audited the cross-workspace Mission Control overview and workspace quick-access -dock against seeded local data at desktop and mobile viewports, then implemented -the selected Operations Shelf direction across both surfaces. Expanded workspace -Mission Control now reflows content above a summary / triage queue / agent -presence shelf on desktop and becomes a near-full-height, single-scroll sheet on -mobile. The persistent pill and glance clear the mobile bottom navigation. - -The global overview now leads with runtime and dispatch posture, then separates -workspace queue, assigned attention, agent presence, runtime coverage, and -recent activity into a coherent operator scan. Its summary is derived from real -global query data, retains read-only navigation to durable surfaces, and renders -independent loading, empty, and retry states without inventing mutation paths. - -Corrected queue truth while restructuring the surface: tab and pill badges now -show the total queue, summary copy keeps total and unassigned counts distinct, -and issue references use the workspace's canonical `FRG-*` key. Added explicit -queue/agent loading and error states, real tab semantics, one Collapse action, -platform-correct shortcut copy, 44 px mobile controls, and focused model tests. - -Verification: `pnpm lint` passed with existing repository warnings only, -`pnpm typecheck` passed, focused Vitest passed (**5/5**), `git diff --check` -passed, desktop/mobile interaction and responsive checks passed in the in-app -browser for both surfaces, browser logs contained no attributable warnings or -errors, and `design-qa.md` finished with `final result: passed`. - -## 2026-07-13 — Issue Workstream + durable realtime progress - -Consolidated the issue's repeated agent-status surfaces into one first-class -Workstream beneath the issue author metadata. It now keeps agent presence, -operational state, engagement mode, execution runtime, effective tool policy, -elapsed/last-signal timing, rolling semantic status, approval handling, -operator controls, and an expandable ten-event trace together. Active rolling -STATUS comments no longer jump through the conversation timeline; terminal -summaries remain in history. Activity now leads the issue-rail navigation and -shows live work, while bare issue URLs preserve the historical Attachments -default. - -Kept the browser transport on SSE and closed its reliability gap with durable -cursor replay across `ActivityEvent` and `AgentRunEvent`, subscription-before- -replay buffering, bounded reconciliation, persisted per-workspace cursors, and -visible connecting/live/reconnecting/offline health. Granular run publishes now -use their durable row id/timestamp instead of an unrelated transient id. - -Added workspace settings and migration 0100 for semantic progress cadence and -the non-terminal quiet threshold. The versioned agent protocol now separates -mechanical trace events from human-facing STATUS checkpoints, asks for concise -phase/result updates at the configured cadence, and explicitly rejects -chain-of-thought/tool-log narration. Run diagnostics expose acknowledgement, -output, progress, and completion signals independently, and UI copy keeps Quiet -distinct from the canonical persisted STALLED state. - -Verification: `pnpm lint` passed with existing repository warnings; `pnpm -typecheck` passed; the full Vitest suite passed (**1,187 passed; 1 skipped**); -`git diff --check` passed; and a fresh production build completed. The full -Playwright run passed **36/37** before Chromium itself segfaulted while creating -one workspace-switcher context; that test passed in an isolated single-worker -rerun (**1/1**). No deployment or push performed. - -## 2026-07-13 — AXI-102 expired runtime approval recovery - -Diagnosed AXI-102's failed approval against Hermes: Forge retained WAITING run -`cmrjhkjbf037mo1089vzolecc`, but the gateway had already swept provider run -`run_10446861a003426f88b2c692d30a2542` and returned `run_not_found`. The -approval visibility release had intentionally excluded live approvals from -generic stale recovery, while the worker still polled only ACTIVE runs, so the -orphan could not self-reconcile. - -Hermes now reports provider 404s as explicit missing-run state rather than a -successful completion. The worker polls ACTIVE and WAITING provider runs, -retires swept approvals as STALLED, clears their pending decision data, and -posts the existing failure evidence back to issue surfaces. If an operator's -approval discovers the expiry first, Forge retires the orphan and emits a -fresh same-agent/same-mode assignment atomically; the UI explicitly says the -old approval was not applied and may be requested again. - -Verification for v0.10.2: `pnpm ci:local` passed — lint (existing repository -warnings only), typecheck, the full Vitest suite (**1,167 passed; 1 skipped**), -a fresh Next.js production build, and the full Playwright suite (**37 passed**). - -## 2026-07-13 — AXI-102 human-action visibility - -Traced AXI-102 from production: its intentionally unassigned Backlog issue had -a valid Victor RESEARCH run in WAITING with a connector permission request, -but no ActionRequest, notification, or approval-specific activity event. The -floating agent overlay read the run directly; Command Center's priority queue -only understood ActionRequests/recovery/gates; and issue detail rejected the -run because it did not match `Issue.assignedAgentId`. - -Made runtime approvals a first-class Command Center decision with inline -approve/reject controls and badge counts. Issue detail now resolves live runs -from the issue relationship rather than mutable assignment, prioritizes -approval/waiting work, renders the approval in the main flow and rail, and -surfaces open issue-bound asks that were created outside a comment. Approval -waits are excluded from generic stale recovery instead of recommending an -incorrect abandon action. - -Added an atomic approval lifecycle boundary: poll and subscription producers -deduplicate capture, late subscription detail enriches poll-first records, -and capture writes both the run event and audited/realtime BLOCKED event with -issue context. Provider and operator resolution races similarly produce one -refresh event. - -Verification for v0.10.1: `pnpm ci:local` passed — lint (existing repository -warnings only), typecheck, the full Vitest suite (**1,164 passed; 1 skipped**), -a fresh Next.js production build, and the full Playwright suite (**37 passed**). -The stale-work redispatch test raced once during the first parallel suite run; -its isolated rerun passed **9/9**, and the complete clean rerun passed. - -## 2026-07-13 — Rich-rendering recovery + human delivery acceptance - -Recovered the six AXI-95–99 rich-rendering commits from the stale Codex bridge -clone onto current production `main`, resolving conflicts against the newer -issue context and URL-safety work. Issue descriptions, comments, and Focus now -share the rich renderer; direct media and allowlisted provider links get -bounded, accessible preview controls with inert fallbacks for unsafe schemes. - -Closed the lifecycle defect that let successful agent reviews certify their -own delivery. All-DONE steps now complete the Plan while the Goal remains -ACTIVE with `OUTCOME_REVIEW` health. A signed-in operator must explicitly -accept a non-empty outcome summary plus durable delivery evidence; premature -Goals can be reopened with an audited reason. Added migration 0099 for the -structured evidence record. Agents intentionally cannot accept/reopen Goals -over MCP. - -Made Plan collaboration visible instead of burying a generic composer under -each step. Step rows now expose **Ask @agent**, prefill the assigned agent, -focus the shared mention-aware composer, and retain ordinary comments for -context that should not dispatch work. Goal outcome review links directly to -the final step discussion. - -Verification for the v0.10.0 release: `pnpm lint` (existing repository -warnings only), `pnpm typecheck`, the full Vitest suite (**1,159 passed; 1 -skipped**), `git diff --check`, a fresh Next.js production build, and the full -Playwright suite (**37 passed**). - -## 2026-07-12 — AXI-97 rich issue rendering integration - -Integrated the shared rich body renderer into the remaining issue-adjacent -description surface. The main issue detail description and comment timeline -already render through `RichContentRenderer`; the fullscreen focus issue view -now uses the same renderer instead of raw `whitespace-pre-wrap` text, so image -URLs, video URLs, normal links, provider embeds, and Forge attachment/link -tokens preview consistently without changing stored issue description data or -the existing edit/save flows. - -Verification: `corepack pnpm lint` passed and `corepack pnpm typecheck` passed. -DB-backed tests were not run in this bridge container because it has no -Postgres/Redis/MinIO/Docker services. - -## 2026-07-11 — AXI-95 rich preview rendering contract - -Defined the shared rich preview contract for chat/message markdown surfaces. -`docs/agents/chat.md` now specifies inline image, video, file, LINK attachment, -`forge-link`, normal URL, and allowlisted provider embed behavior; max preview -sizing; collapsed/expanded/hidden states; per-preview actions; and safe -fallback/error behavior. `docs/guide/time-and-attachments.md` now links -attachment readers back to that shared contract. - -Verification: documentation diff reviewed. `corepack pnpm lint` and -`corepack pnpm typecheck` were run in the bridge container. - -## 2026-07-13 — Plan context integrity + reversible issue archive - -Closed the execution-context gaps that let materialized plan issues look like -standalone tasks, then carried the same integrity pass through issue creation, -handling, and archive/restore. - -- **Durable plan context.** Materialized issue detail now shows Goal → Plan → - Step provenance, instructions, output/verification contracts, dependencies, - dependents, and sibling progress. Agent runs capture a versioned immutable - orchestration-context snapshot at dispatch; inbox and MCP context prefer that - snapshot so later plan edits cannot rewrite a run's instructions. -- **Orchestration invariants.** Step dependencies, assignees, roles, and source - runs are workspace/plan validated; structural edits are draft-only; role - resolution never guesses between multiple crew members. Crew `maxParallel` - is enforced across all of a crew's plans under row locks, with fair refill. - Materialized Issue and ExecutionStep terminal lifecycle now synchronizes in - both directions, including reaper/abandon paths. -- **Reversible issue archive.** Added `issue.archive`, `restore`, and - `bulkRestore`, an archived-only Issues view, archived detail tombstones, and - single/bulk restore. Archive atomically clears queue/claim/snooze state, - abandons live runs, cancels one unambiguous active materialized step, and - records audit/activity; restore returns visibility without resurrecting old - work. MCP, agent inbox, relations, comments, labels, and active mutations now - respect the archive boundary. Added migrations `0097` (run context snapshot) - and `0098` (archive-list index). -- **Issue-flow cleanup.** Board/status quick-add preserves its originating - status; command-palette create always opens issue mode; MCP create accepts - status/labels and enforces narrowed-key creation lanes; bulk copy says - “Select loaded”; list/count invalidation stays coherent. Reversible archive - confirmations no longer use destructive type-to-confirm friction. Comment - deletion is now tenant-scoped and label assignment validates workspace ids. -- **Usability evidence.** The flow pass was grounded in the live deployment - behavior already inspected plus current UI/contracts and existing Forge - components/tokens. No screenshot-based browser audit was claimed because an - in-app browser surface was unavailable; Playwright CLI was intentionally not - substituted without an explicit browser choice. - -Verification: Prisma validate/format, `pnpm lint` (existing repository warnings -only), `pnpm typecheck`, the full Vitest suite (**1,149 passed; 1 skipped**), -`git diff --check`, a fresh Next.js production build, and the full Playwright -suite (**37 passed**). Prepared as the v0.9.0 production release; rollout and -live smoke verification follow the merge to `main`. - -## 2026-07-11 — Recoverable agent review handoff - -Closed the Plan state-machine gap where a step could say “Needs review” after -the judge webhook returned `202`, but no reviewer run, trace, fallback gate, or -operator action existed. - -- **Canonical reviewer work.** `plans.judge` and automatic judging now open a - step-bound `AgentRun` stamped `REVIEW`, materialize an issue when the Plan has - no anchor, preserve wake diagnostics, and give RUNS-engine reviewers the full - judge prompt. Agent verdicts finish the reviewer run and resolve any fallback - gate; a human verdict abandons the superseded reviewer turn cleanly. -- **Actionable Plan review.** REVIEW steps now include a reviewer picker, - Start/Retry agent review, live dispatch/acknowledgement state, and an inline - admin-only “Review myself” path with Pass & continue / Request changes. -- **Human fallback.** Added workspace setting - `reviewStartTimeoutMinutes` (default 5, 0 disables) and migration 0096. The - orchestration watchdog marks an unacknowledged reviewer run STALLED and opens - one human Review Gate after the configured window instead of leaving the Plan - parked invisibly. -- **Coverage.** Added integration coverage for reviewer run creation and - completion, exact step wake telemetry, timeout fallback, operator-triggered - review, human verdicts, and the workspace setting. - -Verification: `pnpm lint` (existing repository warnings only), `pnpm -typecheck`, full Vitest suite (**1,104 passed; 1 skipped**), fresh production -build, and full Playwright suite (**37 passed**). Migration 0096 was applied to -the local development and isolated E2E databases. Not deployed. - -## 2026-07-11 — Goals + Plans live operations cockpit - -Reworked orchestration UI from record/status pages into run-aware operating -surfaces, reusing the existing Mission Control event timeline and Forge tokens. - -- **Shared operational run model.** Added a reusable status/trace component - that derives operator-facing phases from the real AgentRun lifecycle: - queued, dispatched, acknowledged, working, waiting, quiet/stalled, review, - done, and stopped. Freshness re-evaluates every 30 seconds and run traces - subscribe to the existing AgentRunEvent SSE stream while expanded. -- **Plan cockpit.** Added a live summary for agents active, work in progress, - operator attention, queued work, and last activity. Every list step now shows - the actual agent, current action, last-event freshness, and an expandable - event trace. Crew highlights use live runs instead of assuming that a step - assignment means the agent is working. Plan content is read-first; title, - body, expected-output, removal, and add-step controls sit behind an explicit - Edit plan mode. -- **Goal operations.** Goal detail now leads with live counts and bounded - Working now / Needs you / Up next lanes (four visible items per lane, linked - to the exact Plan step). The Goals index carries the freshest active task and - agent state inline, while Goal crew presence is likewise derived from runs. -- **Data contract.** Goal and Plan queries now include acknowledgement, - output-start, wake, and freshness fields needed to distinguish a sent wake - from real work. No schema migration was required. -- **Coverage.** Added unit tests for phase derivation and a production-build - Playwright flow that creates a Goal and templated Plan, verifies the live - operations regions, checks read/edit mode, and asserts desktop/mobile - overflow constraints. - -Verification: `pnpm lint` (passes with existing repository warnings), -`pnpm typecheck`, full Vitest suite (**1,099 passed; 1 skipped**), fresh Next.js -production build, and full Playwright suite (**37 passed**). This iteration was -committed and intentionally not deployed. - -## 2026-07-11 — Review gates, actionable Plan comments, and outcome-driven Goals - -Closed the orchestration visibility gaps that made active work look stuck and -made completed work difficult to review from Forge itself. - -- **Review gates now drive the step lifecycle.** Execution-step gates hydrate - the plan, goal, issue, worker, latest run summary, checks, artifacts, and - expected output. Review and Command Center deep-link to the exact Plan step. - Passing a gate records a PASS verdict, marks the step done, and releases its - dependents; requesting changes requires feedback and records a FAIL verdict - through the existing retry/block state machine. -- **Plan comments are agent-capable.** `plans.comments.list/create` are - available over MCP. An explicit `@profileKey` on a freestanding Plan step - materializes its issue-backed work record, creates a canonical step-bound - AgentRun, and includes the full operator comment in the run instruction so - the agent can respond to the request with the right context. -- **Goals are now an operating surface.** Added success criteria, target date, - and outcome summary (migration `0095`). Goal list/detail derive health and - next action from the active Plan, steps, and pending gates; attention states - link directly to Review or the blocked step. Goal detail rolls up recent gate - decisions and artifacts produced by completed step runs. Goal create/update - support the new fields in UI, tRPC, and MCP (`goals.update` added). -- **Regression coverage.** Added integration tests proving gate approval - advances the DAG, Plan mentions materialize and wake canonical agent work, - and Goal outcome/health fields round-trip correctly. - -Verification: `pnpm lint` (passes with existing native-select warnings), -`pnpm typecheck`, full Vitest suite (**1,096 passed; 1 skipped**), and Playwright -(**36 passed**). Migration `0095` was applied to the local development/test -database only. This iteration was committed but intentionally not deployed. - -## 2026-07-08 — CLI: standalone binaries + one-line installers (curl|bash / irm|iex) - -Made the `forge` CLI installable without cloning the repo or publishing to npm. -Bun compiles the built `dist/index.js` to standalone per-platform executables -(no Node needed) — smoke-tested the Linux binary end-to-end under `env -i` -(runs `--version`, `whoami` → graceful "not logged in", `daemon status`). - -- **Binaries** (`tools/forge-cli/scripts/build-binaries.sh`): Bun cross-compile - → `forge-{linux,darwin}-{x64,arm64}` + `forge-windows-x64.exe` + `SHA256SUMS` - (59–111 MB each; bundled runtime). -- **Release**: cut `cli-v0.1.0` (marked **not-latest** so it doesn't hijack the - app's `v0.6.0` "latest"). Install scripts resolve the newest `cli-v*` via the - GitHub API — no fixed URL to bump per release. -- **Installers** (`install.sh` POSIX + `install.ps1`): detect OS/arch, download - the matching binary from the latest `cli-v*` release, verify checksum, drop on - PATH. `install.sh` tested against the live release (downloaded + ran, no Node). - URL: `curl -fsSL /tools/forge-cli/install.sh | bash` / - `irm /tools/forge-cli/install.ps1 | iex`. -- **CI** (`.github/workflows/release-cli.yml`): on a `cli-v*` tag → build + - attach. Tag input passed via `env:` + validated `cli-v[0-9]*` (workflow- - injection-safe per the security hook). -- **Docs**: README Install section now leads with the binary install (npm + - from-source below); maintainer binary-release notes added. - -Not done (offered): a branded app route (`forge.axiom-labs.dev/cli/install.sh`) -that bakes the instance origin — needs a deploy; github-raw works today. And an -in-app one-liner on the Agent Clients page. - -## 2026-07-08 — Ephemeral quickstart: idle auto-archive, runtimes.archive, `forge task`, `runs.open` - -The agreed "ephemeral quickstart" set (Bailey chose: **(B)** auto-tidy over -register-on-connect; **admin-gated** throughout; runs opened both **by agents on -themselves AND by us** via the CLI). Four slices, deploy-held on main. - -- **1a — auto-archive idle EPHEMERAL agents.** New `Workspace.ephemeralAgentIdleMinutes` - (migration `0093`, default `0` = disabled, per the "0 disables the sweep" - convention). `sweepIdleEphemeralAgents` (`src/server/services/ephemeral-idle.ts`) - - worker job `ephemeral-idle-sweep` (5-min). Archives EPHEMERAL agents idle - past the window (reversible); skips BUSY; a never-heartbeated agent is only - reaped once `createdAt < cutoff` (grace to connect). PERSISTENT agents never - touched. Wired into `workspace.get` + `workspace.update` (0..10080). Test: - `ephemeral-idle.test.ts`. -- **1b — `runtimes.archive` (deregister).** New MCP tool (ADMIN), the teardown - counterpart of `runtimes.register`; sets `archivedAt`. Exposed as - `forge runtimes archive `. **Deferred:** did NOT auto-wire the daemon to - archive-on-stop — `register` does a plain create (reuse is via the daemon's - cached id), so a clean archive→restart cycle needs unarchive-on-register - first. Manual deregister works now. -- **2 — `forge task`.** `forge task "" [--agent X] [--project P] [--priority] -[--title]` → `issues.create` then `issues.assign` (`--agent`) or - `issues.setQueued` (else → auto-dispatch). NB: `issues.assign` takes `issueId`, - `issues.setQueued` takes `id` — the tools disagree; the command handles both. -- **3 — `runs.open` (agent opens a run on itself).** New MCP tool (WRITE_ISSUES, - linked-agent required), **issue-anchored** per the AXI-55-avoidance decision. - Delegates to `openOrTouchRun` → resumes an existing ACTIVE/WAITING run for - (issue, agent) rather than duplicating, stamps STARTED for Mission Control. - Drive with `runs.recordUsage` / `setWaiting` / `complete`. Test: - `runs-open.test.ts` (opens+resumes, rejects no-linkedAgentId, rejects foreign - issue). - -Verification: typecheck + lint clean; CLI builds; mcp registry (120) + -runs-open (3) + ephemeral-idle (1) + api-key-purge (1) green. (Worktree env -needed a `server-only` stub + a `tools/forge-cli/node_modules` copy to -build/test.) Migration `0093` is a plain ADD COLUMN (default 0) — safe/online; -worker sweeps activate only after `forge-worker` is redeployed. - -## 2026-07-08 — Ephemeral quick-fixes: `forge issue assign` param + SESSION-key auto-purge - -From the ephemeral / quick-CLI gap analysis (agent-mapped, evidence-cited). Two -concrete bugs, fixed ahead of the larger "ephemeral quickstart" work: - -1. **`forge issue assign` was broken.** The CLI sent - `issues.assign({ id, profileKey })` (`tools/forge-cli/src/commands/issues.ts`) - but the MCP tool requires `issueId` (`mcp.ts:2075`) with no alias — every - assign failed Zod "issueId Required". Renamed the param. Users need - `pnpm build:cli` to pick it up (CLI isn't part of the docker image). -2. **SESSION keys weren't actually auto-purged.** `CLAUDE.md` promised - "auto-purged when expired," but expiry was only enforced lazily at auth - (`api-key-auth.ts:58` rejects an expired key but never deletes the row), so - expired rows lingered in the DB + Clients UI forever. Added - `purgeExpiredSessionKeys` (`src/server/services/api-key-purge.ts`) + an hourly - worker sweep `expired-key-purge-sweep`. Scoped to SESSION only (PERSONAL/AGENT - expiry stays a deliberate admin action). Delete is FK-safe — nothing holds an - inbound FK to `ApiKey`. - -Test `api-key-purge.test.ts`: expired SESSION purged; live SESSION + expired -PERSONAL + permanent AGENT kept. typecheck + lint clean; CLI compiles -(`pnpm build:cli`). Note: worker sweep takes effect only after the -`forge-worker` image is rebuilt/redeployed. - -Reconciliation note: the worktree branched from `origin/main` (missing the -unpushed agent-removal commit), so this was rebased onto local `main` before -landing. - -## 2026-07-08 — Agent removal: smart delete for agents + profiles (both surfaces) - -Gap report from Bailey: the workspace Agents settings + Instance Admin could -disable/unbind agents but never _remove_ them (esp. ephemeral CLI / temp -agents, a mistakenly-created "claude"). Root cause: the `agent.*` router had -`archive`/`unarchive`/`delete`, but the only wired client call was -`agent.update` (chat engine) — no UI surfaced removal. Instance Admin only -wired `agents.profiles.setDisabled`. And `agent.delete` was a raw -`db.agent.delete()`, which is dangerous because `AgentRun.agentId` is -`onDelete: Cascade` — deleting an agent with runs silently destroys its run -history. - -Design (per Bailey: "smart remove", both surfaces): - -- **`agent.remove`** (adminProcedure) — counts references (runs, comments, - apiKeys, assigned/claimed issues, artifacts, plans, goals, steps, review - gates, action requests, crew). Zero → hard delete; any → archive - (`archivedAt` + status OFFLINE). Returns `{ action, name, references }`. - `bindings.list` already filters `archivedAt: null`, so archived agents drop - out of the list. Raw `delete`/`archive` stay as explicit variants. -- **`agents.profiles.remove`** (instanceAdminProcedure) — counts bindings; 0 → - delete the profile, else archive (`profiles.list` filters `archivedAt`, so it - leaves the admin list + bindable catalog). Avoids orphaning bindings - (profile→agent FK is SetNull). - -UI: - -- Workspace Agents (`/settings/agents`): a "Delete" button on each - BoundAgentRow beside Unbind — Unbind stays the reversible archive, Delete is - the smart remove. Destructive `Confirm`; toast reports deleted vs archived. -- Instance Admin (`admin-agents.tsx`): a "Remove" button beside Enable/Disable - with a `useConfirm()` destructive dialog; toast reports action + bound count. - -Tests (`agent-remove.test.ts`, 5): agent clean→deleted, agent w/ assigned -issue→archived (+references), MEMBER→FORBIDDEN; profile no-bindings→deleted, -profile w/ binding→archived. typecheck + lint clean; agent-transport (5), -action-request-accept (15), runtime-admin-gating (2) green. - -Built in an isolated worktree (the bg-job guard blocks direct main edits), then -fast-forwarded onto local main. Deploy held. - -## 2026-07-08 — Runtime settings: admin-gate config writes + confirm destructive tool-reset - -Follow-up from the `rt_hermes_gateway` RESEARCH/REVIEW tool-profile fix (Victor -couldn't inspect the repo during research because those mode profiles were -explicit empty `[]`, which Hermes reads as "disable all host toolsets"). While -auditing the runtimes settings surface, found two rough edges — both fixed here. -Forge-side only; **holding for deploy** (more changes coming). - -- **Gating asymmetry (privilege boundary).** `runtime.create` / `runtime.update` - were `workspaceProcedure` (any member), but they write `config` — which - carries `modeToolProfiles` / `localWorkspaceTools`, i.e. the host tool policy - that decides whether an agent gets terminal/filesystem/git on the host. The - same runtime's secrets/repos/githubApp and the MCP `runtimes.configure` - mirror are all `adminProcedure`/ADMIN, and the `/settings` layout has no role - gate — so a non-admin member could reach Settings → Runtimes and widen host - access. Fixed: `create`/`update` → `adminProcedure`. Daemon self-registration - is unaffected (uses `register`, still `workspaceProcedure`); read/diagnostic - paths (list, heartbeat, verifyConnection, runSelfTest) unchanged. New - regression `runtime-admin-gating.test.ts`: OWNER updates; MEMBER gets - FORBIDDEN on create + update; row untouched. -- **Destructive master-toggle footgun.** In `RuntimeToolPolicyFields`, the - "Local workspace tools enabled" checkbox, when switched OFF, wiped - `toolCapabilities` → `[]` and reset every `modeToolProfiles` entry to empty — - silently dropping RESEARCH/REVIEW read grants with no confirm. Wrapped the - off-path in `useConfirm()` (destructive variant) when there's a non-empty - policy to lose; on-path unchanged. Controlled checkbox reverts on cancel. - -Verification: typecheck + lint clean; `runtime-admin-gating` (2) + -`runtime-secrets` (5) + `runtime-github-app` (8) + `runtime-dispatch-contract` -(10) + `action-request-accept` (15) + `mcp` (120) all green. UI confirm is -typecheck-verified + follows the established `useConfirm` pattern; not -live-clicked (no prod login from this runtime; dev:local boot deferred). - -## 2026-07-07 — Auto-transition to review on EXECUTE completion (`reviewStatusId`) - -Follow-up to today's "does a successful run properly resolve the issue" gap -(found in the Cockpit/false-STALLED session earlier today). Bailey's policy: -an agent should be inclined to move work to In Review when it believes it's -done, never straight to Done — Done stays a human call (or an explicit -instruction) unless confirmed. - -- **Schema** (migration `0092`): `Workspace.reviewStatusId String?` + - `reviewStatus` relation, mirroring the existing `startedStatusId` / - `maybeAutoTransitionOnAssign` pattern exactly (same shape, same validation - style, same opt-in-null-disables semantics). -- **`maybeAutoTransitionOnComplete()`** (`src/server/audit.ts`, new, exported - — sits right beside `maybeAutoTransitionOnAssign`): EXECUTE-only; skips if - the workspace has no `reviewStatusId`, the target isn't an IN_REVIEW- - category status, or the issue is already IN_REVIEW/DONE/CANCELED. Unlike - the assign-time version, BACKLOG/TODO/IN_PROGRESS are **not** skipped — - completing work should surface for review regardless of prior state - (including issues that were never auto-started at all). -- **Hooked into `runs.complete` directly** (`mcp.ts`), not the generic - `recordChange`/`AGENT_RUN_COMPLETED` audit fan-out — verified live data - shows `AGENT_RUN_COMPLETED` fires for abandons/stops too (e.g. "Stopped by - operator to restart as RESEARCH"), so gating on that event kind alone would - have wrongly auto-transitioned stopped runs. `runs.complete`'s own handler - already has `run.issueId` + `run.engagementMode` in scope, so this needed - zero extra plumbing. -- **Settings UI**: new "Auto-transition on completion" section on - Settings → Workspace, right after the existing "Auto-transition on - assignment" one. Used the themed `Combobox` (not a native `` in the orchestration + - runtime-settings surfaces (adapter / sandbox / approval / crew / plan-status / - 2× step-status) are now the themed `Combobox`. `runtime-management.spec.ts` - rewired from Playwright `selectOption` (native-only) to the role="combobox" - trigger + role="option" contract. The app-wide `react/forbid-elements` guard - stays `warn` (~58 sites remain, untouched). -- **Modals → QuickForm.** The 3 hand-rolled orchestration modals (new-plan, - new-goal, GoalEditModal) now use `QuickForm` — one graphite scrim, Enter/Esc, - draft-safe, inline error banner. GoalEditModal `onSave` + the goal-router shim - gained `mutateAsync` so a failing save keeps the modal open with the server - message instead of a fire-and-forget toast. - -Left in P3.2 scope but intentionally out: the ~58 other native selects and the -app-wide `destructive` dead-class (both broader than these surfaces). - -## 2026-07-06 — Agent-runtime audit: Phase 2 (integrity + security) - -Generalized the Phase-1 patches into their classes + fixed the two security holes. -Commits on `worktree-audit-fixes`. - -- **Budget/lifecycle integrity (`orchestration-service.ts`).** `reapPlanRuns()`: - abandonGoal + re-decompose/re-generate cancel non-terminal steps and - stop/abandon in-flight runs (best-effort `connector.stop`); a superseded prior - RUNNING plan is CANCELED, not just demoted. `assertNoStepCycles()` (Kahn) rejects - dependency-cycle plans in both create paths. `sweepOrchestrationBudget()` — new - 60s worker watchdog enforcing plan wall-time independent of cost events + logging - wedged plans. -- **Dispatch (`run-dispatcher.ts`).** Poll-mirrored RUNS cost now flows into - `applyRunCostToPlan` (delta vs last-known, persisted per tick) so poll-only cost - trips `maxTotalCostUsd`. `maxConcurrent` enforced on the RUNS dispatch path - (`startNewRuns`). A disabled runtime's in-flight runs are left alone (disable - blocks new dispatch only) instead of being sentinel-STALLed. Unresolvable - connector annotates the run instead of silent-spin. -- **Security.** `setMemberRole` owner-only gate (a non-owner ADMIN can't self- - promote to OWNER then delete the tenant). `ADMIN_EMAIL` env bootstrap honored - only while zero INSTANCE_ADMIN exist; `auth.ts` no longer re-stamps INSTANCE_ADMIN - every login — a demoted operator stays demoted (`trpc.ts` + `data.ts` + `auth.ts`). -- **CLI (`daemon.ts`).** Reconcile-on-(re)connect via `agent.inbox.list` (recovers - dropped AGENT_ASSIGNED across a deploy) + fatal-auth exit on repeated 401/403. -- **Atomicity/dedup.** `recordUsage` computes its cost delta under a - `SELECT … FOR UPDATE` row lock (no double-count). `runtimes.register` upserts on - (workspaceId, name, kind) instead of stacking duplicates. -- **P2.7 Codex restart.** The confirmed false-STALL is resolved by the Phase-1 - `unknown`-is-non-terminal fix (Codex `getStatus` returns `unknown` for a run lost - after a worker restart → stays ACTIVE, watchdog arbitrates). Full WebSocket- - session reattach (durable cross-process session state) is a Codex-connector - re-architecture, deliberately deferred — out of scope for a fix pass. - -Verification: `pnpm typecheck` clean; `pnpm build:cli` clean; orchestration (26) + -members (18) + mcp (118) tests green; new regression tests for cycle-rejection, -abandon-cancels-steps, and non-owner-can't-grant-OWNER. Phase 3 (cross-workspace -move, UI consistency, connector parity, view-hierarchy legibility, housekeeping) -pending. - -## 2026-07-06 — Agent-runtime audit: Phase 1 safety fixes - -Acted on the 2026-07-06 agent-runtime audit (73 findings; report artifact + -[[agent-runtime-audit-2026-07]] memory). Phase 1 = the small, confirmed -safety/lifecycle/false-terminal fixes. Landed as four commits on branch -`worktree-audit-fixes`. - -- **Orchestration loop (`orchestration-service.ts`).** `cascadeReadiness` and - `transitionStepToReady` now load `plan.status` and early-return unless RUNNING - — a BLOCKED/CANCELED plan stops cascading finishing steps into fresh dispatch - (was: kept spending past the budget block / after abandon). `recordVerdict` - throws CONFLICT on a settled step (DONE/BLOCKED/CANCELED) so a stale/dup verdict - can't reopen finished work. `ExecutionPlan.startedAt` (migration **0091**) - stamped in `activatePlan`; `checkAndBlockBudget` measures wall-time from - `startedAt ?? createdAt` so planning + approval-wait isn't charged to execution. -- **Migration 0091** also adds `AgentRun @@index([status,lastEventAt])` + - `@@index([assignmentEventId])` — the cross-tenant 5s/60s sweeps - (`pollActiveRuns`/`ensureSubscriptions`/stale watchdog) previously seq-scanned - (every AgentRun index led with `workspaceId`). Left the pre-existing - `ExternalResource` rename-index drift out of the migration (unrelated). -- **Run dispatch (`run-dispatcher.ts` / `hermes-runs.ts`).** `getStatus` - `'unknown'` is now NON-terminal (leave ACTIVE, let the stale watchdog arbitrate) - — fixes the Codex worker-restart / deploy false-STALL and momentary blips. - `mapStatus` maps an unrecognized non-empty status → `running` (not `unknown`). - On a live `running` poll we always bump `lastEventAt` even when the step label - is unchanged, so a quiet-but-alive run isn't watchdog-STALLED. Hermes - `approve`/`stop` now inspect `res.ok` and throw; `agent-run.approve` (reject too) - surfaces the failure as a TRPCError instead of clearing the block + returning ok. - `runtime.register` now calls `assertEndpointTransport` like create/update. -- **CLI daemon (`daemon.ts`).** `acquirePidLock()` writes the pid via - `fs.open(..., "wx")` (O_CREAT|O_EXCL) before SSE opens → closes the - double-daemon TOCTOU. `refreshLinkedAgent` returns `undefined` on a transient - `agents.me` failure vs `null` on genuine unlink; the heartbeat only overwrites - `linkedAgent` on a definitive answer (was: one blip nulled linkage, dropping all - dispatch ~60s). -- **UI.** Goals/Plans list+detail get an `isError` branch (themed "couldn't load … - Retry") so a fetch failure isn't rendered as "No goals yet"/"Goal not found". - `global.runtimes` returns each runtime's `homeWorkspace`; the global Runtimes - settings gear routes via `workspacesInUse[0] ?? homeWorkspace` so a fresh - LOCAL_DAEMON isn't a read-only dead-end. Widened the `useGoalRouter` query shim - type with `isError`/`refetch`. - -Verification: `pnpm typecheck` clean; `pnpm lint` 0 errors (only pre-existing -native-`` replaced with a themed `ProjectPickerChip` (dot + name, ember - tint, ModeChip-style popover). New `CommittedChips` row. -- **Issues filter/sort/group** — `IssueFacetChips` (`saved-views/facet-chips.tsx`): - multi-select Status/Priority/Project/Assignee/Label chips projecting onto the - existing `SavedViewFilters` arrays (server already accepted them). `SortChip` + - `GroupChip` single-pick controls. Server: `issue.list` gains a `sort` enum - (`ISSUE_SORT_VALUES`) → orderBy switch; default unchanged (priority desc, created - desc). `issue-list.tsx` `statusGroups` generalized to `groups` keyed on - `groupBy` (status/project/assignee/priority/none); per-group add-issue button kept - for status+project. Sort/group persisted via `useStoredPref` (localStorage), - shared `IssueSort`/`IssueGroupBy` in `saved-view-filters.ts`. - -Verified: typecheck, lint, slash-commands unit tests green. (Full unit suite passes in -the main checkout; 10 server-module files fail to load _in the worktree only_ because -`vitest.config.ts` aliases `server-only` to a worktree-relative path with no -`node_modules/server-only` — environmental, not a code regression.) - -Follow-up wave (same session, mid-session asks): - -- **#6 runtime label** — `audit.ts` assignment SYSTEM comment rendered the raw - `RuntimeKind` enum inside `_…_`; the embedded underscore broke the markdown emphasis. - New shared `src/lib/runtime-kind.ts` (`RUNTIME_KIND_LABEL`/`runtimeKindLabel`); audit - uses it and omits the line when the agent has no runtime. -- **#5 inline composer highlight** — `MentionInput` gains opt-in `highlightMentions`/ - `highlightCommands`: a transparent-text backdrop layer (identical box model, scroll- - synced) tints `@mentions` (indigo) + recognised `/command` lines (ember) live. Real - text rides on top via `bg-transparent` textarea (twMerge overrides the field bg). - `buildHighlightSegments` tokenizer (reuses `parseLine`). Enabled on the issue - description, comment, and edit-comment composers. -- **#7 run strip settle** — `AgentRunStrip.deriveStripState` kept "running" (pulsing) - for the full 5-min stale window after the last event. Added an `idle` state: no events - within a 90s LIVE window (but before STALE) → calm static dot + " · idle", no - ping. Presentational only; run lifecycle untouched (so a quiet-but-active run isn't - dropped). RunActivityChip already gated on 60s. NOTE: fully clearing the strip still - needs the run to reach terminal (agent `runs.complete`, or the `agentRunStaleMinutes` - sweep which defaults to 0/disabled). -- **#4 responsive** — issue-detail Attachments "attach link" inputs used - `min-w-[18rem]`/`min-w-[14rem]` and overflowed the ~22rem rail on smaller laptops → - switched to `w-full min-w-0 basis-full` (stack). Relations search input → `flex-1 -min-w-0`. Issues-list search → `w-32 sm:w-48`. Shell layout (capped 1600, `min-w-0` - main, `shrink-0` aside) was already sound; the overflow was inner content. - -Verified (follow-ups): typecheck + lint green. The inline-highlight backdrop is the one -change that benefits from an eyeball in the running app (alignment), though the technique -degrades gracefully — plain prose shows no tint. - -## 2026-05-27 — Release visibility & docs (v0.2.0) - -Post-v0.1.0 polish + docs. Relaxed `system.changelog`/`changelogFull`/`buildInfo` to -`protectedProcedure` (data isn't tenant-specific) so they work in the global/admin shells. - -- **Global What's New** — `(app)/whats-new/page.tsx` + `whats-new-content.tsx` render the - canonical CHANGELOG grouped per release (Added/Changed/Fixed/Removed) with a Version·Title - heading; marks `changelogSeenAt` on mount. Reachable from Mission Control + Instance Admin - via the version chip. Workspace dashboard tile + `/w/[slug]/whats-new` unchanged (same source). -- **Version chip** — `version-chip.tsx` reads `system.buildInfo`; in the global concourse + - admin rail footers (hover = release/SHA/build-time), links to What's New. -- **CHANGELOG heading convention** — `## [YYYY-MM-DD] — vX.Y.Z · Title` (bracket date drives - the unseen dot; tail is the release name). Retrofitted v0.1.0; RELEASE.md updated. -- **Docs** — new guides: mission-control, connections, instance-admin, agents/profiles-and-bindings - (registered in the VitePress sidebar). -- **Ops** — pruned ~190 GB of Docker build cache + dangling images. - -Verified: typecheck, lint, next+docs build green. Released v0.2.0 via GitHub Flow. - -## 2026-05-27 — Multi-workspace restructure · follow-up wave - -Executed `docs/plans/multiws-followup-goal.md` (agent-team spec) — the deferred/larger -items after the core restructure shipped. Migration 0071 (`AgentProfile.requestedById` - -- `approvedAt`; `Agent.requireApprovalBeforeStart`, additive). Six lanes, parallel - subagents, integrated centrally: - -* **Connections live OAuth/OIDC** — `/api/connections/[id]/authorize` + `/callback` - (PKCE + signed state), generic OIDC discovery + GitHub/Google/Slack, encrypted token - bundle + clientSecret (`config.clientSecretEnc`), `connection.refreshIfNeeded`. Global - connections page wired with Authorize/Re-authorize + add-connection flow. Per-connection - callback URL `/api/connections//callback`. Reuses AUTH_SECRET via crypto.ts. -* **Activity dock 7-tab fidelity** — Live/Queue/Agents/History/Chat/Admin/Plans brought to - `screens-activity.jsx` (stat-card headers, section labels, dispatch hints, outcome words) - without changing data sources / keybindings / namespaces. -* **Profile request→approve** — `agents.profiles.request` (member) / `approve` / `reject` / - `listPending`; `/admin/agents` pending queue; workspace catalog "Request a profile" dialog. - Bind catalog hides pending (unapproved) profiles; bind rejects pending. -* **Wired admin + binding affordances** — `instanceAdmin.createTenant` / `inviteUser` / - `backup` (best-effort ack) behind the Overview/Users buttons; per-binding require-approval - toggle; connection-mapping default labels. -* **MCP/CLI profile-awareness** — `agents.profiles.list`/`get` MCP tools, `agents.me` now - returns `profileId` + `instanceRole`; `forge agents --global`, `forge whoami` instanceRole. - -Verified: typecheck, lint, next+docs build, unit **736 passed/1 skipped** (sequential), -e2e **17 passed** (incl. 3 new follow-up specs: admin agent-policy, connections authorize, -Activity dock open+tab-switch). Known scope note: full OAuth round-trip + member/admin -two-user flows are covered by the unit/integration layer rather than browser e2e (a mock -IdP + second session would be disproportionate); backup is an acknowledgement, not a real -dump job. - -## 2026-05-26 — Multi-workspace restructure · Phase 1 (schema foundation) - -Kicked off the three-tier ownership restructure from the Claude Design -handoff ("Forge Screens Board" + the three design chats). Full brief and -phase plan in `docs/plans/multiws-restructure.md`. Working on branch -`worktree-multiws-restructure`. - -Confirmed three architectural forks with Bailey before cutting any migration -(all on a LIVE system — prod builds from the working tree, entrypoint -auto-runs `migrate deploy`): - -1. **`Agent` row = the binding, not a rename.** Pushed back on the handoff's - "rename Agent → AgentProfile + new Binding table" framing — that repoints - every FK (`AgentRun.agentId`, `Issue.assignedAgentId`, `ChatThread.agentId`, - `ApiKey.linkedAgentId`, dispatch, MCP, CLI) on a live DB. Instead: keep - `Agent` as the per-workspace binding, add a global `AgentProfile` - definition it points at (`Agent.profileId`). Zero FK churn. -2. **`profileKey` unique per owner** (`@@unique([ownerId, profileKey])`). -3. **Connections** = model + mapping + read UI now, generic OAuth/OIDC - (modelled on the existing `SsoType` pattern: OIDC/GitHub/Google/Slack/ - Custom — Authelia-style), not hardcoded vendors. - -**Phase 1 shipped (additive only — existing code compiles untouched):** - -- `enum InstanceRole`, `User.instanceRole` (default MEMBER). -- `AgentProfile` (global, `ownerId`, base capabilities, `instanceShared`, - `disabledAt`); `Agent.profileId` + binding policy columns - (`autoDispatchEligible`, `engagementMode`); `@@index([profileId])`. -- `Connection` (global, user-owned identity) + `ConnectionMapping` - (workspace-scoped) + `enum ConnectionProvider`/`ConnectionStatus`. -- Migration `0069_multiws_restructure_phase1` (generated via a throwaway - shadow PG, replaying 0001–0068; DDL-only, no destructive ops). -- `prisma/backfill-multiws.ts` — idempotent, **run-explicitly** data - backfill (NOT auto-applied on deploy): promotes ADMIN_EMAIL to - INSTANCE_ADMIN, creates one AgentProfile per (workspace-owner, - profileKey), links `Agent.profileId`, backfills `Runtime.ownerId`. -- `instanceAdminProcedure` now checks `User.instanceRole` (DB) with the - `ADMIN_EMAIL` env match kept as bootstrap fallback; injects - `ctx.instanceRole`. `auth.ts` stamps INSTANCE_ADMIN on the bootstrap - operator's upsert (self-heals existing rows on next sign-in). - -Verified: `prisma validate` clean, `prisma format` clean, full -`pnpm typecheck` passes, ESLint clean on touched files. Migration is safe -to `migrate deploy`; backfill is decoupled and idempotent. - -Next (Phase 2): globalize `Runtime` (nullable `workspaceId`, `instanceShared`), -split routers into `agents.profiles.*`/`agents.bindings.*`, -`runtimes.*`/`connections.*` global+workspace, add `global.*` aggregation -router + `globalProcedure`. - -## 2026-05-26 — Forge MCP Orca integration contract - -Closed the Orca-facing MCP gaps in `src/server/services/mcp.ts`: - -- `issues.list` now accepts `orderBy` (`updatedAt`, `createdAt`, `priority`, - `identifier`, `title`), `order`, `cursor`, and `createdByViewer`, and returns - cursor envelopes with a stable Issue DTO (`identifier`, canonical URL, nested - status/project/assignedAgent/labels, dates, and legacy scalar ids for - compatibility). -- `issues.list` keeps `projectId`, supports `statusCategories`, and treats - `includeDone:false` as "exclude DONE" without hiding CANCELED. `issues.assigned` - now also accepts `projectId` and returns the same Issue DTO envelope. -- Added `workspaces.list` (`{id,name,slug}`) and narrowed `agents.list` to the - stable assignable shape (`{id,name,profileKey}`), both in `{data:[...]}` envelopes. -- `comments.list` now returns `{data:[...], nextCursor?}`. Issue-returning parity - mutations (`issues.create`, `issues.update`, `issues.transition`, `issues.assign`, - `issues.setLabels`) now surface the same Issue DTO while preserving the legacy - scalar ids clients already used. -- The REST MCP route now passes through tool-level `{data:...}` envelopes instead - of double-wrapping them. - -Coverage: - -- `DATABASE_URL=postgresql://forge:forge@localhost:55432/forge?schema=public REDIS_URL=redis://localhost:56379 pnpm test src/server/services/__tests__/mcp.test.ts` → 102 passed -- `pnpm typecheck` → pass -- `pnpm lint` → pass -- `E2E_FORCE_BUILD=1 pnpm test:e2e` → 9 passed -- Full `pnpm test` was also run with the dev Postgres/Redis; the MCP suite passed, - but the full run still has unrelated shared-dev-DB/env failures in - provider-credentials without `AUTH_SECRET`, plus existing chat hard-delete, - dispatch-rule, dispatcher, and heartbeat sweep tests. Re-running provider - credentials with `AUTH_SECRET=test-auth-secret-for-vitest` passes; the remaining - failures are outside this MCP contract path. - -## 2026-05-26 — User-docs cleanup for the AXI-55 epic (convergence + modes) - -Post-ship doc pass. The CHANGELOG/What's-New entries were already correct, but -the reference docs hadn't caught up with the shipped epic. Fixed three gaps: - -- **`docs/agents/engagement-modes.md`** — rewrote from spec voice (addressed to - "you asked…", "Decisions resolved", Phase 2 reading as maybe-live) into guide - voice describing what's live. Phase 2 scoped-tool enforcement is now clearly - labelled planned-not-shipped. -- **`docs/agents/overview.md`** — added a **"Two ways to run agent work"** - section (direct dispatch vs. Goal orchestration), the question users actually - had. Calls out the shared `AgentRun` substrate and the two orthogonal dials - (engagement mode, execution engine). Added orchestration/engines/modes to the - cross-references (overview previously didn't link to orchestration at all). -- **`docs/concepts/orchestration.md`** — wove in the convergence: added - **"Steps, runs, and issues"** (AXI-57 steps open observable runs; AXI-56 - materialize-step-as-issue), noted `Goal.initiativeId` (AXI-58) on the Goal - primitive, and corrected the cost-folding note to the new `executionStepId` - FK (legacy `sourceRunId` fallback). - -No code/feature change, no new CHANGELOG entry (docs describe already-shipped -behaviour). VitePress build passes (dead-link check is build-failing by default). - -## 2026-05-26 — Orchestration↔issue convergence + engagement modes (AXI-55 epic) - -Shipped the full epic (AXI-55) per `/home/bailey/engagement-convergence-plan.md` -runbook, five phases, backend-first with per-phase migrations applied to local -via `prisma db execute` (the repo's local dev DB has migration-history drift, so -`migrate dev`/`reset` are unusable here; prod picks up the migration folders via -`migrate deploy` on boot). Migrations 0064–0067. - -- **AXI-56** (0064): `ExecutionStep.issueId` + `materializeStepAsIssue` (idempotent; - carries title/body/expectedOutput/verification/assignee). tRPC + MCP - `executionPlans.materializeStep`. -- **AXI-57** (0065): `AgentRun.executionStepId`. `transitionStepToReady` now opens an - observable run (bound to the step's issue, else the plan anchor; tagged with the - step) IN ADDITION to the webhook dispatch — orchestrated turns are visible in - Mission Control without collapsing the two control modes. `applyRunCostToPlan` - resolves via the FK (legacy `sourceRunId` fallback). -- **AXI-53** (0066): engagement modes. `EngagementMode` + `MentionEngagementPolicy` - enums; `AgentRun.engagementMode`; 3 `Workspace` knobs. `resolveEngagementMode()` + - per-mode instruction blocks (`engagement-mode.ts`, unit-tested). Runs carry the - mode (threaded from the AGENT_ASSIGNED payload through ensureCanonicalFromEvent + - the RUNS dispatcher, which also injects the instruction). Auto-transition gated on - EXECUTE. MCP `issues.assign` + tRPC `issue.update` accept `mode`. UI: mode glyph + - chips (run strip / RunRow), assign-picker segmented control, Settings → Dispatch - Rules controls. -- **AXI-58** (0067): `Goal.initiativeId` (+ initiative back-relation); `createGoal` - accepts it. (Cycle↔goal/plan largely subsumed by step-as-issue.) -- **AXI-54 gap 2**: `executionPlans.create` accepts `steps[].dependsOnStepIndexes` - (index→id in-txn), so a DAG is authorable in one create call. - -Tests: orchestration suite 10→17, new engagement-mode suite (9), all green; lint + -typecheck clean. SLA/watchdog mode-gating left as a follow-up (the run carries the -mode; no behavior regression — only auto-transition is gated so far). - -## 2026-05-25 — ExecutionPlan create supports indexed step deps (AXI-54 gap 2) - -Closed Gap 2 of AXI-54: `executionPlans.create` can now seed a step DAG in the -same call using `steps[].dependsOnStepIndexes`, matching the already-working -`plans.addSteps` authoring shape. `createExecutionPlan` now creates seeded steps -in a first pass, records their real ids, then resolves index dependencies in a -second pass inside the same transaction. Explicit `dependsOnStepIds` remain -supported and are de-duped with resolved index deps. - -Surface updates: - -- Service input accepts `dependsOnStepIndexes` on seeded steps. -- tRPC `executionPlans.create` schema forwards `dependsOnStepIndexes` and router - coverage verifies the seeded child receives the resolved parent id. -- MCP `executionPlans.create` schema forwards `dependsOnStepIndexes`, so agents - can hand-author a goal-linked DAG without falling back to `plans.addSteps`. -- Changelog updated under Unreleased. - -Verification: - -- RED confirmed first: new service test failed with `dependsOnStepIds` still - empty for child steps. -- `pnpm test src/server/services/__tests__/orchestration.test.ts -t "createExecutionPlan resolves seeded step dependencies by input index"` → pass -- `pnpm test src/server/services/__tests__/mcp.test.ts -t "orchestration loop tools"` → pass -- `pnpm test src/server/routers/__tests__/execution-plan.test.ts -t "creates a plan with ordered steps"` → pass -- `pnpm test src/server/routers/__tests__/execution-plan.test.ts src/server/services/__tests__/orchestration.test.ts src/server/services/__tests__/mcp.test.ts` → 124 passed -- `pnpm typecheck` → pass -- `pnpm lint` → pass -- `env -u OPENAI_API_KEY pnpm test` → 703 passed / 1 skipped, with 3 unrelated dispatcher/heartbeat failures when run against the shared `/home/bailey/forge/.env` database. The changed execution-plan/orchestration/MCP suites are green; the failures are in dispatch-rule/dispatcher/heartbeat selection tests and do not touch this code path. - -## 2026-05-25 — Link hand-authored ExecutionPlans to Goals (AXI-54 gap 1) - -Closed Gap 1 of AXI-54: there was no API path to attach a hand-authored -`ExecutionPlan` to a `Goal` — only `goals.decompose` (the LLM planner) ever set -`ExecutionPlan.goalId`, and `executionPlans.update` didn't accept it. Surfaced -while building the engagement-mode Goal/Plan (AXI-53): the plan could only be -associated to its goal via a shared `issueId`, never the `goalId` FK, so the -Goal DAG view didn't claim it. - -Changes (no migration — `goalId` column already existed): - -- `createExecutionPlan` (+ tRPC `executionPlans.create` + MCP) now accepts - `goalId`. When set, the plan is created as the goal's active attempt and any - prior active attempt is demoted (mirrors `decomposeGoal`). Cross-tenant + - not-ACHIEVED/ABANDONED guards. -- New `attachPlanToGoal` service + tRPC `goals.attachPlan` + MCP - `goals.attachPlan({ goalId, planId, makeActive? })` to link an _existing_ - plan. Refuses to steal a plan already owned by a different goal; keeps the - single-active-attempt invariant. -- Tests: 3 new orchestration cases (attach links + activates; create-with-goalId - demotes prior attempt; refuse cross-goal steal). 13/13 orchestration green; - full plan/mcp suites green; typecheck + lint clean. - -Gap 2 of AXI-54 (index-based step deps on `executionPlans.create`) still open — -`plans.addSteps` remains the DAG-authoring path. Note: the `mcp__forge__*` -stdio bridge drops array-of-object params, so step bulk-ops go via a direct -`POST /api/mcp/`. - -## 2026-05-25 — Agent run completion closes lifecycle rows - -Fixed the false-stalled-agent path where an agent could finish a no-op / -comment-only response but leave its `AgentRun` ACTIVE. The MCP -`runs.complete` tool already had the agent-facing completion contract and the -regression expectation was clear: it should store summary/artifact/checklist -metadata **and** close the run. Implementation now wraps the metadata write in a -transaction and calls shared `finishRun()` with `COMPLETED`, preserving the -standard `AGENT_RUN_COMPLETED` audit/activity event while leaving the Issue -itself open for human/operator follow-up. Also narrowed watched-issue agent -fan-out to actionable events only, so low-signal status changes and rolling -STATUS comment updates do not create/touch canonical agent work; BODY comments, -stalls, SLA breaches, nudges, and priority changes still page watchers. - -Verification: - -- `pnpm vitest run src/server/services/__tests__/audit.test.ts -t "watcher fan-out"` → 3 passed / 4 skipped -- `pnpm vitest run src/server/services/__tests__/mcp.test.ts -t "runs.complete"` → 2 passed / 97 skipped -- `pnpm vitest run src/server/services/__tests__/agent-run-stale.test.ts src/server/services/__tests__/stale-work.test.ts` → 9 passed -- `pnpm typecheck && pnpm lint` → clean -- `pnpm vitest run src/server/services/__tests__/audit.test.ts src/server/services/__tests__/mcp.test.ts -t "watcher fan-out|runs.complete"` → 5 passed / 101 skipped -- `pnpm test` full-suite smoke → 700 passed / 1 skipped, with two unrelated/environment-sensitive failures observed: `artifact-lifecycle` duplicate fixture key passed immediately when isolated; `agent-transport` expected no OpenAI model and passed with `OPENAI_API_KEY=`. -- Docker deploy smoke: rebuilt/recreated `forge` + `forge-worker`; migrations reported no pending migrations; both containers started; live bridge fallback confirmed AXI-47 is Done, `queued=false`, and has no ACTIVE/current run. - -## 2026-05-24 — Design-board parity: glow-grid background + Settings IA (Agents + Connections) - -Implemented the "Forge Screens Board" design handoff (claude.ai/design bundle). -Two pillars: theme/chrome token parity, and the settings information-architecture -the board landed on after the operator agreed to "replace existing mockups." - -**Theme/chrome parity.** Live shell already mirrored the board (sidebar-nav.ts -is byte-identical to the design's reference copy), so the deltas were small: - -- **`.forge-glow-grid*` family** added to `globals.css` (BG3 — the Atlas - dashboard background ported to Forge tokens): static dot grid + two soft - radial "lights" drifting on non-syncing 28s/36s paths, `mix-blend-mode: -plus-lighter` so they add luminance to the dots beneath. Blob B uses ember - for the single warm-accent bias. Transform-only, gated on `[data-motion="on"]` - - reduced-motion. Registered `forge-glow-drift-a/-b` keyframes + animations in - `tailwind.config.ts`. Did NOT regress `.forge-grid-bg` — live's `::before` - translate3d version is the intentionally-smoother one vs the design's - background-position form. -- `topbar.tsx` gained `bg-background` for strict parity with the board's - `PageTopbar`. -- Sidebar `NavRow` badge colour split: decisions → ember accent, inbox → quiet - subtle/foreground (matches the design `NavRow`; live had everything ember). - -**Settings IA refactor (Agents + Connections).** The board collapsed the three -overlapping agentic-config surfaces (Agents / Runtimes / Integrations) into two: - -- `settings-nav.ts`: renamed the `integrations` group → `connections` - (`/integrations` item → `/connections`, new copy), removed the standalone - `/runtimes` nav item, refreshed the Agents description. The settings index - - compact navbar render from this list, so both auto-reflect the change. -- **New `/settings/connections`** page — strict inbound/outbound external I/O. - Banner explains where Hermes/Claude/Codex went. The one real backend - (Email-to-issue, `/api/ingest/email` + HMAC) is fully preserved as the wired - inbound card; GitHub/Slack/Linear/PagerDuty/Discord/custom-webhook show as - honest "available" cards (no OAuth backend exists yet). Event-vocabulary grid - links to Webhook deliveries. -- `/settings/integrations` → server redirect to `/settings/connections` - (preserves query string, mirrors the existing `integrations/deliveries` - redirect shim). -- **Agents page** absorbed the old first-class integration adapter cards as an - "Add an agent" recipe gallery (provider recipes that pre-seed the onboarding - wizard via `openNewWithProvider`) + a read-only "Provider matrix" reference. - Reworded the wizard's stale "Settings → Runtimes" hint. -- **Runtimes route kept alive** (not in the rail) as the advanced editor, since - it owns real CRUD the board's Infrastructure accordion doesn't cover (adapter - picker, Codex sandbox/approval policy, planned adapters, tier explainer). - Reachable via the provider-matrix "runtime editor" deep link + existing - runtime-detail links from mission-control / chat-status-rail / agent detail. - The runtime-management + accessibility e2e specs still target it, so they pass. -- Repointed the two functional `/settings/integrations` links (fleet-checklist - chat-ready step → `/settings/agents`; chat-thread "Integrations →" → - "Connections →" `/settings/connections`) and the workspace.ts comment. - -`pnpm lint` + `pnpm typecheck` clean. `pnpm test` = 689 pass (the only 4 -failures are a pre-existing `AUTH_SECRET`-not-set env issue in -`provider-credentials.test.ts`, untouched by this work — pass with the secret -set). Done in worktree `worktree-design-parity`. - -**Per-screen parity sweep (all 13 Screens-Board artboards).** Diffed every -board artboard against its live page. Finding: live is consistently a -_superset_ of the mock (the board was built _from_ live, then live grew -further — customizable dashboard widgets, the chat diagnostics rail, inbox -extras). So "parity" = porting the design's _additive_ signal elements live was -missing, NOT regressing live to the simpler mock. Implemented (add-only, -verified data exists first — skipped anything that would need a new tRPC field -or a router change): - -- **Cycles summary card** — 3 stats → 4-up bordered metric tiles - (Scope / Done / In progress / Remaining); in-progress from - `status.category === "IN_PROGRESS"`, remaining = total − done. Kept the - timeline bar + burndown sparkline. -- **Goals card** — segmented step-ladder (done/current/todo), crew + plan-count - meta row, labeled budget meter (turns warning past 80%). All from already - computed `stepsDone/stepsTotal/planCount` + `totalCostUsd/maxTotalCostUsd`. -- **Command Center** live-goals card — `$used / $cap` budget figure + ember - budget bar (rendered only when a cap exists). Step bar skipped (no step - counts on that row). -- **Artifacts** — client-side type-filter chip row (only types present) + - search over title/summary; resets stale filter on tab switch. Kept the - Active/Archived tabs. -- **Roadmap** — visible mono project KEY chip on each bar, legend strip - (Active sprint / Planned sprint / Project bar / Today), status-aware sprint - band tint (ACTIVE ember/0.18 vs planned ember/0.05). Per-project progress - fill skipped (`project.list` has no done split). -- **Issues list** (`issue-list.tsx`) — Linear-style **status grouping** with - sticky per-status headers (dot + name + count) over the flat list. Grouping - is purely visual: the flat selection model / select-all toolbar / Shift-range - / `x`-hotkey / hover previews / unread dots / snooze all preserved. Plus up to - 2 label chips + a comment-count icon per row (both already on `issue.list`). -- **Issues board** — "+" add-issue button per column header (fires the existing - `forge:quick-create`, passes `projectId` when project-scoped; status prefill - not supported by quick-create, noted in a comment). -- **Plans card** — "Updated {relative}" footer (`updatedAt` was on the row). - DAG step strip skipped — `executionPlan.list` exposes only `_count.steps`, no - per-step status / done count / owner relation. -- **Dashboard** "By status" — inline horizontal bars scaled to count (status - color over bg-subtle), kept the dot + CountUp. -- **Chat** stream header — `.forge-breath` live presence dot, gated on the - existing reachability derivation so it never falsely claims liveness. - -Deliberately NOT regressed (live is the newer intent): chat right-rail -diagnostics (design wanted Linked-work + Members), inbox single-column vs the -mock's 2-pane agent rail, dashboard widget system vs the mock's fixed -`grid-cols-12`, project-detail single-column Overview vs the mock's 2-col rail, -Plans tabbed grid vs the mock's template-rail split. Skipped for missing data: -Initiatives per-project nested list, Projects-list per-card progress bar + -initiative chip, issue-detail Reassign/Release affordance (no `issue.reassign` -mutation). All flagged in the parity reports. - -Final: `pnpm typecheck` + `pnpm lint` clean; `pnpm test` 688 pass / 1 skip -(with `AUTH_SECRET` set). - -**Settings-page redesign sweep (the "specifically designed" settings screens).** -The second design bundle (`iCVaz0otBlYcWUMr4POAdA`) is byte-identical to the -first; the operator re-pointed at it to flag that the prior passes did the -settings _IA_ but not the per-subpage _internal_ redesigns. Brought the live -settings pages up to the design's vocabulary (SettingsLayout / FormSection + -hints / DangerZone / SaveBar / TeachEmpty / FormSegmented) — add-only, every -input + tRPC mutation + handler preserved (the design's FormInput/Select are -display stubs; we reorganized live's _functional_ controls, never replaced -them): - -- **Agents** — replaced the flat list rows with the design's **AgentMergedCard**: - header strip (avatar / @key / presence dot / N-of-capacity concurrent / - description), an inline **provider · runtime · connection · last-heartbeat** - 4-col strip, a capabilities + workload-bar row, and a collapsible - **Infrastructure** accordion (runtime kind / endpoint / heartbeat read-only + - "Open runtime editor →" deep link). All real data from `agent.list` ⨝ - `runtime`; all actions (QuickActions / View / Edit-wizard / Archive / Delete) - preserved. (Gallery + provider matrix from the earlier pass kept.) -- **Plugins** — added the **Permission reference** grid built from the _real_ - `PluginScope` enum (not the mock's invented scopes), a "scopes" label, and - aligned subtitle. -- **Workspace · General** — split one long form into Identity / Sprint cadence / - Tracking & storage / Agent SLA / Auto-transition / AI provider; pill toggles; - red **Danger zone** (archive/delete, type-to-confirm) wired to existing - Confirms; sticky **Save bar** that diffs against the server snapshot for an - accurate pending count + ⌘S. -- **Members** — Roster section + a Roles teach card (OWNER/ADMIN/MEMBER/GUEST). - **Dispatch rules** — Routing-matrix section + "How rules resolve" walkthrough - - teach-empty (drag-reorder + first-match-wins preserved). -- **Statuses** — Pipeline section + read-only Categories reference + hex chips. - **Labels** — Labels section + Palette reference + hex chips. -- **Saved views** — Yours / Shared split + teach-empty. **Recurring** — Schedules - section + teach-empty (skipped a template-variable reference card — the - backend does no `{{var}}` substitution, would be a false promise). - **Templates** — section-wrapped issue/project partials + teach-empty. -- **Data** — Export / Import as separated groups + "What's portable" ✓/✗ lists + - drop-zone framing. **Admin** — section hints + teach-empty streams. - **Deliveries** — status-aware teach-empty (queue already matched). -- **Account pages** — Profile (inline help, Identity), Auth (admin-only banner + - "what you can connect" + counts), Workspaces (danger zone routing to - per-workspace settings — no account-scoped self-leave mutation exists, so we - route rather than wire a phantom). Appearance + Developer access already - matched the design — left as-is. -- **Onboarding** — live's 5-step add-agent wizard already implements the design's - flow; now entered via the provider gallery. Connection/plugin _detail_ flows - from the mock (GitHub etc.) are aspirational with no backend — not built. - -Validation: `pnpm typecheck` clean · `pnpm lint` clean · `pnpm test` 688 pass / -1 skip (AUTH_SECRET set) · **`pnpm build` succeeds** across all settings routes. - -## 2026-05-24 — Agent merged card, data-backed deltas, plugin detail route, user-set backgrounds - -Follow-up pass completing the operator's punch-list (second design bundle -`iCVaz0otBlYcWUMr4POAdA` — byte-identical to the first). - -**Agent merged card (the centerpiece).** Replaced the flat agent-list rows on -`/settings/agents` with the design's `AgentMergedCard`: header (avatar / @key / -presence dot / N-of-capacity / description) → inline **provider · runtime · -connection · last-heartbeat** strip → capabilities + workload bar → -collapsible **Infrastructure** accordion (runtime kind / endpoint / heartbeat -read-only + "Open runtime editor →"). Real `agent.list ⨝ runtime` data; all -actions (QuickActions / View / Edit-wizard / Archive / Delete) preserved. -Agent reassign/release already existed (AgentChip → AgentPickerModal w/ Unassign, -via `issue.update({ assignedAgentId })`). - -**Data-backed deltas (added the data paths, then the UI — all query-side, no -schema change).** - -- Projects list: `project.list` now includes `initiative` + a groupBy-computed - `_count.doneIssues`; cards show a done/total progress bar + initiative chip. -- Initiatives: `initiative.list` now carries per-project `{key,name,color,done, -total}`; card renders the nested project list (cap 4 + "+N more"). -- Plans: `executionPlan.list` now includes `steps{position,status}` + - `createdBy`/`createdByAgent` + `doneSteps`; card renders a DAG step strip - (color-by-status pips + connectors, cap 12) + owner footer. -- Command Center: `liveGoals` aggregates `plans[].steps` → `doneSteps/totalSteps`; - goal card adds a step-progress bar (budget bar was already there). - -**Plugin detail route (prep for real plugin support, e.g. GitHub).** Added -`plugin.byId` (workspace-scoped, includes skills/active apiKeys/webhooks, strips -`secret`) + `remove` + `rotateSecret` (admin). New manifest-driven route -`/settings/plugins/[id]` (identity header, "what it does", approved-scopes with -shared blurbs, events, skills, webhooks/activity, danger zone with -type-to-confirm REMOVE). Extracted `src/lib/plugin-scopes.ts` -(`PLUGIN_SCOPE_HELP` from the real `PluginScope` enum) shared by list + detail; -list rows now link to the detail. - -**Backgrounds as a user setting + grid fix.** New `User.backgroundStyle` -(migration `0063`, additive nullable; "grid" default | "glow" | "dots" | -"none") wired through `user.updateAppearance` + `ME_SELECT`, the cookie bridge, -`AppearanceProvider`, and the root `` SSR stamp. New Background -section on `/settings/appearance` with live-swap + honest preview swatches. -**Grid audit fixes:** consolidated the three per-page `.forge-grid-bg` mounts -(dashboard/inbox/command-center) into ONE `.forge-page-bg` layer mounted once in -the app-shell `
` (`relative isolate`, `absolute inset-0 -z-10`). Because -`
` is the non-scrolling viewport-height flex parent, the layer always -covers the visible area (fixes command-center's "only first viewport" bug where -the grid sat on the scroll container) and the animated compositor layer is one -viewport tall instead of full-scroll-height (fixes the lag on tall pages). -`data-bg` selectors drive grid/dots/glow variants; `none` hides it. - -Sign-in confirmed already live (`(auth)/signin`, LiveStatusPanel split) — no -work needed. Connection-detail pages (#10) deferred (need real GitHub/Slack -backends). Add-agent wizard 1:1 styling (#13) + canvas-interactive backgrounds -B1/B2 remain optional follow-ups. - -Validation: applied `0063` to the local `:55432` dev stack; `pnpm typecheck` + -`pnpm lint` clean · `pnpm test` **688 pass / 1 skip** · **`pnpm build` succeeds** -(incl. new `/settings/plugins/[id]`). NOTE: deploy must run the `0063` migration. - -## 2026-05-25 — Settings left rail + granular detail parity (status glyphs, dispatch matrix, dashed +Add) - -Post-deploy review pass — the operator flagged that the settings _shell_ kept -the horizontal navbar (design wanted a left rail) and that lots of small details -didn't match. Ran a granular detail audit and closed the HIGH items. - -- **Settings shell → 244px left rail.** Replaced `SettingsNavbar` (horizontal) - with `SettingsRail` (`components/settings/settings-rail.tsx`): grouped nav with - per-item hint subtitles + admin pills, ember-tinted active row, a "Search - settings" box with `/` focus shortcut. Wired into both the workspace and - account settings shells; deleted `settings-navbar.tsx`. -- **Shared status/priority primitives** (the audit's high-leverage fix): new - `ui/status-dot.tsx` (category-aware SVG glyph: backlog hollow-dashed / - in-progress half-fill / done check / blocked diagonal / canceled line) and - `ui/priority-glyph.tsx` (`!!!`/`!!`/… with per-priority tone). Wired into - issue-list, issue-board, issue-hover-preview; de-duped the local glyph maps. -- **Dispatch rules → routing matrix.** Rewrote the page from a one-line badge - sentence to the columnar matrix (On / Name / Priority / Label / Project / - Target agent) + ember toggle switch + PriorityGlyph + bordered LabelChip + - project color square + key + GripVertical handle + kebab. Read-only - Fall-through indicator (autoDispatchMode editable would need a backend field). -- **Dashed "+ Add" affordances**: agents merged card capabilities + Providers - field in the Infrastructure accordion; issue-rail labels as inline chips + - dashed "+ Add" + a dashed Bot "Assign agent" button; sprint backlog rail in a - dashed border + Inbox icon + "drag → plan" hint. -- **Missed card deltas finished on standalone pages**: `/plans` DAG step strip + - owner footer; roadmap bars got the left-accent + interior done-so-far fill; - goals loop explainer now a slim always-on variant above the list. - -Validation: `pnpm typecheck` + `pnpm lint` clean · `pnpm test` **699 pass / 1 -skip** · **`pnpm build` succeeds**. - -## 2026-05-23 — Chat session management: delete, stop runtime run, connector-aware status rail - -Closing the gaps in chat session/thread management on top of the in-progress -connector/provider work (`chat-readiness.ts` steering banner). - -**1. Hard delete (`chat.deleteThread`).** Archive (reversible hide) stays; this -is the irreversible purge. Order: best-effort stop a live runs-backed run → -purge `chat-message` attachments (polymorphic, no FK cascade — uses -`deleteAttachment` for the MinIO object + row) → delete the `ChatThread` -(`ChatMessage` rows cascade via FK) → audit. Owner-scoped. A deleted default -thread re-creates empty on next open. Two-click confirm in the UI. - -**2. Stop the live runtime session (`chat.stopThreadRun`).** For a RUNS-engine -agent whose run is owned by a managed runtime (Hermes today; Codex -app-server / ACP later), calls `connector.stop(externalRunId)` then marks the -`AgentRun` ABANDONED via `finishRun` (mirrors agent-run `respondApproval` -reject). Best-effort on the external call so Forge can always close its mirror. -COMPLETIONS agents have no external session — the composer's Stop button owns -that. Surfaced as a "Stop run" action (only when an active runs-backed run -exists). - -**3. Connector-aware status rail.** `chat-status-rail.tsx` rewritten to -self-fetch (`agent.byId` + `chat.chatReadiness` + `threadDiagnostics`) and lead -with a **Connection** card: effective provider · engine (`runs`/`completions` -from `readiness.mode`) · managed runtime (name + kind, links to Settings → -Runtimes) · readiness chip (reaches a model / amber hint). Replaces the old -hardcoded "Hermes-backed conversation" copy that mislabeled Claude/Codex/ -local-daemon threads. Actions now: retry · stop · kick · compact · archive/ -restore · **delete**. `full` + `compact` variants. - -**4. Both surfaces.** Full Chat page (`chat-workspace.tsx`) uses the rail (folded -the standalone archive button into it). Mission Control Chat tab (`chat-tab.tsx`) -gains a collapsible compact rail toggled from the thread strip — it had no -status/connection surface before. - -**5. Mobile/tablet parity.** The right rail is `xl:block`, so sub-`xl` had no -inspector (mobile got only an archive button; tablet got nothing). Added a -right-side inspector **drawer** (`xl:hidden`) mirroring the existing left -conversations drawer — same overlay/panel classes + theme tokens — carrying the -full rail (connection · stop · kick · compact · archive/restore · delete). The -thread toolbar is now `xl:hidden` (was `md:hidden`) so tablet gets it too; the -"Conversations" button within stays `md:hidden`, and the lone archive button -became a connection/status (`SlidersHorizontal`) toggle. - -typecheck + eslint clean; chat router tests 21 pass (5 new: delete cascade + -attachment purge + audit, default re-create, owner-scope forbid, stop no-op/ -cross-agent), chat-readiness unit tests 6 pass. - -## 2026-05-22 — Dispatch /events live, native commands, agent-config parity - -**1. Dispatch runs consume `/events` live (migration 0057: `AgentRun.pendingApproval`).** -The worker now keeps a live SSE subscription per active connector-driven run -(tracked in-process, re-established by the 5s sweep → restart-safe) _alongside_ -the status poll. The poll still owns lifecycle (terminal/usage/awaiting flag); -the subscription enriches the timeline with per-tool + thinking steps and -**captures the exact `approval.request` command** (poll status can't see it). -RunRow's approval banner now shows the command + risk. respondApproval + -poll + subscription all clear `pendingApproval` (Prisma.DbNull). - -**2. Native Hermes command passthrough.** `ai.hermesInfo` proxies the gateway's -real `/api/skills`, `/api/memory`, `/health/detailed` (server-side, token never -leaves the server; agent profileKey forwarded as the memory-scope session key). -New chat commands `/skills`, `/memory`, and `/hermes status` return **live** -data; `/hermes usage` stays prose (no gateway API). Connector also handles the -9th event type `approval.responded` (→ `approval_resolved`). - -**3. Agent config/onboarding parity.** Fixed: the wizard's "Chat engine" -selector was ignored on **create** (only `update` carried `runEngine`). Added -`runEngine` to the create input + mutation + create payload. Integration cards -(`/settings/integrations`) now show each integration's default engine -(runs/completions) next to the runtime badge. Runtime detection (forge CLI -daemon) is orthogonal and unaffected — RUNS agents are driven by the worker via -`/v1/runs`, not the local daemon. - -typecheck + eslint clean; orchestration/chat/dispatch/run-stale tests green. - -## 2026-05-22 — Dispatch approval UI + slash-command enhancements - -**Dispatch permission blocks (the deferred piece).** Migration 0056 adds -`AgentRun.awaitingApprovalAt`. The runs-dispatch poller flips it on when a -run reports `waiting_for_approval` (BLOCKED event, once) and clears it when -the run resumes or finishes; the stale watchdog now skips runs with it set -(intentionally idle, not dead). New `agentRun.respondApproval({ runId, -decision })` tRPC: approve → `connector.approve(once)` + resume; reject → -`connector.stop()` + `finishRun(ABANDONED)`. RunRow shows an amber "needs -permission to run a command" banner with Approve/Reject when the flag is set. -(Won't fire for agents on `approvals.mode: off`, but the path is complete.) - -**Slash commands.** Added `args` usage-hint metadata to `SlashCommand` (shown -in the autocomplete + `/help`; accepting an arg-taking command fills the stub). -New commands: **`/engine [completions|runs]`** (show or switch this agent's -chat engine — wired via `agent.update` through a new `setEngine`/`currentEngine` -slash-context capability) and **`/assign `** (ask the agent to take an -issue). Added arg hints to `/issue`, `/hermes`. Docs: engines.md gained an -"Permission blocks (approvals)" section; agents/chat.md slash table updated. - -typecheck + eslint clean; chat/dispatcher/run-stale tests (27) green; docs build clean. - -## 2026-05-22 — Dispatch approval UI + slash-command enhancements - -**Dispatch approvals (the deferred Phase-2 follow-up).** A connector-driven -run that pauses for operator permission is now actionable from Mission Control. - -- Migration 0056: `AgentRun.awaitingApprovalAt`. -- `run-dispatcher` poll: on `waiting_for_approval` sets the flag + a one-shot - BLOCKED event (keeps the run ACTIVE so it stays in the Live tab); on resume - clears it; terminal always clears it. -- Stale watchdog skips `awaitingApprovalAt` runs (a blocked run is - intentionally idle, not dead). -- `agentRun.respondApproval({ runId, decision })`: approve → connector - `approve('once')` + resume; reject → connector `stop()` + finishRun ABANDONED. -- RunRow shows an amber "needs permission" banner with Approve / Reject. -- engines.md gained a "Permission blocks (approvals)" section (chat + dispatch). - -**Slash commands.** Added an `args` usage-hint field (rendered in the `/` -autocomplete + `/help`), and new commands: - -- `/engine [completions|runs]` — show or switch this agent's chat engine - (ties into the engine work; admin-only switch, errors surfaced inline). -- `/assign ` — ask the agent to take an issue. -- Existing commands tagged with arg hints. -- chat-thread wires `currentEngine` + `setEngine` (via `agent.update`) into the - slash context. - -typecheck + eslint clean; chat/dispatcher/run-stale/orchestration tests green. - -## 2026-05-22 — Hermes default→runs, approval semantics, FREE_FORM answers - -Follow-on to the engine work, after researching the Hermes gateway approval -model (approvals gate dangerous _shell commands_: payload `{command, -description, choices: once|session|always|deny}`, per-session FIFO; a bare -**deny leaves the run blocked → must `/stop`**; `reasoning.available` carries -real thinking; tool args/results aren't streamed). - -- **Hermes integration now defaults to RUNS** (`adapters.ts`). Chatting with - Victor/Mizu talks to _that agent_ (own memory + tools), and dispatch uses - runs too. Per-agent override unchanged; flip to Completions for a stateless - Forge-owned loop. engines.md updated to reflect the new default. -- **Chat permission blocks fixed** (`/api/chat/stream` RUNS path): the - approval card is titled with the actual command (+ risk description in the - args); **Approve → allow once; Decline → `/stop`** (a bare deny would hang - the run per gateway semantics). -- **FREE_FORM asks deliver the answer.** A FREE*FORM ActionRequest is the - agent asking \_us* for info — bare "Accept" resolved it but delivered - nothing. Command Center now shows **Respond** (textarea) for FREE_FORM asks; - on accept-with-answer, `acceptActionRequest` posts a comment `@agent -` on the issue, routing through the normal mention dispatch (+ inbox - row for runs agents). Bound kinds (TRANSITION/ASSIGN/…) keep "Accept". - -Streaming/thinking/tool rendering confirmed intact on the runs chat path -(message.delta → content, reasoning.available → thinking, tool.started/ -completed → tool cards). - -Deferred: a Mission Control approve/reject UI for _autonomous dispatch_ runs -that hit an approval (currently shown as a "waiting for approval" step). Not -urgent — the operator's agents run `approvals.mode: off`, and the interactive -chat path already handles human-in-the-loop approvals. - -typecheck + eslint clean; orchestration/chat tests (26) green; docs build clean. - -## 2026-05-22 — Hermes /v1/runs Phase 2: dispatch ingestion + docs/UX - -Dispatch (assigned work) now runs through the `/v1/runs` connector for -RUNS-engine agents, ingested by the worker. - -- **Connector.getStatus** added (`RunStatus`) + `HermesRunsConnector.getStatus` - (GET `/v1/runs/{id}`, maps status/last_event/output/usage; 404 → completed). -- **`run-dispatcher.ts`** (poll-based, restart-safe): - `startNewRuns` opens an `AgentRun` for fresh RUNS-engine AGENT_ASSIGNED - events (deduped by `assignmentEventId`) and `startRun`s the provider run; - `pollActiveRuns` polls `getStatus` for ACTIVE runs with an `externalRunId` - and mirrors progress (currentStep from last_event, BLOCKED on - waiting_for_approval) / terminal `finishRun` (+ token usage) onto the run. - Chose polling over a live SSE subscription so it fits the worker's - short-job model and survives restarts with no in-memory state. -- **Worker:** `runs-dispatch-sweep` maintenance job every 5s → `ingestRunsDispatch()`. -- **audit.ts branch (a):** suppress the dispatch webhook for RUNS-engine - assignees (so work isn't dispatched twice — they're driven by runs). A RUNS - agent should not also carry a `webhookUrl`. -- **Docs:** new `docs/agents/engines.md` (Completions vs Runs — table, when to - use which, pros/cons, ownership, dispatch behaviour), linked from the agents - sidebar + a tip in `agents/chat.md`. CLAUDE.md gained an "Agent execution - engine" section. -- **UX:** chat header shows a small ember **"runs"** pill when the agent is on - the RUNS engine (the non-default "runs as itself" mode); completions stays - unbadged to avoid clutter. - -typecheck + eslint clean; dispatcher/inbox/chat/orchestration/analytics-dispatch/ -run-stale tests (51) green; vitepress build clean. - -Recommendation captured in docs: **standard consumer chat = Completions** -(fast, predictable, Forge-controlled); **Runs = opt-in** for agent memory + -native tools. Dispatch defaults to Runs. - -## 2026-05-22 — Pluggable agent engine + Hermes /v1/runs (Phase 1: chat) - -Groundwork for routing agents through Hermes' structured agent-run API -(`/v1/runs`) instead of only OpenAI-compat `/v1/chat/completions`, behind a -provider-agnostic abstraction. - -- **Schema (migration 0055):** `RunEngine` enum (COMPLETIONS|RUNS), - nullable `Agent.runEngine` (null = integration default), `AgentRun.externalRunId`. -- **Pluggable `DispatchConnector`** (`src/server/services/dispatch/`): normalised - `RunEvent` union (content_delta/thinking/tool_started/tool_completed/ - approval_required/usage/completed/error) + `startRun`/`subscribe`/`approve`/ - `stop`. `HermesRunsConnector` implements it against POST `/v1/runs`, - GET `/v1/runs/{id}/events` (SSE), `/approval`, `/stop`. `registry.ts` - resolves the per-agent engine (`agent.runEngine ?? adapter.defaultRunEngine`) - and returns the connector by provider. Other providers slot in without - touching call sites. -- **Chat route** (`/api/chat/stream`): per-agent toggle. RUNS path delegates the - turn to the connector and maps its events onto the SAME SSE vocab the client - already speaks — so chat still streams token-by-token (via `message.delta`), - tools render as cards, and `approval_required` surfaces as a tool_confirm - whose response is POSTed back to the run. Stop aborts the live run. - COMPLETIONS path unchanged (Forge owns the loop). Default stays COMPLETIONS - (zero behaviour change until an agent is flipped). -- **UI:** integration `defaultRunEngine` (adapters.ts) + per-agent "Chat engine" - selector in the agent wizard (Integration default / Completions / Hermes runs); - `agent.update` accepts `runEngine`. - -**Who owns what:** Forge always owns the ChatThread record + UI. COMPLETIONS → -Forge owns the loop + tools + approvals (stateless model). RUNS → Hermes owns -the loop + agent memory/persona/its own tools; Forge ingests events. - -**Phase 2 (not yet built):** dispatch/assigned-work via `/v1/runs` — reuse the -same connector but ingest events into `AgentRun` + Mission Control from the -background **worker** (long-lived subscription + reconnect), webhook fallback -for completions/legacy agents. Deferred so the worker + dispatcher tests get a -deliberate pass rather than a rushed one. - -## 2026-05-21 — Canvas: navigation, lock, collapse + component-instance delete - -Round of canvas usability fixes (operator feedback "hard to move around"). - -- **Middle-mouse pan** anywhere (over cards/shapes too) — `onBackgroundMouseDown` - handles `button === 1` with `preventDefault` to kill the autoscroll puck; - shared `startPan()` helper. -- **Inline scroll beats global scroll** — `onWheel` walks ancestors from the - wheel target to the surface; if a scroll container can still scroll in that - direction it returns early (native scroll) instead of panning/zooming. -- **Lock canvas** (topbar toggle, persisted per canvas in localStorage). When - locked: middle/left-drag pans, but move/resize/draw/delete/paste/drop and - the inspector delete are all gated via `lockedRef`. Pan/zoom/select stay live. -- **Collapsible toolbar** — `canvas-toolbar.tsx` gained a collapse chevron + - a compact "show toolbar" pill; collapsed state persists per workspace. -- **Left entity rail hidden by default**, open state persisted per workspace. -- **Right panel shows a vertical "Layers · Components" title when collapsed** - (was a bare chevron — easy to forget it's there). -- **Component-instance delete bug fixed.** Placed instances were selectable but - Delete/Backspace + context menu only handled shapes/frames/edges, so they - couldn't be removed. Added router `canvas.instanceRemove` (deletes the - `CanvasComponentInstance` row; definition untouched) and wired it into the - Delete key, the selection-inspector delete, and a right-click menu - ("Detach into shapes" via existing `instanceDetach`, or "Remove from - canvas"). typecheck + eslint clean; canvas router tests (24) green. - -## 2026-05-21 — Design-system follow-ups: motion toggle, docs, themed tooltips - -Built on the M1–M10 motion work below. - -- **Motion preference (Appearance → Motion).** New persisted user pref - `motion` (`full` | `reduced`), mirroring `density`/`textSize`. Prisma - column + migration `0054_user_motion_pref`, `ME_SELECT` + `updateAppearance` - input, `appearance.ts` + `appearance-cookie.ts` (`AppearanceMotion`, - default `full`), root layout stamps `data-motion` from the pref at SSR, - `AppearanceProvider` keeps it in sync, and a new **Motion** section on the - Appearance page (Full | Reduced choice cards with a live breathing-dot - preview). `reduced` → `data-motion="off"` freezes the whole `forge-*` - layer to static (independent of OS reduced-motion, which still also gates - it). **Apply the migration** (`pnpm prisma migrate deploy`) in each env. -- **`docs/design-system/`.** README + `tokens.md` (every var, light/dark), - `components.md` (the `ui/*` primitives), `principles.md` (10 rules + - allowed/refused), `motion.md` (M1–M10 table + watch-items). Code stays - canonical; docs describe it. -- **Themed tooltips app-wide — no browser tooltips.** Repo had no Tooltip - primitive and no Radix, with **705 `title=` occurrences across 154 files** - — too many to hand-edit. Built a global `NativeTooltips` delegate (mounted - in the root layout) that intercepts every `title` on hover/focus: stashes - - removes the attribute (suppressing the native popup), renders a - token-styled tooltip, and restores `title` at rest for a11y. Net effect: - every existing `title` is themed with zero call-site changes, and no - native tooltips remain. Added a thin `` wrapper - (`ui/tooltip.tsx`) for explicit use (sets `title`, routed through the same - delegate). Fade-in is `motion-safe`; the tooltip itself always works. - -Integration audit: each forge-\* class/hook verified at its intended surface -(M1 dashboard, M4 issue-list, M5 chat, M6 dashboard counts, M7 step-node, -M8 sidebar, M9 section divider, M10 presence dot); M2/M3 remain classes-only -(deferred — don't stack ambient backgrounds). `pnpm typecheck` + `pnpm lint` -clean; `vitest run tests/unit` 161 passed (same 3 pre-existing `server-only` -failures, unrelated). Not yet applied: M4 on the Mission Control swimlane -(scoped to the issue list — staggering kanban cards reads worse); broaden if -wanted. - -## 2026-05-21 — Apply the Claude Design spec: ambient motion M1–M10 - -Implemented the Claude Design handoff (`Forge Primitives Canvas`) as a -**design spec applied to the app** — not a new route/canvas tool. Plan: -`docs/plans/primitives-canvas-design-system.md`. - -- **Token audit (no-op).** `forge-tokens.css` matches `globals.css` - verbatim except `--font-sans` ordering (app keeps `ui-sans-serif` - first — intentional) and two class-scoped `--grid-*` vars (not tokens). - `globals.css` stays canonical; no token edits. -- **Primitive conformance (no changes).** Button (6 variants × sm/default/ - lg/icon) and EmptyState (page/section/card) already match the spec. The - real `Badge` uses a dynamic `color` prop (DB label colors) instead of the - spec's named tones — deliberate, left as-is. -- **Motion foundation.** Added a `/* Motion — forge-* */` block to - `globals.css` with M1–M10 classes + keyframes, double-gated on - `prefers-reduced-motion: no-preference` **and** `[data-motion="on"]`, - each with a static fallback. Registered matching keyframes in - `tailwind.config.ts` (so `animate-forge-*` utilities exist too). New - `useCountUp` hook in `src/lib/` (IntersectionObserver + rAF, reduced- - motion aware, once-per-mount). `data-motion="on"` stamped on `` in - the root layout (SSR, no flash). Vitest guard - (`tests/unit/globals-keyframe-prefix.test.ts`) enforces `forge-`/`ui-`/ - `dag-` prefixes on new keyframes (no stylelint in the repo). -- **Wired into real surfaces:** M1 grid drift behind the dashboard (40% - opacity); M4 staggered row-rise on `IssueList` (initial mount only, via - a ref — won't re-stagger on refetch); M5 streaming ember sweep on agent - draft bubbles in `chat-message.tsx` (dropped on finalize so text stays - selectable); M6 count-up on the dashboard "By status" counts; M7 - active-node ember glow baked into orchestration `StepNode` (running - node only, replacing the generic `animate-pulse` ring); M8 ember caret - on the sidebar "Search or jump" omnibar trigger; M9 `SectionDivider` - hairline-sweep primitive applied between Appearance settings sections; - M10 ONLINE-only "breath" on `AgentPresenceDot` (replaces `animate-ping`). - **M2 (aurora) / M3 (dot drift) shipped as classes only** — deferred in - product per the handoff (don't stack ambient backgrounds). -- **Tweaked the spec's M4 fallback:** the spec's `.forge-row-rise{opacity:0}` - base would leave rows invisible under reduced-motion; reworked so rows - are visible by default and only start hidden when the animation will run. - -Verification: `pnpm typecheck` + `pnpm lint` clean; `vitest run tests/unit` -161 passed incl. the keyframe guard. (3 pre-existing failures in -`rate-limit`/auth unit tests are a vitest `server-only` resolution gap, -unrelated to this diff.) Optional follow-ups left out of scope: a -"Motion: Full/Reduced" toggle in `/settings/appearance` that flips -`data-motion`, and the separate `docs/design-system/` docs site. - -## 2026-05-21 — Mission Control chat: multiple conversations per agent - -The chat tab only ever opened each agent's _default_ thread and had no -way to start another — even though the backend already supports many -threads per agent (`chat.createConversation`) and `ChatThreadView` -already accepted a `threadId`. Added a thread strip above the -conversation (Main + named conversations for the selected agent) and a -"+ New chat" button that creates a fresh conversation and switches to -it. Switching agents resets to that agent's default thread. UI-only -(`chat-tab.tsx`); eslint clean, no type errors. - -Also (ops, no repo change): fixed Hermes agents replying "401 Invalid -API key" — Forge's `HERMES_GATEWAY_TOKEN` (`~/docker/forge/.env`) didn't -match the gateway's `API_SERVER_KEY` (the `:8642/v1` inbound auth in -`~/.hermes/.env`). Set them equal, recreated `forge`+`forge-worker`, -verified 200 on `/v1/models` + a `claude-haiku` completion. Documented -in `~/SYSTEM.md`. - -## 2026-05-21 — Agent-as-actor attribution in audit + activity - -- **Problem.** Agent-performed actions (close/create/transition, - slash commands, comments) attributed to the human key-owner (Bailey) - because `AuditLog` / `ActivityEvent` only had `actorId` (User FK). -- **Model (operator-confirmed): agent is the actor.** When an action - comes through an agent-linked API key, the Agent is the recorded - actor (avatar + name + indigo "agent" chip); the human key-owner stays - as secondary metadata (`actorId`) surfaced in a `via API key owned by -{name}` tooltip. No "on behalf of" primary text. -- **Migration `0053_actor_agent`.** Added nullable `actorAgentId` - (Agent FK, `onDelete: SetNull`) to `AuditLog` AND `ActivityEvent`, - with back-relations on `Agent` (`actorAuditLogs`, - `actorActivityEvents`). Indexes: `AuditLog @@index([workspaceId, -actorAgentId])`, `ActivityEvent @@index([workspaceId, actorAgentId, -createdAt])`. Applied cleanly via `migrate deploy`; client regenerated. -- **`recordChange`** (`src/server/audit.ts`) gained optional - `actorAgentId?: string | null` (default null), written to both - `auditLog.create` and `activityEvent.create`. No other behavior change. -- **Call sites (grep-driven sweep).** Threaded `actorAgentId: -ctx.apiKey?.linkedAgentId ?? null` (or the already-computed agent var) - through every reachable `recordChange`: `issue.ts` (20), `comment.ts` - (create/update/upsertStatus), `mcp.ts` (49 remaining — primary agent - path), plus full sweep of agent-reachable routers (canvas 31, - execution-plan, artifact, context-set, attachment, agent-crew, chat - message-post paths, agent, project, workspace, initiative, cycle, - relation, timeEntry, note, ai, agent-run). Services that already - receive `actorAgentId` as a param (artifact-, action-request-, - execution-plan-, context-set-, agent-crew-service) now forward it to - their `recordChange`. Pure-system events (dispatcher, heartbeat, - sla-breach, recurring, stale-work, orchestration, ai-coach, etc.) and - service fns without an `actorAgentId` param stay null (the actor is - the system, not an agent). -- **Activity query + UI.** `issue.activity` and `admin.audit` now - include `actorAgent { id, name, profileKey, avatar }`. The issue - Activity panel and the admin Audit tab render the agent as actor - (`AgentAvatar` + name + indigo `agent` chip + owner tooltip) when - `actorAgent` is set, else unchanged (user actor). Skipped: dashboard - "Recent activity" column and admin Events tab — both render only - kind + time, no actor, so nothing to attribute. -- **Tests.** Extended `comment.test.ts`: agent-key comment records - `actorAgentId = agent.id` + `actorId = human` on both audit + activity - rows; human session leaves `actorAgentId` null. Full audit/issue/ - comment/mcp/action-request/artifact/canvas/chat suites green. - -## 2026-05-21 — Dashboard is the consistent home + sticky agent panel on issues - -- **Home page made consistent.** The workspace root already redirected - to `/dashboard`, but the global root (`src/app/page.tsx`, post-login / - last-workspace) sent users to `/inbox` — and the dashboard topbar - called Inbox "the daily driver." Flipped both `page.tsx` redirects to - `/dashboard`, fixed the "unified landing" comment, and reframed the - dashboard's "Back to Inbox / daily driver" link to a plain "Inbox →" - (the action queue, `g i`). Dashboard is now unambiguously home. -- **Sticky agent-status panel on issues** (`issue-agent-panel.tsx`, - new). Lives at the top of the issue right rail (which is already - sticky), so the assigned agent's presence dot + live run state - (working / waiting on you) + current step stay visible while a long - comment thread scrolls — the top-of-page `AgentRunStrip` scrolls away. - Reads `agentRun.activeForIssue` + the issue's `assignedAgent`, refreshes - over SSE, self-hides when there's no agent/run. Read-only (kick/resume - are MCP-only, no tRPC mutation). - -## 2026-05-21 — Command Center: realtime + inline decisions + de-overlap from Inbox - -Command Center was a fetch-once, read-only summary that only deep-linked -out, overlapping conceptually with the Inbox. Made it a live decision -surface and gave both pages distinct identities. - -- **Realtime.** Wired `useRealtime()` into the command-center page (same - pattern as `agent-run-strip` / Mission Control — direct - `utils.invalidate()`, no debounce). Invalidates - `commandCenter.summary` + `commandCenter.decisionsCount` when an event - arrives whose `subjectType` is `action-request`, `review-gate`, - `agent-run`, or `goal`, or whose kind is in the `AGENT_RUN_*` family - or `GOAL_CREATED` / `GOAL_STATUS_CHANGED`. Action-request and - review-gate resolutions surface as `ISSUE_UPDATED` with a - distinguishing `subjectType`, so keying off `subjectType` is what - catches "ask resolved / gate resolved elsewhere." Broad but scoped — - unrelated `ISSUE_*` edits are ignored. -- **Inline decisions.** Action-request "Asks for you" cards now - accept/decline inline via `actionRequest.accept` / `.decline` (the - same mutations the issue-timeline `ActionRequestCard` uses; reused the - mutation contract, not the comment-bound component, since CC has raw - `actionRequest` rows rather than a `commentId`). Review-gate cards - resolve inline (Approve / Reject + optional note) via - `reviewGate.resolve` — that's an `adminProcedure`, so the inline - affordance only shows for OWNER/ADMIN; everyone else still gets the - deep link to the target. Both do an optimistic drop of the acted item - from the cached summary for instant feedback; `onSettled` invalidates - to reconcile (and the realtime sub catches the server-side event too). - Other cards (goals, runs, due, artifacts, timer) keep deep-linking. -- **De-overlap decision.** Investigated the overlap and it was already - minimal: the Inbox surfaces _your work_ (assigned/unblocked, mentions, - waiting-on-me, human/agent-stalled, watching, sprint burn, agent - queue) and **never surfaced action requests or review gates** as a - decision affordance. The old CC "ask" card even deep-linked to - `inbox?actionRequest=`, a param the Inbox does not consume — a - dead link. Decision: **Command Center is the canonical place to act on - decisions** (action requests + review gates); the Inbox stays "your - work" and keeps all its buckets untouched. Sharpened both subtitles to - read as complementary — CC: "Decisions & live agent ops"; Inbox: "Your - work — assignments, mentions, stalled, watching." Replaced the dead - inbox deep-link on the ask card's title with a link to the related - issue (a real destination) when one exists. No Inbox functionality - removed. -- **No regressions.** `commandCenter` router untouched, so the sidebar / - dashboard `decisionsCount` badge is unchanged (and now refreshes in - realtime via the added invalidation). Mission Control untouched. - Validated: `pnpm typecheck`, eslint on both touched files, and the - action-request + inbox vitest suites (18 passing). - -## 2026-05-21 — Immediate agent feedback on issue comments + status near the composer - -Operator feedback: commenting to trigger an agent gave no immediate UI -signal, the run banner kept showing "waiting on you" after a reply, and -on long threads the top-of-page run strip scrolls out of view. - -- **Instant "waiting → working" on reply.** The server already - auto-resumes a WAITING run to ACTIVE in the same transaction as the - comment (`openOrTouchRun`), but the strip only refreshed via SSE/ - refetch (~5–15s). `comment.create.onSuccess` now optimistically patches - the `agentRun.activeForIssue` cache (WAITING → ACTIVE, bump - `lastEventAt`) and invalidates it, so the banner flips the moment you - send and a freshly-dispatched run surfaces fast. -- **Status where you type.** Render `` directly above the - comment composer (in addition to the top-of-page instance). Same query - key, so it's free and live; self-hides when no run. On a long thread - the top strip is scrolled away — the composer-adjacent one keeps - "working… / waiting on you" in view right where the operator replies. -- Validated: `pnpm typecheck` + `eslint` clean. - -Follow-up considered, not built: a persistent agent-status panel in the -(already sticky) right rail — researched (reuse `AgentAvatar` / -`AgentPresenceDot` / run-status), deferred pending operator preference vs -the near-composer strip. - -## 2026-05-21 — @mention + / coexistence: chainable in one comment, edit-mode dispatch - -Operator bug: "after I @ an agent in a comment, I can't use a / command," -plus "when editing, allow agent triggering properly." - -**Root cause.** The slash picker (`slash-autocomplete.tsx`) only opened -when the caret line lived in a _top-of-body command block_ — every -preceding non-blank line had to start with `/`. Typing an @mention as -prose on line 0 made any later `/` line "not top-of-body", so the picker -never opened. The same gate lived in `parseSlashCommands` -(`lib/slash-commands.ts`): it stopped extracting at the first -non-command line, so a `/assign @victor` line under prose was never -applied. The two were never mutually exclusive either — both could open -at the caret. - -**Interaction model chosen.** Slash commands are recognised when `/` -begins ANY line (after optional whitespace) outside a fenced code block; -@mentions work anywhere inline. The two dropdowns are mutually exclusive -at the caret: `MentionInput` now emits `onMentionOpenChange`, and the -slash hook takes a `suppressed` flag so only one dropdown is ever open -and owns Arrow/Enter/Tab/Esc. Picking a slash command after an @mention -on a new line now opens the picker as expected. - -**Parser.** `parseSlashCommands` rewritten to scan all top-level lines -and pull out RECOGNISED command lines wherever they sit, keeping prose -(and unknown `/foo`) verbatim and squashing the blank-line damage left -by removed lines. Conservative: only whole lines starting with `/` whose -keyword+arg parse are taken, so "and/or" and "https://…" are never -eaten. Fenced code blocks are tracked and preserved. Updated the -"stops at first non-command line" unit test (that encoded the OLD, -now-fixed behaviour) and added chained-@+/ and mid-line-slash cases. - -**Edit mode (new UI).** There was no comment-edit UI at all. Added an -inline `CommentEditor` (BODY comments only) reusing `MentionInput` + -the slash picker with the same suppression guard. On save it applies -slash-command lines via `issue.applyCommands` and persists the prose via -`comment.update`. The issue DESCRIPTION editor got the same slash -support + apply-on-save. - -**Edit-time agent dispatch.** `comment.update` previously emitted NO -event, so editing in an @agent triggered nothing. It now diffs old→new -mention tokens (`extractMentions`), resolves only the ADDED agents/users, -auto-watches them, and emits `COMMENT_UPDATED` with `edited: true` and a -`mentions.agentIds` carrying ONLY the diff. `audit.ts` branch (c) now -also fires on `COMMENT_UPDATED` _when `edited === true`_, so newly-added -mentions dispatch exactly like a fresh comment while pre-existing -mentions stay quiet (idempotent typo-fix). Rolling STATUS upserts also -emit COMMENT_UPDATED but without `edited`, so they never re-page. Branch -(e) watcher fan-out is skipped for `edited` events so a typo fix doesn't -re-page every stakeholder. Execution-step comments (`issueId` null) take -a plain update with no fan-out. - -**Files.** `src/components/slash-autocomplete.tsx` (any-line trigger + -`suppressed` + fenced-block guard); `src/lib/slash-commands.ts` (parser - -- hint copy); `src/components/inputs/mention-input.tsx` - (`onMentionOpenChange`); `src/components/issue-detail/issue-main.tsx` - (comment composer suppression, `CommentEditor`, description slash - support, parser-driven hint); `src/server/routers/comment.ts` - (update mention diff + event); `src/server/audit.ts` (branch c on edited - COMMENT_UPDATED, branch e skip for edits); tests in - `tests/unit/slash-commands.test.ts` and - `src/server/routers/__tests__/comment.test.ts`. - -**Validation.** `pnpm typecheck` clean; `pnpm lint` clean; -`vitest run` unit suite 178/178 + slash/templates 35; comment router -integration 10/10 (incl. 2 new edit-dispatch tests) + comment-history -7 + quick-reply 6, all green against dev Postgres+Redis. QuickCreate and -the Mission Control chat composer (home-grown, doesn't use MentionInput) -untouched. - -## 2026-05-21 — Dashboard enhancements: customizable widgets, resume tile, unseen badge, context CTAs, canvas round-trip - -Five operator-requested dashboard ideas. Migration `0052_dashboard_prefs` -(two nullable `User` columns) — applied via `migrate deploy` (status was -clean, no drift); client regenerated. - -- **Migration**: `User.dashboardPrefs` (Json) + `User.changelogSeenAt` - (DateTime?). Added to `ME_SELECT`. New user-router mutations - `setDashboardPrefs` (zod `{ order, collapsed, hidden }`) and - `markChangelogSeen`. -- **Customizable widgets** (`dashboard-stack.tsx`, new): the dashboard's - movable tiles (today, resume, agent-activity, ideas, quick-notes, - standup, whats-new) now render through `DashboardStack`. A "Customize" - topbar toggle reveals per-widget drag handles (HTML5 DnD, no dep) + - hide buttons and a hidden-widgets tray; Reset clears. Order/hidden - persist via `setDashboardPrefs`, seeded once from `user.me`. Widgets - own their card chrome, so edit mode adds a dashed control strip above - each; non-editing renders pristine and empty widgets collapse via - `empty:hidden` (no gaps). New widget ids append at the end of a saved - order, so future widgets need no prefs migration. **Scope note:** - collapse was dropped from v1 (the existing widgets render their own - chrome with no external header to collapse into) — delivered reorder + - hide instead. Fixed anchors (greeting, needs-you, onboarding, focus/ - suggestions, the bottom 3-col grid) stay put; the movable region sits - between focus and the grid (this moved today/ideas/etc. below focus — - a deliberate hierarchy bump, and now user-reorderable anyway). -- **Resume tile** (`resume-tile.tsx`, new): `recentItem.list` → - `RecentItemsRail`. Per-user recency (distinct from the workspace-wide - "Recent issues" column). Null when empty. -- **Unseen What's New dot**: `WhatsNewTile` takes `seenAt`; shows an - ember dot when the newest dated changelog entry is newer than - `changelogSeenAt`. `/whats-new` stamps `markChangelogSeen` on mount + - invalidates `user.me` so the dot clears. -- **Context-aware greeting**: "Browse templates" → "New project" once - projects exist; "Invite member" only for admins/owners. -- **Canvas round-trip**: personal canvases (`kind === "PERSONAL"`) get a - "Dashboard" topbar button that persists view=list and navigates back. - -Validated: `pnpm typecheck` + `eslint` clean; `changelog-parser` + -`sidebar-nav` unit tests pass. - -## 2026-05-21 — Dashboard tie-together: templates link, Personal dedup, CHANGELOG refresh - -Three operator-flagged dashboard fixes. No schema changes. - -- **"Browse templates" now opens the templates dialog.** The - GreetingBar button linked to `/projects` (bare list), where starter - templates only render in the zero-projects empty state — so with - projects present, the button led nowhere useful. Now links to - `/projects?templates=1`; the projects page grew a `?templates` - effect (mirroring the existing `?new` handler) that opens the - already-present `StarterTemplates` dialog regardless of project count. -- **Removed the duplicate "Personal" sidebar item.** It redirected to - the user's personal canvas — the same destination as the Dashboard's - List/Canvas view toggle, which the operator prefers. Dropped the nav - entry (`sidebar-nav.ts`, freed chord `g e`, removed orphaned `Home` - icon import) and the breadcrumb `SectionId` / label entries. The - `/personal` route stays as a harmless working redirect (still - auto-provisions the canvas); the Dashboard toggle uses - `user.personalCanvas` and is untouched. -- **Refreshed the stale What's New.** `CHANGELOG.md`'s latest entry was - 2026-05-04 (17 days stale). Added dated entries for 05-18 (Chat - surface), 05-19 (chat streaming, confirm modals, canvas previews), - 05-20 (orchestration loop, on-canvas authoring, crews), and 05-21 - (canvas motion overhaul, issues-page polish). The What's New rail - reads this file (mtime-cached), so it picks up automatically. - -Validated: `pnpm typecheck` + `eslint` clean; `sidebar-nav` unit test -7/7 (chord uniqueness intact after the removal). - -## 2026-05-21 — Issues-page polish: agent profile icons, composer discoverability, hover previews, snooze chips, debounced search - -Tying together the recent UI/UX work. No schema changes; all client/render. - -- **Agent replies show their real profile icon.** `CommentAvatar` - (`issue-detail/issue-main.tsx`) was hardcoded to a generic `` - glyph and explicitly forced `image` to null for agents — throwing - away `authoringAgent.avatar`. It now renders the shared `AgentAvatar` - (emoji / image / profileKey monogram), matching Mission Control, the - agent picker, and crews. Applied to both the timeline card and the - live-status pin. -- **Comment composer advertises @ and /.** The features already - existed (`MentionInput` + `useSlashAutocomplete`) but nothing told - users. New placeholder (`@ to mention · / for commands · paste or -drop to attach`) plus a persistent `@ mention · / commands · ⌘↵ send` - hint under the box — previously the hint only appeared _after_ you'd - typed a `/`. -- **Chat composer parity.** Did NOT swap `MentionInput` into the - Mission Control chat composer: its dropdown anchors below the caret - with no flip-up logic, which would render off-screen in the - bottom-docked chat (which renders popovers upward); chat also has - auto-resize, Enter-to-send, file-context toggles, and a chat-specific - slash set. Instead added the same adaptive `@ / ↵` hint (gated to - what's actually wired up, shown only while the composer is empty) so - the _experience_ matches. True component-level dedup would need - placement-aware dropdown support in `MentionInput` — noted as - follow-up. -- **Issue list: hover previews** — wired the existing - `IssueHoverPreview` (350ms-delay portal, `issue.summary` fetch) onto - each row's title. -- **Issue list: snooze chips** — rows whose `snoozedUntil` is in the - future now show a `CalendarClock` "Snoozed" chip (with exact - until-date tooltip). Snoozed issues already appear in the default - list (`excludeSnoozed` defaults false), so the state was previously - invisible. -- **Issues search debounced** — the list-view search now debounces - 300ms before hitting `issue.list` (was firing per keystroke) and - shows a spinner in the input while a search is settling. - -Validated: `pnpm typecheck` + `eslint` clean on all touched files. No -unit/integration tests cover these client components; e2e chat specs -don't reference the touched selectors. - -## 2026-05-21 — Canvas: Excalidraw-grade motion, images, present mode, virtualization, sketch, undo - -Six-part pass to close the UX gap vs Excalidraw. No schema migration — -images reuse the attachment system; everything else is client/render. - -- **Motion (`canvas-camera.ts`, new)**: pure easing/lerp/fit helpers - (`easeOutCubic`, `lerpViewport`, `computeFitViewport`, - `prefersReducedMotion`). Page now eases all camera jumps via - `animateViewportTo` (fit / fit-selection / reset / present), adds - inertial-pan momentum (velocity sampled on drag, friction decay), and - `RemoteCursorsLayer` lerps peer cursors toward their 10Hz targets so - they glide instead of stepping. `will-change: transform` on the - pan/zoom container. Honors `prefers-reduced-motion`. Unit tests in - `tests/unit/canvas-camera.test.ts` (10). -- **Images**: new `canvas` attachment targetType (storage allowlist + - `assertTargetInWorkspace`). `image` shape kind; style holds - `attachmentId`, and `canvas.hydrate` resolves it to a fresh presigned - `src` (15-min TTL, refreshes on refetch). Paste / drag-drop / toolbar - picker all funnel through `uploadImageAt` → standard initUpload→PUT→ - finalize. -- **Present mode (`canvas-presentation.tsx`, new)**: frames become slides - (reading order), eased fit-to-frame per slide, laser pointer with - fading trail, slide HUD, arrow/space/Esc/Home/End nav. "Present" - button in the topbar (disabled with 0 frames). -- **Reconciliation + virtualization**: remote-event hydrate invalidation - is now coalesced (≤1 refetch / 220ms) instead of one-per-event, so peer - edits / bulk agent adds don't flash. Shape render list culls to the - visible viewport (+1-screen margin) above 200 shapes; path shapes - (freehand/line/arrow) never culled; hit-test/fit/inspector keep the - full set. (True element-level diffing still needs richer event - payloads — left as follow-up.) -- **Drawing polish (`canvas-rough.ts`, new — adds `roughjs` dep)**: - diamond shape; hand-drawn "sketch" rendering for box/ellipse/diamond; - 5 arrowhead styles (none/triangle/line/circle/diamond, both ends, via - `context-stroke` markers); fill-color UI + adjustable corner radius. - Toolbar gains diamond tool, image button, fill swatches, sketch - toggle; the selection inspector gains fill / sketch / radius / ends. -- **Undo/redo**: broadened from move-only to cover shape create + delete - across every entry point (draw tools, stamps, images, paste, duplicate, - eraser, keyboard/inspector/context-menu delete) via `createShape` / - `removeShapeUndoable` helpers (mutable id box re-mints rows across - redo/undo cycles). -- Validation: `pnpm lint` + `pnpm typecheck` clean; canvas unit + router - tests 64/64. - -## 2026-05-20 — Orchestration loop (Goal → decompose → judge → retry) - -Migration `0051_orchestration_loop` (authored manually + `migrate -resolve --applied` because `migrate dev` wanted a full reset over -unrelated pre-existing drift in 0012/0050; the SQL itself applied -cleanly via psql). - -- **Schema**: new `Goal` model + `GoalStatus` enum - (OPEN/PLANNING/ACTIVE/ACHIEVED/ABANDONED). `ExecutionPlan` gains - `goalId`, `maxStepRetries`(2), `maxTotalCostUsd`, `maxWallTimeMinutes`, - `totalCostUsd`, `isActiveAttempt`, `autoJudge`(true). `ExecutionStep` - gains `judgeVerdict` Json, `retryCount`, `lastFeedback`, `childPlanId`. - 5 new `EventKind` values (GOAL_CREATED, GOAL_STATUS_CHANGED, - EXECUTION_STEP_READY, EXECUTION_STEP_JUDGED, PLAN_BUDGET_EXCEEDED). - No new `ExecutionStepStatus` values — reused the existing enum. -- **`orchestration-service.ts`** (new): goal CRUD, `decomposeGoal` - (DRAFT plan + planner dispatch), `addStepsToPlan` (index-based deps), - `cascadeReadiness` (TODO→READY when deps DONE + worker dispatch), - `dispatchJudge` / `recordVerdict` (PASS→DONE+cascade / FAIL→retry or - BLOCKED+gate), `maybeAutoJudge`, `requestPlanApproval` / `activatePlan`, - `applyRunCostToPlan` + `checkAndBlockBudget` (budget watchdog). -- Step dispatch reuses the per-agent `agent:dispatch:{id}` webhook shim - (worker resolves it for any subject type), so execution-step events - fan out without needing an issue. -- **Wiring**: `updateExecutionStep` now cascades on DONE + auto-judges on - REVIEW; `acceptActionRequest` activates a plan when - `sourceType==="execution-plan"`; `runs.recordUsage` folds cost deltas - into plan/goal totals + trips the budget watchdog. -- **MCP**: goals.{list,get,create,abandon}, plans.{decompose,addSteps, - requestApproval,activate,judge,recordVerdict}, agentCrews.{create, - update,addMember,removeMember,setMemberRole,archive}. tRPC `goal` - router mirrors goals + decompose + requestApproval. -- **Tests**: `orchestration.test.ts` (10) + an MCP-registry orchestration - test. Full suite: 571 pass / 1 pre-existing unrelated fail - (`slash-templates` — a parallel UI agent's uncommitted `/goal` - template not yet reflected in its own test). -- **Docs**: new `docs/concepts/orchestration.md`; `docs/reference/mcp.md` - - `events.md` updated. - -## 2026-05-20 — Canvas Polish Wave 1 - -Plan: `docs/plans/canvas-polish-wave.md`. Goal: stop the canvas feeling -like a placeholder — first-paint smoothness, on-canvas authoring, real -selection inspector, alignment guides, and sticky-note primitives. - -- **W1.1**: frame drag cascade is now O(descendants), not O(frames²). - Added unified `frameChildIndex` memo (`childFramesByParent`, - `childNodesByFrame`, `childShapesByFrame`, `descendantsByFrame`) - and refactored `onFrameTitleMouseDown` + `activePageDescendantIds` - to use it. Click-to-first-paint on nested frames is no longer - perceptibly delayed. -- **W1.2**: new `entity-create` tool (toolbar icon + `I` shortcut). - Click on the canvas opens an inline composer popover with - Issue / Note tabs; `Enter` commits via `issue.create` / `note.create` - and drops a `CanvasNode` at the click position. Returns to Select. -- **W1.3**: sticky / comment-pin / stamp shape kinds + toolbar palettes. -- **W1.4**: smart alignment guides during shape drag. New - `src/lib/canvas-snap-guides.ts` + 8 unit tests. Edge/center snap - within 4px (scaled by zoom), guide lines rendered in ember dashed, - inline distance labels between the active item and its nearest - sibling, plus a `W × H` size label on the active bbox. Grid-snap - still works when no smart-snap fires. -- **W1.5**: floating selection inspector. New - `src/components/canvas/canvas-selection-inspector.tsx` — mini - toolbar that hovers 8px above the selection bbox (auto-flips - below near the top of the viewport). Per-kind property surfaces: - shape (color / stroke / opacity / lock), frame (name / - auto-layout direction / gap / padding), edge (kind), node (open - detail), multi (count chip). Patches debounced 200ms, routed - through `shapePatch` / `framePatch` / `edgePatch`. Delete button - reuses the existing keyboard-delete path. - -## 2026-05-20 — Canvas Polish Wave 2 - -- **W2.1**: tool ergonomics. Auto-return-to-Select after one shape - commit (already existed); Shift-click on a tool button now locks - it sticky with an ember dot indicator (skips the auto-return). - Space-held temporarily flips any tool to Pan, releasing restores. - Eraser cursor is no longer `not-allowed`; clicks delete the shape - under cursor. -- **W2.2**: comment-pin thread UI (v1). Replaced the W1.3 - placeholder popover with a real thread inside the pin's - foreignObject. Comments stored on `shape.style.comments` JSON, - patched via `shapePatch` (no Comment-table migration needed yet). - Add / resolve / re-open / delete; unresolved count drives the - red badge; all-resolved pins switch from ember to translucent - success ring. -- **W2.3**: hover + selection polish. SVG drop-shadow on any - `[data-canvas-shape]:hover` for a 1px outline glow that reads on - light + dark. Live marquee count badge floats to the right of - the rubber-band rect, hitting nodes/shapes/frames in real time. -- **W2.4**: grid-snap visual feedback. During drag, when grid-snap - applies (and no smart-guide fired), `SnapGuidesLayer` renders a - row + column ember band at the snap target so the operator sees - where they're being pulled. - -## 2026-05-20 — Canvas Polish Wave 3 - -- **W3.1**: client-side undo / redo. New `src/lib/canvas-undo.ts` - with a 100-entry stack. ⌘Z / ⌘⇧Z fire `undo` / `redo` and emit a - short toast (`Undone: moved 3 shapes`). v1 wires the most-common - op (shape move) — add/delete and frame/edge ops can extend by - following the same `pushCommand` shape. -- **W3.2**: copy / paste. ⌘C serialises the shape selection to an - in-memory clipboard with relative positions; ⌘V pastes at +20px - offset. ⌘D continues to duplicate in place (pre-existing). - Chains of pastes cascade rather than stacking on the same spot. -- **W3.3**: right-click context menu. New - `src/components/canvas/canvas-context-menu.tsx`. Context-aware: - shape (Duplicate / Delete), card (Remove from canvas), - background (Paste / New issue here / New note here / Reset view). -- **W3.4**: focus / zoom modes. `F` zooms-to-fit the selected - frame(s) with 80px padding; `Shift+F` still picks the frame tool - even when frames are selected. `Shift+2` zooms-to-fit the current - selection regardless of kind. `0` (reset) and `1` (fit-all) stay - as-is. - -## 2026-05-20 — Unified workspace flow — Wave 1 - -### Summary - -Landed the **unified-workspace-flow** plan -(`docs/plans/unified-workspace-flow.md`) Wave 1: schema + server -foundation for notes-as-ideas, dashboard-as-canvas, Figma-grade canvas -primitives, agent storyboard MCP, and canvas-UX polish. Agent team -ran in parallel after the schema migration landed. - -### Schema (migration `0046_unified_workspace_flow`) - -- `Note.status` enum (IDEA | SOMEDAY | ACTIVE | ARCHIVED) + - `promotedToType` / `promotedToId` backlinks. Existing JOURNAL rows - backfilled to ACTIVE; existing archived rows to ARCHIVED. -- `Issue.sourceNoteId`, `Project.sourceNoteId`, `Initiative.sourceNoteId` - — backlinks for `notes.promote`. -- `User.dashboardView` ("list" | "canvas") preference. -- `WorkspaceCanvas.kind` enum (PROJECT | INITIATIVE | CYCLE | ISSUE | - PERSONAL | DESIGN), `ownerUserId`, `activePageId`. Unique key on - `(workspaceId, kind, ownerUserId)` enforces one PERSONAL per user. -- New tables: `CanvasFrame`, `CanvasGroup`, `CanvasComponent`, - `CanvasComponentInstance`, `CanvasStyle`. -- New columns on `WorkspaceCanvasNode` + `CanvasShape`: - `parentFrameId`, `groupId` / `canvasGroupId`, `styleRefs`, - `lockedAt`, `hiddenAt`. - -### Notes upgrade (Workstream D) - -- `notes.list` extended with `status` (single | array), `pinned`, - `search` filters. `notes.create` accepts initial `status` - (defaults: IDEA for NOTE, ACTIVE for JOURNAL). -- `notes.setStatus` mutation + MCP tool. -- `notes.promote({ noteId, kind, … })` mutation + MCP tool — creates - Issue/Project/Initiative, stamps `sourceNoteId`, flips note to - ACTIVE. Refuses to re-promote. -- Frontend: `QuickNotesWidget` gets a status filter chip row + inline - status menu + "Convert →" popover. New `IdeasTile` lists top-N - IDEA-status notes on the dashboard with one-click promote. - -### Canvas core (Workstream B) - -- `canvas.frameAdd / framePatch / frameRemove`, - `canvas.groupCreate / groupDissolve`, - `canvas.pageAdd / pageRemove / pageReorder / pageActivate`, - `canvas.alignSelection`. Pure-function alignment / distribute / - tidy-up lives in `src/server/services/canvas-alignment.ts` so the - router and MCP path share one compute. -- MCP wrappers added for `canvases.frameAdd` and - `canvases.alignSelection`; the rest of the wrappers - (group/page/styleRefs) will follow as the frontend learns to - render those primitives. - -### Styling / components / layers (Workstream C) - -- Style tokens: `canvas.styleCreate / List / Update / Delete` (soft - delete via `archivedAt`). -- Components: `canvas.componentCreate / List / Get / Update / Archive` - - instances: `instanceCreate / Patch / Detach` (detach materializes - the definition into raw rows under the host frame). -- Layers: `layerSetLocked / SetHidden / Rename / Reorder` (shared - helper across nodes/shapes/frames/groups/instances). -- All exposed as `canvases.*` MCP tools. - -### Dashboard-as-canvas (Workstream E) - -- `user.personalCanvas` query auto-provisions one PERSONAL canvas per - user on first call. `user.setDashboardView` persists the preferred - view. -- New `DashboardViewToggle` in the dashboard topbar — flips to the - Personal canvas via `next/navigation`. `\` chord toggles from - anywhere on the dashboard. - -### Canvas polish (Workstream F) - -- Per-tool cursor management (`cursorForTool`) — crosshair for - draw/connect tools, text I-beam for text, grab/grabbing for pan, - default for select. -- Connector preview switched from orthogonal A\* to a cheap quadratic - curve during drag — the root cause of "drawing node flows is - delayed". Orthogonal routing still runs once on drop. -- Escape clears in-progress connector + active selection + reverts to - select tool. -- `0` resets to 100% zoom centered; `1` zooms-to-fit all content - (nodes + shapes, with 80px padding). - -### Agent storyboard (Workstream G) - -- New compound MCP tools `canvases.storyboardPlan` and - `canvases.storyboardIssue` — drop a labeled frame containing the - primary card + a notes lane + a sources/links column + a next-steps - lane (or related / comments / attachments for issues). Audit emits - `storyboard_plan` / `storyboard_issue`. - -### Verification - -- `pnpm lint` → clean. -- `pnpm typecheck` → clean. -- `pnpm test` → 421/421 (52 files) pass. -- Plan doc lives at `docs/plans/unified-workspace-flow.md`. - -### Wave 5 follow-on — frontend renderers + Today + storyboard fills - -Closing the DoD gap. A second pair of parallel agents went after the -canvas frontend rendering while I did the server-side Today zone + -the two remaining storyboard MCPs in this session. - -**Today zone server (`src/server/services/today-zone.ts`)** - -- Idempotent `refreshTodayZone(db, workspaceId, userId, canvasId)`. - Finds (or creates) a Today frame on the Personal canvas, identified - by `backgroundFill.kind = "today-zone"` so renames don't break - lookup. Locked + auto-arranged. -- Collects assigned-active issues (top 7), issues due today (4), - recent chat threads from the last 7 days (3). Dedupes, places into - a 4-column grid inside the Today frame. Re-runs cleanly — wipes - prior children before re-inserting. -- `user.personalCanvas` now calls `refreshTodayZone` on every fetch. - Cheap enough (small N) to run inline; no background job needed. - -**storyboardResearch + storyboardCustom MCP** - -- `canvases.storyboardResearch({ canvasId, topic })` — frame with - scratchpad + sources column + next-steps lane. -- `canvases.storyboardCustom({ canvasId, name, panels })` — escape - hatch with up to 12 caller-defined text panels at arbitrary - positions inside the frame. -- Chat system prompt updated with the full storyboard grammar (all - four gestures listed with their input shapes). -- Chat tools allowlist now exposes all storyboard MCP entries + - `canvases.frameAdd` + `canvases.alignSelection` + `notes.promote` - - `notes.setStatus`. - -**Frontend canvas renderers** (delegated to parallel agents) - -- Visible frames (`src/components/canvas/canvas-frames.tsx`), - components panel, layers panel, draw-frame F-key tool, multi-page - tab bar for DESIGN canvases, drag-children-with-frame semantics. - See agent reports for the per-component breakdown. - -### Verification (post Wave 5) - -- `pnpm lint && pnpm typecheck && pnpm test` → clean. 53 files / - 429 tests pass (8 new — 3 today-zone, 5 canvas-router Workstream B). -- All DoD criteria addressed at the code level: - - Visible frames + drag-children-with-frame (`canvas-frames.tsx` - - `framePatch` server cascade). - - Multi-page tab bar for DESIGN canvases (`canvas-page-tabs.tsx`). - - Components panel + drag-onto-canvas (`canvas-components-panel.tsx` - - `CanvasComponentInstances` renderer; drop emits - `application/x-forge-canvas-component`). - - Layers panel — right-edge tree, hide/lock/rename/reorder - (`canvas-layers-panel.tsx` + `canvas-right-panel.tsx`). - - Today zone — `refreshTodayZone` runs inside `canvas.hydrate` - when `canvas.kind === "PERSONAL"`. Entry route - `/w/[slug]/personal` auto-provisions + redirects so the - Personal canvas has a stable bookmarkable URL. - - F-key + draw-frame gesture + per-tool cursor for "frame". - - All four storyboard MCP gestures (Plan, Issue, Research, - Custom) + chat-tools allowlist entries. -- Visual smoke needs a browser pass — typecheck and lint passes - are the strongest signal we have without one. - -## 2026-05-19 — Chat inbox backstop duplicate-wake guard - -### Summary - -Fixed AXI-44: the durable agent inbox/backstop path no longer retries -old chat USER turns after an AGENT reply already exists later in the -same thread. - -### What changed - -- `agent.inbox.list` now filters chat inbox rows by canonical conversation - state, not only `acknowledgedAt`: dispatched USER messages are suppressed - when a later AGENT message exists in the thread. -- `chat.kickThread` now no-ops for the same already-answered condition, so - the `inbox-poll-backstop` retry path does not re-emit stale - `CHAT_MESSAGE_POSTED` wakes. -- Added MCP regression coverage for both inbox listing and kick retry - behavior on already-answered chat turns. - -### Verification - -- Added tests first; both new targeted tests failed on the old behavior. -- `pnpm vitest run src/server/services/__tests__/mcp.test.ts -t "agent.inbox.list suppresses chat turns|chat.kickThread is a no-op when the latest unacked"` → pass. -- `pnpm vitest run src/server/services/__tests__/mcp.test.ts` → 93/93 pass. -- `pnpm lint && pnpm typecheck && pnpm test` → clean, 52 files / 408 tests pass. -- `pnpm build` → clean. - -## 2026-05-19 — Confirm modal, canvas perf, CRUD lifecycle, chat streaming, canvas previews (5-agent round) - -### Summary - -Five-agent parallel team (I/J/K/L/M) covering four operator asks in -one session: replace `window.confirm` with the existing polished -`` modal, fix canvas drag/click lag, add restore/duplicate/ -delete CRUD lifecycle to plans + artifacts, swap chat from dispatch- -webhook to a proper streaming endpoint with thinking + tool-use -rendering, and turn canvas attachment/artifact cards into real inline -preview surfaces (images / PDFs / text / sandboxed HTML). - -### What changed - -**Confirm modal sweep (Agent I)** — every `window.confirm` in -`plans/[planId]`, `settings/crews`, `artifacts/[artifactSlug]` now -uses the existing `Confirm` modal (`src/components/ui/modal/ -confirm.tsx`) with destructive variant + `typeToConfirm` gating where -appropriate. Discriminated `confirmState` union pattern for files -that host multiple confirms. Mutation loading state threads through -via the modal's `loading` prop. - -**Canvas perf + canvas confirms (Agent J)** — substantial work on -`canvas/[canvasId]/page.tsx`: - -- Drag state moved from `setData` cache writes to a `dragOverridesRef: -Map` + rAF-bumped `dragRev` counter. Only the moving - card re-renders, not the whole tree. -- `CanvasCard` wrapped in `React.memo` with a custom equality - comparing id/x/y/width/height/viewMode/meta-by-reference. Parent - callbacks lifted into `useCallback` taking `nodeId` so they don't - close over per-node state. -- `EdgesOverlay` memoized with bbox `useMemo`. -- Realtime hydrate refetch paused during drag (single trailing - invalidate on mouseup); narrowed the invalidation filter to - canvas-relevant subjectTypes. -- Remote-cursor positions moved to a `useRef` + `cursorsRev` - counter so cursor ticks repaint only the cursor subtree. Local - presence broadcast rAF-throttled. -- `window.confirm` sites swapped: archive canvas, remove card, - convert-to-plan (default variant with dry-run preview). - -**CRUD lifecycle (Agent K)** — both routers gained the full set: - -- `executionPlan.restore({ id }) → { ok }`. -- `executionPlan.duplicate({ id, newTitle? }) → { id }` — clones - description, steps, and remaps `dependsOnStepIds` from old→new - step ids. New plan is DRAFT. -- `executionPlan.delete({ id, confirm }) → { ok }` — admin-gated - (OWNER/ADMIN); `confirm` must match plan title. Cascades to - `ExecutionStep`. -- `executionPlan.list({ archivedOnly? | includeArchived? })`. -- `artifact.restore`, `artifact.duplicate`, `artifact.delete`, - `artifact.list` archived variant — symmetric. -- 20 new integration tests covering restore/duplicate/delete + - cross-workspace + admin gates + cascade. -- `plans/page.tsx` + `artifacts/page.tsx` UI: Active/Archived - segmented tabs, per-row "..." menu (Duplicate/Archive on Active, - Restore/Delete on Archived), destructive `Confirm` with - `typeToConfirm={row.title}` for hard delete. - -**Chat streaming (Agent L)** — `/api/chat/stream` is the new -primary chat path for text-only sends: - -- POST endpoint accepts `{ threadId, body }`, persists the USER - ChatMessage immediately, streams the agent's reply back as - Server-Sent Events. -- Provider routing via `Agent.provider`: HERMES → gateway, - CLAUDE → Anthropic (with `thinking: { type: "enabled", -budget_tokens: 4000 }`), CODEX → OpenAI, CUSTOM → custom base - URL. Each falls back to Hermes if its provider env is unset. -- SSE event types: `meta` (placeholder messageId), `thinking` - (extended-thinking delta), `content` (token delta), `tool_use` - (tool intent — display only for v1), `done`, `error`. -- `chat-thread.tsx`: `AgentStreamBubble` renders thinking + - content + tool-use cards live; persisted-row dedupe for ~800ms - during swap so users never see a flash of two copies. -- `chat-message.tsx`: `StreamedRehydration` re-renders thinking + - tool-use blocks on page reload from `ChatMessage.contextSnapshot`. -- `audit.ts` short-circuits webhook fan-out when payload - `streamed: true` so the stream endpoint and Hermes webhook - don't both reply. -- Attachment messages still flow through the dispatch path - (existing `chat.send` mutation); only text-only sends go via - the streaming path. Existing `chat.send` tests still pass. - -**Canvas attachment previews (Agent M)** — new -`canvas-preview.tsx` shared component renders attachments + non-NOTE -artifacts inline on the canvas: - -- Kind matrix: image (`` cover, click → lightbox), PDF - (`