diff --git a/AGENTS.md b/AGENTS.md index 0f93c82..92684f0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,11 @@ Stave is an installable product. Do not bake author-specific machine state into ## Git, PR, And Release Rules +- Before the first edit in a task, establish three facts and report them: where `main` is (`git fetch origin && git rev-list --left-right --count origin/main...HEAD`), which PR already owns the current branch (`gh pr list --head "$(git branch --show-current)"`), and whether that PR is green (`gh pr view --json mergeable,mergeStateStatus,statusCheckRollup`). +- Rebase onto a moved `main` before doing substantial work, not after. A late rebase invalidates every verification already run, so the test suite gets paid for twice. +- When an open PR already owns the branch, everything committed joins that PR. Unrelated work must not join it: create a new workspace and branch before the first edit. +- Do not stack commits onto a red or conflicted PR. Fix it or branch away from it first. +- Re-check `main` and the PR's checks immediately before pushing. - Use Conventional Commits for every commit. - Keep PR titles in Conventional Commits form and keep the subject lowercase. - Do not name competing or reference products as design or implementation inspiration in tracked files, code comments, tests, fixtures, commit messages, PR titles or bodies, issues, changelogs, or release notes. diff --git a/config/reliability-gates.json b/config/reliability-gates.json index 7fb570f..00ef93d 100644 --- a/config/reliability-gates.json +++ b/config/reliability-gates.json @@ -31,14 +31,20 @@ "id": "terminal-font-and-unicode", "title": "Terminal glyph and renderer addons are defensive", "invariant": "Unicode, link, search, and WebGL addons cannot prevent the DOM renderer from booting.", - "testFiles": ["tests/terminal-instance.test.ts"], - "assertionRefs": ["src/components/layout/useTerminalInstance.ts"] + "testFiles": [ + "tests/terminal-instance.test.ts" + ], + "assertionRefs": [ + "src/components/layout/useTerminalInstance.ts" + ] }, { "id": "lens-component-capture", "title": "Lens keeps component context with element picks", "invariant": "A Lens element selection may include a parent-to-leaf React component chain without requiring it for non-React pages.", - "testFiles": ["tests/lens-element-message.test.ts"], + "testFiles": [ + "tests/lens-element-message.test.ts" + ], "assertionRefs": [ "electron/main/browser/browser-element-picker.ts", "src/lib/lens/lens-element-message.ts" @@ -48,7 +54,9 @@ "id": "review-feedback-to-agent", "title": "Diff comments can return to the active agent", "invariant": "A review comment retains its diff line anchor and can be submitted as the next agent prompt.", - "testFiles": ["tests/review-feedback.test.ts"], + "testFiles": [ + "tests/review-feedback.test.ts" + ], "assertionRefs": [ "src/lib/review-feedback.ts", "src/components/panes/surfaces/EditorSurfacePanel.tsx" @@ -58,7 +66,9 @@ "id": "codex-checkpoint-provenance", "title": "Codex turns expose checkpoint provenance", "invariant": "A normal Codex turn emits a checkpoint boundary with the current Git ref when available.", - "testFiles": ["tests/codex-app-server-runtime.test.ts"], + "testFiles": [ + "tests/codex-app-server-runtime.test.ts" + ], "assertionRefs": [ "electron/providers/codex-app-server-runtime.ts", "docs/providers/provider-runtimes.md" @@ -99,7 +109,9 @@ "id": "provider-utility-inference", "title": "Utility inference remains provider neutral", "invariant": "Task naming, route classification, and commit suggestions choose an explicit or active provider deterministically and degrade without blocking the primary task.", - "testFiles": ["tests/utility-inference.test.ts"], + "testFiles": [ + "tests/utility-inference.test.ts" + ], "assertionRefs": [ "electron/providers/utility-inference.ts", "src/lib/providers/utility-inference.ts", @@ -135,6 +147,21 @@ "src/components/layout/TaskExecutionSummarySurface.tsx", "src/store/app-notification-builders.ts" ] + }, + { + "id": "agent-platform-boundaries", + "title": "Agent platform layers stay distinct", + "invariant": "A routine cannot target an existing task, the run ledger never imports execution runtimes, the secondary executor never writes ledger rows, utility inference stays mechanical, and the work queue assigns one lane per workspace.", + "testFiles": [ + "tests/agent-platform-boundaries.test.ts", + "tests/fleet-sidebar-work-queue.test.ts" + ], + "assertionRefs": [ + "docs/architecture/agent-platform-taxonomy.md", + "src/lib/fleet/sidebar-work-queue.ts", + "src/lib/providers/utility-inference.ts", + "src/lib/routines.ts" + ] } ] } diff --git a/docs/architecture/agent-platform-taxonomy.md b/docs/architecture/agent-platform-taxonomy.md new file mode 100644 index 0000000..5c801a3 --- /dev/null +++ b/docs/architecture/agent-platform-taxonomy.md @@ -0,0 +1,102 @@ +# Agent Platform Taxonomy And Boundaries + +Stave grew several ways to make an agent do more work — Advisor, Worker, Fleet, +Routines, the run ledger — and each was added for its own reason. This file +fixes what each one is, what it is not, and which vocabulary the product uses, +so the next capability lands in the right layer instead of beside a similar one. + +Read this before adding anything that runs work, schedules work, delegates +work, or shows work. + +## Vocabulary + +Use these words in code, UI copy, and plans. Do not introduce synonyms. + +| Word | Means | +| --- | --- | +| Task | One conversation with one provider inside one workspace. The unit everything else attaches to. | +| Turn | One request/response cycle inside a task. Ends with exactly one terminal event. | +| Attention item | One thing that wants the user: a question, an approval, a failed run, a PR state. `FleetAttentionItem`. | +| Action required | The lane and inbox heading for blocking attention items. Replaces the older "Needs me". | +| Ledger | The durable runs/steps/receipts record in `src/lib/runs/`. It records; it never executes. | +| Receipt | One bounded record of how something started or ended. Never transcript text, never secrets. | +| Occurrence | One firing of a schedule. | + +Lane names for workspace state are fixed and ordered: +`action-required` > `in-progress` > `in-review` > `idle`. + +## Three Layers + +Every concept belongs to exactly one layer, classified by scope (turn / task / +fleet) and lifetime (ephemeral / durable). + +### Layer 1 — Turn runtime: help the current turn + +Ephemeral, turn-scoped, minimal product branding. These are task options. + +| Concept | Role | Does not | +| --- | --- | --- | +| Advisor | One read-only advice call before a turn, injected as context | Execute; persist | +| Worker | Same-provider delegation inside a turn | Survive a restart; cross providers | +| Utility inference | Mechanical meta calls: task name, route classification, commit message | Block the task; give advice | + +Boundary: Advisor produces *content* the user would recognize as an opinion. +Utility inference produces *metadata* the user never argues with. + +### Layer 2 — Supervision: see everything, intervene from anywhere + +Fleet-scoped, read plus control, no new execution semantics. + +| Concept | Role | +| --- | --- | +| Fleet | The cross-workspace surface: attention inbox, workspace cards, task control | +| Task control plane | Identity (`projectPath + workspaceId + taskId + turnId`) and staleness validation for remote actions | +| Task execution summary | Provenance-tagged scorecard; missing data is never rendered as zero | +| Sidebar work queue | The same lane model as one of the sidebar's two views (`Projects` / `Work queue`) | +| Run ledger (run core) | Durable bookkeeping for delegated execution: runs, steps, receipts, idempotency, claims | + +The run ledger is shared machinery, not a feature. Compare Judge is its first +client; durable child tasks are planned as its second. Widen it for a new +client instead of building a second ledger beside it. + +### Layer 3 — Continuity: keep going without me + +Durable (SQLite), reconciled on restart, always carrying an explicit terminal +reason. Two axes: + +| | Ephemeral (exists) | Durable (planned) | +| --- | --- | --- | +| Time — run again | — | Routine (new task per occurrence) / Heartbeat (same task, same session) | +| Delegation — hand work off | Worker (Layer 1) | Child tasks (cross-provider, normal tasks + ledger receipts) | + +Routine is the only concept that lives outside a task: it mints tasks. +Everything else in this layer attaches to one existing task. + +## Boundary Statements + +These are the statements that keep the layers from collapsing into each other. +Each one is registered in `config/reliability-gates.json` and asserted by a test +whose name repeats it. + +1. A routine never wakes an existing task; its definition cannot target one. +2. A heartbeat never creates a task; it only adds a turn to one that exists. +3. A worker never survives a restart; a child task always does. +4. The ledger records and never executes; executors execute and never write + ledger rows except through coordinator transitions. +5. Advisor advises content; utility inference computes metadata. +6. The work queue assigns a workspace to exactly one lane, in fixed priority + order. + +Statements 2 and 3 are partly forward-looking: heartbeats and child tasks are +not built yet. They are recorded here so the capability lands inside the +boundary rather than beside it, and the gate test asserts the half that exists +today. + +## Adding Something New + +1. Name the layer it belongs to. If it seems to span two, it is two things. +2. Name its consumer. A module with no visible consumer does not ship — the run + ledger spent a year with exactly one client because that rule did not exist. +3. Reuse the vocabulary above. A new synonym is a new concept to everyone + reading the code later. +4. If it changes a boundary statement, change it here first, then the gate. diff --git a/docs/architecture/index.md b/docs/architecture/index.md index 07d641c..fb5c60a 100644 --- a/docs/architecture/index.md +++ b/docs/architecture/index.md @@ -10,11 +10,12 @@ This file is the fast entrypoint for codebase orientation. 4. `docs/providers/provider-runtimes.md` for provider-specific paths 5. `docs/architecture/entrypoints.md` for task-to-file routing 6. `docs/architecture/contracts.md` for multi-file contract checklists -7. `docs/architecture/run-core.md` before adding durable secondary execution consumers -8. `docs/architecture/workspace-integrity.md` before changing project/workspace/task ownership or hydration logic -9. `docs/architecture/chat-message-rendering.md` before changing assistant message UI semantics -10. `docs/architecture/agent-message-ux-catalog.md` for the provider-event-to-UX matrix and Preview baseline -11. `docs/architecture/session-cursor.md` for provider switch-back delta history and safe fresh-session fallback +7. `docs/architecture/agent-platform-taxonomy.md` before adding anything that runs, schedules, delegates, or displays work +8. `docs/architecture/run-core.md` before adding durable secondary execution consumers +9. `docs/architecture/workspace-integrity.md` before changing project/workspace/task ownership or hydration logic +10. `docs/architecture/chat-message-rendering.md` before changing assistant message UI semantics +11. `docs/architecture/agent-message-ux-catalog.md` for the provider-event-to-UX matrix and Preview baseline +12. `docs/architecture/session-cursor.md` for provider switch-back delta history and safe fresh-session fallback ## Top-Level Layout diff --git a/docs/architecture/run-core.md b/docs/architecture/run-core.md index 06e9194..33b3962 100644 --- a/docs/architecture/run-core.md +++ b/docs/architecture/run-core.md @@ -5,6 +5,11 @@ read-only background provider execution. Its first and current consumer is Compare Judge. It is not a generic workflow engine or a prerequisite for Fleet, Advisor, or Crane. +The read-only limit belongs to the executor, not to the ledger: the ledger is +shared bookkeeping and is meant to gain further clients rather than be copied. +See `docs/architecture/agent-platform-taxonomy.md` for where it sits among the +other layers. + ## Ownership - The renderer owns user intent, presentation, and consumer-specific result diff --git a/docs/features/fleet-needs-me.md b/docs/features/fleet-needs-me.md index 45e5483..ea5a91a 100644 --- a/docs/features/fleet-needs-me.md +++ b/docs/features/fleet-needs-me.md @@ -146,4 +146,4 @@ optional shortcuts on top of that. - [Notifications](notifications.md) - [Latest Turn Summary](workspace-latest-turn-summary.md) -- [Sidebar Active Workspaces](sidebar-active-workspaces.md) +- [Sidebar Views](sidebar-views.md) diff --git a/docs/features/sidebar-active-workspaces.md b/docs/features/sidebar-active-workspaces.md deleted file mode 100644 index 32bd35f..0000000 --- a/docs/features/sidebar-active-workspaces.md +++ /dev/null @@ -1,115 +0,0 @@ -# Sidebar Active Workspaces - -## Summary - -The workspace sidebar keeps a ranked `Active workspaces` shortlist above the -project list so the workspaces that matter right now stay one click away. Each -row can be removed by hand when you consider it unimportant, and removed rows -come back on their own once the workspace needs you again or you open it. - -## When To Use It - -- Use the list to jump between the handful of workspaces you are actively - driving across projects. -- Remove rows that the ranking keeps surfacing but that you do not care about - right now, such as a long-running background workspace or another project's - last-opened workspace. -- Use Fleet's `Needs me` rail instead when you want the full actionable queue - rather than a navigation shortlist. - -## Before You Start - -- Open at least one project in Stave. -- Keep `Settings → Design → Sidebar → Active Workspaces` enabled (it is on by - default). - -## Quick Start - -1. Expand the left sidebar. The `Active workspaces` heading appears above - `Projects` whenever at least one workspace qualifies. -2. Click a row to open that workspace, switching projects if needed. -3. Hover a row you do not care about and click the `×` (`Hide from Active - Workspaces`) button to remove it. - -## Interface Walkthrough - -### Entry Points - -- Expanded sidebar: the `Active workspaces` section above the `Projects` - header. -- `Settings → Design → Sidebar`: the `Active Workspaces` toggle, the row limit - slider, and the `Hidden Workspaces` restore control. - -### How Rows Are Selected And Ordered - -A workspace earns a row for any of these reasons: - -- It is the workspace you are standing in. -- It is a project's representative workspace — the one that project would - reopen into. -- It is noteworthy: an agent is waiting for your input or approval, it carries - a visible attention need from Fleet, or its best task status is `error` or - `running`. A completed result waiting for review never promotes a workspace - by itself. - -Rows are ordered: current workspace first, then the most urgent attention -need, then status (waiting → error → running → idle), then most recently -opened project. The list is capped by the `Active Workspace Rows` setting. - -### Removing A Row - -- Hover (or keyboard-focus) any row except the current workspace and click the - `×` button. -- Removal hides the low-urgency listing reasons only: representative, - `error`, and `running` rows disappear. -- A removed workspace still returns while an agent is waiting on you — a - pending question, approval, or visible attention need always resurfaces it. - Hiding a stalled agent would bury the signal the list exists to show. -- The current workspace has no remove button; it is the "you are here" marker. - -### Getting A Row Back - -- Open the workspace from the project list (or anywhere else). Deliberately - activating a workspace clears its removal automatically. -- Or use `Settings → Design → Sidebar → Hidden Workspaces → Restore` to clear - every removal at once. The control only appears while at least one removal - is in effect. - -## Files And Data - -- Removals are stored per workspace id with the removal time and persist - across restarts alongside the rest of the sidebar state. -- A removal is compared against the workspace's last deliberate activation: - once you activate the workspace again, the stamp lapses and the row can - reappear. -- Stamps for workspaces Stave no longer remembers are pruned on startup. - -## Limitations And Advanced Options - -- Removal affects only the sidebar `Active workspaces` shortlist. Fleet View, - the `Needs me` rail, notifications, and the project list are unaffected. -- You cannot hide a workspace that is currently waiting on your input or - approval; answer or dismiss the underlying request instead. -- Turning the whole section off is still available via the `Active - Workspaces` toggle in Settings. - -## Troubleshooting - -### A Removed Workspace Came Back - -- Symptom: a row you removed reappears. -- Cause: the workspace started waiting on you, or you opened it again — - both intentionally end the removal. -- Fix: none needed; remove it again once the attention state is resolved if - you still do not want it listed. - -### The Remove Button Is Missing - -- Symptom: no `×` appears on hover. -- Cause: the row is the workspace you are currently in, which cannot be - hidden. -- Fix: switch to another workspace first if you really want to hide it. - -## Related Docs - -- [Fleet Needs Me](fleet-needs-me.md) diff --git a/docs/features/sidebar-views.md b/docs/features/sidebar-views.md new file mode 100644 index 0000000..52db945 --- /dev/null +++ b/docs/features/sidebar-views.md @@ -0,0 +1,122 @@ +# Sidebar Views + +## Summary + +The left sidebar has two views and shows one at a time. `Projects` is the +project → workspace tree: it sorts by where a workspace lives. `Work queue` +groups every workspace into attention lanes: it sorts by what the workspace +wants from you. A toggle in the sidebar header swaps between them, and the +sidebar reopens in whichever view you used last. + +Both views list the same workspaces, so either one on its own is a complete way +to navigate. Switching is a change of question, not a change of scope. + +## When To Use It + +- Use `Projects` when you know where you are going — you want a specific + project's workspace, or you want to reorder, rename, or archive one. +- Use `Work queue` when you want the app to tell you where to go — which agents + are blocked, which are still running, which finished and are waiting for a + look. +- Use Fleet View instead when you want the full cross-project detail view rather + than a navigation surface. + +## Before You Start + +- Open at least one project in Stave. +- Expand the left sidebar (the toggle lives in the sidebar's own header bar and + is hidden while the sidebar is collapsed to its icon rail). + +## Quick Start + +1. Expand the left sidebar. The two-button toggle sits at the left of the bar + above the search box. +2. Click the checklist icon to switch to `Work queue`. Lane headings replace the + project tree. +3. Click any row to open that workspace, switching projects if needed. +4. Click the folder-tree icon to go back to `Projects`. + +## Interface Walkthrough + +### Entry Points + +- Sidebar header bar: the `Projects` / `Work queue` toggle. +- `Settings → Design → Sidebar → Sidebar View`: the same two choices. Both + controls write the same preference, so neither can disagree with the other. + +### Projects View + +The project → workspace tree, unchanged: drag to reorder, rename in place, the +`⋮` row menu for task history, workspace settings, and archive, and the row +density menu (`Expanded` / `Compact`) in the header bar. + +### Work Queue View + +Every workspace, grouped into four lanes in fixed priority order: + +| Lane | Meaning | +| --- | --- | +| `Action required` | Blocked on you — a question, an approval, a failed run, a PR that cannot merge, or a task sitting in a waiting/error state | +| `In progress` | An agent is running right now | +| `In review` | Finished work nobody has looked at yet | +| `Idle` | Nothing pending | + +- Inside a lane, rows are ordered: the workspace you are standing in first, then + the most urgent attention item, then status, then most recently opened + project. +- A workspace appears in exactly one lane, and an empty lane renders no header. +- Each lane header shows its row count and collapses on click. Collapsing is + session-local — it answers "what am I ignoring right now", not "how do I like + my sidebar" — so it resets on restart, the same way collapsed projects do. +- The trailing text on a row is the project name. The queue is the one view that + interleaves projects, so it has to state in text what the tree states by + position. + +### Search + +The search box filters both views through the same predicate, so a query narrows +the queue exactly the way it narrows the tree. + +## Files And Data + +- The current view is stored as a single preference and persists across + restarts. The header toggle and the settings control write the same key. +- An unrecognized stored value falls back to `Projects`. + +## Limitations And Advanced Options + +- The collapsed icon rail shows one flat list regardless of view; the toggle is + an expanded-sidebar control. +- Row actions (`⋮` menu, drag-to-reorder, rename in place) exist only in + `Projects`. Open the workspace from the queue and use the tree, Fleet View, or + workspace settings for those. +- The `Work queue` lanes are derived from attention items and task state only. + The last lane is `Idle`, not `Done`: a merged PR and a workspace nobody has + touched are indistinguishable without subscribing the sidebar to PR status, so + claiming "Done" would overstate what the data supports. + +## Troubleshooting + +### The Toggle Is Missing + +- Symptom: no view toggle in the sidebar. +- Cause: the sidebar is collapsed to the icon rail. +- Fix: expand the sidebar first. + +### A Lane Disappeared + +- Symptom: a lane you saw earlier is gone. +- Cause: it has no members. Empty lanes are dropped rather than rendered as a + bare header. +- Fix: none needed. + +### The Queue Looks Long + +- Symptom: many rows under `Idle`. +- Cause: the queue lists every workspace on purpose, so it can reach anything + the tree can reach. +- Fix: collapse the `Idle` lane header, or filter with the search box. + +## Related Docs + +- [Fleet Needs Me](fleet-needs-me.md) diff --git a/site/src/public-docs.ts b/site/src/public-docs.ts index fc4f84a..17c11c3 100644 --- a/site/src/public-docs.ts +++ b/site/src/public-docs.ts @@ -132,11 +132,11 @@ export const PUBLIC_DOC_SECTIONS: PublicDocSection[] = [ "Work through questions, approvals, failed runs, results, and pull request blockers across every workspace.", }, { - routePath: "sidebar-active-workspaces", - sourcePath: "docs/features/sidebar-active-workspaces.md", - title: "Sidebar Active Workspaces", + routePath: "sidebar-views", + sourcePath: "docs/features/sidebar-views.md", + title: "Sidebar Views", description: - "Keep a ranked shortlist of the workspaces that matter now, and remove rows you consider unimportant.", + "Switch the left sidebar between the Projects tree and the Work queue, which groups every workspace by what it needs from you.", }, ], }, diff --git a/src/components/layout/FleetNeedsInbox.tsx b/src/components/layout/FleetAttentionInbox.tsx similarity index 86% rename from src/components/layout/FleetNeedsInbox.tsx rename to src/components/layout/FleetAttentionInbox.tsx index efcb155..0ec9f60 100644 --- a/src/components/layout/FleetNeedsInbox.tsx +++ b/src/components/layout/FleetAttentionInbox.tsx @@ -16,15 +16,15 @@ import { import { PrStatusIcon } from "@/components/layout/PrStatusIcon"; import { Badge, Button } from "@/components/ui"; import { - getFleetNeedTier, - type FleetNeedItem, - type FleetNeedKind, + getFleetAttentionTier, + type FleetAttentionItem, + type FleetAttentionKind, } from "@/lib/fleet/attention-projection"; import { PR_STATUS_VISUAL } from "@/lib/pr-status"; import { formatTaskUpdatedAt } from "@/lib/tasks"; import { cn } from "@/lib/utils"; -const FLEET_NEED_LABEL: Record = { +const FLEET_NEED_LABEL: Record = { "user-input": "Question", approval: "Approval", "run-failed": "Run failed", @@ -36,7 +36,7 @@ const FLEET_NEED_LABEL: Record = { "pr-ready-to-merge": "Ready to merge", }; -const FLEET_NEED_BADGE_CLASS: Record = { +const FLEET_NEED_BADGE_CLASS: Record = { "user-input": "border-warning/40 bg-warning/10 text-warning", approval: "border-warning/40 bg-warning/10 text-warning", "run-failed": "border-destructive/30 bg-destructive/10 text-destructive", @@ -51,7 +51,7 @@ const FLEET_NEED_BADGE_CLASS: Record = { "pr-ready-to-merge": "border-success/35 bg-success/10 text-success", }; -function getFleetNeedIcon(item: FleetNeedItem): ReactNode { +function getFleetNeedIcon(item: FleetAttentionItem): ReactNode { switch (item.kind) { case "user-input": return