diff --git a/.changeset/agent-workspace-minor.md b/.changeset/agent-workspace-minor.md
new file mode 100644
index 000000000..9cffb86f3
--- /dev/null
+++ b/.changeset/agent-workspace-minor.md
@@ -0,0 +1,5 @@
+---
+"@spencer-kit/coder-studio": minor
+---
+
+Add expanded provider runtime support, agent instruction generation, skills management, and work analysis dashboard updates.
diff --git a/.gitignore b/.gitignore
index e2b4a05d2..ac042aa6e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@ build/
output/
.cache/
.turbo/
+.vite-style-build-*/
# Test outputs
coverage/
@@ -39,6 +40,12 @@ temp/
.worktrees/
.claude/
+# Local agent instruction overrides
+/AGENTS.md
+/AGENTS.override.md
+/CLAUDE.local.md
+/GEMINI.md
+
# Acceptance runtime artifacts
docs/验收报告/**/*.json
@@ -72,4 +79,3 @@ tsconfig.tsbuildinfo
# Rust build artefacts (from lsp-test/ fixture or any ad-hoc cargo)
target/
Cargo.lock
-
diff --git a/README.md b/README.md
index 55f56d993..8c1cf83bd 100644
--- a/README.md
+++ b/README.md
@@ -4,11 +4,13 @@
# Coder Studio
-**Coder Studio, made for vibe coding.**
+**The all-in-one vibe coding workspace for AI agents.**
-An agentic workspace for real development. Run, inspect, and supervise coding agents with terminals, files, Git, sessions, and review in one browser workspace.
+Coder Studio brings your code editor, Git, terminals, AI coding agents, session review, notifications, work analysis, and Skills into one browser workspace.
-Built-in support today: Claude Code and Codex. Your code and runtime stay on your machine.
+It helps keep agent context, progress, and follow-up work visible across desktop, tablet, and phone, so vibe coding feels less scattered and more controllable.
+
+Works with popular coding agents including Claude Code, Codex, Gemini CLI, Cursor Agent, OpenCode, and Aider-style CLI agents.
[](https://www.npmjs.com/package/@spencer-kit/coder-studio)
[](https://opensource.org/licenses/MIT)
@@ -25,31 +27,20 @@ Built-in support today: Claude Code and Codex. Your code and runtime stay on you
Preview the full workspace layout built for agent runs, review, supervision, and device switching.
-## Why It Feels Different
-
-- **One browser workspace for real agent work** — Keep terminals, files, Git, sessions, and review in one place.
-- **Built for device switching** — Start on desktop, continue on tablet, and check progress from your phone.
-- **Keep control local** — Your code and runtime stay on your machine.
-
## Why Coder Studio?
-Vibe coding agents are fast, but real development still gets fragmented:
-
-- the agent runs in one terminal
-- files and diffs live in another editor
-- verification happens in separate shell tabs
-- long-running tasks are hard to monitor away from your desk
-- mobile access usually means SSH or remote desktop
+Vibe coding feels fast until the agent output turns into real project work: you still need to run agents, inspect edits, manage Git, monitor long tasks, and improve the next run. Coder Studio keeps that loop in one programming workbench.
-Coder Studio turns that scattered workflow into one local browser workspace.
-
-| Pain | Without Coder Studio | With Coder Studio |
-|------|----------------------|-------------------|
-| Long agent tasks | Watch a terminal or come back later and reconstruct context | Keep sessions, terminal output, files, and Git changes visible in one workspace |
-| Cross-device work | Use SSH, remote desktop, or rebuild context on another machine | Reopen the same local workspace from desktop, tablet, or phone |
-| Reviewing AI changes | Jump between terminal, editor, and Git tools | Inspect files and diffs beside the agent session |
-| Multiple agents | Manage separate terminal windows and histories | Run built-in Claude Code and Codex sessions side by side in one workspace today |
-| Local-first control | Move work into a hosted IDE or cloud VM | Keep the runtime and project files on your own machine |
+| Feature | Pain It Solves | What Coder Studio Provides |
+|---------|----------------|----------------------------|
+| **Agent Sessions** | Prompts, terminals, and histories scatter across tools. | Launch Claude Code, Codex, Gemini CLI, Cursor Agent, OpenCode, and CLI-style agents from one workspace. |
+| **Editor, Terminal, and Git** | Understanding one task means jumping between editor, shell tabs, Git tools, and diff viewers. | Keep code editing, terminal output, Git status, changed files, and diffs together. |
+| **Reviewable AI Changes** | The agent says it is done, but you still need to know what is safe to keep. | Inspect changed files and diffs beside the agent session before adjusting, rejecting, or accepting edits. |
+| **Supervisor Loops** | Long tasks stall, drift, or require repeated manual follow-up. | Evaluate progress and continue follow-up steps around the objective. |
+| **Status and Notifications** | You keep checking terminal output just to know whether work finished or needs attention. | Surface session state changes and completion notices in the workspace. |
+| **Cross-Device Workspace** | SSH, remote desktop, or another machine breaks the task context. | Reopen the same workspace from desktop, tablet, or phone to check progress and review changes. |
+| **Work Analysis** | Logs and diffs do not make it easy to understand what happened over time. | Review activity, agent usage, bottlenecks, repeated patterns, and skill candidates. |
+| **Skills Management** | The same instructions and workflows get repeated across agent runs. | Install and mount reusable Skills so agents start with stronger context and need fewer reminders. |
## Quick Start
@@ -61,9 +52,9 @@ npm install -g @spencer-kit/coder-studio
coder-studio open
```
-Your browser opens automatically. Select your project folder and start working with Claude Code or OpenAI Codex today.
+Your browser opens automatically. Select your project folder and start an AI coding agent session.
-> **No AI CLI installed yet?** You can still browse files and use the terminal. Install Claude Code or Codex later when needed.
+> **No AI coding agent CLI installed yet?** You can still browse files and use the terminal. Install your preferred agent CLI later when needed.
---
@@ -73,20 +64,26 @@ Your browser opens automatically. Select your project folder and start working w
- Start an Agent task at the office, check progress on your phone during commute
- Review code changes on a tablet without opening your laptop
-- Continue work from a home computer with zero setup
+- Reopen the same workspace from another device without rebuilding session context
### Long-Running AI Workflows
- Let Supervisor push multi-step tasks toward an objective without constant babysitting
- Check evaluation cycles and follow-up actions from your phone instead of watching terminal output
-- Reduce repetitive prompting and manual coordination during long agent runs
+- Use completion notices and status updates to know when agent work needs attention
### AI-Assisted Coding
-- Run Claude Code and Codex sessions side by side today
+- Run Claude Code, Codex, Gemini CLI, Cursor Agent, OpenCode, or Aider-style CLI agent sessions
- Keep terminal, editor, Git, and supervisor state in one unified interface
- Resume active AI work from another device without rebuilding context
+### Work Review and Skills
+
+- Use Work Analysis to review agent sessions, activity patterns, bottlenecks, and follow-up ideas
+- Manage Skills from the workspace so agents can reuse the right workflow knowledge
+- Turn repeated review findings into better future agent runs
+
---
## 📱 Cross-Device Experience
@@ -113,14 +110,16 @@ The same workspace URL works across all devices — interface adapts automatical
| Feature | Description |
|---------|-------------|
+| **One-Stop Programming Workbench** | Combine code editing, PTY terminals, Git status, diffs, agent sessions, and review in one browser UI |
| **Cross-Device Workspace** | Reopen the same coding environment from desktop, tablet, or phone without rebuilding context |
| **Supervisor Loops** | Run objective-driven evaluation and follow-up cycles for long AI tasks with less manual babysitting |
-| **Built-in Agent Providers** | Use Claude Code and Codex inside one workspace today instead of splitting your workflow across separate tools |
-| **Unified Terminal, Files, and Git** | Keep PTY terminals, Monaco editing, diffs, and changed files in one browser UI |
+| **Popular Coding Agents** | Run Claude Code, Codex, Gemini CLI, Cursor Agent, OpenCode, and CLI-style agents from one workspace |
+| **Notifications and Status Updates** | Surface errors, state changes, and session completion notices without leaving the workspace |
+| **Work Analysis** | Recap workspace activity, agent sessions, patterns, bottlenecks, and possible skill opportunities |
+| **Skills Management** | Search, install, mount, repair, and review Skills that help agents follow reusable workflows |
| **Reviewable AI Work** | Inspect changed files and diffs beside the session before trusting the result |
| **Responsive Workspace UI** | Use layouts tuned for desktop, tablet, and mobile instead of a desktop-only interface squeezed onto small screens |
| **Session Continuity** | Resume active sessions and keep AI work visible across device switches |
-| **Local Runtime Control** | Keep code and runtime on your machine instead of relying on a cloud IDE |
---
@@ -129,8 +128,7 @@ The same workspace URL works across all devices — interface adapts automatical
| Dependency | Version | Notes |
|------------|---------|-------|
| Node.js | ≥ 24.0.0 | Required for running Coder Studio |
-| Claude Code CLI | Latest | Optional — for Claude Agent sessions |
-| OpenAI Codex CLI | Latest | Optional — for Codex Agent sessions |
+| AI coding agent CLI | Latest | Optional — install the CLI for each agent you want to run |
---
@@ -140,9 +138,10 @@ The same workspace URL works across all devices — interface adapts automatical
|----------|-------------|
| [Quick Start Guide](docs/help/quick-start.md) | Installation to first workspace |
| [App Overview](docs/help/app-overview.md) | Core concepts and features |
-| [Provider Setup](docs/help/providers.md) | Claude Code / Codex CLI installation |
+| [Agent CLI Setup](docs/help/providers.md) | Install and connect coding agent CLIs |
| [Desktop Guide](docs/help/desktop-guide.md) | PC interface and shortcuts |
| [Mobile & Remote Access Guide](docs/help/mobile-guide.md) | Phone / tablet usage, LAN access, Tailscale/ngrok/Cloudflare Tunnel |
+| [Work Analysis](docs/help/work-analysis.md) | Review workspace activity, agent sessions, and improvement opportunities |
| [Common Workflows](docs/help/workflows.md) | Task-based tutorials |
| [Troubleshooting](docs/help/troubleshooting.md) | FAQ and known issues |
| [CLI Reference](docs/help/cli.md) | Command-line options |
@@ -154,9 +153,9 @@ The same workspace URL works across all devices — interface adapts automatical
## 👥 Who Should Use Coder Studio
- **Developers Running Coding Agents** — Want terminals, files, Git, sessions, and review in one place
+- **Vibe Coding Users** — Want an agentic workspace instead of scattered terminal-only workflows
- **Multi-Device Developers** — Switch between office, home, and mobile devices frequently
- **Developers Running Long AI Tasks** — Want Supervisor to keep multi-step work moving without constant babysitting
-- **Privacy-Conscious Developers** — Want code to stay on local machine, not cloud IDE
---
@@ -166,7 +165,7 @@ The same workspace URL works across all devices — interface adapts automatical
- [ ] Session replay and history navigation
- [ ] Multi-workspace management
- [ ] Plugin system for custom integrations
-- [ ] Cloud sync for workspace preferences
+- [ ] Workspace preference sync
---
@@ -174,7 +173,7 @@ The same workspace URL works across all devices — interface adapts automatical
We welcome contributions! See [Contributing Guide](CONTRIBUTING.md) for details.
-### Local Development
+### Development Setup
```bash
git clone https://github.com/spencerkit/coder-studio.git
@@ -208,4 +207,4 @@ MIT License — see [LICENSE](LICENSE) for details.
## 🔍 Keywords
-`ai coding assistant` `browser ide` `claude code` `codex` `remote development` `web-based ide` `self-hosted ide` `cross-device coding` `ai agent workspace` `local-first development` `mobile coding` `tablet coding` `developer tools` `terminal in browser` `git web interface` `monaco editor` `websocket terminal` `ai pair programming` `coding anywhere` `cloud ide alternative`
+`vibe coding` `agentic coding` `ai coding agent` `coding agent workspace` `browser ide` `claude code` `codex` `gemini cli` `cursor agent` `opencode` `aider` `cross-device coding` `ai agent workspace` `mobile coding` `tablet coding` `developer tools` `terminal in browser` `git web interface` `monaco editor` `websocket terminal` `ai pair programming` `supervisor loops`
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 885c7c11b..42a7dfda3 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -4,11 +4,13 @@
# Coder Studio
-**Coder Studio,生来就是 vibe coding。**
+**一站式 vibe coding 编程工作台。**
-面向真实开发的 agentic workspace。用一个浏览器工作区运行、检查和监督 coding agent,把终端、文件、Git、会话和代码审查放在一起。
+Coder Studio 把代码编辑器、Git、终端、AI coding agent、会话审查、消息提醒、工作复盘和 Skills 放进同一个浏览器工作区。
-当前内置支持:Claude Code 和 Codex。你的代码和运行时保留在自己的机器上。
+它帮助你在桌面、平板和手机之间保持 Agent 上下文、任务进度和后续动作可见,让 vibe coding 不再散落在一堆窗口和工具里。
+
+支持 Claude Code、Codex、Gemini CLI、Cursor Agent、OpenCode,以及 Aider 这类 CLI coding agent。
[](https://www.npmjs.com/package/@spencer-kit/coder-studio)
[](https://opensource.org/licenses/MIT)
@@ -25,31 +27,20 @@
+ The global rounded card is removed. The page shell stays flat, while summary
+ blocks, controls, and helper content become local panels. This gives complex
+ settings room to breathe on large desktop screens.
+
+
+
+
Open diagnostics
+
Apply profile
+
+
+
+
+
+
Runtime Mode
+
Standard
+
Host and managed process sampling are enabled.
+
+
+
Refresh Interval
+
5s
+
Lower cadence keeps overhead controlled.
+
+
+
Host Pressure
+
Moderate
+
CPU 41%, memory 68%, no throttling detected.
+
+
+
Managed Processes
+
18
+
Across 3 workspaces and 6 active agent sessions.
+
+
+
+
+
+
+
+
Performance Monitoring
+
+ Wide-mode sections can show explanation, status, and controls at the same
+ time instead of compressing everything into a narrow single column.
+
+
+
Wide Section
+
+
+
+
+
+
Enable monitoring
+
+ Master switch for runtime sampling and status summaries. Disabling this
+ stops server-side collection and hides monitoring details in the app.
+
+
+
+
+
+
+
+
+
+
Sampling level
+
+ Choose how deep the system should inspect managed processes. Higher levels
+ improve drill-down but cost more CPU budget.
+
+
+
+
Standard
+
+
+
+
+
+
Refresh frequency
+
+ Set how often the monitoring page and related summaries are refreshed from
+ the server. This stays readable because the controls have more lateral
+ space.
+
+
+
+
Every 5 seconds
+
+
+
+
+
+
Subprocess drill-down
+
+ Capture subprocess-level attribution for active workspaces. This is now
+ easier to explain because helper content can live beside the main form.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Operator Notes
+
+ The right rail is optional. It carries context that used to compete with the
+ form in the same narrow column.
+
+
+
+
+
Why this layout works better
+
+
Summary stays visible above the controls.
+
Instructions do not push switches downward.
+
Longer labels and values wrap less aggressively.
+
+
+
+
+
+
+
+
Status Snapshot
+
Example of local panels replacing the old global card.
+
+
+
+
+
CPU
+
41%
+
host average
+
+
+
Memory
+
7.4 GB
+
used by managed tree
+
+
+
Load Avg
+
2.8
+
15-minute window
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/superpowers/plans/2026-06-01-agent-instructions-publish.md b/docs/superpowers/plans/2026-06-01-agent-instructions-publish.md
new file mode 100644
index 000000000..e5b73c701
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-01-agent-instructions-publish.md
@@ -0,0 +1,500 @@
+# Agent Instructions Publish Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Stop session-start instruction injection and instead publish the workspace's effective agent instructions into provider-native project files that Claude and Codex already read.
+
+**Architecture:** Keep `.coder-studio/AGENTS.md`, `.coder-studio/AGENTS.generated.md`, and `.coder-studio/AGENTS.effective.md` as the source of truth. Add one server-side publisher that materializes the effective markdown into provider-specific files (`AGENTS.override.md` for Codex, `CLAUDE.local.md` for Claude), then wire that publisher into workspace open, dirty events, and `session.create`. Remove the terminal-input rewrite path so the default behavior is file-based, not submit-time injection.
+
+**Tech Stack:** TypeScript, Node `fs/promises`, existing event bus, Chokidar watcher, Vitest, React.
+
+---
+
+## File Map
+
+- Create: `packages/server/src/agent-instructions/publish-targets.ts`
+ Owns the provider-to-target-file map and keeps the target names centralized.
+- Create: `packages/server/src/agent-instructions/publisher.ts`
+ Resolves the effective instructions, writes/deletes the managed target files, and serializes per-workspace syncs.
+- Create: `packages/server/src/__tests__/agent-instructions-publisher.test.ts`
+ Covers publish, delete, no-op, and overlap behavior.
+- Modify: `packages/server/src/ws/dispatch.ts`
+ Adds the optional publisher dependency to command handlers.
+- Modify: `packages/server/src/server.ts`
+ Instantiates the publisher, hydrates it on startup, and subscribes it to dirty events.
+- Modify: `packages/server/src/commands/workspace.ts`
+ Triggers an eager publish after `workspace.open`.
+- Modify: `packages/server/src/commands/session.ts`
+ Forces a publish before `session.create` starts the agent process.
+- Modify: `packages/server/src/commands/terminal.ts`
+ Removes the submit-time rewrite path and its auto-attach metadata side effects.
+- Modify: `packages/server/src/commands/agent-instructions.ts`
+ Removes the unused auto-attach payload helper.
+- Modify: `packages/server/src/fs/gitignore.ts`
+ Ignores the managed target files so publisher writes do not loop back into `fs.dirty`.
+- Modify: `packages/server/src/__tests__/fs/watcher.test.ts`
+ Verifies the watcher ignores the managed target files but still watches internal source files.
+- Modify: `packages/server/src/__tests__/workspace-commands.test.ts`
+ Verifies workspace open still succeeds and no longer persists auto-attach UI state.
+- Modify: `packages/server/src/__tests__/session-commands.test.ts`
+ Verifies `session.create` waits for publish before starting the session.
+- Modify: `packages/server/src/__tests__/terminal-commands.test.ts`
+ Verifies submit payloads are no longer rewritten.
+- Modify: `packages/web/src/features/workspace/actions/use-agent-instructions-actions.ts`
+ Removes auto-attach state management and related dispatches.
+- Modify: `packages/web/src/features/workspace/views/shared/agent-instructions-section.tsx`
+ Removes the auto-attach switch and leaves manual attach as the explicit fallback.
+- Modify: `packages/web/src/features/workspace/views/shared/agent-instructions-section.test.tsx`
+ Updates the panel tests to match the reduced UI.
+- Modify: `packages/web/src/locales/en.json`
+ Removes stale auto-attach copy.
+- Modify: `packages/web/src/locales/zh.json`
+ Removes stale auto-attach copy.
+- Modify: `packages/core/src/domain/types.ts`
+ Removes `agentInstructionsAutoAttach` from `UiState`.
+
+## Guardrails
+
+- Keep `.coder-studio/AGENTS.*` as the internal source of truth; never make the published target files the source of truth.
+- Use `AGENTS.override.md` for Codex and `CLAUDE.local.md` for Claude; do not target `.codex`.
+- Keep the sync best-effort. A transient write failure should log and continue, not block workspace open or session creation.
+- Ignore managed target writes in the workspace watcher so publisher output does not re-trigger the publisher.
+- Keep `agentInstructions.attachToSession` as a manual fallback only.
+- Do not add a new public publish-status API in this phase.
+
+### Task 1: Add the publisher service and target registry
+
+**Files:**
+- Create: `packages/server/src/agent-instructions/publish-targets.ts`
+- Create: `packages/server/src/agent-instructions/publisher.ts`
+- Create: `packages/server/src/__tests__/agent-instructions-publisher.test.ts`
+
+- [ ] **Step 1: Write the failing publisher tests**
+
+Add tests that exercise the service against a temp workspace:
+
+```ts
+it("publishes the effective instructions into provider-native files", async () => {
+ await mkdir(join(rootPath, ".coder-studio"), { recursive: true });
+ await writeFile(
+ join(rootPath, ".coder-studio", "AGENTS.generated.md"),
+ "# Generated\n\n- Generated rule.\n"
+ );
+ await writeFile(
+ join(rootPath, ".coder-studio", "AGENTS.md"),
+ "# Custom\n\n- Custom rule.\n"
+ );
+
+ const result = await publisher.syncWorkspace("ws-1");
+
+ expect(result.targets).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ providerId: "codex", path: "AGENTS.override.md" }),
+ expect.objectContaining({ providerId: "claude", path: "CLAUDE.local.md" }),
+ ])
+ );
+ expect(await readFile(join(rootPath, "AGENTS.override.md"), "utf8")).toContain(
+ "# Effective Agent Instructions"
+ );
+ expect(await readFile(join(rootPath, "CLAUDE.local.md"), "utf8")).toContain(
+ "# Effective Agent Instructions"
+ );
+});
+```
+
+Add a second test for the empty-source case:
+
+```ts
+it("deletes managed targets when no effective instructions exist", async () => {
+ await publisher.syncWorkspace("ws-1");
+
+ await expect(stat(join(rootPath, "AGENTS.override.md"))).rejects.toMatchObject({
+ code: "ENOENT",
+ });
+ await expect(stat(join(rootPath, "CLAUDE.local.md"))).rejects.toMatchObject({
+ code: "ENOENT",
+ });
+});
+```
+
+Add a third test for a second sync staying unchanged:
+
+```ts
+it("leaves managed targets unchanged on a second sync", async () => {
+ await publisher.syncWorkspace("ws-1");
+
+ const second = await publisher.syncWorkspace("ws-1");
+
+ expect(second.targets).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ providerId: "codex", action: "unchanged" }),
+ expect.objectContaining({ providerId: "claude", action: "unchanged" }),
+ ])
+ );
+});
+```
+
+- [ ] **Step 2: Run the new test file and confirm it fails**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server test -- src/__tests__/agent-instructions-publisher.test.ts
+```
+
+Expected: the new assertions fail because the publisher does not exist yet.
+
+- [ ] **Step 3: Implement the minimal publisher**
+
+Create `publish-targets.ts` with a small registry:
+
+```ts
+export const AGENT_INSTRUCTION_PUBLISH_TARGETS = [
+ { providerId: "codex", path: "AGENTS.override.md" },
+ { providerId: "claude", path: "CLAUDE.local.md" },
+] as const;
+```
+
+Implement `AgentInstructionsPublisher` so it:
+
+- resolves the effective markdown from `.coder-studio/AGENTS.*`
+- writes that markdown into each managed target when the content differs
+- deletes the managed target when the effective markdown is absent
+- keeps per-workspace syncs serialized
+- returns a small result object with the action taken per target
+
+- [ ] **Step 4: Rerun the publisher tests**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server test -- src/__tests__/agent-instructions-publisher.test.ts
+```
+
+Expected: pass.
+
+### Task 2: Wire publishing into workspace open, dirty events, and session startup
+
+**Files:**
+- Modify: `packages/server/src/ws/dispatch.ts`
+- Modify: `packages/server/src/server.ts`
+- Modify: `packages/server/src/commands/workspace.ts`
+- Modify: `packages/server/src/commands/session.ts`
+- Modify: `packages/server/src/__tests__/workspace-commands.test.ts`
+- Modify: `packages/server/src/__tests__/session-commands.test.ts`
+- Modify: `packages/server/src/__tests__/workspace-watcher-hydrate-restart.test.ts`
+
+- [ ] **Step 1: Add failing wiring tests**
+
+Add a workspace-open test that proves the publisher is called before the command returns:
+
+```ts
+it("publishes agent instructions during workspace.open", async () => {
+ const calls: string[] = [];
+ ctx.agentInstructionPublisher = {
+ syncWorkspace: vi.fn(async () => {
+ calls.push("publish");
+ }),
+ scheduleWorkspaceSync: vi.fn(),
+ syncAllOpenWorkspaces: vi.fn(),
+ } as never;
+
+ const result = await dispatch(
+ {
+ kind: "command",
+ id: "workspace-open-publish",
+ op: "workspace.open",
+ args: { path: dir },
+ },
+ ctx
+ );
+
+ expect(result.ok).toBe(true);
+ expect(calls).toEqual(["publish"]);
+});
+```
+
+Add a session-create ordering test:
+
+```ts
+it("publishes agent instructions before session.create starts the agent", async () => {
+ const calls: string[] = [];
+ const sessionStub = {
+ id: "sess-1",
+ workspaceId,
+ providerId: "claude",
+ terminalId: "term-1",
+ capability: "full",
+ state: "starting",
+ startedAt: Date.now(),
+ lastActiveAt: Date.now(),
+ };
+ ctx.agentInstructionPublisher = {
+ syncWorkspace: vi.fn(async () => {
+ calls.push("publish");
+ }),
+ scheduleWorkspaceSync: vi.fn(),
+ syncAllOpenWorkspaces: vi.fn(),
+ } as never;
+ sessionMgr.create = vi.fn(async () => {
+ calls.push("create");
+ return sessionStub;
+ }) as never;
+
+ await dispatch(
+ {
+ kind: "command",
+ id: "session-create-publish",
+ op: "session.create",
+ args: { workspaceId, providerId: "claude" },
+ },
+ ctx
+ );
+
+ expect(calls).toEqual(["publish", "create"]);
+});
+```
+
+Add a restart test that proves startup hydration republishes missing target files:
+
+```ts
+it("restores managed target files after restart", async () => {
+ mkdirSync(join(workspaceDir, ".coder-studio"), { recursive: true });
+ writeFileSync(
+ join(workspaceDir, ".coder-studio", "AGENTS.generated.md"),
+ "# Generated\n\n- Generated rule.\n"
+ );
+ writeFileSync(
+ join(workspaceDir, ".coder-studio", "AGENTS.md"),
+ "# Custom\n\n- Custom rule.\n"
+ );
+
+ const openResult = await dispatch(
+ {
+ kind: "command",
+ id: "workspace-open",
+ op: "workspace.open",
+ args: { path: workspaceDir },
+ },
+ firstCtx
+ );
+ expect(openResult.ok).toBe(true);
+
+ rmSync(join(workspaceDir, "AGENTS.override.md"), { force: true });
+ rmSync(join(workspaceDir, "CLAUDE.local.md"), { force: true });
+
+ server = await createServer({ stateDir, host: "127.0.0.1", port: 0 });
+ await server.stop();
+ server = await createServer({ stateDir, host: "127.0.0.1", port: 0 });
+
+ expect(await readFile(join(workspaceDir, "AGENTS.override.md"), "utf8")).toContain(
+ "# Effective Agent Instructions"
+ );
+});
+```
+
+- [ ] **Step 2: Run the wiring tests and confirm they fail**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server test -- \
+ src/__tests__/workspace-commands.test.ts \
+ src/__tests__/session-commands.test.ts \
+ src/__tests__/workspace-watcher-hydrate-restart.test.ts
+```
+
+Expected: the new publisher-related assertions fail until the wiring exists.
+
+- [ ] **Step 3: Implement the server wiring**
+
+Update `CommandContext` to carry an optional publisher.
+
+In `server.ts`:
+
+- create one `AgentInstructionsPublisher` after `workspaceMgr` is available
+- call `await publisher.syncAllOpenWorkspaces()` after `workspaceMgr.hydrateWatchers()`
+- subscribe `eventBus` to `fs.dirty` and call `publisher.scheduleWorkspaceSync(event.workspaceId)`
+- add the publisher to `commandContext`
+
+In `workspace.ts`:
+
+- after `workspaceMgr.open()` returns, call `await ctx.agentInstructionPublisher?.syncWorkspace(workspace.id)`
+
+In `session.ts`:
+
+- before `ctx.sessionMgr.create(...)`, call `await ctx.agentInstructionPublisher?.syncWorkspace(args.workspaceId)`
+
+- [ ] **Step 4: Rerun the wiring tests**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server test -- \
+ src/__tests__/workspace-commands.test.ts \
+ src/__tests__/session-commands.test.ts \
+ src/__tests__/workspace-watcher-hydrate-restart.test.ts
+```
+
+Expected: pass.
+
+### Task 3: Remove submit-time auto-attach and the auto-attach UI state
+
+**Files:**
+- Modify: `packages/server/src/commands/terminal.ts`
+- Modify: `packages/server/src/commands/agent-instructions.ts`
+- Modify: `packages/core/src/domain/types.ts`
+- Modify: `packages/server/src/commands/workspace.ts`
+- Modify: `packages/web/src/features/workspace/actions/use-agent-instructions-actions.ts`
+- Modify: `packages/web/src/features/workspace/views/shared/agent-instructions-section.tsx`
+- Modify: `packages/web/src/features/workspace/views/shared/agent-instructions-section.test.tsx`
+- Modify: `packages/web/src/locales/en.json`
+- Modify: `packages/web/src/locales/zh.json`
+- Modify: `packages/server/src/__tests__/terminal-commands.test.ts`
+- Modify: `packages/server/src/__tests__/workspace-commands.test.ts`
+
+- [ ] **Step 1: Write the failing regression tests**
+
+Replace the current submit-rewrite expectation with a plain-submit expectation:
+
+```ts
+it("leaves submit payload untouched", async () => {
+ const result = await dispatch(
+ {
+ kind: "command",
+ id: "terminal-input-submit",
+ op: "terminal.input",
+ args: {
+ terminalId: "term-1",
+ bytes: Buffer.from("ship it\r").toString("base64"),
+ activity: "submit",
+ submittedText: "ship it",
+ },
+ },
+ ctx
+ );
+
+ expect(sendInput).toHaveBeenCalledWith("sess-1", Buffer.from("ship it\r"), "submit", "ship it");
+ expect(sessionMetadataRepo.get("sess-1")?.attachedAgentInstructions).toBeUndefined();
+});
+```
+
+Update the workspace UI test to remove the auto-attach switch assertion and the `workspace.uiState.set` write for `agentInstructionsAutoAttach`.
+
+- [ ] **Step 2: Run the focused tests and confirm they fail**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server test -- src/__tests__/terminal-commands.test.ts src/__tests__/workspace-commands.test.ts
+pnpm --filter @coder-studio/web test -- src/features/workspace/views/shared/agent-instructions-section.test.tsx
+```
+
+Expected: the tests still reference the removed auto-attach behavior.
+
+- [ ] **Step 3: Remove the auto-attach path and state**
+
+In `terminal.ts`:
+
+- delete the `maybeRewriteSessionSubmit` branch entirely
+- stop importing `resolveEffectiveAgentInstructions` and `buildAutoAttachSubmitPayload`
+- keep plain `sessionMgr.sendInput(...)`
+
+In `agent-instructions.ts`:
+
+- delete `buildAutoAttachSubmitPayload`
+- keep `attachToSession` as the explicit manual fallback
+
+In `types.ts` and the workspace UI:
+
+- remove `agentInstructionsAutoAttach` from `UiState`
+- remove the switch, related action, and stale locale copy
+- keep the manual attach button and the three internal status pills
+
+- [ ] **Step 4: Rerun the terminal and UI tests**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server test -- src/__tests__/terminal-commands.test.ts src/__tests__/workspace-commands.test.ts
+pnpm --filter @coder-studio/web test -- src/features/workspace/views/shared/agent-instructions-section.test.tsx
+```
+
+Expected: pass.
+
+### Task 4: Ignore managed target files in the watcher
+
+**Files:**
+- Modify: `packages/server/src/fs/gitignore.ts`
+- Modify: `packages/server/src/__tests__/fs/watcher.test.ts`
+
+- [ ] **Step 1: Write the failing ignore test**
+
+Add assertions that the watcher ignores the managed target files but still watches the internal source files:
+
+```ts
+it("ignores managed target files used for agent instruction publishing", () => {
+ new WorkspaceWatcher("test-workspace-id", testDir, broadcaster);
+
+ const options = watchSpy.mock.calls[0]?.[1];
+ const ignored = options?.ignored;
+
+ expect(ignored?.(join(testDir, "AGENTS.override.md"))).toBe(true);
+ expect(ignored?.(join(testDir, "CLAUDE.local.md"))).toBe(true);
+ expect(ignored?.(join(testDir, ".coder-studio", "AGENTS.md"))).toBe(false);
+});
+```
+
+- [ ] **Step 2: Run the watcher test and confirm it fails**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server test -- src/__tests__/fs/watcher.test.ts
+```
+
+Expected: the new ignore assertions fail until the filter is updated.
+
+- [ ] **Step 3: Add the ignore patterns**
+
+Update the watcher ignore regexes so they skip the managed root files only:
+
+```ts
+/(^|\/)(AGENTS\.override\.md|CLAUDE\.local\.md)$/
+```
+
+Keep the existing `.git`, `node_modules`, and transient lock-file filters unchanged.
+
+- [ ] **Step 4: Rerun the watcher test**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server test -- src/__tests__/fs/watcher.test.ts
+```
+
+Expected: pass.
+
+## Final Verification
+
+Run the focused slice first:
+
+```bash
+pnpm --filter @coder-studio/server test -- \
+ src/__tests__/agent-instructions-publisher.test.ts \
+ src/__tests__/workspace-commands.test.ts \
+ src/__tests__/session-commands.test.ts \
+ src/__tests__/terminal-commands.test.ts \
+ src/__tests__/fs/watcher.test.ts \
+ src/__tests__/workspace-watcher-hydrate-restart.test.ts
+pnpm --filter @coder-studio/web test -- src/features/workspace/views/shared/agent-instructions-section.test.tsx
+```
+
+Then run the broader workspace slice if the focused tests pass:
+
+```bash
+pnpm --filter @coder-studio/server test
+pnpm --filter @coder-studio/web test
+```
+
+Expected: all relevant tests pass, the auto-attach toggle is gone, `terminal.input` no longer rewrites submissions, and the provider-native instruction files are present after workspace open and after source changes.
diff --git a/docs/superpowers/plans/2026-06-03-agent-instructions-multi-provider-generation.md b/docs/superpowers/plans/2026-06-03-agent-instructions-multi-provider-generation.md
new file mode 100644
index 000000000..abbde70a1
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-03-agent-instructions-multi-provider-generation.md
@@ -0,0 +1,347 @@
+# Agent Instructions Multi-Provider Generation Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Expand `agent.md` generation from Codex-only headless execution to the built-in `codex`, `claude`, `gemini`, and `cursor` providers while keeping server-owned validation and file writes.
+
+**Architecture:** Keep `agent_instructions_generate` as an explicit headless scenario, but add it to the built-in providers that already support stable headless execution. Replace the Codex-only output parser with provider-aware envelope extraction plus a unified generation JSON payload parser, then update prompt, command, and UI filtering tests around the new contract.
+
+**Tech Stack:** TypeScript, Vitest, React Testing Library, Jotai, existing provider/server command architecture
+
+---
+
+### Task 1: Expand Built-In Provider Generation Capability
+
+**Files:**
+- Modify: `packages/providers/src/claude/definition.ts`
+- Modify: `packages/providers/src/gemini/definition.ts`
+- Modify: `packages/providers/src/cursor/definition.ts`
+- Test: `packages/providers/src/claude/definition.test.ts`
+- Test: `packages/providers/src/gemini/definition.test.ts`
+- Test: `packages/providers/src/cursor/definition.test.ts`
+
+- [ ] **Step 1: Write the failing provider definition tests**
+
+Add assertions that `supportedScenarios` includes `agent_instructions_generate` and that the provider `headless.buildCommand(...)` returns a non-null command for that scenario.
+
+```ts
+expect(claudeDefinition.headless?.supportedScenarios).toEqual([
+ "supervisor_eval",
+ "agent_instructions_generate",
+ "session_analysis",
+]);
+expect(
+ claudeDefinition.headless?.buildCommand({}, "agent_instructions_generate", {
+ prompt: "Return strict JSON",
+ sessionId: "sess-1",
+ workspacePath: "/workspace",
+ })
+).not.toBeNull();
+```
+
+- [ ] **Step 2: Run the targeted provider tests and verify they fail**
+
+Run:
+
+```bash
+pnpm vitest run packages/providers/src/claude/definition.test.ts packages/providers/src/gemini/definition.test.ts packages/providers/src/cursor/definition.test.ts
+```
+
+Expected: FAIL because the scenario list does not yet include `agent_instructions_generate` and `buildCommand(...)` returns `null` for that scenario.
+
+- [ ] **Step 3: Update the provider definitions**
+
+Extend each built-in headless definition to accept `agent_instructions_generate` and reuse the existing headless command builder.
+
+```ts
+headless: {
+ supportedScenarios: ["supervisor_eval", "agent_instructions_generate", "session_analysis"],
+ buildCommand(config, scenario, req) {
+ if (
+ scenario !== "supervisor_eval" &&
+ scenario !== "agent_instructions_generate" &&
+ scenario !== "session_analysis"
+ ) {
+ return null;
+ }
+
+ return buildClaudeSupervisorEvalCommand(config, req);
+ },
+},
+```
+
+- [ ] **Step 4: Re-run the targeted provider tests and verify they pass**
+
+Run:
+
+```bash
+pnpm vitest run packages/providers/src/claude/definition.test.ts packages/providers/src/gemini/definition.test.ts packages/providers/src/cursor/definition.test.ts
+```
+
+Expected: PASS
+
+### Task 2: Replace Codex-Only Output Parsing With Provider-Aware Extraction
+
+**Files:**
+- Modify: `packages/server/src/agent-instructions/output.ts`
+- Modify: `packages/server/src/__tests__/agent-instructions/output.test.ts`
+
+- [ ] **Step 1: Write the failing parser tests**
+
+Add tests for:
+
+- extracting final text from Codex JSONL
+- extracting final text from Claude/Gemini/Cursor JSON envelopes
+- parsing unified generation payload JSON
+- surfacing `ok: false`
+- rejecting invalid JSON and invalid headings
+
+Representative fixtures:
+
+```ts
+const claudeEnvelope = JSON.stringify({
+ type: "result",
+ result: '{"ok":true,"content":"# Agent Instructions\\n\\n## Project Overview\\n"}',
+});
+
+const codexJsonl = [
+ JSON.stringify({
+ type: "item.completed",
+ item: {
+ type: "agent_message",
+ text: '{"ok":true,"content":"# Agent Instructions\\n\\n## Project Overview\\n"}',
+ },
+ }),
+].join("\n");
+```
+
+- [ ] **Step 2: Run the parser tests and verify they fail**
+
+Run:
+
+```bash
+pnpm vitest run packages/server/src/__tests__/agent-instructions/output.test.ts
+```
+
+Expected: FAIL because only Codex markdown extraction exists today and there is no unified payload parser.
+
+- [ ] **Step 3: Implement provider-aware extraction and unified payload parsing**
+
+Refactor `output.ts` to expose two layers:
+
+- provider-specific final-text extraction
+- provider-agnostic generation payload parsing
+
+Target API:
+
+```ts
+export function extractAgentInstructionsReplyText(providerId: string, stdout: string): string
+export function parseGeneratedAgentInstructionsPayload(replyText: string): string
+```
+
+Behavior:
+
+- `extractAgentInstructionsReplyText(...)`
+ - `codex`: read JSONL `item.completed` / `agent_message`
+ - `claude`, `gemini`, `cursor`: decode the final text field from the JSON envelope
+- `parseGeneratedAgentInstructionsPayload(...)`
+ - parse JSON
+ - require `ok === true`
+ - require non-empty `content`
+ - normalize markdown through `normalizeGeneratedAgentInstructionsMarkdown(...)`
+
+- [ ] **Step 4: Re-run the parser tests and verify they pass**
+
+Run:
+
+```bash
+pnpm vitest run packages/server/src/__tests__/agent-instructions/output.test.ts
+```
+
+Expected: PASS
+
+### Task 3: Switch Generation Prompt and Server Flow to the Unified Contract
+
+**Files:**
+- Modify: `packages/server/src/agent-instructions/prompt.ts`
+- Modify: `packages/server/src/agent-instructions/agent-generator.ts`
+- Test: `packages/server/src/__tests__/agent-instructions-command.test.ts`
+
+- [ ] **Step 1: Write the failing generation command tests**
+
+Update or add command tests so each supported provider returns a provider-specific envelope containing a unified payload, and the command result still returns normalized markdown content.
+
+Representative assertions:
+
+```ts
+expect(result.data).toEqual({
+ content: "# Agent Instructions\n\nGenerated for tests\n",
+ meta: {
+ providerId: "claude",
+ model: "sonnet",
+ },
+});
+```
+
+Add explicit failure cases for:
+
+- payload `{ "ok": false, "error": "..." }`
+- malformed payload JSON
+- valid payload with invalid heading
+
+- [ ] **Step 2: Run the targeted command tests and verify they fail**
+
+Run:
+
+```bash
+pnpm vitest run packages/server/src/__tests__/agent-instructions-command.test.ts
+```
+
+Expected: FAIL because `agent-generator.ts` still calls the Codex-only extractor and `prompt.ts` still requests raw markdown output.
+
+- [ ] **Step 3: Update the generation prompt**
+
+Rewrite `buildAgentInstructionsGenerationPrompt(...)` so it requires exactly one JSON object with:
+
+```json
+{
+ "ok": true,
+ "content": "# Agent Instructions\n..."
+}
+```
+
+and optionally:
+
+```json
+{
+ "ok": false,
+ "error": "..."
+}
+```
+
+Keep the existing fixed section order and required bullet lists.
+
+- [ ] **Step 4: Update the generator flow**
+
+In `agent-generator.ts`:
+
+- keep provider resolution logic
+- run the provider headless command
+- call `extractAgentInstructionsReplyText(provider.id, stdout)`
+- call `parseGeneratedAgentInstructionsPayload(replyText)`
+- return normalized markdown content and metadata
+
+Representative change:
+
+```ts
+const replyText = extractAgentInstructionsReplyText(provider.id, stdout);
+const content = parseGeneratedAgentInstructionsPayload(replyText);
+
+return {
+ content,
+ meta: {
+ providerId: provider.id,
+ model,
+ },
+};
+```
+
+- [ ] **Step 5: Re-run the targeted command tests and verify they pass**
+
+Run:
+
+```bash
+pnpm vitest run packages/server/src/__tests__/agent-instructions-command.test.ts
+```
+
+Expected: PASS
+
+### Task 4: Verify Provider Listing and UI Filtering Regression Coverage
+
+**Files:**
+- Modify: `packages/web/src/features/workspace/views/shared/agent-instructions-section.test.tsx`
+- Modify: `packages/server/src/__tests__/provider-list.test.ts`
+- Modify: `packages/server/src/__tests__/provider-runtime/runtime-status.test.ts`
+
+- [ ] **Step 1: Write the failing listing/filtering assertions**
+
+Update test fixtures so `claude`, `gemini`, and `cursor` can appear as generation-capable providers when runtime-available.
+
+Representative UI assertion:
+
+```ts
+expect(within(providerSelect).getByRole("option", { name: "Claude" })).toBeInTheDocument();
+expect(within(providerSelect).getByRole("option", { name: "Codex" })).toBeInTheDocument();
+```
+
+- [ ] **Step 2: Run the targeted provider listing and UI tests**
+
+Run:
+
+```bash
+pnpm vitest run packages/web/src/features/workspace/views/shared/agent-instructions-section.test.tsx packages/server/src/__tests__/provider-list.test.ts packages/server/src/__tests__/provider-runtime/runtime-status.test.ts
+```
+
+Expected: FAIL where old assumptions still treat only Codex as generation-capable.
+
+- [ ] **Step 3: Adjust fixtures or assertions to match the expanded capability set**
+
+Make the tests assert the intended rule:
+
+- generation-capable means provider advertises `agent_instructions_generate`
+- visible in UI only when runtime-available
+
+Do not change the filtering rule itself unless a failing test shows an actual logic gap.
+
+- [ ] **Step 4: Re-run the targeted provider listing and UI tests**
+
+Run:
+
+```bash
+pnpm vitest run packages/web/src/features/workspace/views/shared/agent-instructions-section.test.tsx packages/server/src/__tests__/provider-list.test.ts packages/server/src/__tests__/provider-runtime/runtime-status.test.ts
+```
+
+Expected: PASS
+
+### Task 5: Full Regression Verification
+
+**Files:**
+- No additional code changes expected
+
+- [ ] **Step 1: Run the full targeted regression suite**
+
+Run:
+
+```bash
+pnpm vitest run \
+ packages/providers/src/claude/definition.test.ts \
+ packages/providers/src/gemini/definition.test.ts \
+ packages/providers/src/cursor/definition.test.ts \
+ packages/server/src/__tests__/agent-instructions/output.test.ts \
+ packages/server/src/__tests__/agent-instructions-command.test.ts \
+ packages/server/src/__tests__/provider-list.test.ts \
+ packages/server/src/__tests__/provider-runtime/runtime-status.test.ts \
+ packages/web/src/features/workspace/views/shared/agent-instructions-section.test.tsx
+```
+
+Expected: PASS
+
+- [ ] **Step 2: Run lint or type-aware validation if the changed packages require it**
+
+Run:
+
+```bash
+pnpm vitest run
+```
+
+If that is too slow or noisy in the current branch, run the repo-standard targeted verification command that covers changed packages and record what was actually run.
+
+- [ ] **Step 3: Review git diff for scope**
+
+Run:
+
+```bash
+git diff -- packages/providers/src packages/server/src packages/web/src
+```
+
+Expected: only provider capability, prompt/parser/generator flow, and related tests change.
diff --git a/docs/superpowers/plans/2026-06-03-provider-work-log-analysis.md b/docs/superpowers/plans/2026-06-03-provider-work-log-analysis.md
new file mode 100644
index 000000000..1f5a7eccc
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-03-provider-work-log-analysis.md
@@ -0,0 +1,2037 @@
+# Provider Work Log Analysis Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Rebuild Work Analysis so it scans each built-in provider's local work logs for the selected workspace/time range instead of reading Coder Studio sessions.
+
+**Architecture:** Add provider-specific log adapters under `packages/server/src/work-analysis/log-sources`, normalize their output into `WorkLogSession[]`, and feed that collection into the existing basic/deep analysis flow. Persist analysis results as last-run records only; explicit runs always rescan provider logs.
+
+**Tech Stack:** TypeScript, Node filesystem APIs, `node:child_process` for injectable `sqlite3` CLI access, Zod, Vitest, React, Jotai, existing WebSocket command architecture
+
+---
+
+## Reference Spec
+
+Read first:
+
+- `docs/superpowers/specs/2026-06-03-provider-work-log-analysis-design.md`
+
+Key behavioral constraints:
+
+- Work analysis must not use `SessionManager` as the source of analyzed work activity.
+- All 5 built-in providers are in scope: `claude`, `codex`, `gemini`, `cursor`, `opencode`.
+- `work.analysis.get` may show the last saved result.
+- `work.analysis.runBasic` and `work.analysis.runDeep` must rescan provider log sources.
+- Deep analysis must use bounded sampled provider evidence, not terminal snapshots.
+
+## File Structure
+
+Create these focused server modules:
+
+- `packages/server/src/work-analysis/log-sources/types.ts`
+ - Shared provider-log types: `BuiltInProviderId`, `WorkLogSession`, `ProviderWorkLogSource`, `ProviderWorkLogDiscovery`, `WorkLogSourceRef`, status/warning/evidence types.
+- `packages/server/src/work-analysis/log-sources/path-encoding.ts`
+ - Helpers for home expansion, workspace path encoding, Cursor md5 workspace hash, JSONL iteration, safe timestamp parsing.
+- `packages/server/src/work-analysis/log-sources/collector.ts`
+ - Runs all provider adapters, sorts sessions, computes `sourceDigest`.
+- `packages/server/src/work-analysis/log-sources/codex.ts`
+ - Reads `~/.codex/sessions/YYYY/MM/DD/*.jsonl`.
+- `packages/server/src/work-analysis/log-sources/claude.ts`
+ - Reads `~/.claude/projects//*.jsonl`.
+- `packages/server/src/work-analysis/log-sources/gemini.ts`
+ - Reads `~/.gemini/tmp|history/` using `.project_root`.
+- `packages/server/src/work-analysis/log-sources/cursor.ts`
+ - Reads `~/.cursor/projects//agent-transcripts`.
+- `packages/server/src/work-analysis/log-sources/opencode.ts`
+ - Reads `~/.local/share/opencode/opencode.db` through an injectable SQLite query runner.
+- `packages/server/src/work-analysis/evidence-sampler.ts`
+ - Converts normalized sessions to bounded `WorkAnalysisEvidence`.
+
+Modify these existing modules:
+
+- `packages/server/src/work-analysis/types.ts`
+- `packages/server/src/work-analysis/basic-schema.ts`
+- `packages/server/src/work-analysis/basic-analyzer.ts`
+- `packages/server/src/work-analysis/service.ts`
+- `packages/server/src/work-analysis/deep-prompt.ts`
+- `packages/server/src/work-analysis/deep-runner.ts`
+- `packages/server/src/storage/repositories/work-analysis-repo.ts`
+- `packages/server/src/server.ts`
+- `packages/web/src/features/work-analysis/types.ts`
+- `packages/web/src/features/settings/components/session-analysis-settings.tsx`
+- `packages/web/src/locales/en.json`
+- `packages/web/src/locales/zh.json`
+- `docs/help/work-analysis.md`
+
+Retire these after the new service is wired:
+
+- `packages/server/src/work-analysis/session-selector.ts`
+- `packages/server/src/work-analysis/evidence-collector.ts`
+- `packages/server/src/__tests__/work-analysis-session-selector.test.ts`
+- `packages/server/src/__tests__/work-analysis-evidence-collector.test.ts`
+
+---
+
+### Task 1: Define Provider Log Types And Shared Helpers
+
+**Files:**
+- Create: `packages/server/src/work-analysis/log-sources/types.ts`
+- Create: `packages/server/src/work-analysis/log-sources/path-encoding.ts`
+- Test: `packages/server/src/__tests__/work-analysis-log-source-helpers.test.ts`
+
+- [ ] **Step 1: Write the failing helper tests**
+
+Create `packages/server/src/__tests__/work-analysis-log-source-helpers.test.ts`:
+
+```ts
+import { describe, expect, it } from "vitest";
+import {
+ buildCursorWorkspaceHash,
+ encodeProviderWorkspacePath,
+ parseOptionalTimestamp,
+ safeJsonParse,
+} from "../work-analysis/log-sources/path-encoding.js";
+
+describe("work analysis log source helpers", () => {
+ it("encodes absolute workspace paths for provider project directories", () => {
+ expect(encodeProviderWorkspacePath("/home/spencer/workspace/coder-studio")).toBe(
+ "-home-spencer-workspace-coder-studio"
+ );
+ });
+
+ it("builds the Cursor md5 workspace hash from the absolute workspace path", () => {
+ expect(buildCursorWorkspaceHash("/home/spencer/workspace/coder-studio")).toBe(
+ "cf4c2089ed329fb5e3bba38e6a05f0bc"
+ );
+ });
+
+ it("parses ISO and numeric timestamps and rejects invalid input", () => {
+ expect(parseOptionalTimestamp("2026-06-03T00:00:00.000Z")).toBe(
+ Date.parse("2026-06-03T00:00:00.000Z")
+ );
+ expect(parseOptionalTimestamp(1_770_000_000_000)).toBe(1_770_000_000_000);
+ expect(parseOptionalTimestamp("not-a-date")).toBeUndefined();
+ });
+
+ it("parses JSON safely without throwing", () => {
+ expect(safeJsonParse<{ ok: boolean }>("{\"ok\":true}")?.ok).toBe(true);
+ expect(safeJsonParse("{bad json")).toBeUndefined();
+ });
+});
+```
+
+- [ ] **Step 2: Run the helper test and verify it fails**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/__tests__/work-analysis-log-source-helpers.test.ts
+```
+
+Expected: FAIL because `log-sources/path-encoding.ts` does not exist.
+
+- [ ] **Step 3: Add shared provider-log types**
+
+Create `packages/server/src/work-analysis/log-sources/types.ts`:
+
+```ts
+import type { ProviderDefinition } from "@coder-studio/core";
+import type { ResolvedWorkAnalysisTimeRange } from "../types.js";
+
+export type BuiltInProviderId = ProviderDefinition["id"] & string;
+
+export type WorkLogProviderStatus =
+ | "supported"
+ | "no_logs"
+ | "missing_root"
+ | "partial"
+ | "unsupported";
+
+export interface WorkLogWarning {
+ code: string;
+ message: string;
+ sourceRef?: string;
+}
+
+export interface WorkLogEvidenceExcerpt {
+ role: "user" | "assistant" | "tool" | "system" | "unknown";
+ at?: number;
+ text?: string;
+ toolName?: string;
+ commandKind?: string;
+ filePath?: string;
+}
+
+export interface WorkLogEvidence {
+ providerId: BuiltInProviderId;
+ sessionId: string;
+ workspacePath: string;
+ title?: string;
+ startedAt: number;
+ lastActiveAt: number;
+ excerpts: WorkLogEvidenceExcerpt[];
+}
+
+export interface WorkLogSession {
+ providerId: BuiltInProviderId;
+ sessionId: string;
+ workspacePath: string;
+ startedAt: number;
+ lastActiveAt: number;
+ sourceRef: string;
+ title?: string;
+ modelId?: string;
+ gitBranch?: string;
+ gitCommit?: string;
+ userTurnCount: number;
+ assistantTurnCount: number;
+ toolUseCount: number;
+ parseErrorCount: number;
+ timestampQuality: "explicit" | "file_mtime" | "mixed";
+ evidence?: WorkLogEvidence[];
+}
+
+export interface WorkLogSourceRef {
+ providerId: BuiltInProviderId;
+ kind: "file" | "sqlite";
+ path: string;
+ mtimeMs?: number;
+ sizeBytes?: number;
+ maxUpdatedAt?: number;
+}
+
+export interface ProviderWorkLogDiscoverInput {
+ workspacePaths: string[];
+ timeRange: ResolvedWorkAnalysisTimeRange;
+}
+
+export interface ProviderWorkLogDiscovery {
+ providerId: BuiltInProviderId;
+ status: WorkLogProviderStatus;
+ sessions: WorkLogSession[];
+ sourceRefs: WorkLogSourceRef[];
+ parseErrorCount: number;
+ warnings: WorkLogWarning[];
+}
+
+export interface ProviderWorkLogSource {
+ providerId: BuiltInProviderId;
+ discover(input: ProviderWorkLogDiscoverInput): Promise;
+}
+
+export interface WorkLogCollection {
+ sessions: WorkLogSession[];
+ providers: ProviderWorkLogDiscovery[];
+ sourceDigest: string;
+}
+
+export interface WorkLogCollector {
+ collect(input: ProviderWorkLogDiscoverInput): Promise;
+}
+```
+
+- [ ] **Step 4: Add shared helper functions**
+
+Create `packages/server/src/work-analysis/log-sources/path-encoding.ts`:
+
+```ts
+import { createHash } from "node:crypto";
+import { homedir } from "node:os";
+
+export function resolveHomePath(path: string, home = homedir()): string {
+ return path.startsWith("~/") ? `${home}/${path.slice(2)}` : path;
+}
+
+export function encodeProviderWorkspacePath(workspacePath: string): string {
+ return workspacePath.replaceAll("/", "-").replaceAll("\\", "-");
+}
+
+export function buildCursorWorkspaceHash(workspacePath: string): string {
+ return createHash("md5").update(workspacePath).digest("hex");
+}
+
+export function parseOptionalTimestamp(value: unknown): number | undefined {
+ if (typeof value === "number" && Number.isFinite(value)) {
+ return value;
+ }
+
+ if (typeof value !== "string" || value.trim().length === 0) {
+ return undefined;
+ }
+
+ const parsed = Date.parse(value);
+ return Number.isNaN(parsed) ? undefined : parsed;
+}
+
+export function safeJsonParse(text: string): T | undefined {
+ try {
+ return JSON.parse(text) as T;
+ } catch {
+ return undefined;
+ }
+}
+
+export function isWithinRange(startedAt: number, lastActiveAt: number, range: {
+ startAt: number;
+ endAt: number;
+}): boolean {
+ return lastActiveAt >= range.startAt && startedAt <= range.endAt;
+}
+```
+
+- [ ] **Step 5: Re-run the helper test and verify it passes**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/__tests__/work-analysis-log-source-helpers.test.ts
+```
+
+Expected: PASS
+
+- [ ] **Step 6: Commit Task 1**
+
+Run:
+
+```bash
+git add packages/server/src/work-analysis/log-sources/types.ts packages/server/src/work-analysis/log-sources/path-encoding.ts packages/server/src/__tests__/work-analysis-log-source-helpers.test.ts
+git commit -m "feat: add work analysis log source types"
+```
+
+---
+
+### Task 2: Implement Codex, Claude, Gemini, And Cursor File Adapters
+
+**Files:**
+- Create: `packages/server/src/work-analysis/log-sources/codex.ts`
+- Create: `packages/server/src/work-analysis/log-sources/claude.ts`
+- Create: `packages/server/src/work-analysis/log-sources/gemini.ts`
+- Create: `packages/server/src/work-analysis/log-sources/cursor.ts`
+- Test: `packages/server/src/__tests__/work-analysis-log-sources-file-adapters.test.ts`
+
+- [ ] **Step 1: Write fixture-building tests for file adapters**
+
+Create `packages/server/src/__tests__/work-analysis-log-sources-file-adapters.test.ts` with temp-home fixtures:
+
+```ts
+import { mkdirSync, writeFileSync } from "node:fs";
+import { mkdtemp } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { describe, expect, it } from "vitest";
+import { createClaudeWorkLogSource } from "../work-analysis/log-sources/claude.js";
+import { createCodexWorkLogSource } from "../work-analysis/log-sources/codex.js";
+import { createCursorWorkLogSource } from "../work-analysis/log-sources/cursor.js";
+import { createGeminiWorkLogSource } from "../work-analysis/log-sources/gemini.js";
+
+async function makeHome() {
+ return await mkdtemp(join(tmpdir(), "work-log-home-"));
+}
+
+describe("file provider work log sources", () => {
+ it("reads Codex sessions by metadata cwd and time range", async () => {
+ const home = await makeHome();
+ const dir = join(home, ".codex/sessions/2026/06/03");
+ mkdirSync(dir, { recursive: true });
+ writeFileSync(
+ join(dir, "session.jsonl"),
+ [
+ JSON.stringify({
+ timestamp: "2026-06-03T01:00:00.000Z",
+ type: "session_meta",
+ payload: {
+ id: "codex-session-1",
+ cwd: "/repo/app",
+ model_provider: "openai",
+ git: { branch: "main", commit_hash: "abc123" },
+ },
+ }),
+ JSON.stringify({ type: "user_message", payload: { text: "fix tests" } }),
+ JSON.stringify({ type: "agent_message", payload: { text: "done" } }),
+ JSON.stringify({ type: "tool_call", payload: { name: "shell" } }),
+ ].join("\n")
+ );
+
+ const result = await createCodexWorkLogSource({ home }).discover({
+ workspacePaths: ["/repo/app"],
+ timeRange: {
+ startAt: Date.parse("2026-06-03T00:00:00.000Z"),
+ endAt: Date.parse("2026-06-04T00:00:00.000Z"),
+ label: "custom",
+ },
+ });
+
+ expect(result.status).toBe("supported");
+ expect(result.sessions).toHaveLength(1);
+ expect(result.sessions[0]).toMatchObject({
+ providerId: "codex",
+ sessionId: "codex-session-1",
+ workspacePath: "/repo/app",
+ userTurnCount: 1,
+ assistantTurnCount: 1,
+ toolUseCount: 1,
+ gitBranch: "main",
+ gitCommit: "abc123",
+ });
+ });
+
+ it("reads Claude sessions from encoded workspace project logs", async () => {
+ const home = await makeHome();
+ const dir = join(home, ".claude/projects/-repo-app");
+ mkdirSync(dir, { recursive: true });
+ writeFileSync(
+ join(dir, "claude.jsonl"),
+ [
+ JSON.stringify({
+ type: "user",
+ timestamp: "2026-06-03T02:00:00.000Z",
+ sessionId: "claude-session-1",
+ cwd: "/repo/app",
+ gitBranch: "feature",
+ }),
+ JSON.stringify({
+ type: "assistant",
+ timestamp: "2026-06-03T02:05:00.000Z",
+ sessionId: "claude-session-1",
+ cwd: "/repo/app",
+ }),
+ ].join("\n")
+ );
+
+ const result = await createClaudeWorkLogSource({ home }).discover({
+ workspacePaths: ["/repo/app"],
+ timeRange: {
+ startAt: Date.parse("2026-06-03T00:00:00.000Z"),
+ endAt: Date.parse("2026-06-04T00:00:00.000Z"),
+ label: "custom",
+ },
+ });
+
+ expect(result.sessions[0]).toMatchObject({
+ providerId: "claude",
+ sessionId: "claude-session-1",
+ workspacePath: "/repo/app",
+ userTurnCount: 1,
+ assistantTurnCount: 1,
+ gitBranch: "feature",
+ });
+ });
+
+ it("reads Gemini chats by .project_root", async () => {
+ const home = await makeHome();
+ const dir = join(home, ".gemini/tmp/app");
+ mkdirSync(join(dir, "chats"), { recursive: true });
+ writeFileSync(join(dir, ".project_root"), "/repo/app");
+ writeFileSync(
+ join(dir, "chats/session-2026-06-03T01-00-abcd.json"),
+ JSON.stringify({
+ kind: "chat",
+ sessionId: "gemini-session-1",
+ startTime: "2026-06-03T03:00:00.000Z",
+ lastUpdated: "2026-06-03T03:10:00.000Z",
+ summary: "Fix tests",
+ messages: [
+ { type: "user", timestamp: "2026-06-03T03:00:00.000Z", content: [{ text: "fix" }] },
+ {
+ type: "assistant",
+ timestamp: "2026-06-03T03:10:00.000Z",
+ content: [{ text: "done" }],
+ },
+ ],
+ })
+ );
+
+ const result = await createGeminiWorkLogSource({ home }).discover({
+ workspacePaths: ["/repo/app"],
+ timeRange: {
+ startAt: Date.parse("2026-06-03T00:00:00.000Z"),
+ endAt: Date.parse("2026-06-04T00:00:00.000Z"),
+ label: "custom",
+ },
+ });
+
+ expect(result.sessions[0]).toMatchObject({
+ providerId: "gemini",
+ sessionId: "gemini-session-1",
+ title: "Fix tests",
+ userTurnCount: 1,
+ assistantTurnCount: 1,
+ });
+ });
+
+ it("reads Cursor transcripts by encoded workspace and reports mtime timestamp quality", async () => {
+ const home = await makeHome();
+ const dir = join(
+ home,
+ ".cursor/projects/-repo-app/agent-transcripts/cursor-session-1"
+ );
+ mkdirSync(dir, { recursive: true });
+ writeFileSync(
+ join(dir, "cursor-session-1.jsonl"),
+ [
+ JSON.stringify({ role: "user", message: { content: [{ type: "text", text: "fix" }] } }),
+ JSON.stringify({
+ role: "assistant",
+ message: { content: [{ type: "tool_call", name: "shell" }] },
+ }),
+ ].join("\n")
+ );
+
+ const result = await createCursorWorkLogSource({ home }).discover({
+ workspacePaths: ["/repo/app"],
+ timeRange: { startAt: 0, endAt: Date.now() + 60_000, label: "custom" },
+ });
+
+ expect(result.sessions[0]).toMatchObject({
+ providerId: "cursor",
+ sessionId: "cursor-session-1",
+ workspacePath: "/repo/app",
+ timestampQuality: "file_mtime",
+ userTurnCount: 1,
+ assistantTurnCount: 1,
+ toolUseCount: 1,
+ });
+ });
+});
+```
+
+- [ ] **Step 2: Run the file adapter tests and verify they fail**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/__tests__/work-analysis-log-sources-file-adapters.test.ts
+```
+
+Expected: FAIL because the adapter modules do not exist.
+
+- [ ] **Step 3: Implement the Codex adapter**
+
+Create `packages/server/src/work-analysis/log-sources/codex.ts` with this API:
+
+```ts
+export function createCodexWorkLogSource(options: { home?: string } = {}): ProviderWorkLogSource
+```
+
+Implementation requirements:
+
+- Scan `join(home, ".codex/sessions")` recursively for `.jsonl`.
+- If the root is missing, return `missing_root`.
+- Parse first valid JSON line for metadata.
+- Match `payload.cwd` or `cwd` against `workspacePaths`.
+- Count user records when `type` or role includes `user`.
+- Count assistant records when `type` or role includes `assistant` or `agent_message`.
+- Count tool records when `type` or payload contains a tool-like event.
+- Use explicit timestamp when available; otherwise file mtime.
+- Return `partial` when any matched file has parse errors.
+
+- [ ] **Step 4: Implement the Claude adapter**
+
+Create `packages/server/src/work-analysis/log-sources/claude.ts` with this API:
+
+```ts
+export function createClaudeWorkLogSource(options: { home?: string } = {}): ProviderWorkLogSource
+```
+
+Implementation requirements:
+
+- For each workspace path, scan `~/.claude/projects/${encodeProviderWorkspacePath(path)}`.
+- Match records whose `cwd` equals the workspace path when `cwd` is present.
+- Group lines by `sessionId`; if no `sessionId`, use the file basename.
+- Use min/max explicit timestamps for start/end.
+- Count roles from `type`, `role`, and `message.role`.
+- Treat records with `toolUse` or attachment/tool fields as tool activity.
+
+- [ ] **Step 5: Implement the Gemini adapter**
+
+Create `packages/server/src/work-analysis/log-sources/gemini.ts` with this API:
+
+```ts
+export function createGeminiWorkLogSource(options: { home?: string } = {}): ProviderWorkLogSource
+```
+
+Implementation requirements:
+
+- Scan both `~/.gemini/tmp` and `~/.gemini/history`.
+- Only read project directories whose `.project_root` exactly matches a selected workspace path.
+- Read `chats/*.json`.
+- Use `sessionId`, `startTime`, `lastUpdated`, `summary`, and `messages`.
+- Count `messages[].type` values.
+- Include short evidence excerpts from `messages[].content[].text` when present.
+
+- [ ] **Step 6: Implement the Cursor adapter**
+
+Create `packages/server/src/work-analysis/log-sources/cursor.ts` with this API:
+
+```ts
+export function createCursorWorkLogSource(options: { home?: string } = {}): ProviderWorkLogSource
+```
+
+Implementation requirements:
+
+- For each workspace path, scan `~/.cursor/projects/${encodeProviderWorkspacePath(path)}/agent-transcripts`.
+- Read `*/.jsonl`.
+- Use transcript directory/file name as `sessionId`.
+- Use file mtime for `startedAt` and `lastActiveAt`.
+- Set `timestampQuality: "file_mtime"`.
+- Count `role === "user"` and `role === "assistant"`.
+- Count tool usage when content type/name includes `tool`, `command`, or `function`.
+
+- [ ] **Step 7: Re-run the file adapter tests and verify they pass**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/__tests__/work-analysis-log-sources-file-adapters.test.ts
+```
+
+Expected: PASS
+
+- [ ] **Step 8: Commit Task 2**
+
+Run:
+
+```bash
+git add packages/server/src/work-analysis/log-sources/codex.ts packages/server/src/work-analysis/log-sources/claude.ts packages/server/src/work-analysis/log-sources/gemini.ts packages/server/src/work-analysis/log-sources/cursor.ts packages/server/src/__tests__/work-analysis-log-sources-file-adapters.test.ts
+git commit -m "feat: read provider work logs from file sources"
+```
+
+---
+
+### Task 3: Implement The OpenCode SQLite Adapter
+
+**Files:**
+- Create: `packages/server/src/work-analysis/log-sources/opencode.ts`
+- Test: `packages/server/src/__tests__/work-analysis-log-source-opencode.test.ts`
+
+- [ ] **Step 1: Write the failing OpenCode adapter test**
+
+Create `packages/server/src/__tests__/work-analysis-log-source-opencode.test.ts`:
+
+```ts
+import { execFileSync } from "node:child_process";
+import { mkdirSync } from "node:fs";
+import { mkdtemp } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { describe, expect, it } from "vitest";
+import { createOpenCodeWorkLogSource } from "../work-analysis/log-sources/opencode.js";
+
+async function createDbFixture() {
+ const home = await mkdtemp(join(tmpdir(), "opencode-home-"));
+ const dir = join(home, ".local/share/opencode");
+ mkdirSync(dir, { recursive: true });
+ const dbPath = join(dir, "opencode.db");
+ execFileSync("sqlite3", [
+ dbPath,
+ `
+ create table project (
+ id text primary key,
+ worktree text not null,
+ time_created integer not null,
+ time_updated integer not null
+ );
+ create table session (
+ id text primary key,
+ project_id text not null,
+ directory text not null,
+ title text not null,
+ version text not null,
+ summary_files integer,
+ summary_additions integer,
+ summary_deletions integer,
+ time_created integer not null,
+ time_updated integer not null
+ );
+ create table message (
+ id text primary key,
+ session_id text not null,
+ time_created integer not null,
+ time_updated integer not null,
+ data text not null
+ );
+ create table part (
+ id text primary key,
+ message_id text not null,
+ session_id text not null,
+ time_created integer not null,
+ time_updated integer not null,
+ data text not null
+ );
+ insert into project values ('proj-1', '/repo/app', 1000, 3000);
+ insert into session values ('ses-1', 'proj-1', '/repo/app', 'Fix tests', '1.2.15', 2, 10, 1, 1000, 3000);
+ insert into message values ('msg-1', 'ses-1', 1000, 1000, '{"role":"user","text":"fix"}');
+ insert into message values ('msg-2', 'ses-1', 2000, 3000, '{"role":"assistant","text":"done"}');
+ insert into part values ('part-1', 'msg-2', 'ses-1', 2500, 2500, '{"type":"tool","tool":"bash"}');
+ `,
+ ]);
+ return home;
+}
+
+describe("OpenCode work log source", () => {
+ it("reads sessions from the OpenCode SQLite database by workspace path", async () => {
+ const home = await createDbFixture();
+
+ const result = await createOpenCodeWorkLogSource({ home }).discover({
+ workspacePaths: ["/repo/app"],
+ timeRange: { startAt: 0, endAt: 5_000, label: "custom" },
+ });
+
+ expect(result.status).toBe("supported");
+ expect(result.sessions).toHaveLength(1);
+ expect(result.sessions[0]).toMatchObject({
+ providerId: "opencode",
+ sessionId: "ses-1",
+ workspacePath: "/repo/app",
+ title: "Fix tests",
+ userTurnCount: 1,
+ assistantTurnCount: 1,
+ toolUseCount: 1,
+ timestampQuality: "explicit",
+ });
+ expect(result.sourceRefs[0]).toMatchObject({
+ providerId: "opencode",
+ kind: "sqlite",
+ });
+ });
+});
+```
+
+- [ ] **Step 2: Run the OpenCode test and verify it fails**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/__tests__/work-analysis-log-source-opencode.test.ts
+```
+
+Expected: FAIL because `opencode.ts` does not exist.
+
+- [ ] **Step 3: Implement an injectable SQLite query runner**
+
+Create `packages/server/src/work-analysis/log-sources/opencode.ts` with:
+
+```ts
+import { execFile } from "node:child_process";
+import { promisify } from "node:util";
+
+const execFileAsync = promisify(execFile);
+
+export type OpenCodeSqliteRunner = (dbPath: string, sql: string) => Promise;
+
+export async function runSqliteJsonQuery(dbPath: string, sql: string): Promise {
+ const { stdout } = await execFileAsync("sqlite3", ["-json", dbPath, sql], {
+ windowsHide: true,
+ maxBuffer: 10 * 1024 * 1024,
+ });
+ return stdout;
+}
+```
+
+Testing/runtime notes:
+
+- In adapter tests, gate the real `sqlite3` fixture with `if (!hasSqlite3())` and `it.skip(...)` when the CLI is unavailable.
+- Add a small `hasSqlite3()` helper that probes `execFileSync("sqlite3", ["-version"])`.
+- In production code, catch `ENOENT` from `sqlite3` launch and return provider status `unsupported` with a warning that the SQLite CLI is unavailable.
+
+- [ ] **Step 4: Implement `createOpenCodeWorkLogSource`**
+
+Implementation requirements:
+
+- Accept options `{ home?: string; sqliteRunner?: OpenCodeSqliteRunner }`.
+- Look for `~/.local/share/opencode/opencode.db`.
+- Return `missing_root` when the DB does not exist.
+- Query sessions joined to projects:
+
+```sql
+select
+ s.id as sessionId,
+ p.worktree as worktree,
+ s.directory as directory,
+ s.title as title,
+ s.version as version,
+ s.summary_files as summaryFiles,
+ s.summary_additions as summaryAdditions,
+ s.summary_deletions as summaryDeletions,
+ s.time_created as startedAt,
+ s.time_updated as lastActiveAt,
+ (
+ select count(*) from message m
+ where m.session_id = s.id and lower(m.data) like '%"role":"user"%'
+ ) as userTurnCount,
+ (
+ select count(*) from message m
+ where m.session_id = s.id and lower(m.data) like '%"role":"assistant"%'
+ ) as assistantTurnCount,
+ (
+ select count(*) from part p2
+ where p2.session_id = s.id and lower(p2.data) like '%tool%'
+ ) as toolUseCount
+from session s
+join project p on p.id = s.project_id
+where
+ (p.worktree in (__WORKSPACES__) or s.directory in (__WORKSPACES__))
+ and s.time_updated >= __START__
+ and s.time_created <= __END__
+order by s.time_updated asc;
+```
+
+- Build the workspace `in (...)` list by SQL-escaping single quotes.
+- Parse `sqlite3 -json` output with `JSON.parse`.
+- Use explicit session timestamps.
+- Return `partial` with a warning if the query fails.
+
+- [ ] **Step 5: Re-run the OpenCode test and verify it passes**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/__tests__/work-analysis-log-source-opencode.test.ts
+```
+
+Expected: PASS
+
+- [ ] **Step 6: Commit Task 3**
+
+Run:
+
+```bash
+git add packages/server/src/work-analysis/log-sources/opencode.ts packages/server/src/__tests__/work-analysis-log-source-opencode.test.ts
+git commit -m "feat: read opencode work logs from sqlite"
+```
+
+---
+
+### Task 4: Add The Work Log Collector And Source Digest
+
+**Files:**
+- Create: `packages/server/src/work-analysis/log-sources/collector.ts`
+- Test: `packages/server/src/__tests__/work-analysis-log-collector.test.ts`
+
+- [ ] **Step 1: Write the failing collector tests**
+
+Create `packages/server/src/__tests__/work-analysis-log-collector.test.ts`:
+
+```ts
+import { describe, expect, it } from "vitest";
+import { createWorkLogCollector } from "../work-analysis/log-sources/collector.js";
+import type { ProviderWorkLogSource } from "../work-analysis/log-sources/types.js";
+
+function source(input: Awaited>): ProviderWorkLogSource {
+ return {
+ providerId: input.providerId,
+ discover: async () => input,
+ };
+}
+
+describe("WorkLogCollector", () => {
+ it("runs sources, sorts sessions, and reports provider statuses", async () => {
+ const collector = createWorkLogCollector({
+ sources: [
+ source({
+ providerId: "codex",
+ status: "supported",
+ parseErrorCount: 0,
+ warnings: [],
+ sourceRefs: [{ providerId: "codex", kind: "file", path: "/b", mtimeMs: 2, sizeBytes: 20 }],
+ sessions: [
+ {
+ providerId: "codex",
+ sessionId: "b",
+ workspacePath: "/repo",
+ startedAt: 20,
+ lastActiveAt: 30,
+ sourceRef: "/b",
+ userTurnCount: 0,
+ assistantTurnCount: 0,
+ toolUseCount: 0,
+ parseErrorCount: 0,
+ timestampQuality: "explicit",
+ },
+ ],
+ }),
+ source({
+ providerId: "claude",
+ status: "no_logs",
+ parseErrorCount: 0,
+ warnings: [],
+ sourceRefs: [],
+ sessions: [],
+ }),
+ ],
+ });
+
+ const result = await collector.collect({
+ workspacePaths: ["/repo"],
+ timeRange: { startAt: 0, endAt: 100, label: "custom" },
+ });
+
+ expect(result.sessions.map((session) => session.sessionId)).toEqual(["b"]);
+ expect(result.providers.map((provider) => provider.providerId)).toEqual(["codex", "claude"]);
+ expect(result.sourceDigest).toMatch(/^[a-f0-9]{64}$/);
+ });
+
+ it("changes sourceDigest when source refs change", async () => {
+ const left = await createWorkLogCollector({
+ sources: [
+ source({
+ providerId: "codex",
+ status: "supported",
+ parseErrorCount: 0,
+ warnings: [],
+ sourceRefs: [{ providerId: "codex", kind: "file", path: "/a", mtimeMs: 1, sizeBytes: 10 }],
+ sessions: [],
+ }),
+ ],
+ }).collect({ workspacePaths: ["/repo"], timeRange: { startAt: 0, endAt: 1, label: "x" } });
+
+ const right = await createWorkLogCollector({
+ sources: [
+ source({
+ providerId: "codex",
+ status: "supported",
+ parseErrorCount: 0,
+ warnings: [],
+ sourceRefs: [{ providerId: "codex", kind: "file", path: "/a", mtimeMs: 2, sizeBytes: 10 }],
+ sessions: [],
+ }),
+ ],
+ }).collect({ workspacePaths: ["/repo"], timeRange: { startAt: 0, endAt: 1, label: "x" } });
+
+ expect(left.sourceDigest).not.toBe(right.sourceDigest);
+ });
+});
+```
+
+- [ ] **Step 2: Run the collector tests and verify they fail**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/__tests__/work-analysis-log-collector.test.ts
+```
+
+Expected: FAIL because `collector.ts` does not exist.
+
+- [ ] **Step 3: Implement the collector**
+
+Create `packages/server/src/work-analysis/log-sources/collector.ts`:
+
+```ts
+import { createHash } from "node:crypto";
+import type {
+ ProviderWorkLogSource,
+ WorkLogCollection,
+ WorkLogCollector,
+ WorkLogSourceRef,
+} from "./types.js";
+
+function buildSourceDigest(input: {
+ sourceRefs: WorkLogSourceRef[];
+ sessionIds: string[];
+}): string {
+ return createHash("sha256")
+ .update(
+ JSON.stringify({
+ sourceRefs: [...input.sourceRefs].sort((left, right) =>
+ `${left.providerId}:${left.path}`.localeCompare(`${right.providerId}:${right.path}`)
+ ),
+ sessionIds: [...input.sessionIds].sort(),
+ })
+ )
+ .digest("hex");
+}
+
+export function createWorkLogCollector(deps: { sources: ProviderWorkLogSource[] }): WorkLogCollector {
+ return {
+ async collect(input: Parameters[0]): Promise {
+ const providers = await Promise.all(deps.sources.map((source) => source.discover(input)));
+ const sessions = providers
+ .flatMap((provider) => provider.sessions)
+ .sort(
+ (left, right) =>
+ left.lastActiveAt - right.lastActiveAt ||
+ left.providerId.localeCompare(right.providerId) ||
+ left.sessionId.localeCompare(right.sessionId)
+ );
+
+ return {
+ sessions,
+ providers,
+ sourceDigest: buildSourceDigest({
+ sourceRefs: providers.flatMap((provider) => provider.sourceRefs),
+ sessionIds: sessions.map((session) => `${session.providerId}:${session.sessionId}`),
+ }),
+ };
+ },
+ };
+}
+```
+
+- [ ] **Step 4: Re-run the collector tests and verify they pass**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/__tests__/work-analysis-log-collector.test.ts
+```
+
+Expected: PASS
+
+- [ ] **Step 5: Commit Task 4**
+
+Run:
+
+```bash
+git add packages/server/src/work-analysis/log-sources/collector.ts packages/server/src/__tests__/work-analysis-log-collector.test.ts
+git commit -m "feat: collect provider work log sessions"
+```
+
+---
+
+### Task 5: Extend Analysis Types, Schema, Repo, And Basic Aggregation
+
+**Files:**
+- Modify: `packages/server/src/work-analysis/types.ts`
+- Modify: `packages/server/src/work-analysis/basic-schema.ts`
+- Modify: `packages/server/src/work-analysis/basic-analyzer.ts`
+- Modify: `packages/server/src/storage/repositories/work-analysis-repo.ts`
+- Modify: `packages/web/src/features/work-analysis/types.ts`
+- Test: `packages/server/src/__tests__/work-analysis-basic-analyzer.test.ts`
+- Test: `packages/server/src/__tests__/work-analysis-repo.test.ts`
+
+- [ ] **Step 1: Update the failing basic analyzer tests**
+
+Modify `packages/server/src/__tests__/work-analysis-basic-analyzer.test.ts` so sessions include provider-log metrics:
+
+```ts
+{
+ sessionId: "sess-1",
+ workspacePath: "/repo/app",
+ providerId: "codex",
+ startedAt: Date.UTC(2026, 0, 1, 18, 0, 0),
+ lastActiveAt: Date.UTC(2026, 0, 1, 18, 30, 0),
+ userTurnCount: 2,
+ assistantTurnCount: 2,
+ toolUseCount: 1,
+ parseErrorCount: 0,
+ timestampQuality: "explicit" as const,
+}
+```
+
+Add provider source status input:
+
+```ts
+dataSources: {
+ providers: [
+ {
+ providerId: "codex",
+ status: "supported",
+ sessionCount: 2,
+ parseErrorCount: 0,
+ warningCount: 0,
+ },
+ {
+ providerId: "cursor",
+ status: "no_logs",
+ sessionCount: 0,
+ parseErrorCount: 0,
+ warningCount: 0,
+ },
+ ],
+}
+```
+
+Add assertions:
+
+```ts
+expect(result.executionSignals).toEqual({
+ sessionsWithActivity: 3,
+ userTurnCount: 5,
+ assistantTurnCount: 4,
+ toolUseCount: 2,
+ fileMtimeTimestampCount: 1,
+});
+expect(result.dataSources.providers).toEqual([
+ {
+ providerId: "codex",
+ status: "supported",
+ sessionCount: 2,
+ parseErrorCount: 0,
+ warningCount: 0,
+ },
+ {
+ providerId: "cursor",
+ status: "no_logs",
+ sessionCount: 0,
+ parseErrorCount: 0,
+ warningCount: 0,
+ },
+]);
+```
+
+- [ ] **Step 2: Add repo persistence tests for source snapshots**
+
+Modify `packages/server/src/__tests__/work-analysis-repo.test.ts` to persist and reload:
+
+```ts
+sourceSnapshot: {
+ sourceDigest: "digest-source",
+ collectedAt: 1_234,
+ providerStatuses: [
+ { providerId: "codex", status: "supported", sessionCount: 1, parseErrorCount: 0 },
+ ],
+},
+```
+
+Assert the reloaded record includes `sourceSnapshot`.
+
+- [ ] **Step 3: Run targeted tests and verify they fail**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/__tests__/work-analysis-basic-analyzer.test.ts src/__tests__/work-analysis-repo.test.ts
+```
+
+Expected: FAIL because schemas and types do not include the new fields.
+
+- [ ] **Step 4: Extend server analysis result types**
+
+Modify `packages/server/src/work-analysis/types.ts`:
+
+- Add `WorkAnalysisSourceSnapshot`.
+- Add `sourceSnapshot?: WorkAnalysisSourceSnapshot` to `WorkAnalysisRecord`.
+- Add `dataSources` and expanded `executionSignals` to `WorkBasicAnalysisResult`.
+- Change basic analysis session input shape indirectly through `basic-analyzer.ts`; do not expose Coder Studio `workspaceId` as session identity.
+
+Representative additions:
+
+```ts
+export interface WorkAnalysisSourceSnapshot {
+ sourceDigest: string;
+ providerStatuses: Array<{
+ providerId: string;
+ status: string;
+ sessionCount: number;
+ parseErrorCount: number;
+ }>;
+ collectedAt: number;
+}
+```
+
+- [ ] **Step 5: Extend the Zod schema**
+
+Modify `packages/server/src/work-analysis/basic-schema.ts`:
+
+```ts
+dataSources: z.object({
+ providers: z.array(
+ z.object({
+ providerId: z.string(),
+ status: z.enum(["supported", "no_logs", "missing_root", "partial", "unsupported"]),
+ sessionCount: nonNegativeIntegerSchema,
+ parseErrorCount: nonNegativeIntegerSchema,
+ warningCount: nonNegativeIntegerSchema,
+ })
+ ),
+}),
+executionSignals: z.object({
+ sessionsWithActivity: nonNegativeIntegerSchema,
+ userTurnCount: nonNegativeIntegerSchema,
+ assistantTurnCount: nonNegativeIntegerSchema,
+ toolUseCount: nonNegativeIntegerSchema,
+ fileMtimeTimestampCount: nonNegativeIntegerSchema,
+}),
+```
+
+- [ ] **Step 6: Update `analyzeWorkBasic`**
+
+Modify `packages/server/src/work-analysis/basic-analyzer.ts` so the input session type uses:
+
+```ts
+type BasicAnalyzerSession = {
+ sessionId: string;
+ workspacePath: string;
+ providerId: string;
+ startedAt: number;
+ lastActiveAt: number;
+ userTurnCount: number;
+ assistantTurnCount: number;
+ toolUseCount: number;
+ parseErrorCount: number;
+ timestampQuality: "explicit" | "file_mtime" | "mixed";
+};
+```
+
+Update output calculation:
+
+```ts
+const userTurnCount = input.sessions.reduce((sum, session) => sum + session.userTurnCount, 0);
+const assistantTurnCount = input.sessions.reduce(
+ (sum, session) => sum + session.assistantTurnCount,
+ 0
+);
+const toolUseCount = input.sessions.reduce((sum, session) => sum + session.toolUseCount, 0);
+const fileMtimeTimestampCount = input.sessions.filter(
+ (session) => session.timestampQuality === "file_mtime"
+).length;
+```
+
+Pass `input.dataSources` through to the parsed result.
+
+- [ ] **Step 7: Update repo normalization**
+
+Modify `packages/server/src/storage/repositories/work-analysis-repo.ts`:
+
+- Accept optional `sourceSnapshot` in `isWorkAnalysisRecord`.
+- Preserve it in `normalizeRecord`.
+
+Representative check:
+
+```ts
+function isSourceSnapshot(value: unknown): value is WorkAnalysisRecord["sourceSnapshot"] {
+ if (!isRecord(value)) return false;
+ return (
+ typeof value.sourceDigest === "string" &&
+ typeof value.collectedAt === "number" &&
+ Array.isArray(value.providerStatuses)
+ );
+}
+```
+
+- [ ] **Step 8: Update frontend work-analysis types**
+
+Modify `packages/web/src/features/work-analysis/types.ts` with the same new fields:
+
+- `sourceSnapshot?: { sourceDigest: string; collectedAt: number; providerStatuses: Array<{ providerId: string; status: string; sessionCount: number; parseErrorCount: number }> }`
+- `basicResult.dataSources.providers`
+- expanded `executionSignals`
+
+- [ ] **Step 9: Re-run targeted tests and typecheck server**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/__tests__/work-analysis-basic-analyzer.test.ts src/__tests__/work-analysis-repo.test.ts
+pnpm --filter @coder-studio/server exec tsc -p tsconfig.json --noEmit
+```
+
+Expected: PASS
+
+- [ ] **Step 10: Commit Task 5**
+
+Run:
+
+```bash
+git add packages/server/src/work-analysis/types.ts packages/server/src/work-analysis/basic-schema.ts packages/server/src/work-analysis/basic-analyzer.ts packages/server/src/storage/repositories/work-analysis-repo.ts packages/web/src/features/work-analysis/types.ts packages/server/src/__tests__/work-analysis-basic-analyzer.test.ts packages/server/src/__tests__/work-analysis-repo.test.ts
+git commit -m "feat: aggregate provider work log metrics"
+```
+
+---
+
+### Task 6: Replace Session-Based WorkAnalysisService With Log Collection
+
+**Files:**
+- Modify: `packages/server/src/work-analysis/service.ts`
+- Modify: `packages/server/src/server.ts`
+- Test: `packages/server/src/__tests__/work-analysis-service.test.ts`
+
+- [ ] **Step 1: Rewrite service tests around `workLogCollector`**
+
+Modify `packages/server/src/__tests__/work-analysis-service.test.ts`.
+
+Replace the old cache test with:
+
+```ts
+it("rescans provider logs when running basic analysis even if a previous result succeeded", async () => {
+ const upsert = vi.fn((record) => record);
+ const collect = vi.fn(async () => ({
+ sourceDigest: "source-1",
+ providers: [
+ {
+ providerId: "codex",
+ status: "supported",
+ sessions: [],
+ sourceRefs: [],
+ parseErrorCount: 0,
+ warnings: [],
+ },
+ ],
+ sessions: [],
+ }));
+
+ const service = new WorkAnalysisService({
+ repo: {
+ findByQueryDigest: vi.fn(() => ({
+ id: "analysis-1",
+ queryDigest: "digest-1",
+ workspaceIds: ["ws-1"],
+ timeRange: { preset: "7d" as const },
+ basicStatus: "succeeded" as const,
+ deepStatus: "idle" as const,
+ })),
+ upsert,
+ },
+ workspaceMgr: { get: vi.fn(() => ({ id: "ws-1", path: "/repo/app" })) },
+ workLogCollector: { collect },
+ skillLibraryRepo: { list: vi.fn(() => []) },
+ skillMountRepo: { list: vi.fn(() => []) },
+ deepRunner: { run: vi.fn() },
+ now: () => 1_000,
+ });
+
+ await service.runBasic({ workspaceIds: ["ws-1"], timeRange: { preset: "7d" } });
+
+ expect(collect).toHaveBeenCalledWith({
+ workspacePaths: ["/repo/app"],
+ timeRange: expect.any(Object),
+ });
+ expect(upsert).toHaveBeenCalled();
+});
+```
+
+Update deep tests so `workLogCollector.collect` returns a session with `evidence`, and `deepRunner.run` receives sampled provider evidence instead of terminal snapshots.
+
+- [ ] **Step 2: Run service tests and verify they fail**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/__tests__/work-analysis-service.test.ts
+```
+
+Expected: FAIL because `WorkAnalysisService` still expects `sessionSelector` and `evidenceCollector`.
+
+- [ ] **Step 3: Change `WorkAnalysisServiceDeps`**
+
+Modify `packages/server/src/work-analysis/service.ts` dependency shape:
+
+```ts
+export interface WorkAnalysisServiceDeps {
+ repo: {
+ findByQueryDigest(queryDigest: string): WorkAnalysisRecord | undefined;
+ upsert(record: WorkAnalysisRecord): WorkAnalysisRecord;
+ };
+ workspaceMgr: Pick;
+ workLogCollector: Pick;
+ skillLibraryRepo: Pick;
+ skillMountRepo: Pick;
+ basicAnalyzer?: typeof analyzeWorkBasic;
+ deepRunner: Pick;
+ now?: () => number;
+}
+```
+
+Do not import `session-selector.ts` or `evidence-collector.ts`.
+
+- [ ] **Step 4: Resolve workspace IDs to paths**
+
+Add a helper inside `service.ts`:
+
+```ts
+private resolveWorkspacePaths(workspaceIds: string[]): {
+ workspacePaths: string[];
+ missingWorkspaceIds: string[];
+} {
+ const workspacePaths: string[] = [];
+ const missingWorkspaceIds: string[] = [];
+
+ for (const workspaceId of workspaceIds) {
+ const path = this.deps.workspaceMgr.get(workspaceId)?.path;
+ if (typeof path === "string" && path.length > 0) {
+ workspacePaths.push(path);
+ } else {
+ missingWorkspaceIds.push(workspaceId);
+ }
+ }
+
+ return { workspacePaths, missingWorkspaceIds };
+}
+```
+
+If any selected workspace id cannot be resolved, fail the run instead of silently dropping it. Throw:
+
+```ts
+{
+ code: "work_analysis_workspace_unavailable",
+ message: `Some selected workspaces were unavailable for work analysis: ${missingWorkspaceIds.join(", ")}`,
+}
+```
+
+- [ ] **Step 5: Add a private collection helper and rework `runBasic`**
+
+Add a private helper that both `runBasic` and `runDeep` can call:
+
+```ts
+private async collectForQuery(input: {
+ normalized: WorkAnalysisQuery;
+ timeRange: ResolvedWorkAnalysisTimeRange;
+}) {
+ const { workspacePaths, missingWorkspaceIds } = this.resolveWorkspacePaths(
+ input.normalized.workspaceIds
+ );
+ if (missingWorkspaceIds.length > 0) {
+ throw {
+ code: "work_analysis_workspace_unavailable",
+ message: `Some selected workspaces were unavailable for work analysis: ${missingWorkspaceIds.join(", ")}`,
+ };
+ }
+
+ const collection = await this.deps.workLogCollector.collect({
+ workspacePaths,
+ timeRange: input.timeRange,
+ });
+ const skillInventory = {
+ installedSkills: this.deps.skillLibraryRepo.list(),
+ mounts: this.deps.skillMountRepo.list(),
+ };
+
+ return { workspacePaths, collection, skillInventory };
+}
+```
+
+Behavior:
+
+- Always create/update a running record.
+- Always call `collectForQuery(...)`.
+- Pass normalized sessions to `basicAnalyzer`.
+- Save `sourceSnapshot`.
+- Do not return early for existing `basicStatus === "succeeded"`.
+
+Provider status mapping:
+
+```ts
+const dataSources = {
+ providers: collection.providers.map((provider) => ({
+ providerId: provider.providerId,
+ status: provider.status,
+ sessionCount: provider.sessions.length,
+ parseErrorCount: provider.parseErrorCount,
+ warningCount: provider.warnings.length,
+ })),
+};
+```
+
+- [ ] **Step 6: Rework `runDeep`**
+
+Behavior:
+
+- Do not call `runBasic(query)` and then scan again.
+- Use a private `runBasicWithCollection(query)` helper that returns `{ record, collection, skillInventory, workspacePaths }`.
+- Public `runBasic(query)` should call `runBasicWithCollection(query)` and return `record`.
+- Public `runDeep(query)` should call `runBasicWithCollection(query)` once, then use that same `collection` for evidence sampling.
+- Add a private `buildEvidenceFromWorkLogSessions(...)` helper in `service.ts` for this task:
+
+```ts
+private buildEvidenceFromWorkLogSessions(input: {
+ sessions: WorkLogSession[];
+ skillInventory: WorkAnalysisEvidence["skillInventory"];
+}): WorkAnalysisEvidence {
+ return {
+ sessions: input.sessions.slice(0, 12).map((session) => {
+ const evidence = session.evidence?.[0];
+ return {
+ providerId: session.providerId,
+ sessionId: session.sessionId,
+ workspacePath: session.workspacePath,
+ title: session.title,
+ startedAt: session.startedAt,
+ lastActiveAt: session.lastActiveAt,
+ excerpts: (evidence?.excerpts ?? []).slice(0, 8),
+ };
+ }),
+ skillInventory: input.skillInventory,
+ };
+}
+```
+
+- Do not add a session-count-based `resolveDeepProviderId(...)` helper in `service.ts`. Deep execution provider selection belongs in `deep-runner.ts`, where the runner can prefer a provider inferred from sampled evidence but safely fall back to any configured provider that supports `session_analysis`.
+
+- [ ] **Step 7: Wire the new collector in `server.ts`**
+
+Modify imports:
+
+```ts
+import { createWorkLogCollector } from "./work-analysis/log-sources/collector.js";
+import { createClaudeWorkLogSource } from "./work-analysis/log-sources/claude.js";
+import { createCodexWorkLogSource } from "./work-analysis/log-sources/codex.js";
+import { createCursorWorkLogSource } from "./work-analysis/log-sources/cursor.js";
+import { createGeminiWorkLogSource } from "./work-analysis/log-sources/gemini.js";
+import { createOpenCodeWorkLogSource } from "./work-analysis/log-sources/opencode.js";
+```
+
+Construct:
+
+```ts
+const builtInProviderIds = new Set(providerRegistry.map((provider) => provider.id));
+
+const workLogCollector = createWorkLogCollector({
+ sources: [
+ ...(builtInProviderIds.has("claude") ? [createClaudeWorkLogSource()] : []),
+ ...(builtInProviderIds.has("codex") ? [createCodexWorkLogSource()] : []),
+ ...(builtInProviderIds.has("gemini") ? [createGeminiWorkLogSource()] : []),
+ ...(builtInProviderIds.has("cursor") ? [createCursorWorkLogSource()] : []),
+ ...(builtInProviderIds.has("opencode") ? [createOpenCodeWorkLogSource()] : []),
+ ],
+});
+```
+
+Pass into service:
+
+```ts
+const workAnalysisService = new WorkAnalysisService({
+ repo: workAnalysisRepo,
+ workspaceMgr: {
+ get: (workspaceId) => workspaceMgr.get(workspaceId),
+ } as WorkspaceManager,
+ workLogCollector,
+ skillLibraryRepo,
+ skillMountRepo,
+ deepRunner: new WorkDeepAnalysisRunner({
+ providerRegistry: activeProviderRegistry,
+ providerConfigRepo,
+ }),
+});
+```
+
+- [ ] **Step 8: Re-run service tests and server typecheck**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/__tests__/work-analysis-service.test.ts
+pnpm --filter @coder-studio/server exec tsc -p tsconfig.json --noEmit
+```
+
+Expected: PASS
+
+- [ ] **Step 9: Commit Task 6**
+
+Run:
+
+```bash
+git add packages/server/src/work-analysis/service.ts packages/server/src/server.ts packages/server/src/__tests__/work-analysis-service.test.ts
+git commit -m "feat: run work analysis from provider logs"
+```
+
+---
+
+### Task 7: Add Evidence Sampling And Deep Provider Selection
+
+**Files:**
+- Create: `packages/server/src/work-analysis/evidence-sampler.ts`
+- Modify: `packages/server/src/work-analysis/types.ts`
+- Modify: `packages/server/src/work-analysis/deep-prompt.ts`
+- Modify: `packages/server/src/work-analysis/deep-runner.ts`
+- Modify: `packages/server/src/work-analysis/service.ts`
+- Test: `packages/server/src/__tests__/work-analysis-evidence-sampler.test.ts`
+- Test: `packages/server/src/__tests__/work-analysis-deep-runner.test.ts`
+- Test: `packages/server/src/__tests__/work-analysis-service.test.ts`
+
+- [ ] **Step 1: Write the failing evidence sampler tests**
+
+Create `packages/server/src/__tests__/work-analysis-evidence-sampler.test.ts`:
+
+```ts
+import { describe, expect, it } from "vitest";
+import { sampleWorkLogEvidence } from "../work-analysis/evidence-sampler.js";
+import type { WorkLogSession } from "../work-analysis/log-sources/types.js";
+
+function session(id: string, providerId: WorkLogSession["providerId"], lastActiveAt: number): WorkLogSession {
+ return {
+ providerId,
+ sessionId: id,
+ workspacePath: "/repo/app",
+ startedAt: lastActiveAt - 100,
+ lastActiveAt,
+ sourceRef: `/logs/${id}`,
+ title: id,
+ userTurnCount: 1,
+ assistantTurnCount: 1,
+ toolUseCount: 1,
+ parseErrorCount: 0,
+ timestampQuality: "explicit",
+ evidence: [
+ {
+ providerId,
+ sessionId: id,
+ workspacePath: "/repo/app",
+ startedAt: lastActiveAt - 100,
+ lastActiveAt,
+ excerpts: [
+ { role: "user", text: "x".repeat(1000) },
+ { role: "tool", toolName: "shell", commandKind: "test" },
+ ],
+ },
+ ],
+ };
+}
+
+describe("sampleWorkLogEvidence", () => {
+ it("caps excerpts and truncates long text", () => {
+ const result = sampleWorkLogEvidence({
+ sessions: [session("s1", "codex", 100)],
+ skillInventory: { installedSkills: [], mounts: [] },
+ maxSessions: 1,
+ maxExcerptsPerSession: 1,
+ maxTextChars: 20,
+ });
+
+ expect(result.sessions).toHaveLength(1);
+ expect(result.sessions[0]?.excerpts).toHaveLength(1);
+ expect(result.sessions[0]?.excerpts[0]?.text?.length).toBeLessThanOrEqual(20);
+ });
+
+ it("keeps provider diversity before filling remaining slots", () => {
+ const result = sampleWorkLogEvidence({
+ sessions: [
+ session("old-codex", "codex", 10),
+ session("new-codex", "codex", 30),
+ session("claude", "claude", 20),
+ ],
+ skillInventory: { installedSkills: [], mounts: [] },
+ maxSessions: 2,
+ maxExcerptsPerSession: 2,
+ maxTextChars: 100,
+ });
+
+ expect(new Set(result.sessions.map((entry) => entry.providerId))).toEqual(
+ new Set(["codex", "claude"])
+ );
+ });
+});
+```
+
+- [ ] **Step 2: Run sampler tests and verify they fail**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/__tests__/work-analysis-evidence-sampler.test.ts
+```
+
+Expected: FAIL because `evidence-sampler.ts` does not exist.
+
+- [ ] **Step 3: Update `WorkAnalysisEvidence` type**
+
+Modify `packages/server/src/work-analysis/types.ts`:
+
+```ts
+export interface WorkAnalysisSessionEvidence {
+ providerId?: string;
+ sessionId?: string;
+ workspacePath?: string;
+ title?: string;
+ startedAt: number;
+ lastActiveAt: number;
+ excerpts?: Array<{
+ role: "user" | "assistant" | "tool" | "system" | "unknown";
+ at?: number;
+ text?: string;
+ toolName?: string;
+ commandKind?: string;
+ filePath?: string;
+ }>;
+}
+```
+
+Remove `latestUserInput` and `terminalSnapshot` from required usage. They can remain optional for backward compatibility if needed, but new code must not set them from Coder Studio sessions.
+
+- [ ] **Step 4: Implement `sampleWorkLogEvidence`**
+
+Create `packages/server/src/work-analysis/evidence-sampler.ts`:
+
+```ts
+import type { WorkAnalysisEvidence } from "./types.js";
+import type { WorkLogSession } from "./log-sources/types.js";
+
+interface SampleInput {
+ sessions: WorkLogSession[];
+ skillInventory: WorkAnalysisEvidence["skillInventory"];
+ maxSessions?: number;
+ maxExcerptsPerSession?: number;
+ maxTextChars?: number;
+}
+
+function truncateText(text: string, maxChars: number): string {
+ return text.length <= maxChars ? text : text.slice(0, maxChars);
+}
+
+export function sampleWorkLogEvidence(input: SampleInput): WorkAnalysisEvidence {
+ const maxSessions = input.maxSessions ?? 12;
+ const maxExcerptsPerSession = input.maxExcerptsPerSession ?? 8;
+ const maxTextChars = input.maxTextChars ?? 1_000;
+
+ const newestByProvider = new Map();
+ for (const session of [...input.sessions].sort((left, right) => right.lastActiveAt - left.lastActiveAt)) {
+ if (!newestByProvider.has(session.providerId)) {
+ newestByProvider.set(session.providerId, session);
+ }
+ }
+
+ const selected = [...newestByProvider.values()];
+ for (const session of [...input.sessions].sort((left, right) => right.lastActiveAt - left.lastActiveAt)) {
+ if (selected.length >= maxSessions) break;
+ if (!selected.includes(session)) selected.push(session);
+ }
+
+ return {
+ sessions: selected.slice(0, maxSessions).map((session) => {
+ const evidence = session.evidence?.[0];
+ return {
+ providerId: session.providerId,
+ sessionId: session.sessionId,
+ workspacePath: session.workspacePath,
+ title: session.title,
+ startedAt: session.startedAt,
+ lastActiveAt: session.lastActiveAt,
+ excerpts: (evidence?.excerpts ?? [])
+ .slice(0, maxExcerptsPerSession)
+ .map((excerpt) => ({
+ ...excerpt,
+ text:
+ typeof excerpt.text === "string"
+ ? truncateText(excerpt.text, maxTextChars)
+ : undefined,
+ })),
+ };
+ }),
+ skillInventory: input.skillInventory,
+ };
+}
+```
+
+- [ ] **Step 5: Update the deep prompt test**
+
+Modify `packages/server/src/__tests__/work-analysis-deep-runner.test.ts` evidence fixture:
+
+```ts
+evidence: {
+ sessions: [
+ {
+ providerId: "codex",
+ sessionId: "session-1",
+ workspacePath: "/repo/project",
+ title: "Session",
+ startedAt: 100,
+ lastActiveAt: 200,
+ excerpts: [{ role: "user", text: "investigate" }],
+ },
+ ],
+ skillInventory: {
+ installedSkills: [{ slug: "review" }],
+ mounts: [{ skillSlug: "review", enabled: true }],
+ },
+}
+```
+
+Assert prompt contains `"excerpts"` instead of `"snapshot"`.
+
+- [ ] **Step 6: Add deep provider selection to runner**
+
+Modify `packages/server/src/work-analysis/deep-runner.ts`:
+
+- Add method:
+
+```ts
+resolveProviderId(preferredProviderId?: string): string
+```
+
+Behavior:
+
+- If preferred provider id is present and supports `session_analysis`, return it.
+- Otherwise return the first provider in `providerRegistry` whose `headless.supportedScenarios` includes `session_analysis`.
+- Throw `work_analysis_provider_unavailable` if none exists.
+
+Use this method for all deep runs. The service may pass a preferred provider id derived from sampled evidence, but the runner owns the final selection and fallback behavior.
+
+- [ ] **Step 7: Update service to use sampler and runner selection**
+
+Modify `packages/server/src/work-analysis/service.ts`:
+
+- Build skill inventory once.
+- Pass `sampleWorkLogEvidence({ sessions: collection.sessions, skillInventory })` to `deepRunner.run`.
+- Derive an optional preferred provider id from sampled evidence only if at least one sampled session exists.
+- Pass that preferred provider id into `deepRunner.resolveProviderId(preferredProviderId)` or an equivalent runner-owned selection path.
+- Do not keep or reintroduce the old "provider with most sessions" selection helper in `service.ts`.
+
+- [ ] **Step 8: Re-run deep and service tests**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/__tests__/work-analysis-evidence-sampler.test.ts src/__tests__/work-analysis-deep-runner.test.ts src/__tests__/work-analysis-service.test.ts
+```
+
+Expected: PASS
+
+- [ ] **Step 9: Commit Task 7**
+
+Run:
+
+```bash
+git add packages/server/src/work-analysis/evidence-sampler.ts packages/server/src/work-analysis/types.ts packages/server/src/work-analysis/deep-prompt.ts packages/server/src/work-analysis/deep-runner.ts packages/server/src/work-analysis/service.ts packages/server/src/__tests__/work-analysis-evidence-sampler.test.ts packages/server/src/__tests__/work-analysis-deep-runner.test.ts packages/server/src/__tests__/work-analysis-service.test.ts
+git commit -m "feat: sample provider log evidence for deep analysis"
+```
+
+---
+
+### Task 8: Update Work Analysis UI And Localization
+
+**Files:**
+- Modify: `packages/web/src/features/settings/components/session-analysis-settings.tsx`
+- Modify: `packages/web/src/locales/en.json`
+- Modify: `packages/web/src/locales/zh.json`
+- Test: `packages/web/src/features/settings/components/settings-page.test.tsx`
+
+- [ ] **Step 1: Extend the existing settings page work-analysis test**
+
+Modify the existing work-analysis coverage in `packages/web/src/features/settings/components/settings-page.test.tsx`. The relevant tests already dispatch `work.analysis.get`, `work.analysis.runBasic`, and `work.analysis.runDeep`; extend the fixture returned by `work.analysis.get` so `basicResult` includes `dataSources.providers` and expanded `executionSignals`.
+
+- [ ] **Step 2: Write failing UI assertions**
+
+Add assertions that a completed analysis with `basicResult.dataSources.providers` renders:
+
+- provider status section
+- all provider rows supplied by the result
+- provider log wording, not current session wording
+
+Representative expected text:
+
+```ts
+expect(screen.getByText(/Provider log sources/i)).toBeInTheDocument();
+expect(screen.getByText(/codex/i)).toBeInTheDocument();
+expect(screen.getByText(/local log matches/i)).toBeInTheDocument();
+```
+
+For Chinese locale updates, ensure keys exist but do not add a separate i18n test unless the repo already has one.
+
+- [ ] **Step 3: Run the UI test and verify it fails**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/web exec vitest run src/features/settings/components/settings-page.test.tsx
+```
+
+Expected: FAIL until the component renders provider source status.
+
+- [ ] **Step 4: Add localization keys**
+
+Modify `packages/web/src/locales/en.json` under `settings.analysis`:
+
+```json
+"source_hint": "Analysis scans each provider's local logs for the selected workspace and time range.",
+"provider_sources": "Provider log sources",
+"provider_source_row": "{providerId}: {sessionCount} local log matches, {status}",
+"provider_status_supported": "Ready",
+"provider_status_no_logs": "No matching logs",
+"provider_status_missing_root": "Log root missing",
+"provider_status_partial": "Partial data",
+"provider_status_unsupported": "Unsupported",
+"mtime_fallback_hint": "{count} sessions used file modification time because the provider log did not include explicit timestamps.",
+"log_coverage_summary": "Found {sessionCount} provider-local sessions across {workspaceCount} workspaces and {providerCount} providers."
+```
+
+Modify `packages/web/src/locales/zh.json` with equivalent Chinese strings:
+
+```json
+"source_hint": "分析会扫描各 provider 在本机保存的日志,并按所选工作区和时间范围筛选。",
+"provider_sources": "Provider 日志来源",
+"provider_source_row": "{providerId}: 命中 {sessionCount} 个本地日志会话,状态 {status}",
+"provider_status_supported": "可用",
+"provider_status_no_logs": "无匹配日志",
+"provider_status_missing_root": "日志目录不存在",
+"provider_status_partial": "部分数据",
+"provider_status_unsupported": "暂不支持",
+"mtime_fallback_hint": "{count} 个会话使用了文件修改时间,因为 provider 日志没有明确时间戳。",
+"log_coverage_summary": "在 {workspaceCount} 个工作区、{providerCount} 个 provider 中命中 {sessionCount} 个 provider 本地会话。"
+```
+
+- [ ] **Step 5: Update `SessionAnalysisSettings` rendering**
+
+Modify `packages/web/src/features/settings/components/session-analysis-settings.tsx`:
+
+- Add `formatProviderStatusLabel(status, t)`.
+- Replace `coverage_summary` display with `log_coverage_summary` when new data exists.
+- Render `analysis.basicResult.dataSources.providers` as a compact list under `provider_sources`.
+- Render `mtime_fallback_hint` when `executionSignals.fileMtimeTimestampCount > 0`.
+- Keep existing provider mix list.
+
+Do not redesign the whole settings page in this task.
+
+- [ ] **Step 6: Re-run UI test and web typecheck**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/web exec vitest run src/features/settings/components/settings-page.test.tsx
+pnpm --filter @coder-studio/web exec tsc -p tsconfig.json --noEmit
+```
+
+Expected: PASS
+
+- [ ] **Step 7: Commit Task 8**
+
+Run:
+
+```bash
+git add packages/web/src/features/settings/components/session-analysis-settings.tsx packages/web/src/locales/en.json packages/web/src/locales/zh.json packages/web/src/features/settings/components/settings-page.test.tsx
+git commit -m "feat: show provider log sources in work analysis"
+```
+
+---
+
+### Task 9: Retire Session-Based Work Analysis Modules And Update Docs
+
+**Files:**
+- Delete: `packages/server/src/work-analysis/session-selector.ts`
+- Delete: `packages/server/src/work-analysis/evidence-collector.ts`
+- Delete: `packages/server/src/__tests__/work-analysis-session-selector.test.ts`
+- Delete: `packages/server/src/__tests__/work-analysis-evidence-collector.test.ts`
+- Modify: `docs/help/work-analysis.md`
+
+- [ ] **Step 1: Verify no production imports remain**
+
+Run:
+
+```bash
+rg -n "createWorkAnalysisSessionSelector|createWorkAnalysisEvidenceCollector|session-selector|evidence-collector" packages/server/src
+```
+
+Expected before deletion: only old modules/tests remain. If production imports remain, finish Task 6/7 wiring first.
+
+- [ ] **Step 2: Delete old modules and tests**
+
+Remove the four files listed above after imports are gone.
+
+- [ ] **Step 3: Update help docs**
+
+Modify `docs/help/work-analysis.md`:
+
+- Remove the prerequisite that a workspace must have an open agent session.
+- Explain that data comes from provider local logs/cache.
+- Mention all 5 built-in providers.
+- Mention that provider data can be missing, partial, or timestamp-fallback.
+- Explain that deep analysis uses sampled evidence.
+
+Use this replacement for the "前置条件" section:
+
+```md
+## 前置条件
+
+- 至少打开一个工作区,让 Coder Studio 知道要分析哪个 workspace path
+- 该 workspace 在所选时间范围内最好有 provider 本地日志
+- 不要求当前有打开中的 Coder Studio session
+```
+
+- [ ] **Step 4: Run deletion/doc checks**
+
+Run:
+
+```bash
+rg -n "createWorkAnalysisSessionSelector|createWorkAnalysisEvidenceCollector|terminalSnapshot|latestUserInput" packages/server/src/work-analysis packages/server/src/__tests__
+pnpm --filter @coder-studio/server exec tsc -p tsconfig.json --noEmit
+```
+
+Expected:
+
+- `rg` has no matches in work-analysis production code. If tests still mention old evidence field names only as legacy fixtures, update them.
+- Typecheck PASS.
+
+- [ ] **Step 5: Commit Task 9**
+
+Run:
+
+```bash
+git add -A packages/server/src/work-analysis/session-selector.ts packages/server/src/work-analysis/evidence-collector.ts packages/server/src/__tests__/work-analysis-session-selector.test.ts packages/server/src/__tests__/work-analysis-evidence-collector.test.ts docs/help/work-analysis.md
+git commit -m "docs: document provider log work analysis"
+```
+
+---
+
+### Task 10: Final Verification
+
+**Files:**
+- No planned edits unless verification finds a defect.
+
+- [ ] **Step 1: Run all targeted server work-analysis tests**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run \
+ src/__tests__/work-analysis-log-source-helpers.test.ts \
+ src/__tests__/work-analysis-log-sources-file-adapters.test.ts \
+ src/__tests__/work-analysis-log-source-opencode.test.ts \
+ src/__tests__/work-analysis-log-collector.test.ts \
+ src/__tests__/work-analysis-basic-analyzer.test.ts \
+ src/__tests__/work-analysis-service.test.ts \
+ src/__tests__/work-analysis-deep-runner.test.ts \
+ src/__tests__/work-analysis-repo.test.ts \
+ src/__tests__/work-analysis-commands.test.ts
+```
+
+Expected: PASS
+
+- [ ] **Step 2: Run package typechecks**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec tsc -p tsconfig.json --noEmit
+pnpm --filter @coder-studio/web exec tsc -p tsconfig.json --noEmit
+```
+
+Expected: PASS
+
+- [ ] **Step 3: Run full workspace tests if time allows**
+
+Run:
+
+```bash
+pnpm ci:test:workspace
+```
+
+Expected: PASS
+
+- [ ] **Step 4: Run lint/check**
+
+Run:
+
+```bash
+pnpm ci:lint
+```
+
+Expected: PASS
+
+- [ ] **Step 5: Manual smoke check with local logs**
+
+Start the app:
+
+```bash
+pnpm dev
+```
+
+Manual check:
+
+- Open Settings > Work Analysis.
+- Select `/home/spencer/workspace/coder-studio`.
+- Select `Last 7 days`.
+- Run Basic Analysis.
+- Confirm provider source rows appear for `claude`, `codex`, `gemini`, `cursor`, and `opencode`.
+- Confirm Codex reports more than one local log match when local logs exist.
+- Confirm no text implies only currently open Coder Studio sessions are analyzed.
+
+- [ ] **Step 6: Commit any verification fixes**
+
+If Step 1-5 required fixes:
+
+```bash
+git status --short
+git add docs/help/work-analysis.md packages/server/src packages/web/src
+git commit -m "fix: stabilize provider work log analysis"
+```
+
+If no fixes were required, do not create an empty commit.
diff --git a/docs/superpowers/plans/2026-06-04-agent-instructions-richer-generation.md b/docs/superpowers/plans/2026-06-04-agent-instructions-richer-generation.md
new file mode 100644
index 000000000..18d45e0b8
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-04-agent-instructions-richer-generation.md
@@ -0,0 +1,594 @@
+# Richer Agent Instructions Generation Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Upgrade generated `.coder-studio/agent.md` from a thin repository summary into a compact project operating guide with architecture map, key directories, stronger commands, and file constraints.
+
+**Architecture:** Expand the structured workspace intelligence summary first, because generation quality is limited by missing input facts today. Then update the agent-generation prompt and the deterministic fallback generator to consume the richer summary while keeping output pure Markdown, compact, and conservative.
+
+**Tech Stack:** TypeScript, Vitest, Node.js filesystem inspection, existing server-side workspace intelligence and agent-instructions generation pipeline
+
+---
+
+## File Map
+
+- Modify: `packages/core/src/domain/types.ts`
+ - Extend `WorkspaceIntelligenceSummary` and related command types to carry richer inferred repository facts.
+- Modify: `packages/server/src/workspace/intelligence.ts`
+ - Add monorepo/package/directory/doc/command/constraint inference.
+- Modify: `packages/server/src/agent-instructions/prompt.ts`
+ - Change prompt contract to request richer sections and pure Markdown architecture hierarchy.
+- Modify: `packages/server/src/agent-instructions/generator.ts`
+ - Keep deterministic non-agent generation aligned with the richer section structure.
+- Modify: `packages/server/src/__tests__/agent-instructions/generator.test.ts`
+ - Update deterministic output expectations.
+- Modify: `packages/server/src/__tests__/agent-instructions-command.test.ts`
+ - Verify richer prompt-driven generation behavior and summary-derived content.
+- Create: `packages/server/src/__tests__/workspace/intelligence.test.ts`
+ - Add focused tests for summary inference and prioritization.
+- Optional modify if needed by exports only: `packages/core/src/index.ts` or existing export barrel files
+ - Only if `WorkspaceIntelligenceSummary` type changes require export updates.
+
+## Task 1: Expand Workspace Intelligence Types
+
+**Files:**
+- Modify: `packages/core/src/domain/types.ts`
+- Test: `packages/server/src/__tests__/workspace/intelligence.test.ts`
+
+- [ ] **Step 1: Write the failing type-driven workspace intelligence tests**
+
+Create `packages/server/src/__tests__/workspace/intelligence.test.ts` with focused cases for richer inference:
+
+```ts
+import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { afterEach, describe, expect, it } from "vitest";
+import { inspectWorkspaceIntelligence } from "../../workspace/intelligence.js";
+
+describe("inspectWorkspaceIntelligence", () => {
+ const tempDirs: string[] = [];
+
+ afterEach(async () => {
+ await Promise.all(
+ tempDirs.map(async (dir) => {
+ const { rm } = await import("node:fs/promises");
+ await rm(dir, { recursive: true, force: true });
+ })
+ );
+ });
+
+ it("infers a monorepo architecture summary with key directories and stronger verification commands", async () => {
+ const rootPath = await mkdtemp(join(tmpdir(), "workspace-intelligence-"));
+ tempDirs.push(rootPath);
+
+ await writeFile(join(rootPath, "pnpm-workspace.yaml"), "packages:\\n - packages/*\\n");
+ await writeFile(
+ join(rootPath, "package.json"),
+ JSON.stringify({
+ scripts: {
+ dev: "tsx scripts/dev.ts",
+ build: "tsx scripts/build.ts",
+ lint: "biome lint .",
+ "ci:test": "pnpm -r test",
+ "ci:typecheck": "pnpm -r exec tsc -p tsconfig.json --noEmit",
+ "ci:verify": "pnpm ci:test && pnpm ci:typecheck",
+ "acceptance:phase1": "pnpm --dir e2e exec playwright test --grep @phase1",
+ },
+ })
+ );
+ await writeFile(join(rootPath, "README.md"), "# Repo\\n");
+ await mkdir(join(rootPath, "docs", "help"), { recursive: true });
+ await writeFile(join(rootPath, "docs", "help", "quick-start.md"), "# Quick Start\\n");
+ await mkdir(join(rootPath, "packages", "web"), { recursive: true });
+ await writeFile(
+ join(rootPath, "packages", "web", "package.json"),
+ JSON.stringify({ name: "@repo/web", scripts: { test: "vitest run" } })
+ );
+ await mkdir(join(rootPath, "packages", "server"), { recursive: true });
+ await writeFile(
+ join(rootPath, "packages", "server", "package.json"),
+ JSON.stringify({ name: "@repo/server" })
+ );
+
+ const summary = await inspectWorkspaceIntelligence({
+ workspaceId: "ws-1",
+ rootPath,
+ });
+
+ expect(summary.workspaceKind).toBe("monorepo");
+ expect(summary.keyDirectories.map((entry) => entry.path)).toEqual([
+ "packages/web",
+ "packages/server",
+ "docs",
+ ]);
+ expect(summary.packages).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ path: "packages/web", role: "frontend_ui" }),
+ expect.objectContaining({ path: "packages/server", role: "backend_runtime" }),
+ ])
+ );
+ expect(summary.verificationCommands.map((entry) => entry.command)).toEqual(
+ expect.arrayContaining(["pnpm ci:test", "pnpm ci:typecheck", "pnpm ci:verify"])
+ );
+ expect(summary.fileConstraints).toEqual(
+ expect.arrayContaining([
+ expect.stringContaining("package boundaries"),
+ expect.stringContaining("unrelated refactors"),
+ ])
+ );
+ });
+});
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `pnpm vitest packages/server/src/__tests__/workspace/intelligence.test.ts`
+
+Expected: FAIL because `WorkspaceIntelligenceSummary` and `inspectWorkspaceIntelligence` do not yet expose `workspaceKind`, `keyDirectories`, `packages`, `verificationCommands`, or `fileConstraints`.
+
+- [ ] **Step 3: Extend the shared summary types with richer repository facts**
+
+Update `packages/core/src/domain/types.ts` to add the new summary structures while preserving existing fields:
+
+```ts
+export interface WorkspaceIntelligenceKeyDirectory {
+ path: string;
+ kind:
+ | "frontend"
+ | "backend"
+ | "providers"
+ | "shared"
+ | "cli"
+ | "docs"
+ | "tests"
+ | "scripts"
+ | "other";
+ reason: string;
+}
+
+export interface WorkspaceIntelligencePackageSummary {
+ path: string;
+ name?: string;
+ role:
+ | "frontend_ui"
+ | "backend_runtime"
+ | "provider_integrations"
+ | "shared_contracts"
+ | "cli_entrypoint"
+ | "shared_utilities"
+ | "shared_package";
+ scripts: string[];
+}
+
+export interface WorkspaceIntelligenceCommand {
+ command: string;
+ reason: string;
+ priority: "verification" | "quality" | "dev";
+}
+
+export interface WorkspaceIntelligenceDocEntry {
+ path: string;
+ kind: "readme" | "docs" | "guide" | "wiki";
+}
+
+export interface WorkspaceIntelligenceSummary {
+ // existing fields...
+ workspaceKind?: "monorepo" | "node_app" | "unknown";
+ topLevelDirectories?: string[];
+ keyDirectories?: WorkspaceIntelligenceKeyDirectory[];
+ packages?: WorkspaceIntelligencePackageSummary[];
+ documentationEntries?: WorkspaceIntelligenceDocEntry[];
+ verificationCommands?: WorkspaceIntelligenceCommand[];
+ fileConstraints?: string[];
+}
+```
+
+- [ ] **Step 4: Run test to verify the new type shape compiles but behavior still fails**
+
+Run: `pnpm vitest packages/server/src/__tests__/workspace/intelligence.test.ts`
+
+Expected: FAIL on assertions rather than type/property-missing errors.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add packages/core/src/domain/types.ts packages/server/src/__tests__/workspace/intelligence.test.ts
+git commit -m "feat: extend workspace intelligence summary types"
+```
+
+## Task 2: Implement Richer Workspace Intelligence Inference
+
+**Files:**
+- Modify: `packages/server/src/workspace/intelligence.ts`
+- Test: `packages/server/src/__tests__/workspace/intelligence.test.ts`
+
+- [ ] **Step 1: Write the next failing test for conservative selection limits**
+
+Add a second test in `packages/server/src/__tests__/workspace/intelligence.test.ts`:
+
+```ts
+it("caps key directories and skips noisy root folders", async () => {
+ const rootPath = await mkdtemp(join(tmpdir(), "workspace-intelligence-noise-"));
+ tempDirs.push(rootPath);
+
+ await writeFile(join(rootPath, "package.json"), JSON.stringify({ scripts: {} }));
+ await mkdir(join(rootPath, "packages", "core"), { recursive: true });
+ await writeFile(join(rootPath, "packages", "core", "package.json"), JSON.stringify({ name: "@repo/core" }));
+ await mkdir(join(rootPath, "packages", "providers"), { recursive: true });
+ await writeFile(join(rootPath, "packages", "providers", "package.json"), JSON.stringify({ name: "@repo/providers" }));
+ await mkdir(join(rootPath, "node_modules"), { recursive: true });
+ await mkdir(join(rootPath, ".git"), { recursive: true });
+ await mkdir(join(rootPath, "scripts"), { recursive: true });
+ await mkdir(join(rootPath, "e2e"), { recursive: true });
+
+ const summary = await inspectWorkspaceIntelligence({ workspaceId: "ws-1", rootPath });
+
+ expect(summary.keyDirectories?.length).toBeLessThanOrEqual(6);
+ expect(summary.keyDirectories?.map((entry) => entry.path)).not.toContain("node_modules");
+ expect(summary.topLevelDirectories).not.toContain(".git");
+});
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `pnpm vitest packages/server/src/__tests__/workspace/intelligence.test.ts`
+
+Expected: FAIL because the current intelligence builder does not collect or filter these structures.
+
+- [ ] **Step 3: Implement deterministic repository inference helpers**
+
+Refactor `packages/server/src/workspace/intelligence.ts` to:
+
+- read root `package.json` scripts once and preserve current command behavior
+- discover top-level directories with filtering for hidden/system noise
+- scan `packages/*/package.json`
+- infer package roles from path/name
+- select 3-6 key directories in stable order
+- derive documentation entries from `README.md`, `docs/help/*`, and `docs/wiki/*`
+- derive verification commands from root scripts using explicit priority
+- derive compact file-constraint strings from repo shape
+
+Use helpers like:
+
+```ts
+function inferWorkspaceKind(rootPath: string, packageEntries: PackageEntry[]): WorkspaceKind
+function inferPackageRole(packagePath: string, packageName?: string): WorkspaceIntelligencePackageSummary["role"]
+function selectKeyDirectories(input: {
+ packageEntries: PackageEntry[];
+ rootDirectories: string[];
+ docs: WorkspaceIntelligenceDocEntry[];
+}): WorkspaceIntelligenceKeyDirectory[]
+function buildVerificationCommands(
+ packageManager: PackageManager | undefined,
+ rootScripts: Record
+): WorkspaceIntelligenceCommand[]
+function buildFileConstraints(summary: {
+ workspaceKind: WorkspaceKind;
+ keyDirectories: WorkspaceIntelligenceKeyDirectory[];
+}): string[]
+```
+
+Keep the inference conservative:
+
+- omit uncertain facts instead of guessing
+- prefer stable ordering over filesystem order
+- cap list sizes aggressively
+
+- [ ] **Step 4: Run the focused intelligence tests to verify they pass**
+
+Run: `pnpm vitest packages/server/src/__tests__/workspace/intelligence.test.ts`
+
+Expected: PASS
+
+- [ ] **Step 5: Run the existing agent instructions tests that depend on workspace intelligence**
+
+Run: `pnpm vitest packages/server/src/__tests__/agent-instructions-command.test.ts packages/server/src/__tests__/agent-instructions/generator.test.ts`
+
+Expected: FAIL in deterministic output expectations because the section contract has not been updated yet.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add packages/server/src/workspace/intelligence.ts packages/server/src/__tests__/workspace/intelligence.test.ts
+git commit -m "feat: infer richer workspace intelligence for agent instructions"
+```
+
+## Task 3: Update Prompt Contract for Richer Agent Output
+
+**Files:**
+- Modify: `packages/server/src/agent-instructions/prompt.ts`
+- Test: `packages/server/src/__tests__/agent-instructions-command.test.ts`
+
+- [ ] **Step 1: Add a failing prompt expectation test**
+
+In `packages/server/src/__tests__/agent-instructions-command.test.ts`, add a focused case that captures the prompt passed into the provider command builder:
+
+```ts
+it("builds a richer generation prompt with architecture and file-constraint sections", async () => {
+ const rootPath = await mkdtemp(join(tmpdir(), "agent-instructions-prompt-rich-"));
+ tempDirs.push(rootPath);
+
+ const commandBuilder = vi.fn((_config, _scenario, req) => ({
+ argv: ["codex", "exec", req.prompt],
+ }));
+ runCommandAsStringMock.mockResolvedValue({
+ stdout: codexJsonlPayload(generationPayload("# Agent Instructions\\n\\n## Project Overview\\n")),
+ stderr: "",
+ });
+
+ await dispatch(
+ {
+ kind: "command",
+ id: "agent-instructions-rich-prompt",
+ op: "agentInstructions.generateByAgent",
+ args: { workspaceId: "ws-1", providerId: "codex" },
+ },
+ createContext(rootPath, {
+ providerRegistry: [
+ createAgentGenerationProvider({
+ commandBuilder,
+ }),
+ ],
+ })
+ );
+
+ const prompt = commandBuilder.mock.calls[0]?.[2]?.prompt as string;
+ expect(prompt).toContain("Architecture Map");
+ expect(prompt).toContain("Key Directories");
+ expect(prompt).toContain("File Constraints");
+ expect(prompt).toContain("Review Checklist");
+ expect(prompt).toContain("Use a Markdown hierarchy under 'Architecture Map'");
+ expect(prompt).toContain("List only 3-6 key directories");
+});
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `pnpm vitest packages/server/src/__tests__/agent-instructions-command.test.ts`
+
+Expected: FAIL because the prompt still asks for the old five-section document.
+
+- [ ] **Step 3: Rewrite the prompt to use the richer section contract**
+
+Update `packages/server/src/agent-instructions/prompt.ts` so it:
+
+- requests the new eight-section order
+- explicitly asks for a pure Markdown hierarchy in `Architecture Map`
+- limits `Key Directories` to 3-6 items
+- asks for short, concrete `File Constraints`
+- renames rule-oriented sections to `Workflow Expectations` and `Review Checklist`
+- still requires exact JSON output and no commentary
+
+The section contract should look like:
+
+```ts
+const REQUIRED_WORKFLOW_EXPECTATIONS = [
+ "Keep changes focused on the requested task.",
+ "Do not revert user changes unless explicitly asked.",
+ "Prefer the project's existing patterns.",
+ "Run the relevant verification command before reporting completion.",
+] as const;
+
+const REQUIRED_REVIEW_CHECKLIST = [
+ "Summarize changed files.",
+ "Report verification commands and results.",
+ "Call out risks, skipped tests, and assumptions.",
+] as const;
+```
+
+And the prompt should include lines like:
+
+```ts
+"Use exactly these second-level sections in this order:",
+"- Project Overview",
+"- Architecture Map",
+"- Key Directories",
+"- Development Commands",
+"- Workflow Expectations",
+"- File Constraints",
+"- Review Checklist",
+"- Provider Notes",
+"Use a Markdown hierarchy under 'Architecture Map'.",
+"List only 3-6 entries under 'Key Directories'.",
+```
+
+- [ ] **Step 4: Run the prompt-focused command test to verify it passes**
+
+Run: `pnpm vitest packages/server/src/__tests__/agent-instructions-command.test.ts`
+
+Expected: PASS for the new prompt expectations, with possible failures still remaining in deterministic generator tests.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add packages/server/src/agent-instructions/prompt.ts packages/server/src/__tests__/agent-instructions-command.test.ts
+git commit -m "feat: enrich agent instructions generation prompt"
+```
+
+## Task 4: Align the Deterministic Generator with the New Section Shape
+
+**Files:**
+- Modify: `packages/server/src/agent-instructions/generator.ts`
+- Modify: `packages/server/src/__tests__/agent-instructions/generator.test.ts`
+
+- [ ] **Step 1: Update the deterministic generator test first**
+
+Rewrite `packages/server/src/__tests__/agent-instructions/generator.test.ts` to assert the new compact structure:
+
+```ts
+expect(buildAgentInstructionsMarkdown(summary)).toBe(
+ [
+ "# Agent Instructions",
+ "",
+ "## Project Overview",
+ "",
+ "- This workspace is a monorepo on branch `main`.",
+ "",
+ "## Architecture Map",
+ "",
+ "- packages/",
+ " - web: frontend UI",
+ " - server: backend runtime and websocket commands",
+ "",
+ "## Key Directories",
+ "",
+ "- `packages/web`: frontend UI and workspace interactions.",
+ "- `packages/server`: backend runtime and command dispatch.",
+ "",
+ "## Development Commands",
+ "",
+ "- `pnpm ci:verify`",
+ "- `pnpm test`",
+ "- `pnpm dev`",
+ "",
+ "## Workflow Expectations",
+ "",
+ "- Keep changes focused on the requested task.",
+ // ...
+ ].join("\\n")
+);
+```
+
+- [ ] **Step 2: Run the deterministic generator test to verify it fails**
+
+Run: `pnpm vitest packages/server/src/__tests__/agent-instructions/generator.test.ts`
+
+Expected: FAIL because the current static generator still emits the old five-section layout.
+
+- [ ] **Step 3: Implement the minimal deterministic generator update**
+
+Update `packages/server/src/agent-instructions/generator.ts` to render:
+
+- compact overview from new summary fields
+- Markdown hierarchy under `Architecture Map`
+- bullet list for `Key Directories`
+- stronger command list using `verificationCommands` first, then existing recommended commands
+- `Workflow Expectations`, `File Constraints`, and `Review Checklist` sections
+
+Use helper-style rendering functions so the output remains deterministic:
+
+```ts
+function renderArchitectureMap(summary: WorkspaceIntelligenceSummary): string[]
+function renderKeyDirectories(summary: WorkspaceIntelligenceSummary): string[]
+function renderDevelopmentCommands(summary: WorkspaceIntelligenceSummary): string[]
+function renderFileConstraints(summary: WorkspaceIntelligenceSummary): string[]
+```
+
+Keep the fallback compact even if optional richer fields are missing.
+
+- [ ] **Step 4: Run the deterministic generator and command-level test suite**
+
+Run: `pnpm vitest packages/server/src/__tests__/agent-instructions/generator.test.ts packages/server/src/__tests__/agent-instructions-command.test.ts`
+
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add packages/server/src/agent-instructions/generator.ts packages/server/src/__tests__/agent-instructions/generator.test.ts
+git commit -m "feat: align static agent instructions generator with richer layout"
+```
+
+## Task 5: Full Verification and Real Codex Generation
+
+**Files:**
+- Verify only: `packages/server/src/workspace/intelligence.ts`
+- Verify only: `packages/server/src/agent-instructions/prompt.ts`
+- Verify only: `packages/server/src/agent-instructions/generator.ts`
+- Output: `.coder-studio/agent.md`
+
+- [ ] **Step 1: Run the full targeted server verification suite**
+
+Run:
+
+```bash
+pnpm vitest \
+ packages/server/src/__tests__/workspace/intelligence.test.ts \
+ packages/server/src/__tests__/agent-instructions/generator.test.ts \
+ packages/server/src/__tests__/agent-instructions-command.test.ts \
+ packages/server/src/__tests__/provider-runtime/command-runner.test.ts
+```
+
+Expected: PASS
+
+- [ ] **Step 2: Start a real server instance without provider mocks**
+
+Run:
+
+```bash
+env HOST=127.0.0.1 PORT=35153 STATE_DIR=/tmp/coder-studio-real-e2e.a0UvvH/state RUNTIME_DIR=/tmp/coder-studio-real-e2e.a0UvvH/runtime NO_AUTH=true pnpm exec tsx packages/server/src/server.ts
+```
+
+Expected: server listens on `http://127.0.0.1:35153`
+
+- [ ] **Step 3: Exercise the real WS command path with Codex**
+
+Run a minimal WS client that sends:
+
+```js
+activation.claim
+workspace.open
+workspace.activate
+agentInstructions.generateAndWriteByAgent
+```
+
+Use:
+
+```bash
+node scripts/tmp-run-agent-instructions-ws-check.js
+```
+
+Expected: success result with `meta.providerId === "codex"` and a written `.coder-studio/agent.md`
+
+If a temporary script is needed, create it under `scripts/` and delete it before final commit unless the repository benefits from keeping it.
+
+- [ ] **Step 4: Inspect the generated file content**
+
+Run:
+
+```bash
+sed -n '1,220p' .coder-studio/agent.md
+```
+
+Expected content includes:
+
+- `## Architecture Map`
+- `## Key Directories`
+- `## File Constraints`
+- more than just `pnpm dev/build/lint`
+
+- [ ] **Step 5: Commit implementation if all verification passes**
+
+```bash
+git add packages/core/src/domain/types.ts \
+ packages/server/src/workspace/intelligence.ts \
+ packages/server/src/agent-instructions/prompt.ts \
+ packages/server/src/agent-instructions/generator.ts \
+ packages/server/src/__tests__/workspace/intelligence.test.ts \
+ packages/server/src/__tests__/agent-instructions/generator.test.ts \
+ packages/server/src/__tests__/agent-instructions-command.test.ts
+git commit -m "feat: enrich generated agent instructions"
+```
+
+## Self-Review
+
+Spec coverage check:
+
+- richer architecture map: covered by Tasks 2-4
+- key directories: covered by Tasks 1-4
+- stronger command guidance: covered by Tasks 2-4
+- file constraints and workflow/review guidance: covered by Tasks 2-4
+- real Codex verification: covered by Task 5
+
+Placeholder scan:
+
+- no `TODO` or `TBD`
+- all file paths are explicit
+- all verification commands are explicit
+
+Type consistency check:
+
+- new summary fields are introduced in Task 1 before later tasks consume them
+- prompt section names are defined in Task 3 before generator alignment in Task 4
+- real verification path in Task 5 matches the known working WS flow
diff --git a/docs/superpowers/plans/2026-06-04-tasks-verification-git-review.md b/docs/superpowers/plans/2026-06-04-tasks-verification-git-review.md
new file mode 100644
index 000000000..6fefe119d
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-04-tasks-verification-git-review.md
@@ -0,0 +1,3426 @@
+# Tasks Verification And Git Review Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Add a bottom-panel `Tasks / Verification` workflow that runs managed verification commands through task terminals, then upgrade Git review with hunk-level stage, unstage, and discard actions.
+
+**Architecture:** Ship this as two ordered milestones. Milestone A adds shared task contracts, a server-side `TaskManager`, task discovery, task commands, task events, and a bottom-panel tab beside Terminal that launches task output in the existing xterm surface. Milestone B enriches Git diff payloads with server-generated hunk IDs and applies hunk operations server-side with `git apply`, then renders local hunk actions in the diff viewer.
+
+**Tech Stack:** TypeScript, Zod, React, Jotai, xterm.js, node-pty, Git CLI, Fastify WebSocket command handlers, Vitest, Biome, pnpm
+
+---
+
+## Execution Rules
+
+- Implement Milestone A completely before starting Milestone B. Tasks must be usable before Git hunk staging work begins.
+- Do not add VS Code extension compatibility, Debug Adapter Protocol support, a full Problems panel, line-level staging, commit-message AI, or SQLite task history in this plan.
+- Keep task run history in memory for MVP. Store the latest run per task and a short tail summary only; terminal replay remains the full output source.
+- Preserve existing terminal behavior for `shell` and `agent` terminals. `task` terminals are managed but still use the same terminal output, replay, snapshot, resize, close, and mobile rendering infrastructure.
+- The client must not send patch text for Git hunk operations. It sends `workspaceId`, `path`, `staged`, `hunkId`, and `operation`; the server validates the hunk against the current diff before applying.
+
+## File Map
+
+- `packages/core/src/domain/types.ts`
+ - Add `TerminalKind`, task contracts, and Git hunk contracts.
+ - Extend `Terminal.kind` and `GitFileDiffPayload`.
+- `packages/core/src/domain/events.ts`
+ - Add task domain events and extend terminal-created kind to `TerminalKind`.
+- `packages/core/src/protocol/topics.ts`
+ - Add workspace task topics.
+- `packages/core/src/domain/types.test.ts`
+ - Lock task and Git hunk contracts with type assertions.
+- `packages/server/src/terminal/types.ts`
+ - Use shared `TerminalKind` for `TerminalSpec.kind` and terminal spawn details.
+- `packages/server/src/terminal/manager.ts`
+ - Allow `task` terminals to get snapshot buffers and emit `terminal.created` with `kind: "task"`.
+- `packages/server/src/storage/repositories/terminal-repo.ts`
+ - Accept persisted `task` terminal records.
+- `packages/server/src/tasks/discovery.ts`
+ - Discover tasks from `.coder-studio/tasks.json`, `package.json`, `pnpm-workspace.yaml`, `Cargo.toml`, `go.mod`, `pyproject.toml`, and `Makefile`.
+- `packages/server/src/tasks/discovery.test.ts`
+ - Verify explicit config, package scripts, monorepo verify preference, ecosystem detection, and malformed-source warnings.
+- `packages/server/src/tasks/manager.ts`
+ - Own in-memory task definitions, latest runs, terminal-run mapping, output tail summaries, run/stop/rerun, and task events.
+- `packages/server/src/tasks/manager.test.ts`
+ - Verify task terminal creation, run status transitions, output summary capping, stop, and rerun.
+- `packages/server/src/commands/task.ts`
+ - Register `task.discover`, `task.list`, `task.run`, `task.stop`, `task.rerun`, and `task.history`.
+- `packages/server/src/commands/index.ts`
+ - Import `./task.js`.
+- `packages/server/src/ws/dispatch.ts`
+ - Add `taskMgr` to `CommandContext`.
+- `packages/server/src/ws/hub.ts`
+ - Broadcast task events on task topics.
+- `packages/server/src/server.ts`
+ - Construct `TaskManager`, inject it into command context, and stop workspace tasks during workspace teardown.
+- `packages/server/src/__tests__/task-commands.test.ts`
+ - Verify command registration and command-to-manager behavior.
+- `packages/server/src/__tests__/terminal-commands.test.ts`
+ - Verify `terminal.list` includes task terminals server-side.
+- `packages/web/src/features/bottom-panel/atoms.ts`
+ - Store active bottom-panel tab per workspace.
+- `packages/web/src/features/bottom-panel/index.ts`
+ - Export the bottom-panel atoms.
+- `packages/web/src/features/workspace/views/shared/workspace-bottom-panel.tsx`
+ - Render top-level bottom-panel tabs: `Terminal` and `Tasks`.
+- `packages/web/src/features/workspace/views/desktop/workspace-desktop-view.tsx`
+ - Replace direct `TerminalPanel` mount with `WorkspaceBottomPanel`.
+- `packages/web/src/features/workspace/views/mobile/workspace-mobile-view.tsx`
+ - Keep mobile terminal fullscreen behavior intact; do not show the task list inside the mobile terminal overlay in MVP.
+- `packages/web/src/features/terminal-panel/atoms/terminals.ts`
+ - Extend terminal metadata kind to include `task`.
+- `packages/web/src/features/terminal-panel/actions/use-terminal-actions.ts`
+ - Include `shell` and `task` terminals in the bottom Terminal selector, while continuing to exclude `agent` terminals.
+- `packages/web/src/features/terminal-panel/components/title-format.ts`
+ - Preserve task titles such as `Task: Verify` instead of formatting them as shell labels.
+- `packages/web/src/features/terminal-panel/views/shared/terminal-selector-item.tsx`
+ - Show a managed/task marker for `task` terminals.
+- `packages/web/src/features/terminal-panel/views/shared/terminal-tab.tsx`
+ - Show a managed/task marker for `task` terminal tabs.
+- `packages/web/src/features/tasks/atoms.ts`
+ - Store discovered task definitions, latest runs, loading state, and errors per workspace.
+- `packages/web/src/features/tasks/actions/use-task-actions.ts`
+ - Fetch tasks, subscribe to task events, run/stop/rerun tasks, and switch output to the Terminal tab.
+- `packages/web/src/features/tasks/views/shared/tasks-panel.tsx`
+ - Render discovered tasks with status, duration, command preview, and `Run` / `Stop` / `Rerun`.
+- `packages/web/src/features/tasks/__tests__/tasks-panel.test.tsx`
+ - Verify rendering, run, stop, rerun, event updates, and terminal-tab switching.
+- `packages/web/src/features/terminal-panel/__tests__/terminal-panel.test.tsx`
+ - Verify task terminals appear beside shell terminals and agent terminals remain excluded.
+- `packages/web/src/features/workspace/views/desktop/workspace-desktop-view.test.tsx`
+ - Verify the bottom panel renders both top-level tabs on desktop.
+- `packages/web/src/features/agent-panes/components/session-card.test.tsx`
+ - Verify compact latest verify status in an agent pane.
+- `packages/web/src/features/agent-panes/views/shared/session-card.tsx`
+ - Render latest verify state with `View output` and `Rerun`.
+- `packages/web/src/features/workspace/views/shared/git-panel.tsx`
+ - Render compact verification banner in Git review context.
+- `packages/web/src/features/workspace/views/shared/git-panel.test.tsx`
+ - Verify verification banner and rerun behavior.
+- `packages/web/src/locales/en.json`
+ - Add English task and Git hunk labels.
+- `packages/web/src/locales/zh.json`
+ - Add Chinese task and Git hunk labels.
+- `packages/server/src/git/hunks.ts`
+ - Parse unified diffs into stable server-generated hunk descriptors.
+- `packages/server/src/git/hunks.test.ts`
+ - Verify hunk parsing, stable IDs, and single-hunk patch construction.
+- `packages/server/src/git/hunk-operations.ts`
+ - Validate current diff hunk ID and apply stage, unstage, and discard with `git apply`.
+- `packages/server/src/git/hunk-operations.test.ts`
+ - Verify hunk stage, unstage, discard, and stale hunk rejection in real temporary Git repositories.
+- `packages/server/src/git/diff.ts`
+ - Include `hunks` on text diff payloads.
+- `packages/server/src/commands/git.ts`
+ - Register `git.hunk` command.
+- `packages/server/src/__tests__/git-commands.test.ts`
+ - Verify command validation and state-change event emission for `git.hunk`.
+- `packages/web/src/features/workspace/actions/use-git-actions.ts`
+ - Add hunk operation action, refresh affected preview, and surface stale diff errors.
+- `packages/web/src/features/workspace/views/shared/git-diff-viewer.tsx`
+ - Render file-level and hunk-level review actions in the diff surface.
+- `packages/web/src/features/workspace/views/shared/git-diff-viewer.test.tsx`
+ - Verify hunk actions, stale error behavior, file actions, and stable selected preview.
+
+## Milestone A: Tasks / Verification
+
+### Task 1: Add Shared Task, Terminal, and Git Hunk Contracts
+
+**Files:**
+- Modify: `packages/core/src/domain/types.ts`
+- Modify: `packages/core/src/domain/events.ts`
+- Modify: `packages/core/src/protocol/topics.ts`
+- Modify: `packages/core/src/domain/types.test.ts`
+
+- [ ] **Step 1: Write the failing shared-contract tests**
+
+Append these imports to `packages/core/src/domain/types.test.ts`:
+
+```ts
+import type {
+ GitDiffHunk,
+ GitHunkOperation,
+ TaskDefinition,
+ TaskRun,
+ Terminal,
+ TerminalKind,
+} from "./types";
+```
+
+Append these tests to `packages/core/src/domain/types.test.ts`:
+
+```ts
+describe("Task contracts", () => {
+ it("defines managed workspace task definitions", () => {
+ expectTypeOf().toEqualTypeOf<{
+ id: string;
+ workspaceId: string;
+ kind: "verify" | "test" | "lint" | "build" | "dev" | "custom";
+ label: string;
+ command: string;
+ args: string[];
+ cwdPath?: string;
+ source:
+ | "coder-studio"
+ | "package-json"
+ | "pnpm-workspace"
+ | "cargo"
+ | "go"
+ | "python"
+ | "makefile"
+ | "inferred";
+ priority: number;
+ }>();
+ });
+
+ it("defines managed task run state", () => {
+ expectTypeOf().toEqualTypeOf<{
+ id: string;
+ workspaceId: string;
+ taskId: string;
+ terminalId: string;
+ status: "queued" | "running" | "passed" | "failed" | "stopped";
+ command: string;
+ args: string[];
+ cwdPath?: string;
+ startedAt: number;
+ finishedAt?: number;
+ exitCode?: number;
+ summary?: {
+ tailLines: string[];
+ };
+ }>();
+ });
+
+ it("allows task terminals as managed terminal DTOs", () => {
+ expectTypeOf().toEqualTypeOf<"agent" | "shell" | "task">();
+ expectTypeOf().toEqualTypeOf();
+ });
+});
+
+describe("Git hunk contracts", () => {
+ it("defines hunk descriptors returned by diff payloads", () => {
+ expectTypeOf().toEqualTypeOf<{
+ id: string;
+ header: string;
+ oldStart: number;
+ oldLines: number;
+ newStart: number;
+ newLines: number;
+ patch: string;
+ lines: string[];
+ }>();
+ expectTypeOf().toEqualTypeOf();
+ });
+
+ it("defines server-validated hunk operations", () => {
+ expectTypeOf().toEqualTypeOf<{
+ workspaceId: string;
+ path: string;
+ staged: boolean;
+ hunkId: string;
+ operation: "stage" | "unstage" | "discard";
+ }>();
+ });
+});
+```
+
+- [ ] **Step 2: Run the focused core test and confirm it fails**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/core exec vitest run src/domain/types.test.ts
+```
+
+Expected: FAIL with missing exports for `TaskDefinition`, `TaskRun`, `TerminalKind`, `GitDiffHunk`, and `GitHunkOperation`.
+
+- [ ] **Step 3: Add shared contracts**
+
+Add these types in `packages/core/src/domain/types.ts` near the existing `Terminal` and Git types:
+
+```ts
+export type TerminalKind = "agent" | "shell" | "task";
+
+export type TaskKind = "verify" | "test" | "lint" | "build" | "dev" | "custom";
+
+export type TaskSource =
+ | "coder-studio"
+ | "package-json"
+ | "pnpm-workspace"
+ | "cargo"
+ | "go"
+ | "python"
+ | "makefile"
+ | "inferred";
+
+export type TaskRunStatus = "queued" | "running" | "passed" | "failed" | "stopped";
+
+export interface TaskDefinition {
+ id: string;
+ workspaceId: string;
+ kind: TaskKind;
+ label: string;
+ command: string;
+ args: string[];
+ cwdPath?: string;
+ source: TaskSource;
+ priority: number;
+}
+
+export interface TaskRun {
+ id: string;
+ workspaceId: string;
+ taskId: string;
+ terminalId: string;
+ status: TaskRunStatus;
+ command: string;
+ args: string[];
+ cwdPath?: string;
+ startedAt: number;
+ finishedAt?: number;
+ exitCode?: number;
+ summary?: {
+ tailLines: string[];
+ };
+}
+
+export interface GitDiffHunk {
+ id: string;
+ header: string;
+ oldStart: number;
+ oldLines: number;
+ newStart: number;
+ newLines: number;
+ patch: string;
+ lines: string[];
+}
+
+export interface GitHunkOperation {
+ workspaceId: string;
+ path: string;
+ staged: boolean;
+ hunkId: string;
+ operation: "stage" | "unstage" | "discard";
+}
+```
+
+Change `Terminal.kind` in `packages/core/src/domain/types.ts` from:
+
+```ts
+kind: "agent" | "shell";
+```
+
+to:
+
+```ts
+kind: TerminalKind;
+```
+
+Add `hunks` to `GitFileDiffPayload`:
+
+```ts
+hunks?: GitDiffHunk[];
+```
+
+- [ ] **Step 4: Add task domain events and topics**
+
+Modify imports in `packages/core/src/domain/events.ts`:
+
+```ts
+import type { SessionState, TaskDefinition, TaskRun, TerminalKind, Workspace } from "./types";
+```
+
+Change `terminal.created.kind` in `DomainEvent` to:
+
+```ts
+kind: TerminalKind;
+```
+
+Add these task event variants before the LSP event variant:
+
+```ts
+| { type: "task.discovered"; workspaceId: string; tasks: TaskDefinition[] }
+| { type: "task.run.started"; workspaceId: string; run: TaskRun }
+| { type: "task.run.updated"; workspaceId: string; run: TaskRun }
+| { type: "task.run.finished"; workspaceId: string; run: TaskRun }
+| { type: "task.run.stopped"; workspaceId: string; run: TaskRun }
+```
+
+Add these topic helpers in `packages/core/src/protocol/topics.ts` after terminal helpers:
+
+```ts
+ // Task-level
+ workspaceTaskDiscovered: (workspaceId: string) => `workspace.${workspaceId}.task.discovered`,
+ workspaceTaskRun: (workspaceId: string, runId: string) =>
+ `workspace.${workspaceId}.task.${runId}`,
+ workspaceTasksAll: (workspaceId: string) => `workspace.${workspaceId}.task.*`,
+```
+
+- [ ] **Step 5: Run the focused core test and typecheck**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/core exec vitest run src/domain/types.test.ts
+pnpm --filter @coder-studio/core exec tsc -p tsconfig.json --noEmit
+```
+
+Expected: PASS for tests and no TypeScript errors.
+
+- [ ] **Step 6: Commit shared contracts**
+
+Run:
+
+```bash
+git add packages/core/src/domain/types.ts packages/core/src/domain/events.ts packages/core/src/protocol/topics.ts packages/core/src/domain/types.test.ts
+git commit -m "feat: add task and hunk contracts"
+```
+
+### Task 2: Extend Terminal Infrastructure for Task Terminals
+
+**Files:**
+- Modify: `packages/server/src/terminal/types.ts`
+- Modify: `packages/server/src/terminal/manager.ts`
+- Modify: `packages/server/src/storage/repositories/terminal-repo.ts`
+- Modify: `packages/server/src/terminal/manager.test.ts`
+- Modify: `packages/server/src/__tests__/terminal-commands.test.ts`
+
+- [ ] **Step 1: Write failing terminal kind tests**
+
+Add this test to `packages/server/src/terminal/manager.test.ts` in the create/snapshot behavior section:
+
+```ts
+it("creates task terminals with snapshot support", async () => {
+ const terminal = manager.create({
+ workspaceId: "ws-123",
+ kind: "task",
+ argv: ["pnpm", "ci:verify"],
+ cwd: "/test",
+ title: "Task: Verify",
+ cols: 100,
+ rows: 24,
+ });
+
+ expect(terminal.kind).toBe("task");
+ expect(terminal.title).toBe("Task: Verify");
+ expect(manager.get(terminal.id)?.snapshotBuffer).toBeDefined();
+});
+```
+
+Add this test to `packages/server/src/__tests__/terminal-commands.test.ts` in the `terminal.list` section:
+
+```ts
+it("returns task terminals from terminal.list", async () => {
+ const ctx = createContext({
+ terminalMgr: {
+ getAll: vi.fn(() => [
+ {
+ id: "term-task",
+ workspaceId: "ws-1",
+ kind: "task",
+ title: "Task: Verify",
+ alive: true,
+ },
+ ]),
+ } as never,
+ });
+
+ const result = await dispatch(
+ {
+ kind: "command",
+ id: "cmd-1",
+ op: "terminal.list",
+ args: { workspaceId: "ws-1" },
+ },
+ ctx,
+ "client-1"
+ );
+
+ expect(result.ok).toBe(true);
+ expect(result.data).toEqual([
+ {
+ id: "term-task",
+ workspaceId: "ws-1",
+ kind: "task",
+ title: "Task: Verify",
+ alive: true,
+ },
+ ]);
+});
+```
+
+- [ ] **Step 2: Run focused terminal tests and confirm failures**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/terminal/manager.test.ts src/__tests__/terminal-commands.test.ts
+```
+
+Expected: FAIL because server terminal types and repository validation only allow `agent` and `shell`.
+
+- [ ] **Step 3: Update server terminal kind types**
+
+Modify `packages/server/src/terminal/types.ts`:
+
+```ts
+import type { Terminal, TerminalKind } from "@coder-studio/core";
+```
+
+Change `TerminalSpec.kind` to:
+
+```ts
+kind: TerminalKind;
+```
+
+Change `TerminalSpawnError.details.terminalKind` to:
+
+```ts
+terminalKind?: TerminalKind;
+```
+
+- [ ] **Step 4: Give task terminals snapshot buffers**
+
+In `packages/server/src/terminal/manager.ts`, change:
+
+```ts
+if (spec.kind === "shell" || spec.kind === "agent") {
+```
+
+to:
+
+```ts
+if (spec.kind === "shell" || spec.kind === "agent" || spec.kind === "task") {
+```
+
+- [ ] **Step 5: Accept persisted task terminals**
+
+In `packages/server/src/storage/repositories/terminal-repo.ts`, import `TerminalKind`:
+
+```ts
+import type { Terminal, TerminalKind } from "@coder-studio/core";
+```
+
+Add:
+
+```ts
+const TERMINAL_KINDS = new Set(["agent", "shell", "task"]);
+```
+
+Change the `NewTerminal.kind` type to:
+
+```ts
+kind: TerminalKind;
+```
+
+Change `isTerminal` kind validation to:
+
+```ts
+TERMINAL_KINDS.has(value.kind as TerminalKind)
+```
+
+- [ ] **Step 6: Run focused terminal tests**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/terminal/manager.test.ts src/__tests__/terminal-commands.test.ts
+pnpm --filter @coder-studio/server exec tsc -p tsconfig.json --noEmit
+```
+
+Expected: PASS for tests and no TypeScript errors.
+
+- [ ] **Step 7: Commit task terminal support**
+
+Run:
+
+```bash
+git add packages/server/src/terminal/types.ts packages/server/src/terminal/manager.ts packages/server/src/storage/repositories/terminal-repo.ts packages/server/src/terminal/manager.test.ts packages/server/src/__tests__/terminal-commands.test.ts
+git commit -m "feat: support managed task terminals"
+```
+
+### Task 3: Implement Server Task Discovery
+
+**Files:**
+- Create: `packages/server/src/tasks/discovery.ts`
+- Create: `packages/server/src/tasks/discovery.test.ts`
+
+- [ ] **Step 1: Write failing discovery tests**
+
+Create `packages/server/src/tasks/discovery.test.ts` with:
+
+```ts
+import { mkdir, rm, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { discoverTasks } from "./discovery.js";
+
+describe("discoverTasks", () => {
+ let root: string;
+
+ beforeEach(async () => {
+ root = join(tmpdir(), `coder-studio-task-discovery-${Date.now()}-${Math.random()}`);
+ await mkdir(root, { recursive: true });
+ });
+
+ afterEach(async () => {
+ await rm(root, { recursive: true, force: true });
+ });
+
+ it("uses explicit .coder-studio/tasks.json definitions first", async () => {
+ await mkdir(join(root, ".coder-studio"), { recursive: true });
+ await writeFile(
+ join(root, ".coder-studio", "tasks.json"),
+ JSON.stringify({
+ version: 1,
+ tasks: [
+ {
+ id: "verify",
+ label: "Verify",
+ kind: "verify",
+ command: "pnpm",
+ args: ["ci:verify"],
+ cwdPath: ".",
+ },
+ ],
+ })
+ );
+ await writeFile(
+ join(root, "package.json"),
+ JSON.stringify({ scripts: { test: "vitest run" } })
+ );
+
+ const result = await discoverTasks({ workspaceId: "ws-1", rootPath: root });
+
+ expect(result.tasks[0]).toEqual({
+ id: "verify",
+ workspaceId: "ws-1",
+ kind: "verify",
+ label: "Verify",
+ command: "pnpm",
+ args: ["ci:verify"],
+ cwdPath: ".",
+ source: "coder-studio",
+ priority: 1000,
+ });
+ expect(result.tasks.map((task) => task.source)).toContain("package-json");
+ });
+
+ it("prefers pnpm ci:verify as the default verify task", async () => {
+ await writeFile(
+ join(root, "package.json"),
+ JSON.stringify({
+ scripts: {
+ "ci:verify": "pnpm changeset:validate && pnpm ci:lint && pnpm ci:test && pnpm ci:build",
+ test: "vitest run",
+ lint: "biome lint .",
+ build: "tsc -p tsconfig.json",
+ },
+ })
+ );
+ await writeFile(join(root, "pnpm-lock.yaml"), "");
+
+ const result = await discoverTasks({ workspaceId: "ws-1", rootPath: root });
+
+ expect(result.tasks[0]).toMatchObject({
+ id: "verify",
+ kind: "verify",
+ label: "Verify",
+ command: "pnpm",
+ args: ["ci:verify"],
+ source: "package-json",
+ });
+ expect(result.tasks.map((task) => task.id)).toEqual(["verify", "test", "lint", "build"]);
+ });
+
+ it("discovers ecosystem convention tasks", async () => {
+ await writeFile(join(root, "Cargo.toml"), "[package]\nname = \"demo\"\n");
+ await writeFile(join(root, "go.mod"), "module demo\n");
+ await writeFile(join(root, "pyproject.toml"), "[project]\nname = \"demo\"\n");
+ await writeFile(join(root, "Makefile"), "verify:\n\tpnpm ci:verify\n");
+
+ const result = await discoverTasks({ workspaceId: "ws-1", rootPath: root });
+
+ expect(result.tasks).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ id: "cargo-test", command: "cargo", args: ["test"] }),
+ expect.objectContaining({ id: "go-test", command: "go", args: ["test", "./..."] }),
+ expect.objectContaining({ id: "python-test", command: "python", args: ["-m", "pytest"] }),
+ expect.objectContaining({ id: "make-verify", command: "make", args: ["verify"] }),
+ ])
+ );
+ });
+
+ it("returns warnings for malformed sources without failing all discovery", async () => {
+ await writeFile(join(root, "package.json"), "{ broken json");
+ await writeFile(join(root, "Makefile"), "test:\n\tpnpm test\n");
+
+ const result = await discoverTasks({ workspaceId: "ws-1", rootPath: root });
+
+ expect(result.warnings).toEqual([
+ expect.objectContaining({
+ source: "package-json",
+ message: expect.stringContaining("package.json"),
+ }),
+ ]);
+ expect(result.tasks).toEqual([
+ expect.objectContaining({
+ id: "make-test",
+ command: "make",
+ args: ["test"],
+ }),
+ ]);
+ });
+});
+```
+
+- [ ] **Step 2: Run discovery tests and confirm failure**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/tasks/discovery.test.ts
+```
+
+Expected: FAIL because `packages/server/src/tasks/discovery.ts` does not exist.
+
+- [ ] **Step 3: Implement discovery API and helpers**
+
+Create `packages/server/src/tasks/discovery.ts` with these exported shapes and behavior:
+
+```ts
+import type { TaskDefinition, TaskKind, TaskSource } from "@coder-studio/core";
+import { access, readFile } from "node:fs/promises";
+import { join } from "node:path";
+import { z } from "zod";
+
+export interface TaskDiscoveryInput {
+ workspaceId: string;
+ rootPath: string;
+}
+
+export interface TaskDiscoveryWarning {
+ source: TaskSource;
+ message: string;
+}
+
+export interface TaskDiscoveryResult {
+ tasks: TaskDefinition[];
+ warnings: TaskDiscoveryWarning[];
+}
+
+const coderStudioTaskSchema = z.object({
+ id: z.string().min(1),
+ label: z.string().min(1),
+ kind: z.enum(["verify", "test", "lint", "build", "dev", "custom"]),
+ command: z.string().min(1),
+ args: z.array(z.string()).default([]),
+ cwdPath: z.string().optional(),
+});
+
+const coderStudioTasksFileSchema = z.object({
+ version: z.literal(1),
+ tasks: z.array(coderStudioTaskSchema),
+});
+
+function uniqueTasks(tasks: TaskDefinition[]): TaskDefinition[] {
+ const seen = new Set();
+ const result: TaskDefinition[] = [];
+ for (const task of tasks.sort((left, right) => right.priority - left.priority)) {
+ const key = `${task.kind}:${task.id}`;
+ if (seen.has(key)) {
+ continue;
+ }
+ seen.add(key);
+ result.push(task);
+ }
+ return result;
+}
+
+function packageManagerFor(rootFiles: Set): "pnpm" | "yarn" | "bun" | "npm" {
+ if (rootFiles.has("pnpm-lock.yaml") || rootFiles.has("pnpm-workspace.yaml")) return "pnpm";
+ if (rootFiles.has("yarn.lock")) return "yarn";
+ if (rootFiles.has("bun.lockb") || rootFiles.has("bun.lock")) return "bun";
+ return "npm";
+}
+
+function scriptTask(
+ workspaceId: string,
+ scriptName: string,
+ kind: TaskKind,
+ packageManager: "pnpm" | "yarn" | "bun" | "npm",
+ priority: number
+): TaskDefinition {
+ return {
+ id: kind === "verify" ? "verify" : kind,
+ workspaceId,
+ kind,
+ label: kind[0]!.toUpperCase() + kind.slice(1),
+ command: packageManager,
+ args: [scriptName],
+ cwdPath: ".",
+ source: "package-json",
+ priority,
+ };
+}
+```
+
+Implement `discoverTasks(input)` so it:
+
+- reads `.coder-studio/tasks.json` first and maps valid entries to priority `1000 - index`
+- reads root `package.json` and maps `ci:verify` to `verify` priority `900`
+- maps `verify`, `test`, `lint`, `build`, and `dev` package scripts when present, with priorities `800`, `700`, `600`, `500`, and `100`
+- maps `Cargo.toml` to `cargo test`
+- maps `go.mod` to `go test ./...`
+- maps `pyproject.toml` to `python -m pytest`
+- maps `Makefile` targets named `verify`, `test`, `lint`, and `build` to `make `
+- catches per-source parse/read errors and pushes warnings instead of throwing
+- returns `uniqueTasks(tasks)` sorted by descending priority
+
+Use IDs exactly as asserted in the tests: `verify`, `test`, `lint`, `build`, `dev`, `cargo-test`, `go-test`, `python-test`, and `make-${target}`.
+
+- [ ] **Step 4: Run discovery tests**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/tasks/discovery.test.ts
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Commit discovery**
+
+Run:
+
+```bash
+git add packages/server/src/tasks/discovery.ts packages/server/src/tasks/discovery.test.ts
+git commit -m "feat: discover workspace tasks"
+```
+
+### Task 4: Implement Server Task Manager
+
+**Files:**
+- Create: `packages/server/src/tasks/manager.ts`
+- Create: `packages/server/src/tasks/manager.test.ts`
+
+- [ ] **Step 1: Write failing manager tests**
+
+Create `packages/server/src/tasks/manager.test.ts` with:
+
+```ts
+import type { DomainEvent, TaskDefinition, Terminal } from "@coder-studio/core";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { EventBus } from "../bus/event-bus.js";
+import { TaskManager } from "./manager.js";
+
+function createTask(overrides: Partial = {}): TaskDefinition {
+ return {
+ id: "verify",
+ workspaceId: "ws-1",
+ kind: "verify",
+ label: "Verify",
+ command: "pnpm",
+ args: ["ci:verify"],
+ cwdPath: ".",
+ source: "package-json",
+ priority: 900,
+ ...overrides,
+ };
+}
+
+describe("TaskManager", () => {
+ let eventBus: EventBus;
+ let events: DomainEvent[];
+ let terminalMgr: {
+ create: ReturnType;
+ close: ReturnType;
+ };
+ let manager: TaskManager;
+
+ beforeEach(() => {
+ eventBus = new EventBus();
+ events = [];
+ for (const type of [
+ "task.discovered",
+ "task.run.started",
+ "task.run.updated",
+ "task.run.finished",
+ "task.run.stopped",
+ ] as const) {
+ eventBus.on(type, (event) => events.push(event));
+ }
+ terminalMgr = {
+ create: vi.fn((spec) => ({
+ id: "term-task",
+ workspaceId: spec.workspaceId,
+ kind: spec.kind,
+ title: spec.title,
+ cwd: spec.cwd,
+ argv: spec.argv,
+ cols: 120,
+ rows: 30,
+ alive: true,
+ createdAt: 1,
+ })),
+ close: vi.fn(async () => undefined),
+ };
+ manager = new TaskManager({
+ eventBus,
+ terminalMgr: terminalMgr as never,
+ now: () => 1000,
+ });
+ });
+
+ it("stores discovered tasks and emits task.discovered", () => {
+ const tasks = [createTask()];
+
+ manager.setDiscoveredTasks("ws-1", tasks);
+
+ expect(manager.list("ws-1")).toEqual(tasks);
+ expect(events).toEqual([
+ {
+ type: "task.discovered",
+ workspaceId: "ws-1",
+ tasks,
+ },
+ ]);
+ });
+
+ it("runs a task through a managed task terminal", async () => {
+ manager.setDiscoveredTasks("ws-1", [createTask()]);
+
+ const run = await manager.run({
+ workspaceId: "ws-1",
+ workspacePath: "/repo",
+ taskId: "verify",
+ themeBackground: "#0b1218",
+ });
+
+ expect(terminalMgr.create).toHaveBeenCalledWith({
+ workspaceId: "ws-1",
+ kind: "task",
+ argv: ["pnpm", "ci:verify"],
+ cwd: "/repo",
+ title: "Task: Verify",
+ cols: 120,
+ rows: 30,
+ themeBackground: "#0b1218",
+ });
+ expect(run).toMatchObject({
+ workspaceId: "ws-1",
+ taskId: "verify",
+ terminalId: "term-task",
+ status: "running",
+ command: "pnpm",
+ args: ["ci:verify"],
+ cwdPath: ".",
+ startedAt: 1000,
+ });
+ expect(events.at(-1)).toEqual({
+ type: "task.run.started",
+ workspaceId: "ws-1",
+ run,
+ });
+ });
+
+ it("marks a run passed when its task terminal exits with zero", async () => {
+ manager.setDiscoveredTasks("ws-1", [createTask()]);
+ const run = await manager.run({ workspaceId: "ws-1", workspacePath: "/repo", taskId: "verify" });
+
+ eventBus.emit({
+ type: "terminal.exited",
+ workspaceId: "ws-1",
+ terminalId: run.terminalId,
+ exitCode: 0,
+ });
+
+ expect(manager.history("ws-1")[0]).toMatchObject({
+ id: run.id,
+ status: "passed",
+ exitCode: 0,
+ finishedAt: 1000,
+ });
+ expect(events.at(-1)?.type).toBe("task.run.finished");
+ });
+
+ it("marks a run failed and records capped output tail on non-zero exit", async () => {
+ manager.setDiscoveredTasks("ws-1", [createTask()]);
+ const run = await manager.run({ workspaceId: "ws-1", workspacePath: "/repo", taskId: "verify" });
+
+ for (let index = 0; index < 14; index += 1) {
+ eventBus.emit({
+ type: "terminal.output",
+ workspaceId: "ws-1",
+ terminalId: run.terminalId,
+ chunk: Buffer.from(`line ${index}\n`),
+ seq: index + 1,
+ });
+ }
+ eventBus.emit({
+ type: "terminal.exited",
+ workspaceId: "ws-1",
+ terminalId: run.terminalId,
+ exitCode: 1,
+ });
+
+ const latest = manager.history("ws-1")[0]!;
+ expect(latest.status).toBe("failed");
+ expect(latest.summary?.tailLines).toEqual([
+ "line 4",
+ "line 5",
+ "line 6",
+ "line 7",
+ "line 8",
+ "line 9",
+ "line 10",
+ "line 11",
+ "line 12",
+ "line 13",
+ ]);
+ });
+
+ it("stops a running task terminal and emits task.run.stopped", async () => {
+ manager.setDiscoveredTasks("ws-1", [createTask()]);
+ const run = await manager.run({ workspaceId: "ws-1", workspacePath: "/repo", taskId: "verify" });
+
+ const stopped = await manager.stop({ workspaceId: "ws-1", runId: run.id });
+
+ expect(terminalMgr.close).toHaveBeenCalledWith(run.terminalId);
+ expect(stopped).toMatchObject({
+ id: run.id,
+ status: "stopped",
+ finishedAt: 1000,
+ });
+ expect(events.at(-1)?.type).toBe("task.run.stopped");
+ });
+});
+```
+
+- [ ] **Step 2: Run manager tests and confirm failure**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/tasks/manager.test.ts
+```
+
+Expected: FAIL because `packages/server/src/tasks/manager.ts` does not exist.
+
+- [ ] **Step 3: Implement TaskManager public API**
+
+Create `packages/server/src/tasks/manager.ts` with:
+
+```ts
+import type { DomainEvent, TaskDefinition, TaskRun } from "@coder-studio/core";
+import { isAbsolute, join } from "node:path";
+import type { EventBus } from "../bus/event-bus.js";
+import { resolveSafe } from "../fs/file-io.js";
+import type { TerminalManager } from "../terminal/manager.js";
+
+interface TaskManagerDeps {
+ eventBus: EventBus;
+ terminalMgr: Pick;
+ now?: () => number;
+}
+
+interface RunTaskInput {
+ workspaceId: string;
+ workspacePath: string;
+ taskId: string;
+ themeBackground?: string;
+}
+
+interface StopTaskInput {
+ workspaceId: string;
+ runId: string;
+}
+
+const TASK_TAIL_LINE_LIMIT = 10;
+
+function createRunId(): string {
+ return `taskrun_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
+}
+
+function normalizeOutputLines(chunk: Buffer): string[] {
+ return chunk
+ .toString("utf8")
+ .split(/\r?\n/)
+ .map((line) => line.trimEnd())
+ .filter((line) => line.trim().length > 0);
+}
+
+export class TaskManager {
+ private readonly now: () => number;
+ private tasksByWorkspace = new Map();
+ private runsByWorkspace = new Map();
+ private runByTerminalId = new Map();
+ private outputTailByRunId = new Map();
+
+ constructor(private readonly deps: TaskManagerDeps) {
+ this.now = deps.now ?? (() => Date.now());
+ this.deps.eventBus.on("terminal.output", (event) => this.onTerminalOutput(event));
+ this.deps.eventBus.on("terminal.exited", (event) => this.onTerminalExited(event));
+ }
+
+ setDiscoveredTasks(workspaceId: string, tasks: TaskDefinition[]): TaskDefinition[] {
+ this.tasksByWorkspace.set(workspaceId, tasks);
+ this.deps.eventBus.emit({ type: "task.discovered", workspaceId, tasks });
+ return tasks;
+ }
+
+ list(workspaceId: string): TaskDefinition[] {
+ return this.tasksByWorkspace.get(workspaceId) ?? [];
+ }
+
+ history(workspaceId: string): TaskRun[] {
+ return this.runsByWorkspace.get(workspaceId) ?? [];
+ }
+
+ latestVerify(workspaceId: string): TaskRun | undefined {
+ return this.history(workspaceId).find((run) => run.taskId === "verify");
+ }
+
+ async run(input: RunTaskInput): Promise {
+ const task = this.list(input.workspaceId).find((candidate) => candidate.id === input.taskId);
+ if (!task) {
+ throw { code: "task_not_found", message: `Task not found: ${input.taskId}` };
+ }
+
+ const cwd = this.resolveTaskCwd(input.workspacePath, task.cwdPath);
+ const terminal = this.deps.terminalMgr.create({
+ workspaceId: input.workspaceId,
+ kind: "task",
+ argv: [task.command, ...task.args],
+ cwd,
+ title: `Task: ${task.label}`,
+ cols: 120,
+ rows: 30,
+ themeBackground: input.themeBackground,
+ });
+
+ const run: TaskRun = {
+ id: createRunId(),
+ workspaceId: input.workspaceId,
+ taskId: task.id,
+ terminalId: terminal.id,
+ status: "running",
+ command: task.command,
+ args: task.args,
+ cwdPath: task.cwdPath,
+ startedAt: this.now(),
+ };
+
+ this.storeRun(run);
+ this.runByTerminalId.set(terminal.id, run);
+ this.deps.eventBus.emit({ type: "task.run.started", workspaceId: input.workspaceId, run });
+ return run;
+ }
+
+ async rerun(input: RunTaskInput): Promise {
+ return this.run(input);
+ }
+
+ async stop(input: StopTaskInput): Promise {
+ const run = this.history(input.workspaceId).find((candidate) => candidate.id === input.runId);
+ if (!run) {
+ throw { code: "task_run_not_found", message: `Task run not found: ${input.runId}` };
+ }
+ if (run.status !== "running" && run.status !== "queued") {
+ return run;
+ }
+
+ await this.deps.terminalMgr.close(run.terminalId);
+ const stopped = this.updateRun(run, {
+ status: "stopped",
+ finishedAt: this.now(),
+ summary: { tailLines: this.outputTailByRunId.get(run.id) ?? [] },
+ });
+ this.runByTerminalId.delete(run.terminalId);
+ this.deps.eventBus.emit({
+ type: "task.run.stopped",
+ workspaceId: input.workspaceId,
+ run: stopped,
+ });
+ return stopped;
+ }
+
+ clearWorkspace(workspaceId: string): void {
+ this.tasksByWorkspace.delete(workspaceId);
+ const runs = this.runsByWorkspace.get(workspaceId) ?? [];
+ for (const run of runs) {
+ this.runByTerminalId.delete(run.terminalId);
+ this.outputTailByRunId.delete(run.id);
+ }
+ this.runsByWorkspace.delete(workspaceId);
+ }
+
+ private resolveTaskCwd(workspacePath: string, cwdPath: string | undefined): string {
+ if (!cwdPath || cwdPath === ".") {
+ return workspacePath;
+ }
+ if (isAbsolute(cwdPath)) {
+ throw { code: "invalid_task_cwd", message: "Task cwdPath must be workspace-relative" };
+ }
+ return resolveSafe(workspacePath, cwdPath);
+ }
+
+ private storeRun(run: TaskRun): void {
+ const current = this.runsByWorkspace.get(run.workspaceId) ?? [];
+ const withoutSameTask = current.filter((candidate) => candidate.taskId !== run.taskId);
+ this.runsByWorkspace.set(run.workspaceId, [run, ...withoutSameTask]);
+ }
+
+ private updateRun(run: TaskRun, patch: Partial): TaskRun {
+ const next = { ...run, ...patch };
+ this.storeRun(next);
+ this.runByTerminalId.set(next.terminalId, next);
+ return next;
+ }
+
+ private onTerminalOutput(event: Extract): void {
+ const run = this.runByTerminalId.get(event.terminalId);
+ if (!run) {
+ return;
+ }
+ const previous = this.outputTailByRunId.get(run.id) ?? [];
+ const next = [...previous, ...normalizeOutputLines(event.chunk)].slice(-TASK_TAIL_LINE_LIMIT);
+ this.outputTailByRunId.set(run.id, next);
+ }
+
+ private onTerminalExited(event: Extract): void {
+ const run = this.runByTerminalId.get(event.terminalId);
+ if (!run || run.status === "stopped") {
+ return;
+ }
+
+ const finished = this.updateRun(run, {
+ status: event.exitCode === 0 ? "passed" : "failed",
+ finishedAt: this.now(),
+ exitCode: event.exitCode,
+ summary: { tailLines: this.outputTailByRunId.get(run.id) ?? [] },
+ });
+ this.runByTerminalId.delete(event.terminalId);
+ this.deps.eventBus.emit({
+ type: "task.run.finished",
+ workspaceId: event.workspaceId,
+ run: finished,
+ });
+ }
+}
+```
+
+- [ ] **Step 4: Run manager tests**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/tasks/manager.test.ts
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Commit manager**
+
+Run:
+
+```bash
+git add packages/server/src/tasks/manager.ts packages/server/src/tasks/manager.test.ts
+git commit -m "feat: manage task runs"
+```
+
+### Task 5: Add Task Commands and WebSocket Events
+
+**Files:**
+- Create: `packages/server/src/commands/task.ts`
+- Create: `packages/server/src/__tests__/task-commands.test.ts`
+- Modify: `packages/server/src/commands/index.ts`
+- Modify: `packages/server/src/ws/dispatch.ts`
+- Modify: `packages/server/src/ws/hub.ts`
+- Modify: `packages/server/src/server.ts`
+
+- [ ] **Step 1: Write failing command tests**
+
+Create `packages/server/src/__tests__/task-commands.test.ts` with:
+
+```ts
+import type { TaskDefinition, TaskRun } from "@coder-studio/core";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import "../commands/index.js";
+import { dispatch, type CommandContext } from "../ws/dispatch.js";
+
+function createContext(): CommandContext {
+ const workspace = { id: "ws-1", path: "/repo" };
+ const task: TaskDefinition = {
+ id: "verify",
+ workspaceId: "ws-1",
+ kind: "verify",
+ label: "Verify",
+ command: "pnpm",
+ args: ["ci:verify"],
+ cwdPath: ".",
+ source: "package-json",
+ priority: 900,
+ };
+ const run: TaskRun = {
+ id: "run-1",
+ workspaceId: "ws-1",
+ taskId: "verify",
+ terminalId: "term-task",
+ status: "running",
+ command: "pnpm",
+ args: ["ci:verify"],
+ cwdPath: ".",
+ startedAt: 100,
+ };
+
+ return {
+ workspaceMgr: {
+ get: vi.fn(() => workspace),
+ },
+ taskMgr: {
+ setDiscoveredTasks: vi.fn((_workspaceId, tasks) => tasks),
+ list: vi.fn(() => [task]),
+ history: vi.fn(() => [run]),
+ run: vi.fn(async () => run),
+ rerun: vi.fn(async () => run),
+ stop: vi.fn(async () => ({ ...run, status: "stopped", finishedAt: 200 })),
+ },
+ terminalMgr: {},
+ sessionMgr: {},
+ eventBus: {},
+ broadcaster: {},
+ settingsRepo: {},
+ providerConfigRepo: {},
+ providerRegistry: [],
+ fencingMgr: {},
+ supervisorMgr: {},
+ autoFetch: {},
+ activationMgr: { getLease: vi.fn(() => ({ wsClientId: "client-1" })) },
+ lspMgr: {},
+ } as unknown as CommandContext;
+}
+
+describe("task commands", () => {
+ let ctx: CommandContext;
+
+ beforeEach(() => {
+ ctx = createContext();
+ });
+
+ it("lists task definitions", async () => {
+ const result = await dispatch(
+ { kind: "command", id: "cmd-1", op: "task.list", args: { workspaceId: "ws-1" } },
+ ctx,
+ "client-1"
+ );
+
+ expect(result.ok).toBe(true);
+ expect(result.data).toEqual(ctx.taskMgr.list("ws-1"));
+ });
+
+ it("runs a task for the workspace root", async () => {
+ const result = await dispatch(
+ {
+ kind: "command",
+ id: "cmd-1",
+ op: "task.run",
+ args: { workspaceId: "ws-1", taskId: "verify", themeBackground: "#0b1218" },
+ },
+ ctx,
+ "client-1"
+ );
+
+ expect(result.ok).toBe(true);
+ expect(ctx.taskMgr.run).toHaveBeenCalledWith({
+ workspaceId: "ws-1",
+ workspacePath: "/repo",
+ taskId: "verify",
+ themeBackground: "#0b1218",
+ });
+ });
+
+ it("stops a running task", async () => {
+ const result = await dispatch(
+ { kind: "command", id: "cmd-1", op: "task.stop", args: { workspaceId: "ws-1", runId: "run-1" } },
+ ctx,
+ "client-1"
+ );
+
+ expect(result.ok).toBe(true);
+ expect(ctx.taskMgr.stop).toHaveBeenCalledWith({ workspaceId: "ws-1", runId: "run-1" });
+ });
+
+ it("returns workspace_not_found for missing workspaces", async () => {
+ vi.mocked(ctx.workspaceMgr.get).mockReturnValueOnce(undefined);
+
+ const result = await dispatch(
+ { kind: "command", id: "cmd-1", op: "task.run", args: { workspaceId: "missing", taskId: "verify" } },
+ ctx,
+ "client-1"
+ );
+
+ expect(result.ok).toBe(false);
+ expect(result.error?.code).toBe("workspace_not_found");
+ });
+});
+```
+
+- [ ] **Step 2: Run command tests and confirm failure**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/__tests__/task-commands.test.ts
+```
+
+Expected: FAIL because task commands are not registered and `CommandContext` lacks `taskMgr`.
+
+- [ ] **Step 3: Register task commands**
+
+Create `packages/server/src/commands/task.ts`:
+
+```ts
+import { z } from "zod";
+import { discoverTasks } from "../tasks/discovery.js";
+import { registerCommand } from "../ws/dispatch.js";
+
+const workspaceSchema = z.object({
+ workspaceId: z.string(),
+});
+
+const taskRunSchema = z.object({
+ workspaceId: z.string(),
+ taskId: z.string(),
+ themeBackground: z
+ .string()
+ .regex(/^#[0-9a-fA-F]{3,8}$/)
+ .optional(),
+});
+
+function getWorkspaceOrThrow(ctx: Parameters[2]>[1], workspaceId: string) {
+ const workspace = ctx.workspaceMgr.get(workspaceId);
+ if (!workspace) {
+ throw { code: "workspace_not_found", message: `Workspace not found: ${workspaceId}` };
+ }
+ return workspace;
+}
+
+registerCommand("task.discover", workspaceSchema, async (args, ctx) => {
+ const workspace = getWorkspaceOrThrow(ctx, args.workspaceId);
+ const result = await discoverTasks({ workspaceId: args.workspaceId, rootPath: workspace.path });
+ const tasks = ctx.taskMgr.setDiscoveredTasks(args.workspaceId, result.tasks);
+ return { tasks, warnings: result.warnings };
+});
+
+registerCommand("task.list", workspaceSchema, async (args, ctx) => {
+ getWorkspaceOrThrow(ctx, args.workspaceId);
+ const existing = ctx.taskMgr.list(args.workspaceId);
+ if (existing.length > 0) {
+ return existing;
+ }
+ const workspace = getWorkspaceOrThrow(ctx, args.workspaceId);
+ const result = await discoverTasks({ workspaceId: args.workspaceId, rootPath: workspace.path });
+ return ctx.taskMgr.setDiscoveredTasks(args.workspaceId, result.tasks);
+});
+
+registerCommand("task.run", taskRunSchema, async (args, ctx) => {
+ const workspace = getWorkspaceOrThrow(ctx, args.workspaceId);
+ if (ctx.taskMgr.list(args.workspaceId).length === 0) {
+ const result = await discoverTasks({ workspaceId: args.workspaceId, rootPath: workspace.path });
+ ctx.taskMgr.setDiscoveredTasks(args.workspaceId, result.tasks);
+ }
+ return ctx.taskMgr.run({
+ workspaceId: args.workspaceId,
+ workspacePath: workspace.path,
+ taskId: args.taskId,
+ themeBackground: args.themeBackground,
+ });
+});
+
+registerCommand("task.rerun", taskRunSchema, async (args, ctx) => {
+ const workspace = getWorkspaceOrThrow(ctx, args.workspaceId);
+ return ctx.taskMgr.rerun({
+ workspaceId: args.workspaceId,
+ workspacePath: workspace.path,
+ taskId: args.taskId,
+ themeBackground: args.themeBackground,
+ });
+});
+
+registerCommand(
+ "task.stop",
+ z.object({
+ workspaceId: z.string(),
+ runId: z.string(),
+ }),
+ async (args, ctx) => {
+ getWorkspaceOrThrow(ctx, args.workspaceId);
+ return ctx.taskMgr.stop({ workspaceId: args.workspaceId, runId: args.runId });
+ }
+);
+
+registerCommand("task.history", workspaceSchema, async (args, ctx) => {
+ getWorkspaceOrThrow(ctx, args.workspaceId);
+ return ctx.taskMgr.history(args.workspaceId);
+});
+```
+
+Use an explicit `CommandContext` import if TypeScript rejects the helper type:
+
+```ts
+import type { CommandContext } from "../ws/dispatch.js";
+
+function getWorkspaceOrThrow(ctx: CommandContext, workspaceId: string) {
+ ...
+}
+```
+
+- [ ] **Step 4: Wire command registration and context**
+
+Add this import to `packages/server/src/commands/index.ts`:
+
+```ts
+import "./task.js";
+```
+
+Add this import to `packages/server/src/ws/dispatch.ts`:
+
+```ts
+import type { TaskManager } from "../tasks/manager.js";
+```
+
+Add this field to `CommandContext`:
+
+```ts
+taskMgr: TaskManager;
+```
+
+Add this import to `packages/server/src/server.ts`:
+
+```ts
+import { TaskManager } from "./tasks/manager.js";
+```
+
+Construct the manager after `terminalMgr`:
+
+```ts
+const taskMgr = new TaskManager({
+ eventBus,
+ terminalMgr,
+});
+```
+
+In workspace teardown, before closing terminals, add:
+
+```ts
+taskMgr.clearWorkspace(workspaceId);
+```
+
+Add `taskMgr` to `commandContext`.
+
+- [ ] **Step 5: Broadcast task events**
+
+In `packages/server/src/ws/hub.ts`, add task event types to `eventTypes`:
+
+```ts
+"task.discovered",
+"task.run.started",
+"task.run.updated",
+"task.run.finished",
+"task.run.stopped",
+```
+
+Add switch cases:
+
+```ts
+case "task.discovered":
+ topic = Topics.workspaceTaskDiscovered(event.workspaceId);
+ data = { tasks: event.tasks };
+ break;
+
+case "task.run.started":
+case "task.run.updated":
+case "task.run.finished":
+case "task.run.stopped":
+ topic = Topics.workspaceTaskRun(event.workspaceId, event.run.id);
+ data = { event: event.type, run: event.run };
+ break;
+```
+
+- [ ] **Step 6: Run command and server type tests**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/server exec vitest run src/__tests__/task-commands.test.ts
+pnpm --filter @coder-studio/server exec tsc -p tsconfig.json --noEmit
+```
+
+Expected: PASS for tests and no TypeScript errors.
+
+- [ ] **Step 7: Commit task commands**
+
+Run:
+
+```bash
+git add packages/server/src/commands/task.ts packages/server/src/commands/index.ts packages/server/src/ws/dispatch.ts packages/server/src/ws/hub.ts packages/server/src/server.ts packages/server/src/__tests__/task-commands.test.ts
+git commit -m "feat: expose task commands"
+```
+
+### Task 6: Add Web Bottom Panel Tabs and Task Terminal Visibility
+
+**Files:**
+- Create: `packages/web/src/features/bottom-panel/atoms.ts`
+- Create: `packages/web/src/features/bottom-panel/index.ts`
+- Create: `packages/web/src/features/workspace/views/shared/workspace-bottom-panel.tsx`
+- Modify: `packages/web/src/features/workspace/views/desktop/workspace-desktop-view.tsx`
+- Modify: `packages/web/src/features/terminal-panel/atoms/terminals.ts`
+- Modify: `packages/web/src/features/terminal-panel/actions/use-terminal-actions.ts`
+- Modify: `packages/web/src/features/terminal-panel/components/title-format.ts`
+- Modify: `packages/web/src/features/terminal-panel/views/shared/terminal-selector-item.tsx`
+- Modify: `packages/web/src/features/terminal-panel/views/shared/terminal-tab.tsx`
+- Modify: `packages/web/src/features/workspace/views/desktop/workspace-desktop-view.test.tsx`
+- Modify: `packages/web/src/features/terminal-panel/__tests__/terminal-panel.test.tsx`
+
+- [ ] **Step 1: Write failing bottom-panel tests**
+
+In `packages/web/src/features/workspace/views/desktop/workspace-desktop-view.test.tsx`, add a mock for the bottom panel if the file currently mocks `TerminalPanel` directly:
+
+```ts
+vi.mock("../shared/workspace-bottom-panel", () => ({
+ WorkspaceBottomPanel: () =>
Terminal Tasks
,
+}));
+```
+
+Add this test:
+
+```ts
+it("renders the shared workspace bottom panel on desktop", () => {
+ renderWorkspaceDesktopView();
+
+ expect(screen.getByTestId("workspace-bottom-panel")).toHaveTextContent("Terminal Tasks");
+});
+```
+
+In `packages/web/src/features/terminal-panel/__tests__/terminal-panel.test.tsx`, add a terminal list response that includes a shell, a task, and an agent, then assert the shell and task appear while the agent does not:
+
+```ts
+it("shows shell and task terminals but excludes agent terminals", async () => {
+ sendCommand.mockImplementation(async (op) => {
+ if (op === "terminal.list") {
+ return {
+ ok: true,
+ data: [
+ { id: "term-shell", workspaceId: "ws-test", kind: "shell", title: "bash", alive: true },
+ { id: "term-task", workspaceId: "ws-test", kind: "task", title: "Task: Verify", alive: true },
+ { id: "term-agent", workspaceId: "ws-test", kind: "agent", title: "Codex", alive: true },
+ ],
+ };
+ }
+ return { ok: true, data: {} };
+ });
+
+ renderWithStore();
+
+ expect(await screen.findByText(/bash/i)).toBeInTheDocument();
+ expect(screen.getByText("Task: Verify")).toBeInTheDocument();
+ expect(screen.queryByText("Codex")).not.toBeInTheDocument();
+});
+```
+
+- [ ] **Step 2: Run focused web tests and confirm failures**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/web exec vitest run src/features/workspace/views/desktop/workspace-desktop-view.test.tsx src/features/terminal-panel/__tests__/terminal-panel.test.tsx
+```
+
+Expected: FAIL because `WorkspaceBottomPanel` does not exist and terminal metadata excludes `task`.
+
+- [ ] **Step 3: Add bottom-panel active tab state**
+
+Create `packages/web/src/features/bottom-panel/atoms.ts`:
+
+```ts
+import { atom } from "jotai";
+import { atomFamily } from "jotai-family";
+
+export type BottomPanelTab = "terminal" | "tasks";
+
+export const bottomPanelActiveTabAtomFamily = atomFamily((_workspaceId: string) =>
+ atom("terminal")
+);
+```
+
+Create `packages/web/src/features/bottom-panel/index.ts`:
+
+```ts
+export * from "./atoms";
+```
+
+- [ ] **Step 4: Add WorkspaceBottomPanel**
+
+Create `packages/web/src/features/workspace/views/shared/workspace-bottom-panel.tsx`:
+
+```tsx
+import { useAtom } from "jotai";
+import { Tab, TabList, Tabs } from "../../../../components/ui";
+import { bottomPanelActiveTabAtomFamily } from "../../../bottom-panel";
+import { TasksPanel } from "../../../tasks/views/shared/tasks-panel";
+import { TerminalPanel } from "../../../terminal-panel";
+import { useTranslation } from "../../../../lib/i18n";
+
+interface WorkspaceBottomPanelProps {
+ workspaceId: string;
+}
+
+export function WorkspaceBottomPanel({ workspaceId }: WorkspaceBottomPanelProps) {
+ const t = useTranslation();
+ const [activeTab, setActiveTab] = useAtom(bottomPanelActiveTabAtomFamily(workspaceId));
+
+ return (
+
+ ) : null}
+
+ );
+}
+```
+
+- [ ] **Step 4: Run the component test to verify it passes**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/web test -- src/features/workspace/views/shared/agent-instructions-token-trend.test.tsx
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Commit the component behavior**
+
+Run:
+
+```bash
+git add packages/web/src/features/workspace/views/shared/agent-instructions-token-trend.tsx packages/web/src/features/workspace/views/shared/agent-instructions-token-trend.test.tsx
+git commit -m "feat(workspace): add agent token trend component"
+```
+
+Expected: commit succeeds.
+
+---
+
+### Task 2: Panel Integration and Placement
+
+**Files:**
+- Modify: `packages/web/src/features/workspace/views/shared/agent-instructions-section.tsx`
+- Modify: `packages/web/src/features/workspace/views/shared/agent-instructions-section.test.tsx`
+
+- [ ] **Step 1: Write the failing placement test**
+
+In `packages/web/src/features/workspace/views/shared/agent-instructions-section.test.tsx`, add this mock near the existing mocks:
+
+```tsx
+vi.mock("./agent-instructions-token-trend", () => ({
+ AgentInstructionsTokenTrend: ({ workspacePath }: { workspacePath: string }) => (
+
+ Token trend mock
+
+ ),
+}));
+```
+
+Add this test in the `describe("AgentInstructionsSection", () => { ... })` block after the existing expanded-default test:
+
+```tsx
+ it("renders the token trend as the first expanded body block for the current workspace", async () => {
+ renderSection({});
+
+ const trend = await screen.findByTestId("agent-token-trend");
+ const projectHeading = await screen.findByRole("heading", { level: 3, name: "Project Agent.md" });
+
+ expect(trend).toHaveAttribute("data-workspace-path", "/repo/project");
+ expect(trend.compareDocumentPosition(projectHeading) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
+ });
+```
+
+- [ ] **Step 2: Run the section test to verify it fails**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/web test -- src/features/workspace/views/shared/agent-instructions-section.test.tsx
+```
+
+Expected: FAIL because `agent-token-trend` is not rendered.
+
+- [ ] **Step 3: Render the token trend at the top of the expanded panel body**
+
+Modify `packages/web/src/features/workspace/views/shared/agent-instructions-section.tsx`.
+
+Add the import:
+
+```tsx
+import { AgentInstructionsTokenTrend } from "./agent-instructions-token-trend";
+```
+
+Add this as the first child inside `
`, before the error notice:
+
+```tsx
+ {workspace?.path ? : null}
+```
+
+- [ ] **Step 4: Run the section test to verify it passes**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/web test -- src/features/workspace/views/shared/agent-instructions-section.test.tsx
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Commit the panel integration**
+
+Run:
+
+```bash
+git add packages/web/src/features/workspace/views/shared/agent-instructions-section.tsx packages/web/src/features/workspace/views/shared/agent-instructions-section.test.tsx
+git commit -m "feat(workspace): show token trend in agent panel"
+```
+
+Expected: commit succeeds.
+
+---
+
+### Task 3: Styling and Localization
+
+**Files:**
+- Modify: `packages/web/src/styles/components.css`
+- Modify: `packages/web/src/locales/en.json`
+- Modify: `packages/web/src/locales/zh.json`
+- Modify: `packages/web/src/styles/components.theme.test.ts`
+
+- [ ] **Step 1: Write the failing style token guard**
+
+In `packages/web/src/styles/components.theme.test.ts`, add this test near the existing workspace agent instruction or monitoring style guard tests:
+
+```ts
+ it("keeps the agent token trend chart on shared theme tokens", () => {
+ const tokenTrend = getLastRuleBlock(".workspace-agent-instructions__token-trend");
+ const chart = getLastRuleBlock(".workspace-agent-instructions__token-trend-chart");
+ const skeleton = getLastRuleBlock(".workspace-agent-instructions__token-trend-skeleton");
+
+ expect(tokenTrend).toContain("border: 1px solid var(--border-subtle)");
+ expect(tokenTrend).toContain("background: var(--surface-subtle)");
+ expect(chart).toContain("height: 72px");
+ expect(skeleton).toContain("color: var(--text-tertiary)");
+ });
+```
+
+- [ ] **Step 2: Run the style test to verify it fails**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/web test -- src/styles/components.theme.test.ts
+```
+
+Expected: FAIL because the new CSS selectors do not exist.
+
+- [ ] **Step 3: Add CSS for the compact chart block**
+
+In `packages/web/src/styles/components.css`, add these rules after the existing `.workspace-agent-instructions__status-action` rule and before `.workspace-agent-instructions__system-list`:
+
+```css
+.workspace-agent-instructions__token-trend {
+ display: flex;
+ min-width: 0;
+ flex-direction: column;
+ gap: var(--gap-tight);
+ padding: var(--sp-2);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-lg);
+ background: var(--surface-subtle);
+}
+
+.workspace-agent-instructions__token-trend-header {
+ display: flex;
+ min-width: 0;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: var(--gap-tight);
+}
+
+.workspace-agent-instructions__token-trend-title {
+ margin: 0;
+ color: var(--text-primary);
+ font-size: var(--type-body-5-size);
+ line-height: var(--type-body-5-line-height);
+ font-weight: var(--type-body-5-weight);
+}
+
+.workspace-agent-instructions__token-trend-subtitle {
+ margin: var(--sp-0-5) 0 0;
+ color: var(--text-tertiary);
+ font-size: var(--type-body-6-size);
+ line-height: var(--type-body-6-line-height);
+}
+
+.workspace-agent-instructions__token-trend-total {
+ flex: 0 0 auto;
+ color: var(--text-primary);
+ font-size: var(--type-body-5-size);
+ line-height: var(--type-body-5-line-height);
+ font-weight: var(--type-body-5-weight);
+ white-space: nowrap;
+}
+
+.workspace-agent-instructions__token-trend-chart {
+ width: 100%;
+ height: 72px;
+ min-width: 0;
+}
+
+.workspace-agent-instructions__token-trend-footer {
+ display: flex;
+ min-width: 0;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ gap: var(--gap-compact);
+ color: var(--text-tertiary);
+ font-size: var(--type-body-6-size);
+ line-height: var(--type-body-6-line-height);
+}
+
+.workspace-agent-instructions__token-trend-skeleton,
+.workspace-agent-instructions__token-trend-state {
+ display: flex;
+ min-height: 72px;
+ align-items: center;
+ justify-content: center;
+ margin: 0;
+ color: var(--text-tertiary);
+ font-size: var(--type-body-6-size);
+ line-height: var(--type-body-6-line-height);
+ text-align: center;
+}
+```
+
+- [ ] **Step 4: Add localization strings**
+
+In `packages/web/src/locales/en.json`, under `workspace.agent_instructions`, add:
+
+```json
+"token_trend": {
+ "title": "Token Trend",
+ "subtitle": "Current project · Last 24 hours",
+ "loading": "Loading token trend...",
+ "empty": "No token data in the last 24 hours.",
+ "error": "Token trend unavailable.",
+ "total": "Total {value}",
+ "peak": "Peak {value}/h",
+ "sessions": "{count} sessions",
+ "chart_label": "Token consumption trend for the current project over the last 24 hours"
+}
+```
+
+In `packages/web/src/locales/zh.json`, under `workspace.agent_instructions`, add:
+
+```json
+"token_trend": {
+ "title": "Token 消耗趋势",
+ "subtitle": "当前项目 · 最近 24 小时",
+ "loading": "正在加载 token 趋势...",
+ "empty": "最近 24 小时暂无 token 数据。",
+ "error": "Token 趋势暂不可用。",
+ "total": "总量 {value}",
+ "peak": "峰值 {value}/h",
+ "sessions": "{count} 个会话",
+ "chart_label": "当前项目最近 24 小时 Token 消耗趋势图"
+}
+```
+
+- [ ] **Step 5: Run style and focused component tests**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/web test -- src/styles/components.theme.test.ts src/features/workspace/views/shared/agent-instructions-token-trend.test.tsx src/features/workspace/views/shared/agent-instructions-section.test.tsx
+```
+
+Expected: PASS.
+
+- [ ] **Step 6: Commit styling and localization**
+
+Run:
+
+```bash
+git add packages/web/src/styles/components.css packages/web/src/styles/components.theme.test.ts packages/web/src/locales/en.json packages/web/src/locales/zh.json
+git commit -m "style(workspace): polish agent token trend"
+```
+
+Expected: commit succeeds.
+
+---
+
+### Task 4: Final Verification
+
+**Files:**
+- Read: `docs/superpowers/specs/2026-06-07-agent-token-trend-design.md`
+- Read: `docs/superpowers/plans/2026-06-07-agent-token-trend.md`
+
+- [ ] **Step 1: Run focused web tests**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/web test -- src/features/workspace/views/shared/agent-instructions-token-trend.test.tsx src/features/workspace/views/shared/agent-instructions-section.test.tsx src/styles/components.theme.test.ts
+```
+
+Expected: PASS.
+
+- [ ] **Step 2: Run web typecheck**
+
+Run:
+
+```bash
+pnpm --filter @coder-studio/web exec tsc -p tsconfig.json --noEmit
+```
+
+Expected: PASS.
+
+- [ ] **Step 3: Review the git diff against the spec**
+
+Run:
+
+```bash
+git diff --stat HEAD~3..HEAD
+git diff HEAD~3..HEAD -- packages/web/src/features/workspace/views/shared/agent-instructions-token-trend.tsx packages/web/src/features/workspace/views/shared/agent-instructions-section.tsx packages/web/src/styles/components.css packages/web/src/locales/en.json packages/web/src/locales/zh.json
+```
+
+Expected: diff shows only the planned frontend chart, integration, style, and locale changes.
diff --git a/docs/superpowers/research/2026-06-04-work-analysis-provider-usage-capability-matrix.md b/docs/superpowers/research/2026-06-04-work-analysis-provider-usage-capability-matrix.md
new file mode 100644
index 000000000..ad6bb3358
--- /dev/null
+++ b/docs/superpowers/research/2026-06-04-work-analysis-provider-usage-capability-matrix.md
@@ -0,0 +1,74 @@
+# Work Analysis Provider Usage Capability Matrix
+
+Date: 2026-06-04
+Owner: docs/superpowers/research
+Scope: current `packages/server/src/work-analysis/log-sources/*` adapters, related tests, and a limited check of local provider roots where available
+
+## Rating Scale
+
+- `full`: current adapter already extracts the metric as a first-class normalized field
+- `partial`: raw logs appear to contain the signal, but the adapter only extracts it indirectly, incompletely, or not at all
+- `none`: no current adapter support and no confirming evidence from the inspected fixtures/roots
+
+## Matrix
+
+| Provider | Workspace path | Timestamps | Session counts | Tool counts | Model identity | Token usage | Cache usage | Reasoning usage | Cost estimation |
+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
+| `codex` | full | full | full | full | full | partial | partial | partial | none |
+| `claude` | full | full | full | full | none | partial | partial | partial | none |
+| `gemini` | full | full | full | none | none | none | none | none | none |
+| `cursor` | full | partial | full | full | none | none | none | none | none |
+| `opencode` | full | full | full | full | full | none | none | none | none |
+
+## Evidence Notes
+
+### `codex`
+
+- Workspace path is extracted from first valid metadata record `payload.cwd`; tests assert matching by `cwd`.
+- Timestamps are explicit when record timestamps exist, otherwise file `mtime` fallback.
+- Session counts are supported because each parsed JSONL becomes one normalized `WorkLogSession`.
+- Tool counts are extracted from `tool` / `command` / `function`-like records into `toolUseCount`.
+- Model identity is normalized as `payload.model ?? payload.model_provider`.
+- Raw local Codex logs include `event_msg` `token_count` records with `input_tokens`, `cached_input_tokens`, `output_tokens`, and `reasoning_output_tokens`, but the adapter does not read them. That makes token, cache, and reasoning usage `partial`, not `full`.
+- No inspected Codex source exposes a normalized cost field or adapter-side cost calculation.
+
+### `claude`
+
+- Workspace path comes from record `cwd`; timestamps come from record `timestamp`; tests cover session grouping and workspace attribution.
+- Session counts are available through grouped `sessionId` records.
+- Tool counts are inferred from `toolUse`, `attachment`, or `tool` presence, so current coverage is broad enough for `full` in V1 terms.
+- Current adapter does not normalize model identity even though raw local Claude assistant records include `message.model`.
+- Raw local Claude logs also include `message.usage` with `input_tokens`, `output_tokens`, `cache_creation_input_tokens`, and `cache_read_input_tokens`, and assistant content may include `thinking`. The adapter ignores all of these, so token/cache/reasoning usage are `partial`.
+- No cost field or cost estimation path is implemented.
+
+### `gemini`
+
+- Workspace path is authoritative via `.project_root`; tests verify tmp/history dedupe and workspace matching.
+- Timestamps come from `startTime` / `lastUpdated` with file `mtime` fallback.
+- Session counts are supported at the chat-file level.
+- Current adapter hardcodes `toolUseCount: 0`, and inspected tests/fixtures do not prove an extractable tool metric from the current Gemini chat shape.
+- No normalized model, token, cache, reasoning, or cost support is present in the current adapter or fixtures.
+
+### `cursor`
+
+- Workspace path is extracted from transcript record `cwd`; tests verify logs without `cwd` are skipped.
+- Timestamps are only file `mtime`, and the design doc explicitly calls this out as a V1 limitation. That keeps timestamps `partial`.
+- Session counts are available because each transcript file becomes one session.
+- Tool counts are inferred from transcript content parts with `tool` / `command` / `function` markers.
+- No normalized model, token, cache, reasoning, or cost support is present in the adapter or tests.
+
+### `opencode`
+
+- Workspace path comes from `project.worktree` with `session.directory` fallback.
+- Timestamps are explicit from `session.time_created` and `session.time_updated`.
+- Session counts, tool counts, and model identity are all normalized from SQLite query results; tests cover the query shape and a real fixture path.
+- The current SQL does not query any token, cache, reasoning, or cost fields. The inspected adapter and tests do not establish those metrics as available for V1.
+
+## V1 Conclusion
+
+The current work-analysis implementation already has the strongest usage-source foundation in `codex` and `claude`.
+
+- `codex` already normalizes workspace, timestamps, session counts, tool counts, and model identity, and its raw logs clearly expose token/cache/reasoning counters that can be added later.
+- `claude` already normalizes workspace, timestamps, session counts, and tool counts, and its raw logs also expose token/cache usage plus model and thinking data that are not yet harvested.
+
+If V1 usage reporting needs the best initial provider coverage with the lowest research risk, `codex` and `claude` are the right starting sources. The findings here support that conclusion.
diff --git a/docs/superpowers/specs/2026-06-02-draft-pane-launcher-internal-redesign-design.md b/docs/superpowers/specs/2026-06-02-draft-pane-launcher-internal-redesign-design.md
new file mode 100644
index 000000000..11078b9b8
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-02-draft-pane-launcher-internal-redesign-design.md
@@ -0,0 +1,202 @@
+# Draft Pane Launcher Internal Redesign
+
+> Status: Draft for review
+> Date: 2026-06-02
+> Scope: `packages/web/src/features/agent-panes/views/shared/draft-launcher.tsx`, `packages/web/src/styles/components.css`, related launcher tests and theme tests
+
+## Goal
+
+Refine the `draft pane` launcher so it feels cleaner, flatter, and more stable without changing the surrounding pane shell or the product's existing visual language.
+
+This redesign is intentionally narrow:
+
+- keep the current `session-card` and `session-header`
+- keep the current theme/token system
+- keep the current two-entry mental model: `Agent` on the left, `file open` on the right
+- only redesign the internal launcher area inside the draft pane
+
+## Non-Goals
+
+- Do not redesign the outer draft pane frame or header actions
+- Do not introduce a new welcome-page-like layout language
+- Do not turn the launcher into a new full-screen entry experience
+- Do not remove provider install guidance, diagnostics links, or drag-and-drop behavior
+- Do not replace the compact-width carousel model with a new navigation pattern
+
+## Confirmed Constraints
+
+The following constraints were explicitly confirmed during design review and are mandatory:
+
+- Preserve the current overall product style
+- Keep the redesign focused inside the `draft pane` body
+- Avoid strong left/right split-panel tension
+- Avoid large, tall provider cards that reduce provider density
+- Avoid moving the helper copy into the outer draft pane header
+- Keep the file-open affordance recognizable and still mapped to the right side on desktop
+
+## Current Problems
+
+### 1. The desktop launcher feels visually split in half
+
+The current implementation renders two separate `agent-draft-panel` blocks with a strong side-by-side presence. This creates a "two panels fighting each other" effect instead of one coherent launcher surface.
+
+### 2. Provider items are too tall for a launcher
+
+The provider buttons behave more like stacked cards than launcher rows. Their vertical size makes the area feel heavy and reduces the number of providers that can be scanned quickly.
+
+### 3. The file area is visually too isolated
+
+The drag-and-drop zone reads like a separate empty module instead of a secondary utility inside the same launcher surface.
+
+### 4. Helper copy lacks a natural home
+
+The helper sentence:
+
+`点击启动 Agent,或将文件拖到右侧区域直接打开。`
+
+should explain the relationship between the two entry paths, but placing it in the outer header makes that header too large, while placing it as a heavy footer pulls attention downward too much.
+
+## Design Conclusion
+
+Adopt a `shared internal workarea` design for the draft launcher.
+
+The draft pane keeps its existing outer shell, but the content area is reorganized into one shared internal surface:
+
+- a lightweight helper-copy strip at the top of the internal workarea
+- a primary `Agent` region on the left
+- a secondary `file utility` region on the right
+- a very light divider between the two regions
+
+This keeps the existing interaction model intact while removing the current visual feeling of two independent cards.
+
+## Detailed Design
+
+### 1. Internal Structure
+
+Replace the visual grammar of two independent `agent-draft-panel` blocks with one shared internal container, conceptually:
+
+- `.agent-draft-workarea`
+ - `.agent-draft-workarea-copy`
+ - `.agent-draft-workarea-body`
+ - `.agent-draft-workarea-main`
+ - `.agent-draft-workarea-side`
+
+Rules:
+
+- The outer `session-card`, `session-header`, action buttons, and pane states remain unchanged
+- The new workarea is the only visually framed surface inside the draft body
+- The left and right regions share the same background and boundary
+- The left region remains dominant at roughly `1.2fr`
+- The right region remains secondary at roughly `0.8fr`
+
+### 2. Helper Copy Placement
+
+Move the helper sentence into the top of the internal workarea, not the outer header and not a heavy footer.
+
+Rules:
+
+- It spans the internal workarea width
+- It uses small, low-contrast supporting text styling
+- It should read as orientation copy, not as a standalone banner or badge
+- It should visually sit above both entry areas so it explains both of them together
+
+### 3. Provider Region
+
+The provider area keeps the current provider list behavior, but changes its visual density and hierarchy.
+
+Rules:
+
+- Provider items shift from tall card-buttons to flatter launcher rows
+- Each row keeps:
+ - provider icon
+ - title
+ - subtitle
+ - CTA
+- Row height should compress to roughly `70% - 80%` of the current visual height
+- Claude / Codex may retain subtle semantic tinting, but only as restrained row treatment
+- The row should scan as one line-first unit, not as a three-layer mini-card
+- Install or diagnostics guidance remains available, but should read as lightweight appended detail instead of expanding the main row into a large block whenever possible
+
+### 4. File Utility Region
+
+The right-side file-open area remains present, but it becomes a quieter utility region inside the same workarea.
+
+Rules:
+
+- Keep the right-side file entry mental model on desktop
+- Reduce the visual size and emphasis of the drag-and-drop box
+- Keep the drop target clear and discoverable
+- Use a lighter dashed treatment and smaller icon footprint than today
+- Ensure its weight is lower than the provider region so the launcher still reads as `Agent-first`
+
+### 5. Shared Surface Styling
+
+The redesign should flatten the internal area without creating a new design system.
+
+Rules:
+
+- Use one shared internal surface instead of two card-like sub-panels
+- Reduce internal padding and gaps
+- Weaken the center divider
+- Reduce the visual contrast between left and right background treatments
+- Preserve existing theme tokens and component vocabulary rather than inventing a separate style language
+
+### 6. Compact Width Behavior
+
+The current compact carousel behavior remains the interaction model for narrow widths.
+
+Rules:
+
+- Preserve the existing `agent / file` panel switching logic
+- Preserve current swipe and dot navigation behavior
+- Only adjust the compact-mode visuals to align with the flatter shared-workarea styling
+- Do not introduce a third mode or a new mobile information architecture
+
+## Interaction and Behavior Constraints
+
+The redesign is visual and structural only. Existing behavior must remain intact:
+
+- provider click still launches or opens install guidance
+- drag-and-drop still targets the file-open area
+- pane assignment / replacement behavior remains unchanged
+- drag overlay and drop-target states remain unchanged
+- diagnostics and provider documentation links remain accessible
+
+## Test and Validation Requirements
+
+Update tests to match the new internal launcher grammar without weakening behavior coverage.
+
+Required updates:
+
+- keep coverage for provider launch behavior
+- keep coverage for drag-and-drop file open behavior
+- keep coverage for compact carousel switching
+- update assertions for helper-copy placement so the sentence is verified in the internal workarea, not the old footer position
+- update structure/style assertions so tests stop depending on two strongly independent launcher panel visuals
+- update theme/style tests so the new shared workarea and flatter rows remain token-aligned
+
+## Scope Boundaries for Implementation
+
+Implementation may adjust:
+
+- markup structure inside the draft launcher body
+- draft launcher class names
+- launcher-specific CSS in `components.css`
+- related unit and style tests
+
+Implementation must not expand into:
+
+- global pane header redesign
+- unrelated workspace panel restyling
+- provider behavior changes
+- new localization copy beyond relocation or minimal wording polish if strictly needed
+
+## Success Criteria
+
+The redesign is successful if:
+
+- the launcher reads as one coherent internal surface instead of two separate cards
+- provider scanning feels denser and faster
+- the right-side file area remains understandable but clearly secondary
+- the helper sentence explains the two entry paths without enlarging the outer header
+- the desktop layout feels less visually split while preserving existing interaction logic
diff --git a/docs/superpowers/specs/2026-06-02-image-preview-zoom-design.md b/docs/superpowers/specs/2026-06-02-image-preview-zoom-design.md
new file mode 100644
index 000000000..926080d2f
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-02-image-preview-zoom-design.md
@@ -0,0 +1,32 @@
+# Image Preview Zoom Design
+
+## Scope
+
+Improve the existing file image preview in `packages/web/src/features/code-editor/components/image-preview.tsx`.
+The change is limited to the normal workspace image preview surface, not image diff or document preview.
+
+## Behavior
+
+- Add image zoom controls inside the preview chrome: zoom out, zoom in, fit to window, and show actual size.
+- Keep the current metadata strip with image type, dimensions, and size.
+- Show the current zoom percentage so the user can see the active scale.
+- Support keyboard-modified wheel zoom on the image canvas with `Ctrl` or `Meta`.
+- Clamp zoom between 25% and 400%.
+- Reset zoom to fit mode when the image URL or version changes.
+
+## UI
+
+Use icon-only `IconButton` controls with `Tooltip`, matching the existing editor toolbar style.
+The controls live in the image preview footer so they do not compete with the editor mode toolbar.
+The image canvas remains the scroll container; when zoomed beyond the viewport, the user can scroll around the enlarged image.
+
+## Testing
+
+Add focused component tests for:
+
+- Rendering zoom controls with accessible names.
+- Zoom-in and zoom-out changing the percentage.
+- Actual-size and fit controls switching the transform state.
+- Version changes resetting the zoom state.
+
+Run the image preview test file after implementation.
diff --git a/docs/superpowers/specs/2026-06-02-skills-panel-target-summary-design.md b/docs/superpowers/specs/2026-06-02-skills-panel-target-summary-design.md
new file mode 100644
index 000000000..9eb1e608f
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-02-skills-panel-target-summary-design.md
@@ -0,0 +1,337 @@
+# Skills Panel Target Summary Design
+
+Date: 2026-06-02
+Status: Draft
+Owner: spencer
+
+## Problem
+
+当前 `Skills` 面板已经支持:
+
+- 已安装 skill 排序在前
+- 顶层分组折叠
+- 每个 skill 展示完整 target 列表并执行挂载操作
+
+但现有已安装 skill 卡片仍然过高。每个卡片默认直接展开所有 target,导致以下问题:
+
+- 当一个 skill 对应多个 agent target 时,卡片高度迅速膨胀
+- 用户很难快速扫描“哪些 skill 已挂到哪些 agent”
+- 挂载状态、健康状态、路径和操作同时暴露,信息层级过重
+- 视觉密度和文件面板的 row 风格不一致
+
+这不是一个单纯的间距问题。核心问题是:当前卡片默认把“摘要信息”和“诊断/操作信息”混在同一层展示,导致面板把垂直空间消耗在低频细节上。
+
+## Goals
+
+- 显著压缩已安装 skill 卡片的默认高度。
+- 让用户在不展开的情况下,快速看清每个 skill 在各 agent 上的挂载概况。
+- 保留三种用户可理解的大状态:`未配置 / 未挂载 / 已挂载`。
+- 通过颜色和 tooltip 传达状态,不把所有异常细节都堆在默认视图里。
+- 展开后仍能查看详细 target 信息并执行挂载、卸载、修复或配置操作。
+- 样式和交互密度向文件面板对齐,而不是继续使用大块卡片式 target 列表。
+
+## Non-Goals
+
+- 这次不重做整个 `Skills` 面板布局。
+- 不改变 skill 安装、卸载、挂载、修复的后端行为。
+- 不引入新的挂载状态枚举;只在前端把现有状态归并成更易读的摘要语义。
+- 不把 target 详细操作迁移到别的页面;仍保留在当前 skill 卡片展开态内。
+
+## Current Context
+
+当前实现位于:
+
+- [`packages/web/src/features/workspace/views/shared/skills-panel.tsx`](../../../packages/web/src/features/workspace/views/shared/skills-panel.tsx)
+- [`packages/web/src/styles/components.css`](../../../packages/web/src/styles/components.css)
+
+当前每个已安装 skill 卡片会:
+
+- 渲染 skill 名称、slug、版本、描述和卸载按钮
+- 在卡片下方直接渲染完整 `.skills-panel__targets`
+- 为每个 target 展示:
+ - agent 名称
+ - health tag
+ - mount relation tag
+ - target path
+ - mount/unmount/repair 按钮
+
+现有 target 数据模型已经提供足够信息完成新的摘要层:
+
+- `AgentSkillTargetEntry.displayName`
+- `AgentSkillTargetEntry.skillDir`
+- `AgentSkillTargetEntry.lastHealthState`
+- `AgentSkillTargetEntry.lastHealthError`
+- `SkillMountRelation.enabled`
+- `SkillMountRelation.status`
+- `SkillMountRelation.targetPath`
+- `SkillMountRelation.lastError`
+
+## User Decisions Captured
+
+- 每个 skill 卡片默认收起 target 详情。
+- 收起态显示 agent 缩写,而不是完整 target 行。
+- 用颜色区分 `未配置 / 未挂载 / 已挂载`。
+- 鼠标悬浮时用 tooltip 展示具体原因。
+- 展开后再显示详细 target 行和执行挂载操作。
+- `未配置` 作为独立状态保留,不合并进 `未挂载`。
+
+## Approaches Considered
+
+### Option A: 缩写状态胶囊行 + 展开看详情(推荐)
+
+收起态只显示一行 agent 缩写胶囊,每个胶囊用颜色表达大状态,tooltip 解释原因;展开后显示完整 target 详情和操作。
+
+优点:
+
+- 压缩高度最明显
+- 仍能一眼看到每个 agent 的状态分布
+- 展开态和收起态之间的心智映射最稳定
+
+缺点:
+
+- 用户需要理解缩写含义
+- 需要新增一层摘要状态和 tooltip 文案逻辑
+
+### Option B: 状态计数摘要 + 少量 target 标记
+
+收起态显示 `已挂载 2 · 未挂载 1 · 未配置 1`,再附少量 agent 标记。
+
+优点:
+
+- 总体状态统计最清楚
+
+缺点:
+
+- 不能一眼看清是哪些 agent 异常
+- 仍然需要展开才能建立 target 对应关系
+
+### Option C: 纯彩色点阵摘要
+
+收起态只展示一排彩色点或方块,每个点代表一个 agent。
+
+优点:
+
+- 最省空间
+
+缺点:
+
+- 语义过弱
+- 不利于和展开态建立连续关系
+- 对低频用户不够自解释
+
+## Final Choice
+
+采用 Option A。
+
+每个已安装 skill 卡片增加自己的 target 展开状态。默认只显示一条摘要行,摘要行用 agent 缩写胶囊展示所有 target 的大状态;点击摘要行后,在当前卡片内部展开明细行和操作区。
+
+## Interaction Design
+
+### 1. Skill Card Structure
+
+每个已安装 skill 卡片保留现有信息层:
+
+- 标题
+- slug / 版本
+- 描述
+- 卸载按钮
+
+在描述区下方新增一个 `target summary row`:
+
+- 左侧是 agent 缩写胶囊组
+- 右侧是展开/收起箭头
+- 点击摘要行空白区域或箭头可展开/收起当前卡片
+
+默认情况下不渲染完整 target 详情区,只显示摘要行。
+
+### 2. Per-Skill Expansion
+
+展开状态是按 skill 卡片独立维护的,不影响其他 skill。
+
+- 默认:全部收起
+- 展开后:只在当前 skill 卡片内部显示 target 详情
+- 收起后:恢复为单行摘要
+
+这样可以避免一个 skill 的详情把整段列表全部推开,也符合文件面板“按项查看细节”的交互节奏。
+
+### 3. Collapsed Summary Row
+
+摘要行展示所有 target 对应的 agent 缩写,例如:
+
+- `CC` for `Claude Code`
+- `CX` for `Codex`
+- `GM` for `Gemini CLI`
+
+内置 provider 使用稳定映射,避免缩写在不同会话中变化。非内置或自定义 provider 使用 `displayName` 自动生成 1-2 个大写字母简称。
+
+摘要行本身使用接近 `workspace-sidebar-row` 的交互风格:
+
+- 紧凑高度
+- 轻 hover 背景
+- 整行可点击
+- 不使用厚重卡片分割
+
+### 4. Expanded Detail Rows
+
+展开后,摘要行下方显示一层内嵌明细区域,而不是恢复成大块 target 列表卡片。
+
+每个 target 明细行结构:
+
+- 第一行左侧:agent 全名
+- 第一行右侧:操作按钮
+- 名称旁边:大状态标签
+- 第二行:target 目录路径或异常原因
+
+布局目标是向文件面板的 row 密度靠拢:
+
+- 行分隔轻量化
+- 弱化每个 target 的独立卡片感
+- 在桌面端保持左右布局
+- 在窄屏下回落为纵向堆叠
+
+### 5. Action Rules
+
+展开态中的操作规则:
+
+- `已挂载`
+ - 显示 `卸载挂载`
+ - 若 mount relation 存在异常状态,则额外显示 `修复`
+- `未挂载`
+ - 显示 `挂载`
+- `未配置`
+ - 不显示禁用的 `挂载`
+ - 显示 `配置目录`
+ - 点击后打开现有 `Agent Targets` drawer
+
+这样可以把 `未配置` 从“不可点击的失败按钮”改成“正确的下一步动作”。
+
+## State Model
+
+### Visible States in Summary Layer
+
+收起态只暴露三种颜色语义:
+
+- 绿色:`已挂载`
+- 琥珀色:`未挂载`
+- 粉红色:`未配置`
+
+收起态不再直接暴露 `stale / missing_target / missing_source / failed` 这些细粒度异常颜色,避免摘要层重新变复杂。
+
+### Mapping Rules
+
+目标状态归类规则:
+
+- `未配置`
+ - `target.skillDir` 为空
+ - 或 `target.lastHealthState === "unconfigured"`
+- `已挂载`
+ - 存在启用中的 relation
+ - 且 `relation.status === "mounted"`
+- `未挂载`
+ - 其余全部情况,包括:
+ - 没有 relation
+ - relation 未启用
+ - `stale`
+ - `missing_target`
+ - `missing_source`
+ - `failed`
+
+这个规则保证摘要层永远稳定在三态,不把诊断状态数量扩展到用户难以快速扫描的程度。
+
+## Tooltip Design
+
+每个缩写胶囊支持 hover tooltip。tooltip 结构固定为三行:
+
+1. agent 全名
+2. 大状态:`未配置 / 未挂载 / 已挂载`
+3. 具体原因
+
+具体原因文案规则:
+
+- `未配置`
+ - 默认:`未配置 skill 目录`
+- `未挂载`
+ - 无 relation:`尚未挂载到此 Agent`
+ - `stale`:`挂载已漂移,需要修复`
+ - `missing_target`:`目标目录缺失`
+ - `missing_source`:`Skill 源目录缺失`
+ - `failed`:`最近一次挂载失败`
+- `已挂载`
+ - `已挂载到 `
+
+若存在 `relation.lastError` 或 `target.lastHealthError`,优先用真实错误摘要覆盖第 3 行原因文案。
+
+路径较长时在视觉上允许截断,但 tooltip 中展示完整值。
+
+## Visual Language
+
+### Summary Tokens
+
+摘要胶囊需要同时满足“紧凑”和“可扫描”:
+
+- 尺寸明显小于展开态按钮和 tag
+- 文本为 1-2 个大写字母
+- 使用轻量背景和边框,而不是大体积 badge
+
+颜色语义:
+
+- `已挂载`:绿色系
+- `未挂载`:琥珀系
+- `未配置`:粉红系
+
+### Row Styling
+
+整体样式对齐文件面板,而不是继续沿用厚重的子卡片:
+
+- skill 卡片本身仍然保持现有容器
+- target 层由“多行卡片组”改为“摘要行 + 轻量内嵌明细”
+- hover、focus 和行间距向 `workspace-sidebar-row` 靠拢
+
+### Click Behavior
+
+- hover 摘要行:整行高亮
+- hover 缩写胶囊:显示 tooltip
+- 点击摘要行空白区域:展开/收起
+- 点击胶囊本身:仅显示 tooltip,不触发展开
+
+这样可以避免用户在查看状态原因时误触发展开。
+
+## Implementation Notes
+
+前端需要新增一层针对 target 的派生视图模型,至少包含:
+
+- 缩写
+- 三态摘要状态
+- tooltip 文案
+- 展开态按钮类型
+- 是否需要修复
+
+建议把这层派生逻辑集中在 `skills-panel.tsx` 附近的纯函数中,而不是把状态分支散落在 JSX 内部。
+
+内置 provider 的稳定缩写映射表本次先放在 `skills-panel.tsx` 附近的本地纯函数内,而不是提取到共享工具文件。原因是这套缩写目前只服务当前面板的摘要层,先保持局部收敛,等第二个消费方出现后再提取共享工具。
+
+样式上建议:
+
+- 保留 `.skills-panel__list-item`
+- 用新的 summary row class 替代当前默认展示的 `.skills-panel__targets`
+- 让展开态 target 行更接近现有 sidebar row 节奏
+
+`配置目录` 动作需要直接打开现有 `Agent Targets` drawer,并自动聚焦到当前 provider 对应项。这样用户从 `未配置` 状态进入配置流时,不需要再在 drawer 里额外查找目标 agent。
+
+## Testing
+
+需要新增或更新组件测试,覆盖以下行为:
+
+- 已安装 skill 默认渲染为收起态摘要行
+- target 胶囊按 `已挂载 / 未挂载 / 未配置` 三态着色
+- hover 胶囊时能看到正确 tooltip 文案
+- 点击摘要行可展开/收起当前 skill
+- 展开后可看到详细 target 行和正确操作按钮
+- `未配置` target 显示 `配置目录` 而不是禁用挂载按钮
+- 异常 relation 在收起态归并为 `未挂载`,但展开态仍能看到具体原因或修复按钮
+
+## Risks
+
+- 缩写如果自动生成不稳定,用户会失去识别连续性。
+- 如果 tooltip 文案直接复用底层错误字符串,可能出现过长或不可读的问题。
+- 如果摘要行点击区和胶囊 hover 区没有处理好,容易出现 tooltip 和展开操作冲突。
diff --git a/docs/superpowers/specs/2026-06-03-agent-instructions-multi-provider-generation-design.md b/docs/superpowers/specs/2026-06-03-agent-instructions-multi-provider-generation-design.md
new file mode 100644
index 000000000..4b48fcf3a
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-03-agent-instructions-multi-provider-generation-design.md
@@ -0,0 +1,242 @@
+# Agent Instructions Multi-Provider Generation Design
+
+## Summary
+
+Extend `.coder-studio/agent.md` generation from the current Codex-only headless path to a unified multi-provider headless flow for built-in providers that already expose headless execution. The server remains responsible for prompt construction, output validation, and file writes. Providers only generate the response payload.
+
+This keeps the existing generate dialog and write path intact while removing the current mismatch where the UI is provider-aware but the backend generation contract is effectively Codex-specific.
+
+## Goals
+
+- Support `agent.md` generation for built-in headless providers beyond Codex.
+- Preserve the existing saved path: `.coder-studio/agent.md`.
+- Keep generation as a request-scoped headless task, not a supervisor workflow.
+- Unify the business-level generation payload across providers.
+- Keep server-side ownership of validation and file writes.
+
+## Non-Goals
+
+- Do not let providers write `.coder-studio/agent.md` directly.
+- Do not add preview, retry queues, background jobs, or scheduling.
+- Do not redesign the generate dialog UX in this change.
+- Do not expand support to providers that do not already have stable built-in headless execution.
+
+## Current State
+
+The current generation flow is provider-selectable in the UI, but the backend only treats providers as generation-capable if they expose the `agent_instructions_generate` headless scenario. Today that scenario is only declared by Codex.
+
+The execution path also parses Codex-specific stdout:
+
+- `packages/server/src/agent-instructions/agent-generator.ts`
+- `packages/server/src/agent-instructions/output.ts`
+- `packages/providers/src/codex/headless.ts`
+
+This creates two problems:
+
+1. Generation capability is narrower than the existing headless provider set.
+2. Output parsing is tied to Codex JSONL rather than a provider-agnostic generation contract.
+
+## Design
+
+### 1. Keep Scenario-Based Headless Support
+
+Retain `agent_instructions_generate` as an explicit headless scenario.
+
+Reasoning:
+
+- It preserves the current capability model and avoids collapsing all future headless uses into one boolean.
+- It remains the correct feature gate for the UI and server.
+- It avoids broadening support to providers that happen to be headless but are not yet wired for generation.
+
+Change:
+
+- Add `agent_instructions_generate` to the built-in providers that already support stable headless execution for `supervisor_eval` and `session_analysis`:
+ - `claude`
+ - `gemini`
+ - `cursor`
+- Keep `codex` support unchanged.
+
+`opencode` stays unchanged unless it first grows a stable built-in headless execution path compatible with the same contract.
+
+### 2. Introduce a Unified Generation Payload
+
+All providers should be prompted to return a strict JSON payload in their final text response:
+
+```json
+{
+ "ok": true,
+ "content": "# Agent Instructions\n..."
+}
+```
+
+Optional failure payload:
+
+```json
+{
+ "ok": false,
+ "error": "..."
+}
+```
+
+Rules:
+
+- The server treats the provider output as invalid unless it can recover a final text response and parse the JSON payload.
+- `content` must still pass existing markdown normalization and heading validation.
+- The server continues to own all file writes.
+
+This separates two concerns cleanly:
+
+- provider-specific envelope extraction
+- provider-agnostic generation payload parsing
+
+### 3. Split Output Handling Into Two Layers
+
+Replace the Codex-only parser with a two-step model:
+
+1. Extract final response text from provider stdout.
+2. Parse the unified generation JSON payload from that text.
+
+#### Provider-Specific Envelope Extraction
+
+Add a provider-aware extractor in `packages/server/src/agent-instructions/output.ts`.
+
+Expected handling:
+
+- `codex`
+ - Extract the final `agent_message` text from the `codex exec --json` JSONL stream.
+- `claude`
+ - Extract the assistant text from the `--output-format json` envelope already used by headless evaluation.
+- `gemini`
+ - Extract the assistant text from its `--output-format json` envelope.
+- `cursor`
+ - Extract the assistant text from its `--output-format json` envelope.
+
+This layer should return one thing only: the final text reply generated by the provider.
+
+#### Unified Payload Parsing
+
+After text extraction, parse the JSON payload and validate:
+
+- payload is valid JSON
+- `ok === true`
+- `content` is a non-empty string
+- normalized markdown starts with exact `# Agent Instructions`
+
+If `ok === false`, surface the provider error message as a typed generation failure.
+
+### 4. Reuse Existing Headless Command Shape
+
+No new request shape is needed for `agent_instructions_generate`.
+
+The current `ProviderHeadlessCommandRequest` is already sufficient:
+
+- `prompt`
+- `sessionId`
+- `workspacePath`
+- `apiKey?`
+- `model?`
+- `outputFile?`
+
+The new scenario should reuse each provider's existing headless command builder pattern. This is already how Codex works today, and the other built-in headless providers already accept the same request shape for evaluation.
+
+### 5. Prompt Contract
+
+`packages/server/src/agent-instructions/prompt.ts` should require:
+
+- the provider must return strict JSON, not raw markdown
+- `content` must contain the full markdown document
+- no extra prose outside the JSON payload
+- the markdown must begin with `# Agent Instructions`
+- unknown facts must be omitted rather than invented
+
+The business contract becomes:
+
+- provider generates structured content
+- server validates and persists it
+
+### 6. File Write Responsibility
+
+Keep file writes on the server.
+
+Reasons:
+
+- fixed write target
+- stronger validation before persistence
+- easier tests
+- cleaner permission boundary
+- simpler future support for preview or diff before write
+
+The provider should never be asked to mutate the workspace directly for this flow.
+
+## Data Flow
+
+1. User opens the generate dialog.
+2. Frontend loads providers from `provider.list` and `provider.runtimeStatus`.
+3. Frontend filters to `available && supportsAgentInstructionsGeneration`.
+4. User chooses provider and optional model.
+5. Server collects workspace intelligence and builds the generation prompt.
+6. Server invokes the selected provider in headless `agent_instructions_generate` mode.
+7. Server extracts the provider's final text reply from stdout.
+8. Server parses the unified generation JSON payload.
+9. Server normalizes and validates markdown.
+10. Server writes `.coder-studio/agent.md`.
+11. Existing downstream attach/view/publish flows continue unchanged.
+
+## Error Handling
+
+Introduce clearer typed failures for these cases:
+
+- provider does not support `agent_instructions_generate`
+- provider stdout cannot be decoded for that provider's envelope format
+- provider reply is not valid generation JSON
+- provider reply reports `ok: false`
+- markdown payload fails normalization or heading validation
+
+This should replace the current ambiguity where non-Codex providers are excluded early and Codex parsing failures are the dominant error mode.
+
+## Testing
+
+Add or update tests in four groups.
+
+### Provider Definition Tests
+
+- `claude`, `gemini`, and `cursor` declare `agent_instructions_generate`
+- existing `codex` coverage remains valid
+
+### Server Generation Tests
+
+- successful generation for each supported built-in provider
+- provider-specific envelope parse failures
+- unified payload parse failures
+- invalid markdown heading failures
+- provider-reported `ok: false` failures
+
+### Provider Listing and Runtime Tests
+
+- provider list exposes `supportsAgentInstructionsGeneration` for the expanded set
+- generate dialog filtering includes runtime-available providers that support generation
+
+### Regression Tests
+
+- existing Codex generation path still succeeds
+- current write command behavior and saved-path semantics remain unchanged
+
+## Rollout
+
+Implement in one patch if practical, but keep the change internally layered:
+
+1. broaden provider scenario declarations
+2. introduce unified output extraction and payload parsing
+3. update generation tests
+4. update UI-facing provider filtering expectations
+
+This allows the product behavior to become genuinely multi-provider without a UI redesign.
+
+## Open Decision
+
+No additional product-level decisions remain for this phase. The intended implementation target is:
+
+- built-in headless providers only
+- explicit `agent_instructions_generate` scenario retained
+- server-owned writes
+- provider-specific envelope extraction plus unified generation payload parsing
diff --git a/docs/superpowers/specs/2026-06-03-provider-work-log-analysis-design.md b/docs/superpowers/specs/2026-06-03-provider-work-log-analysis-design.md
new file mode 100644
index 000000000..f89cab9ff
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-03-provider-work-log-analysis-design.md
@@ -0,0 +1,630 @@
+# Provider Work Log Analysis Design
+
+Date: 2026-06-03
+Status: Draft
+Owner: spencer
+
+## Problem
+
+`工作分析` 当前把 Coder Studio 自己管理的 session 当成分析数据源。这和用户期望不一致:工作分析应该分析选中 workspace 在各个 agent/provider 自己历史日志中的实际工作记录,而不是当前打开或曾由 Coder Studio 管理的会话。
+
+当前实现直接依赖 Coder Studio session:
+
+- [`packages/server/src/work-analysis/session-selector.ts`](../../../packages/server/src/work-analysis/session-selector.ts) 调用 `sessionMgr.getAll()`。
+- [`packages/server/src/work-analysis/evidence-collector.ts`](../../../packages/server/src/work-analysis/evidence-collector.ts) 从 Coder Studio session 读取 terminal snapshot 和 latest user input。
+- [`packages/server/src/work-analysis/service.ts`](../../../packages/server/src/work-analysis/service.ts) 会复用已成功的分析结果,导致 preset 时间范围和新增 provider 日志可能不刷新。
+
+这会导致两个明显错误:
+
+- 已关闭或不在 Coder Studio 中打开的 provider 历史不会被统计。
+- `最近 7 天` 等查询可能显示少量 session,实际 provider 日志中已有更多记录。
+
+## Goals
+
+- 工作分析的数据源改为 provider 自己的本地日志、缓存或数据库。
+- 覆盖所有 5 个内置 provider:`claude`、`codex`、`gemini`、`cursor`、`opencode`。
+- 按用户选中的 workspace path 和时间范围筛选 provider 历史。
+- 将不同 provider 的记录归一化为统一的 `WorkLogSession`。
+- 基础分析只聚合归一化 summary,保持快速、稳定。
+- 深入分析只使用受限、抽样、脱敏倾向的 evidence,不把整份日志交给 headless agent。
+- UI 明确展示每个 provider 的数据源状态和数据质量。
+- 保留分析结果记录用于展示上次结果,但用户重新运行分析时必须重新扫描 provider 数据源。
+
+## Non-Goals
+
+- 不让 Coder Studio 修改或迁移 provider 的原始日志。
+- 不把 provider 历史导入为 Coder Studio session。
+- 不在 v1 中实现长期索引服务或后台定时扫描。
+- 不保证不同 provider 的所有指标完全对称。
+- 不把全部对话正文作为默认分析输入。
+- 不扩展到自定义 provider;自定义 provider 可以后续通过同一接口接入。
+
+## User Decisions Captured
+
+- 工作分析和 Coder Studio session 没有业务关系。
+- 应该去具体 agent 的日志缓存中找和选中 workspace 相关的日志。
+- 不同 agent 有不同的会话日志缓存,先找到日志,再做提取、聚合、呈现。
+- 内置 provider 是 5 个,方案不能只覆盖 Codex 和 Claude。
+- 缓存指分析结果记录,不是 provider 原始日志;分析结果可以保留,但不能阻止重新分析。
+
+## Current State
+
+### Provider Registry
+
+内置 provider 在 [`packages/providers/src/registry.ts`](../../../packages/providers/src/registry.ts) 中固定声明:
+
+- `claude`
+- `codex`
+- `gemini`
+- `cursor`
+- `opencode`
+
+因此工作分析应从 registry 派生内置 provider 范围,不能硬编码只处理部分 provider。
+
+### Work Analysis Service
+
+当前 `WorkAnalysisService` 依赖两类 session-based 输入:
+
+- `sessionSelector`: 选择 Coder Studio session summary。
+- `evidenceCollector`: 读取 Coder Studio session 的 terminal snapshot、latest input 和 workspace path。
+
+这两个依赖都需要替换为 provider-log based 输入。
+
+### Result Persistence
+
+`WorkAnalysisRepo` 保存的是 `WorkAnalysisRecord`,也就是分析结果记录。它不是 provider 日志缓存。
+
+当前 `runBasic` 如果发现同 query digest 的记录已经 `succeeded`,会直接返回旧记录。这对手动重新分析不合适,因为 provider 日志可能已经变化,preset 时间范围也会随时间滑动。
+
+## Provider Log Findings
+
+### Codex
+
+- Root: `~/.codex/sessions/YYYY/MM/DD/*.jsonl`
+- Workspace match: first metadata record `payload.cwd`
+- Session id: `payload.id`
+- Time: `timestamp` or `payload.timestamp`, with file mtime as fallback
+- Useful fields:
+ - `payload.model_provider`
+ - `payload.git.branch`
+ - `payload.git.commit_hash`
+ - `payload.git.repository_url`
+ - message/event records for turn and tool counts
+- Risk: low. JSONL metadata has direct workspace path.
+
+### Claude
+
+- Root: `~/.claude/projects//*.jsonl`
+- Workspace match: encoded project directory plus record `cwd`
+- Session id: `sessionId`
+- Time: record `timestamp`
+- Useful fields:
+ - `cwd`
+ - `gitBranch`
+ - message roles
+ - tool/hook attachment metadata
+- Risk: low. Workspace path and timestamps are present in records.
+
+### Gemini
+
+- Roots:
+ - `~/.gemini/tmp//chats/session-*.json`
+ - `~/.gemini/history/` for historical compatibility when available
+- Workspace match: `~/.gemini/tmp//.project_root` and matching history `.project_root`
+- Session id: chat JSON `sessionId`
+- Time: `startTime`, `lastUpdated`
+- Useful fields:
+ - `kind`
+ - `projectHash`
+ - `summary`
+ - `messages[].type`
+ - `messages[].timestamp`
+ - message content type
+- Risk: medium. Project directory names are not enough; `.project_root` must be authoritative.
+
+### Cursor
+
+- Primary root: `~/.cursor/projects//agent-transcripts//*.jsonl`
+- Secondary root for future enhancement: `~/.cursor/chats///store.db`
+- Workspace match:
+ - primary: encoded workspace project directory
+ - secondary: md5 of absolute workspace path
+- Session id: transcript uuid
+- Time:
+ - v1: transcript file mtime fallback
+ - future: DB metadata if stable fields are confirmed
+- Useful fields:
+ - JSONL `role`
+ - `message.content[].type`
+ - tool-like content entries
+ - `~/.cursor/ai-tracking/ai-code-tracking.db` can support future code contribution signals
+- Risk: medium-high. Agent transcript JSONL may not include explicit timestamps, so v1 must report mtime fallback data quality.
+
+### OpenCode
+
+- Root: `~/.local/share/opencode/opencode.db`
+- Workspace match:
+ - `project.worktree`
+ - `session.directory`
+- Session id: `session.id`
+- Time:
+ - `session.time_created`
+ - `session.time_updated`
+ - message and part timestamps for detail
+- Useful tables:
+ - `project`
+ - `session`
+ - `message`
+ - `part`
+ - `todo`
+ - `session_diff`
+- Useful fields:
+ - `session.title`
+ - `session.version`
+ - `summary_files`
+ - `summary_additions`
+ - `summary_deletions`
+ - message and part counts
+- Risk: medium. SQLite schema is strong locally, but provider is marked experimental in Coder Studio.
+
+## Approaches Considered
+
+### Option A: Patch Current Session Selector
+
+Keep `WorkAnalysisService` mostly unchanged and teach `session-selector.ts` to merge Coder Studio session records with provider logs.
+
+Pros:
+
+- Smallest diff.
+- Existing basic analyzer changes little.
+
+Cons:
+
+- Keeps the wrong mental model: provider logs are not Coder Studio sessions.
+- Makes evidence collection ambiguous.
+- UI would still imply current/open sessions.
+- Hard to represent provider data quality.
+
+### Option B: Provider Log Adapters + Normalized Collector (Recommended)
+
+Replace session selection and evidence collection with provider-specific adapters that return normalized work log sessions.
+
+Pros:
+
+- Matches the required product semantics.
+- Keeps provider-specific parsing isolated.
+- Makes all 5 built-in providers first-class.
+- Enables clear data quality status per provider.
+- Allows basic and deep analysis to share one normalized source.
+
+Cons:
+
+- Requires new adapters and fixtures.
+- Some providers have weaker timestamp guarantees.
+- Existing tests around session selector/evidence collector must be replaced.
+
+### Option C: Persistent Work Log Index
+
+Build a background indexing service that continuously scans provider logs and stores a normalized local index.
+
+Pros:
+
+- Fast queries after indexing.
+- Enables historical trends and freshness detection.
+
+Cons:
+
+- Too large for this correction.
+- Adds background scanning and invalidation complexity.
+- Raises more privacy and storage questions.
+
+## Final Choice
+
+Use Option B.
+
+Implement a provider-log based collector with one adapter per built-in provider. The collector scans on demand when the user runs analysis. Results are normalized into `WorkLogSession[]`, then passed to basic aggregation and deep evidence sampling.
+
+Persisted `WorkAnalysisRecord` remains useful for showing the last result, but it no longer short-circuits explicit `runBasic` or `runDeep`.
+
+## Architecture
+
+### New Source Interface
+
+Create a provider log source interface in the server work-analysis domain:
+
+```ts
+export interface ProviderWorkLogSource {
+ providerId: BuiltInProviderId;
+
+ discover(input: ProviderWorkLogDiscoverInput): Promise;
+}
+
+export interface ProviderWorkLogDiscoverInput {
+ workspacePaths: string[];
+ timeRange: ResolvedWorkAnalysisTimeRange;
+}
+
+export interface ProviderWorkLogDiscovery {
+ providerId: BuiltInProviderId;
+ status: WorkLogProviderStatus;
+ sessions: WorkLogSession[];
+ sourceRefs: WorkLogSourceRef[];
+ parseErrorCount: number;
+ warnings: WorkLogWarning[];
+}
+```
+
+### Normalized Session
+
+```ts
+export interface WorkLogSession {
+ providerId: BuiltInProviderId;
+ sessionId: string;
+ workspacePath: string;
+ startedAt: number;
+ lastActiveAt: number;
+ sourceRef: string;
+ title?: string;
+ modelId?: string;
+ gitBranch?: string;
+ gitCommit?: string;
+ userTurnCount: number;
+ assistantTurnCount: number;
+ toolUseCount: number;
+ parseErrorCount: number;
+ timestampQuality: "explicit" | "file_mtime" | "mixed";
+ evidence?: WorkLogEvidence[];
+}
+```
+
+### Source References
+
+```ts
+export interface WorkLogSourceRef {
+ providerId: BuiltInProviderId;
+ kind: "file" | "sqlite";
+ path: string;
+ mtimeMs?: number;
+ sizeBytes?: number;
+ maxUpdatedAt?: number;
+}
+```
+
+`WorkLogSourceRef` supports result freshness diagnostics and future automatic invalidation.
+
+### Provider Status
+
+```ts
+export type WorkLogProviderStatus =
+ | "supported"
+ | "no_logs"
+ | "missing_root"
+ | "partial"
+ | "unsupported";
+```
+
+Meaning:
+
+- `supported`: adapter ran and returned zero or more valid sessions without material parse failures.
+- `no_logs`: log root exists, but no sessions matched selected workspace/time range.
+- `missing_root`: provider log root is not present on disk.
+- `partial`: some sessions were parsed, but some files/records failed.
+- `unsupported`: built-in provider exists, but this adapter intentionally has no v1 reader.
+
+For this design, all 5 built-in providers should have v1 adapters. `unsupported` remains for forward compatibility and custom providers.
+
+### Collector
+
+Create `WorkLogCollector`:
+
+```ts
+export interface WorkLogCollector {
+ collect(input: {
+ workspacePaths: string[];
+ timeRange: ResolvedWorkAnalysisTimeRange;
+ }): Promise;
+}
+
+export interface WorkLogCollection {
+ sessions: WorkLogSession[];
+ providers: ProviderWorkLogDiscovery[];
+ sourceDigest: string;
+}
+```
+
+The collector:
+
+- Runs all built-in provider adapters.
+- Sorts sessions by `lastActiveAt`, then `providerId`, then `sessionId`.
+- Computes `sourceDigest` from provider id, source ref path, mtime, size, max updated time, and matched session ids.
+- Keeps provider warnings for UI display.
+
+## Basic Analysis Design
+
+Basic analysis should consume `WorkLogSession[]` instead of Coder Studio session summaries.
+
+Existing fields can mostly remain:
+
+- `coverage.workspaceCount`
+- `coverage.sessionCount`
+- `coverage.providerCount`
+- `activity.sessionCount`
+- `activity.totalDurationMs`
+- `activity.averageDurationMs`
+- `workHabits.hourBuckets`
+- `usage.totalSessions`
+- `usage.sessionsByProvider`
+- `agentModelMix.providers`
+- `dataQuality.clampedDurationCount`
+- `dataQuality.emptySessionCount`
+
+Add provider log data quality:
+
+```ts
+dataSources: {
+ providers: Array<{
+ providerId: BuiltInProviderId;
+ status: WorkLogProviderStatus;
+ sessionCount: number;
+ parseErrorCount: number;
+ warningCount: number;
+ }>;
+};
+```
+
+Add execution-like signals from logs:
+
+```ts
+executionSignals: {
+ sessionsWithActivity: number;
+ userTurnCount: number;
+ assistantTurnCount: number;
+ toolUseCount: number;
+ fileMtimeTimestampCount: number;
+};
+```
+
+The previous `skillInventory` section can stay because it is Coder Studio skill inventory, not provider session data. Its label should make clear it is local Coder Studio skill inventory.
+
+## Deep Analysis Design
+
+Deep analysis should use sampled provider-log evidence, not terminal snapshots.
+
+### Evidence Shape
+
+```ts
+export interface WorkLogEvidence {
+ providerId: BuiltInProviderId;
+ sessionId: string;
+ workspacePath: string;
+ title?: string;
+ startedAt: number;
+ lastActiveAt: number;
+ excerpts: Array<{
+ role: "user" | "assistant" | "tool" | "system" | "unknown";
+ at?: number;
+ text?: string;
+ toolName?: string;
+ commandKind?: string;
+ filePath?: string;
+ }>;
+}
+```
+
+### Sampling Rules
+
+- Cap total sessions included in deep evidence.
+- Cap sessions per provider so one provider cannot dominate.
+- Cap excerpts per session.
+- Cap excerpt text length.
+- Prefer recent sessions, sessions with tool activity, and sessions with explicit timestamps.
+- Include provider and session metadata even when textual evidence is sparse.
+- Exclude binary blobs, raw DB blobs, OAuth/account files, and provider config secrets.
+
+### Headless Provider Selection
+
+The provider used to run deep analysis should not be selected by "most sessions found". That was only a side effect of the old session-based model.
+
+Use a dedicated selection policy:
+
+1. Prefer the user's configured default analysis provider if it supports `session_analysis` headless and is runtime-available.
+2. Otherwise choose the first runtime-available built-in provider that supports `session_analysis`.
+3. If no provider is available, basic analysis still succeeds and deep analysis fails with a typed provider-unavailable error.
+
+## Result Record And Cache Behavior
+
+`WorkAnalysisRecord` continues to store analysis results for display. It should also store source freshness metadata:
+
+```ts
+export interface WorkAnalysisSourceSnapshot {
+ sourceDigest: string;
+ providerStatuses: Array<{
+ providerId: BuiltInProviderId;
+ status: WorkLogProviderStatus;
+ sessionCount: number;
+ parseErrorCount: number;
+ }>;
+ collectedAt: number;
+}
+```
+
+Behavior:
+
+- `work.analysis.get` returns the last saved result for the query.
+- `work.analysis.runBasic` always resolves the time range and rescans provider logs.
+- `work.analysis.runDeep` always runs against the latest collected basic result and sampled evidence.
+- A previous `succeeded` record must not short-circuit explicit runs.
+- `sourceDigest` is saved for display and future freshness checks, but v1 does not need automatic background invalidation.
+
+This keeps the useful "last result" behavior while preventing stale analysis from blocking new runs.
+
+## UI Design
+
+The current settings component can remain the entry surface, but labels and result sections need to shift from session wording to provider log wording.
+
+Required UI changes:
+
+- Describe the source as provider local logs/cache, not current sessions.
+- Show one row per built-in provider:
+ - provider name
+ - status
+ - matched session count
+ - parse warning count
+- Show when timestamps are based on file mtime fallback.
+- For no data, say the selected provider has no matching local logs for the selected workspace/time range.
+- For missing roots, say the provider log root was not found.
+- For deep analysis, clarify that only sampled log evidence is used.
+
+The UI should avoid implying that a workspace must have an open Coder Studio session.
+
+## Privacy And Safety
+
+Provider logs may contain user prompts, assistant responses, command outputs, file paths, and sometimes tool results. The implementation must treat them as local sensitive data.
+
+Rules:
+
+- Do not print raw prompts/responses to server logs.
+- Do not send whole provider logs to deep analysis.
+- Limit evidence size.
+- Prefer metadata and short excerpts.
+- Never parse credential/config files as analysis evidence.
+- Avoid reading provider account files such as OAuth tokens.
+- Keep raw provider log content out of persisted `WorkAnalysisRecord` except bounded evidence summaries needed for deep analysis results.
+
+## Error Handling
+
+Adapter errors should not fail the whole basic analysis unless every provider fails due to a shared system problem.
+
+Per-provider outcomes:
+
+- Missing root: report `missing_root`, no sessions.
+- Root exists but no matches: report `no_logs`, no sessions.
+- Some files fail: report `partial`, include parsed sessions and parse error count.
+- SQLite read failure: report `partial` if some data was read, otherwise `missing_root` or `partial` with warning depending on file existence.
+- Unknown record shape: skip the record, increment parse error count, keep scanning.
+
+Deep analysis can fail independently without invalidating basic analysis.
+
+## File And Module Plan
+
+Expected new server modules:
+
+- `packages/server/src/work-analysis/log-sources/types.ts`
+- `packages/server/src/work-analysis/log-sources/collector.ts`
+- `packages/server/src/work-analysis/log-sources/codex.ts`
+- `packages/server/src/work-analysis/log-sources/claude.ts`
+- `packages/server/src/work-analysis/log-sources/gemini.ts`
+- `packages/server/src/work-analysis/log-sources/cursor.ts`
+- `packages/server/src/work-analysis/log-sources/opencode.ts`
+- `packages/server/src/work-analysis/log-sources/path-encoding.ts`
+- `packages/server/src/work-analysis/evidence-sampler.ts`
+
+Expected modified modules:
+
+- `packages/server/src/work-analysis/service.ts`
+- `packages/server/src/work-analysis/basic-analyzer.ts`
+- `packages/server/src/work-analysis/basic-schema.ts`
+- `packages/server/src/work-analysis/types.ts`
+- `packages/server/src/work-analysis/deep-prompt.ts`
+- `packages/server/src/work-analysis/query.ts`
+- `packages/server/src/storage/repositories/work-analysis-repo.ts`
+- `packages/server/src/commands/work-analysis.ts`
+- `packages/web/src/features/settings/components/session-analysis-settings.tsx`
+- `packages/web/src/features/work-analysis/types.ts`
+- `docs/help/work-analysis.md`
+
+Expected removed or retired modules:
+
+- `packages/server/src/work-analysis/session-selector.ts`
+- `packages/server/src/work-analysis/evidence-collector.ts`
+
+They may remain temporarily during migration only if tests still cover old behavior, but the final behavior must not call Coder Studio session manager for work analysis data.
+
+## Testing
+
+### Adapter Tests
+
+Use fixtures rather than the developer's real home directory.
+
+Codex:
+
+- Select sessions by metadata `payload.cwd`.
+- Exclude sessions outside time range.
+- Count user, assistant, and tool records.
+- Preserve model and git metadata when present.
+
+Claude:
+
+- Select sessions from encoded workspace directory.
+- Confirm `cwd` mismatch is excluded or flagged.
+- Count messages by role.
+- Handle parse errors without failing the whole adapter.
+
+Gemini:
+
+- Match workspace via `.project_root`.
+- Read `sessionId`, `startTime`, `lastUpdated`, and messages.
+- Include `summary` as optional title/evidence metadata.
+- Ignore projects whose `.project_root` does not match.
+
+Cursor:
+
+- Match encoded workspace project directory.
+- Use transcript uuid as session id.
+- Use file mtime when explicit timestamps are absent.
+- Report timestamp quality as `file_mtime`.
+- Count role and tool-like content entries.
+
+OpenCode:
+
+- Build a temporary SQLite fixture with `project`, `session`, `message`, and `part`.
+- Match by `project.worktree` and `session.directory`.
+- Count messages and parts by session.
+- Report summary diff fields when present.
+
+### Collector Tests
+
+- Runs all 5 adapters.
+- Aggregates provider statuses.
+- Sorts sessions deterministically.
+- Produces stable `sourceDigest` for unchanged source refs.
+- Changes `sourceDigest` when matched source mtime, size, updated time, or session ids change.
+
+### Service Tests
+
+- `runBasic` rescans even when an existing record is `succeeded`.
+- `get` returns the last saved record without scanning.
+- Basic analysis succeeds when one provider is `partial`.
+- Deep analysis uses sampled provider evidence, not terminal snapshots.
+- Deep analysis provider selection uses headless availability, not largest session count.
+
+### UI Tests
+
+- Provider status rows render for all 5 built-in providers.
+- No-log and missing-root states have distinct messages.
+- Session count is labeled as provider local log matches.
+- Deep analysis button remains disabled until basic result exists.
+
+### Docs Tests
+
+- Help text no longer says an open agent session is required.
+- Help text explains provider local logs and data-source limitations.
+
+## Rollout
+
+1. Introduce types, collector, and fixtures.
+2. Implement provider adapters behind tests.
+3. Replace service dependencies from session selector/evidence collector to work log collector/evidence sampler.
+4. Update schemas and frontend types.
+5. Update UI copy and provider status display.
+6. Update help docs.
+7. Retire old session selector and evidence collector tests.
+
+## Acceptance Criteria
+
+- Running `工作分析` for a selected workspace scans provider local logs for all 5 built-in providers.
+- Recently closed or never-opened-in-Coder-Studio provider sessions can appear in analysis if their provider logs match the workspace and time range.
+- Explicitly running basic analysis does not return an old successful result without scanning.
+- Basic analysis reports provider data-source status.
+- Deep analysis uses bounded provider-log evidence.
+- The UI no longer implies that Coder Studio sessions are the source of truth.
diff --git a/docs/superpowers/specs/2026-06-04-agent-instructions-richer-generation-design.md b/docs/superpowers/specs/2026-06-04-agent-instructions-richer-generation-design.md
new file mode 100644
index 000000000..4e6b0cfa7
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-04-agent-instructions-richer-generation-design.md
@@ -0,0 +1,295 @@
+# Richer Agent Instructions Generation Design
+
+## Goal
+
+Improve generated `.coder-studio/agent.md` so it works as a short project operating guide for coding agents, not just a safe repository summary.
+
+The output should stay compact enough to read in one to two screens, but it should provide better actionability in three areas:
+
+- project structure and architecture boundaries
+- practical command and verification guidance
+- file constraints and collaboration rules
+
+## Non-Goals
+
+- Do not turn `agent.md` into a long handbook or full architecture document.
+- Do not require manual project-specific configuration before generation becomes useful.
+- Do not replace the existing static generator path used by non-agent generation flows unless needed by shared summary code.
+- Do not depend on Mermaid or other diagram formats. Output remains pure Markdown.
+
+## Recommended Approach
+
+Use a richer structured workspace summary as the main improvement point, then update the generation prompt to consume that summary.
+
+This keeps the system mostly automatic, improves output quality across repositories, and remains testable. It also avoids hardcoding full documents in the server while still allowing a small fixed set of high-value rules and constraints.
+
+## Alternatives Considered
+
+### 1. Prompt-only expansion
+
+Keep the current summary model and only ask the model to write more helpful content.
+
+Trade-off:
+- Lowest engineering cost
+- Weakest reliability because the model cannot infer high-value structure that is not present in the input
+
+### 2. Structured summary expansion
+
+Expand workspace intelligence with architecture, key directories, command coverage, and constraints, then update the prompt to use it.
+
+Trade-off:
+- Moderate implementation cost
+- Best balance of quality, determinism, and testability
+
+This is the chosen approach.
+
+### 3. Mostly server-rendered template
+
+Generate most of the final Markdown on the server and let the model fill only a few fields.
+
+Trade-off:
+- Very stable output
+- Too rigid and closer to hardcoded documentation than generation
+
+## Desired Output Shape
+
+Generated `agent.md` should stay pure Markdown and use these second-level sections in this order:
+
+- `Project Overview`
+- `Architecture Map`
+- `Key Directories`
+- `Development Commands`
+- `Workflow Expectations`
+- `File Constraints`
+- `Review Checklist`
+- `Provider Notes`
+
+Content expectations:
+
+- `Project Overview`
+ - Short repository description
+ - Top-level stack/runtime summary
+ - Monorepo/single-package context if applicable
+- `Architecture Map`
+ - Pure Markdown hierarchy
+ - Short role descriptions for major layers or packages
+- `Key Directories`
+ - Only the most relevant 3-6 directories/packages
+ - One-line reason each matters to an agent
+- `Development Commands`
+ - Real commands only
+ - Prefer root verification and CI-style commands when present
+- `Workflow Expectations`
+ - Small fixed ruleset with optional project-specific additions when supported by facts
+- `File Constraints`
+ - Boundaries and editing cautions
+ - Must not invent repo-specific rules without evidence
+ - May include small generic safety rules when no stronger repo facts exist
+- `Review Checklist`
+ - Short, concrete pre-handoff checks
+- `Provider Notes`
+ - Small fixed provider-specific notes carried forward from today
+
+## Data Model Changes
+
+Extend workspace intelligence so generation has richer structured facts.
+
+Current summary is too thin. The new summary should add the following fields or equivalent structure:
+
+- `workspaceKind`
+ - Examples: `monorepo`, `node_app`, `unknown`
+- `topLevelDirectories`
+ - Sorted list of meaningful root directories, filtered to avoid noise
+- `keyDirectories`
+ - Array of objects with:
+ - `path`
+ - `kind`
+ - `reason`
+- `packages`
+ - Array of objects with:
+ - `path`
+ - `name`
+ - `role`
+ - `scripts`
+- `documentationEntries`
+ - Important docs beyond just `README.md` and `docs/`
+- `verificationCommands`
+ - Prioritized list of concrete commands useful before completion
+- `fileConstraints`
+ - Structured constraints inferred from repository shape and known conventions
+
+These do not need to be exposed publicly outside generation if that increases churn, but they must be structured enough to test independently.
+
+## Inference Rules
+
+The summary builder should infer useful facts conservatively.
+
+### Workspace Kind
+
+Infer `monorepo` when workspace markers such as `pnpm-workspace.yaml` exist or multiple `packages/*/package.json` files are present.
+
+### Key Directories
+
+For this repository family and similar monorepos, prioritize directories like:
+
+- `packages/web`
+- `packages/server`
+- `packages/providers`
+- `packages/core`
+- `packages/cli`
+- `docs`
+- `e2e`
+- `scripts`
+
+Selection rules:
+
+- only include directories that exist
+- cap at 3-6 items
+- prefer code-bearing directories over support directories
+- produce short deterministic reasons, not vague summaries
+
+### Package Roles
+
+Infer role from path and package name with conservative heuristics:
+
+- `web` => frontend UI
+- `server` => backend/server/runtime/WS commands
+- `providers` => provider integrations/adapters
+- `core` => shared protocol/types/runtime contracts
+- `cli` => launcher or command-line entrypoint
+- `utils` => shared utilities
+
+If role confidence is weak, fall back to a generic but truthful description such as "shared package" instead of guessing.
+
+### Documentation Entries
+
+Prefer documentation with operational value:
+
+- `README.md`
+- `docs/help/*`
+- `docs/wiki/*`
+- root contribution or architecture docs if present
+
+Cap the list so the output remains compact.
+
+### Command Prioritization
+
+Current generation overemphasizes only `dev/build/lint`. The new prioritization should:
+
+- keep existing script-derived commands
+- include stronger verification commands such as:
+ - `pnpm ci:test`
+ - `pnpm ci:typecheck`
+ - `pnpm ci:verify`
+ - `pnpm acceptance:phase1`
+- include package-level test commands only if clearly available and useful
+- rank commands by likely usefulness to an agent:
+ - verification first
+ - then build/typecheck/lint
+ - then local dev commands
+
+### File Constraints
+
+This section should be assembled from a mix of repository facts and a small safe template.
+
+Examples of allowed automatically generated constraints:
+
+- preserve existing package boundaries in a monorepo
+- keep frontend changes in `packages/web` and backend runtime changes in `packages/server` unless cross-package edits are required
+- prefer existing patterns and naming conventions in the touched package
+- avoid unrelated refactors across packages while solving a targeted task
+- use repository-level verification commands before claiming completion
+
+These are acceptable because they are grounded in repo shape and established collaboration expectations, not arbitrary invention.
+
+## Prompt Changes
+
+The prompt should be updated to:
+
+- request the new section order
+- explicitly ask for a Markdown hierarchy under `Architecture Map`
+- ask for 3-6 key directories only
+- ask for practical commands, not placeholders
+- ask for concise but specific file constraints
+- forbid invented package roles, commands, or rules
+
+The prompt should still require a single JSON object result and preserve strict parsing expectations.
+
+## Error Handling
+
+No new user-visible error classes are required for this enhancement.
+
+If richer summary data cannot be derived:
+
+- generation should fall back to thinner facts rather than fail
+- missing optional fields should simply produce shorter sections
+- the system must still return valid Markdown when enough baseline facts exist
+
+## Testing Strategy
+
+### Workspace Intelligence Tests
+
+Add focused tests for the richer summary builder:
+
+- monorepo detection
+- key directory selection and ordering
+- package role inference
+- command prioritization
+- documentation entry selection
+- file constraint generation
+
+### Prompt Tests
+
+Add tests that assert the prompt now requires:
+
+- the new section order
+- Markdown hierarchy for `Architecture Map`
+- a limited number of key directories
+- concise constraints and review checklist behavior
+
+### Command-Level Tests
+
+Extend agent instructions command tests so mocked provider output can be validated against stronger expectations for the prompt input and expected section presence.
+
+### Real End-to-End Verification
+
+After implementation:
+
+- run the targeted server tests
+- run real `Codex` generation through the existing WS command path
+- inspect the resulting `.coder-studio/agent.md`
+- confirm it includes:
+ - architecture map as pure Markdown hierarchy
+ - key directories with meaningful roles
+ - richer command guidance
+ - file constraints that are short and actionable
+
+## Implementation Outline
+
+1. Expand workspace intelligence data gathering and inference helpers.
+2. Update the generation prompt to consume the richer facts and new section contract.
+3. Update tests for summary inference and prompt expectations.
+4. Run real generation and tune compactness if output becomes too verbose.
+
+## Risks
+
+- Over-inference can create inaccurate constraints or package roles.
+- Too much data can make the prompt noisy and reduce output quality.
+- Excessive command lists can reduce readability.
+
+Mitigations:
+
+- cap lists aggressively
+- prefer deterministic heuristics
+- fall back to omission instead of guessing
+- verify with a real generation pass before considering the work done
+
+## Success Criteria
+
+The enhancement is successful when a real generated `agent.md`:
+
+- clearly explains the repository shape in pure Markdown
+- gives an agent a useful shortlist of where to look first
+- includes better verification guidance than only `dev/build/lint`
+- includes short file constraints that reduce low-quality cross-cutting edits
+- remains compact enough to read quickly
diff --git a/docs/superpowers/specs/2026-06-04-tasks-verification-git-review-design.md b/docs/superpowers/specs/2026-06-04-tasks-verification-git-review-design.md
new file mode 100644
index 000000000..3e28df72c
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-04-tasks-verification-git-review-design.md
@@ -0,0 +1,584 @@
+# Tasks Verification And Git Review Design
+
+## Summary
+
+This design defines a staged upgrade for Coder Studio's post-agent development workflow.
+
+The first track adds a `Tasks` surface beside the existing terminal panel. It provides managed project commands such as `Verify`, `Test`, `Lint`, and `Build`, while still using terminal output as the source of truth.
+
+The second track upgrades Git review with hunk-level staging and stronger diff actions. Together, these features create a tighter loop:
+
+1. an agent changes code
+2. the user runs a managed verification task
+3. the result is visible next to terminal output
+4. the user reviews and stages only the intended Git changes
+
+This design intentionally does not pursue VS Code extension compatibility, Debug Adapter Protocol support, or a full Problems panel in this phase.
+
+## Problem
+
+Coder Studio already combines agent sessions, terminals, files, and Git review in one browser workspace. The remaining workflow gap is not "more IDE surface area" in general. The sharper gap is that users still have to manually coordinate verification and fine-grained Git cleanup after an agent finishes work.
+
+Current friction points:
+
+- verification commands are run manually in shell terminals
+- the UI does not track whether the current workspace last verified successfully
+- terminal output is available, but task intent, duration, exit code, and rerun state are not modeled
+- Git review supports file-level staging and diff viewing, but not hunk-level staging
+- users must switch context to decide what should be staged, discarded, or kept for follow-up
+
+The product should optimize for the agent workflow before adding broader IDE features such as debugger support or extension-host compatibility.
+
+## Goals
+
+- Add a bottom-panel `Tasks` tab beside `Terminal`.
+- Automatically discover common project verification commands.
+- Provide a default `Run Verify` action when a likely verification command exists.
+- Run tasks through managed terminal sessions so output remains live and inspectable.
+- Track task status, duration, exit code, and last run metadata.
+- Support stop and rerun for managed task runs.
+- Show the latest verification result in lightweight agent and Git review contexts.
+- Add hunk-level Git staging and unstaging.
+- Add common review actions directly inside the diff surface.
+
+## Non-Goals
+
+- Full VS Code Tasks compatibility.
+- Debugging, breakpoints, variables, call stack, or `launch.json`.
+- A complete Problems panel.
+- VS Code extension host or Marketplace compatibility.
+- CI service integration.
+- Cross-machine task history sync.
+- Full Git graph, pull request, or issue integration.
+- Three-way merge conflict editor in the first Git review pass.
+
+## Approaches Considered
+
+### 1. Put verification actions in the workspace top bar
+
+This gives high visibility, but it separates the command from its output. Users would still need to move to the terminal panel to understand what happened.
+
+This approach is rejected as the primary surface. A compact top-level status can be added later, but not as the main interaction model.
+
+### 2. Put verification in the Git panel
+
+This aligns with pre-commit review, but verification is not only a Git action. Users often run tests while an agent is still working or before they inspect the diff.
+
+This approach is useful as a secondary shortcut only.
+
+### 3. Put `Tasks` beside `Terminal`
+
+This keeps the feature close to its natural output surface. Tasks are managed commands, not debugger sessions. The existing terminal infrastructure can own process output, resizing, replay, snapshots, and mobile behavior.
+
+This is the recommended approach.
+
+## Recommended Scope
+
+### Phase 1: Managed Tasks MVP
+
+The first phase adds task discovery, task execution, status tracking, and the bottom-panel UI.
+
+#### Task Surface
+
+Add a new bottom-panel tab beside `Terminal`:
+
+```text
+Bottom Panel
+[Terminal] [Tasks] [Run Verify] [Run Test] [Run Lint]
+```
+
+The `Tasks` tab lists discovered tasks for the active workspace:
+
+```text
+Tasks
+ Verify pnpm ci:verify Failed 42s [Rerun]
+ Test pnpm ci:test Passed 18s [Run]
+ Lint pnpm lint Not run [Run]
+ Build pnpm build Not run [Run]
+```
+
+Each row should show:
+
+- task label
+- command preview
+- status: `not_run`, `queued`, `running`, `passed`, `failed`, `stopped`
+- duration for completed runs
+- active action: `Run`, `Stop`, or `Rerun`
+
+#### Task Discovery
+
+The server should discover tasks from workspace files in this order:
+
+1. explicit Coder Studio task config
+2. package scripts
+3. repository conventions
+4. language ecosystem files
+
+Supported first-pass sources:
+
+- `.coder-studio/tasks.json`
+- `package.json`
+- `pnpm-workspace.yaml`
+- `Cargo.toml`
+- `go.mod`
+- `pyproject.toml`
+- `Makefile`
+
+Default inferred task kinds:
+
+- `verify`
+- `test`
+- `lint`
+- `build`
+- `dev`
+- `custom`
+
+For this repository, discovery should prefer `pnpm ci:verify` as the default `Verify` task because it is the documented repository validation command.
+
+#### Task Execution
+
+Task runs should be managed server-side. A run creates or reuses a terminal with a task identity.
+
+Task terminals should be distinct from shell terminals and agent terminals:
+
+```ts
+type TerminalKind = "shell" | "agent" | "task";
+```
+
+The terminal remains responsible for:
+
+- live output
+- replay
+- snapshot hydration
+- resize
+- close behavior
+- mobile terminal rendering
+
+The task layer is responsible for:
+
+- task definition
+- run id
+- status
+- start and finish timestamps
+- exit code
+- command preview
+- terminal id
+- last output summary
+
+#### Task Commands
+
+Add command-level operations:
+
+- `task.discover`
+- `task.list`
+- `task.run`
+- `task.stop`
+- `task.rerun`
+- `task.history`
+
+`task.run` should return a `TaskRun` immediately after the managed terminal has been created. The run then updates over workspace events.
+
+#### Task Events
+
+Add workspace-scoped task events:
+
+- `task.discovered`
+- `task.run.started`
+- `task.run.updated`
+- `task.run.finished`
+- `task.run.stopped`
+
+The web client should subscribe to active workspace task events in the same way it subscribes to terminal and Git events.
+
+#### Failure Summary
+
+The MVP should not parse every compiler and test output. It should still record enough failure context to be useful:
+
+- command
+- cwd
+- exit code
+- duration
+- last non-empty output lines
+- linked terminal id
+
+The last output summary should be capped to avoid storing full logs in task state. The terminal replay remains the source for full output.
+
+### Phase 2: Agent And Git Context Integration
+
+Once task execution is reliable, surface the latest verification result in the workflows where it matters.
+
+#### Agent Context
+
+Agent panes should show a compact latest verification state for the workspace:
+
+```text
+Last verify: Failed
+pnpm ci:verify · exit 1 · 42s
+[View output] [Rerun]
+```
+
+This should be read-only context plus navigation. Agent panes should not become a second task manager.
+
+#### Git Review Context
+
+The Git panel or diff review header should show:
+
+```text
+Verification: Failed [View Tasks] [Rerun Verify]
+```
+
+This helps users avoid committing unverified agent output.
+
+#### Supervisor Context
+
+Supervisor can consume task result metadata later, but automatic repair loops are not part of the MVP. The first integration should only expose enough state for a human to rerun and inspect verification.
+
+### Phase 3: Git Review Upgrade
+
+After managed tasks are usable, Git review should gain fine-grained staging.
+
+#### Hunk-Level Staging
+
+The diff viewer should allow:
+
+- stage hunk
+- unstage hunk
+- discard hunk
+
+For staged diffs:
+
+- `unstage hunk` applies the reverse patch from index to working tree staging state
+
+For unstaged diffs:
+
+- `stage hunk` applies the selected patch to the index
+- `discard hunk` applies the reverse patch to the working tree
+
+The server should execute these operations with `git apply`-based patch application rather than ad hoc text editing. If patch application fails because the file changed, the UI should show a stale diff error and offer refresh.
+
+#### Line-Level Staging
+
+Line-level staging is valuable, but it is more complex than hunk staging because it requires constructing valid partial patches.
+
+Recommended sequence:
+
+1. implement hunk staging first
+2. add selected-line staging once the diff model can generate stable patch slices
+
+#### Diff Review Actions
+
+Add direct actions to the diff surface:
+
+- stage file
+- unstage file
+- discard file
+- stage hunk
+- unstage hunk
+- discard hunk
+- open file
+- copy path
+- refresh diff
+
+This reduces movement between the Git tree and the central editor surface.
+
+#### Commit Assistance
+
+Add commit message assistance after hunk staging exists.
+
+Recommended first version:
+
+- generate a commit message from staged diff
+- keep the result editable
+- do not auto-commit
+- do not require an AI provider if no provider is configured
+
+If no provider is available, the action can be hidden or disabled with a clear message.
+
+## Data Model
+
+### TaskDefinition
+
+```ts
+interface TaskDefinition {
+ id: string;
+ workspaceId: string;
+ kind: "verify" | "test" | "lint" | "build" | "dev" | "custom";
+ label: string;
+ command: string;
+ args: string[];
+ cwdPath?: string;
+ source:
+ | "coder-studio"
+ | "package-json"
+ | "pnpm-workspace"
+ | "cargo"
+ | "go"
+ | "python"
+ | "makefile"
+ | "inferred";
+ priority: number;
+}
+```
+
+### TaskRun
+
+```ts
+interface TaskRun {
+ id: string;
+ workspaceId: string;
+ taskId: string;
+ terminalId: string;
+ status: "queued" | "running" | "passed" | "failed" | "stopped";
+ command: string;
+ args: string[];
+ cwdPath?: string;
+ startedAt: number;
+ finishedAt?: number;
+ exitCode?: number;
+ summary?: {
+ tailLines: string[];
+ };
+}
+```
+
+### Git Hunk Operation
+
+```ts
+interface GitHunkOperation {
+ workspaceId: string;
+ path: string;
+ staged: boolean;
+ hunkId: string;
+ operation: "stage" | "unstage" | "discard";
+}
+```
+
+The client should not send arbitrary patch text as the primary API contract. Prefer sending a stable hunk id derived from the current diff payload, then let the server validate it against a fresh or cached diff. This reduces the risk of applying stale or tampered patches.
+
+## UI Placement
+
+### Bottom Panel
+
+The bottom panel becomes the primary home for command execution:
+
+- `Terminal` remains manual shell terminal management
+- `Tasks` manages discovered and recent task commands
+
+The active task output should still open in an xterm surface. The `Tasks` tab can either show output below the task list or jump to the terminal tab with the task terminal selected. The MVP should prefer jumping to the terminal tab to avoid duplicating terminal rendering.
+
+### Agent Pane
+
+Agent panes get compact task status only:
+
+- latest verify state
+- view output
+- rerun verify
+
+### Git Panel And Diff Surface
+
+Git panel gets verification status as a compact banner. The diff surface gets review actions local to the current file or hunk.
+
+## Error Handling
+
+### Task Discovery
+
+If discovery fails for one source, it should not fail all discovery.
+
+Example:
+
+- invalid `package.json` should report a source warning
+- valid `Makefile` and inferred commands should still appear
+
+### Task Run
+
+Task run errors should map to clear states:
+
+- failed to create terminal: `failed`
+- command exits non-zero: `failed`
+- user stops run: `stopped`
+- process exits zero: `passed`
+
+### Stale Git Diff
+
+If hunk staging fails because the diff changed:
+
+- show `Diff changed. Refresh and try again.`
+- keep the file selected
+- refresh Git status after the user confirms or clicks refresh
+
+## File Boundaries
+
+Expected primary areas:
+
+- `packages/core/src/domain/types.ts`
+- `packages/server/src/commands/terminal.ts`
+- `packages/server/src/commands/git.ts`
+- `packages/server/src/git/diff.ts`
+- `packages/server/src/terminal/manager.ts`
+- `packages/web/src/features/terminal-panel/*`
+- `packages/web/src/features/workspace/actions/use-git-actions.ts`
+- `packages/web/src/features/workspace/views/shared/git-diff-viewer.tsx`
+- `packages/web/src/features/workspace/views/shared/git-panel.tsx`
+
+Likely new server files:
+
+- `packages/server/src/tasks/discovery.ts`
+- `packages/server/src/tasks/manager.ts`
+- `packages/server/src/commands/task.ts`
+- `packages/server/src/git/hunk-operations.ts`
+
+Likely new web files:
+
+- `packages/web/src/features/tasks/actions/use-task-actions.ts`
+- `packages/web/src/features/tasks/atoms.ts`
+- `packages/web/src/features/tasks/views/shared/tasks-panel.tsx`
+
+## Testing Strategy
+
+### Task Tests
+
+Server tests:
+
+- discovers `pnpm ci:verify` as the preferred verify task in this repository
+- discovers `package.json` scripts
+- handles invalid project files without failing all discovery
+- creates a task terminal on `task.run`
+- marks run passed on exit code `0`
+- marks run failed on non-zero exit
+- marks run stopped when stopped by user
+
+Web tests:
+
+- renders `Tasks` tab beside `Terminal`
+- lists discovered tasks
+- starts a task from the task row
+- shows running and completed state
+- exposes rerun after completion
+
+### Git Tests
+
+Server tests:
+
+- stages one hunk from an unstaged text diff
+- unstages one hunk from a staged text diff
+- discards one hunk from an unstaged text diff
+- rejects stale hunk operations
+- refreshes Git state after successful hunk operation
+
+Web tests:
+
+- renders hunk actions in diff view
+- calls the expected Git hunk operation
+- shows stale diff error
+- keeps file selection stable after refresh
+
+## Rollout Plan
+
+### Milestone 1
+
+Deliver:
+
+- task type definitions
+- task discovery
+- `Tasks` tab
+- `task.run`
+- task terminal output
+- basic status tracking
+
+Success criterion:
+
+- a user can open the bottom `Tasks` tab and run `Verify` for the active workspace.
+
+### Milestone 2
+
+Deliver:
+
+- stop and rerun
+- run history for the latest run per task
+- failure summary
+- compact latest verify state in Agent and Git review contexts
+
+Success criterion:
+
+- a user can tell whether the latest verification passed without manually scanning terminal tabs.
+
+### Milestone 3
+
+Deliver:
+
+- hunk-level stage, unstage, and discard
+- diff-local review actions
+
+Success criterion:
+
+- a user can stage only part of an agent-generated file change from the diff surface.
+
+### Milestone 4
+
+Deliver:
+
+- optional line-level staging
+- commit message assistance from staged diff
+
+Success criterion:
+
+- a user can prepare a clean commit from partial agent output without leaving Code Studio.
+
+## Implementation Defaults
+
+The first implementation should use these defaults.
+
+### Task Terminals
+
+Task terminals are visible in the existing terminal selector and visually marked as managed.
+
+The `Tasks` tab does not embed a second xterm instance. Selecting `View output` or starting a task switches to the existing terminal tab with the managed task terminal selected.
+
+### Task Run Retention
+
+Keep the latest run per task in memory for the MVP.
+
+Do not persist task history to SQLite in the first pass. If users need longer history after the MVP, add persistence as a separate follow-up.
+
+### Task Config Schema
+
+The first `.coder-studio/tasks.json` schema is:
+
+```json
+{
+ "version": 1,
+ "tasks": [
+ {
+ "id": "verify",
+ "label": "Verify",
+ "kind": "verify",
+ "command": "pnpm",
+ "args": ["ci:verify"],
+ "cwdPath": "."
+ }
+ ]
+}
+```
+
+Schema rules:
+
+- `version` must be `1`.
+- `tasks` must be an array.
+- `id`, `label`, `kind`, and `command` are required.
+- `args` defaults to an empty array.
+- `cwdPath` defaults to the workspace root.
+- config-defined tasks override discovered tasks with the same `kind` and `id`.
+
+### Hunk Identity
+
+Hunk ids are generated server-side when building Git diff payloads.
+
+The client sends only:
+
+- workspace id
+- file path
+- staged flag
+- hunk id
+- operation
+
+The server validates the hunk id against the current diff before applying any patch. If validation fails, the operation returns a stale-diff error.
diff --git a/docs/superpowers/specs/2026-06-04-work-analysis-workspace-path-filter-design.md b/docs/superpowers/specs/2026-06-04-work-analysis-workspace-path-filter-design.md
new file mode 100644
index 000000000..ac721b69a
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-04-work-analysis-workspace-path-filter-design.md
@@ -0,0 +1,314 @@
+# Work Analysis Workspace Path Filter Redesign
+
+Date: 2026-06-04
+Status: Draft
+Owner: spencer
+
+## Problem
+
+当前 `工作分析` 把“已打开的 workspace”当成分析前置筛选条件:
+
+- 前端设置页只允许从当前 `orderedWorkspaces` 里选 workspace。
+- 服务端 `work.analysis.*` 查询模型要求 `workspaceIds`。
+- `WorkAnalysisService` 先把 `workspaceIds` 解析成 workspace path,再把这些 path 传给 provider log source 做发现。
+
+这和真实需求不一致。
+
+用户希望:
+
+- 工作分析应先扫描 agent/provider 日志里在所选时间范围内命中的全部 workspace 目录。
+- 即使某个目录从未在 Coder Studio 里 `open workspace`,只要它在 provider 日志中出现,也必须进入分析域。
+- workspace 不应该作为“事前屏蔽不相关目录”的条件,而应该作为“分析结果出来后的筛选项”。
+- 用户可以先看全部目录的汇总,再缩小到一个或多个具体路径。
+
+因此,当前“只看已打开 workspace”的设计在产品语义上是错误的。
+
+## Goals
+
+- 工作分析不再依赖 Coder Studio 已打开 workspace 列表作为前置输入。
+- provider log scan 在给定时间范围内收集所有命中的 `workspacePath`。
+- `workspacePath` 成为工作分析域内的一级筛选维度。
+- 前端路径筛选项来自分析结果中的 `availableWorkspacePaths`,而不是 `orderedWorkspacesAtom`。
+- 初次分析默认汇总全部命中路径;用户可再按路径多选缩小结果。
+- 保持 5 个内置 provider 都遵循同一语义。
+
+## Non-Goals
+
+- 不保留旧 `workspaceIds` 查询协议的兼容层。
+- 不尝试把未打开目录映射成伪 workspace id。
+- 不在本轮引入“已打开 workspace 标记”或路径分组 UI。
+- 不把路径筛选扩展成更复杂的 tag、搜索或树形浏览器。
+- 不修改 provider 原始日志结构。
+
+## User Decisions Captured
+
+- 不考虑兼容旧方案,应按正确方案重做。
+- workspace 只作为分析结果筛选项,不再作为分析前置筛选项。
+- UI 采用最简单的“纯路径多选”形式。
+- 不要求路径选项和已打开 workspace 产生任何绑定关系。
+
+## Current State
+
+### Query Model
+
+当前 `work.analysis.get`、`work.analysis.runBasic`、`work.analysis.runDeep` 都要求:
+
+- `workspaceIds: string[]`
+- `timeRange`
+
+这使“分析域”在请求发出前就被限制为已打开 workspace。
+
+### Service Data Flow
+
+当前 [`packages/server/src/work-analysis/service.ts`](../../../packages/server/src/work-analysis/service.ts) 的关键流程是:
+
+1. 规范化 `workspaceIds`
+2. 通过 `workspaceResolver` 解析成 `workspacePaths`
+3. 把这些 `workspacePaths` 传给 `workLogCollector`
+4. provider adapter 只在这些 path 范围内找日志
+5. analyzer 基于这个预筛选后的 session 集合做聚合
+
+这意味着:
+
+- 未打开 workspace 的日志永远不会进入分析域。
+- `available workspaces` 的来源是 app state,而不是 provider 历史。
+
+### Frontend State
+
+当前 [`packages/web/src/features/settings/components/session-analysis-settings.tsx`](../../../packages/web/src/features/settings/components/session-analysis-settings.tsx):
+
+- 用 `orderedWorkspacesAtom` 渲染复选框
+- 用 `activeWorkspaceId` 做默认值
+- 把 `selectedWorkspaceIds` 直接发给 `work.analysis.*`
+
+这使设置页本质上变成了“已打开 workspace 过滤器”,不是“分析结果路径过滤器”。
+
+## Desired Model
+
+### Core Principle
+
+工作分析的输入只有两类:
+
+- 时间范围
+- 可选的 `workspacePaths` 结果过滤器
+
+其中:
+
+- `workspacePaths` 为空或省略,表示“扫描并汇总时间范围内全部命中的路径”
+- `workspacePaths` 非空,表示“只在已发现路径中保留这些路径对应的数据”
+
+### Two-Phase Mental Model
+
+正确语义应是:
+
+1. **发现阶段**:provider 日志扫描命中的全部 session,并记录它们的 `workspacePath`
+2. **分析阶段**:根据用户选择的 `workspacePaths` 对 discovered sessions 进行过滤和聚合
+
+注意这里的“发现”不是单独暴露为用户操作,而是 `runBasic/get` 的内部流程。
+
+### UI Semantics
+
+设置页进入工作分析后:
+
+1. 用户先只指定时间范围
+2. 页面请求“全路径汇总”分析
+3. 返回结果中包含 `availableWorkspacePaths`
+4. 页面用这些路径渲染多选框,默认全选
+5. 用户取消部分路径后,再次请求分析,传所选 `workspacePaths`
+
+因此 UI 中的路径列表是“分析结果的一部分”,不是“app shell 里的 workspace 列表”。
+
+## Proposed Architecture
+
+### 1. Query Schema Rewrite
+
+将工作分析查询模型改为:
+
+- `workspacePaths?: string[]`
+- `timeRange`
+
+规则:
+
+- 未提供 `workspacePaths` 或传空数组:不过滤路径
+- 提供非空数组:按绝对路径字符串精确匹配过滤
+
+不再接受 `workspaceIds`。
+
+### 2. Provider Log Source Contract Rewrite
+
+当前 provider source discover 输入依赖:
+
+- `workspacePaths`
+- `timeRange`
+
+这需要改成只依赖:
+
+- `timeRange`
+
+provider adapter 的职责变为:
+
+- 扫描 provider 自身日志根目录或数据库
+- 找出时间范围内命中的 session
+- 从记录中提取 `workspacePath`
+- 返回归一化 `WorkLogSession[]`
+
+是否保留某个 session,不再由外部 path 白名单决定,而由 provider adapter 根据时间范围和日志完整性决定。
+
+### 3. Service-Level Filtering
+
+`WorkAnalysisService` 的新流程:
+
+1. 规范化查询
+2. 调用 collector,按时间范围收集全部 provider sessions
+3. 从 collection 中提取唯一 `availableWorkspacePaths`
+4. 若查询指定了 `workspacePaths`,则按路径过滤 session
+5. 用过滤后的 session 跑 basic analyzer
+6. 在 record/basic result 中写入 `availableWorkspacePaths`
+7. deep analysis 也只基于过滤后的 session/evidence
+
+这里的关键变化是:
+
+- provider collector 负责“收集全部”
+- service 负责“按用户路径筛选”
+
+### 4. Result Shape Rewrite
+
+基础分析结果新增:
+
+- `availableWorkspacePaths: string[]`
+
+它表示:
+
+- 在当前时间范围内,从 provider 日志实际发现过的全部 workspace 路径
+- 已按 provider parse/时间范围规则归一化后的可选路径全集
+
+同时保留现有汇总字段,但 `workSurface.workspaceIds` 应改为路径语义,例如:
+
+- `workSurface.workspacePaths: string[]`
+
+避免继续使用错误的 id 概念。
+
+### 5. Frontend Filter Rewrite
+
+设置页状态改为:
+
+- `selectedWorkspacePaths: string[]`
+- 初始为空,表示尚未拿到分析结果路径全集
+
+流程:
+
+1. 首次请求只带 `timeRange`
+2. 收到结果后读取 `availableWorkspacePaths`
+3. 如果用户尚未手动改过筛选,则把 `selectedWorkspacePaths` 设为全部路径
+4. 当用户调整路径多选时,重新请求分析,传 `{ workspacePaths: selectedWorkspacePaths, timeRange }`
+
+显示规则:
+
+- 没有结果前,不渲染路径多选列表
+- 有结果但 `availableWorkspacePaths` 为空,显示“该时间范围内没有发现 provider 日志目录”
+- 路径直接显示绝对路径字符串
+
+## Data Flow
+
+### Basic Analysis
+
+1. UI 请求 `work.analysis.get({ timeRange })`
+2. service 扫描 provider logs,得到所有命中 session
+3. service 产出:
+ - `availableWorkspacePaths`
+ - 基于全部路径聚合的 basic result
+4. UI 渲染结果和路径多选,默认全选
+5. 用户选择子集路径
+6. UI 请求 `work.analysis.get({ timeRange, workspacePaths })`
+7. service 复扫 provider logs 并在 service 层过滤路径
+8. UI 渲染所选路径对应分析结果
+
+### Deep Analysis
+
+1. UI 在已有路径筛选状态下触发 `runDeep`
+2. 请求参数沿用当前 `workspacePaths + timeRange`
+3. service 复扫并按路径过滤
+4. evidence sampler 只从过滤后的 session 中抽样
+5. deep runner 基于过滤后的 basic/evidence 运行
+
+## Testing Strategy
+
+### Server Tests
+
+新增或改写以下测试:
+
+- `WorkAnalysisService` 在未提供 `workspacePaths` 时,会聚合多个未打开目录的 session
+- `WorkAnalysisService` 在提供 `workspacePaths` 时,只保留匹配路径的 session
+- `basic-analyzer` 的 `workSurface` 和新 `availableWorkspacePaths` 使用路径语义
+- `work-analysis` commands schema 改为接受 `workspacePaths`
+- provider log collector 相关测试不再依赖 discover 输入里的 `workspacePaths`
+
+### Frontend Tests
+
+新增或改写以下测试:
+
+- settings page 首次进入分析页时,初次请求不发送 `workspaceIds`
+- 收到分析结果后,路径多选项来自 `availableWorkspacePaths`
+- 调整路径多选后,请求发送 `workspacePaths`
+- 页面不再依赖 `orderedWorkspacesAtom` 渲染分析筛选项
+
+### E2E
+
+新增一条真实路径语义的 e2e:
+
+- provider 日志中存在两个未打开目录
+- 设置页工作分析首次展示汇总结果
+- 路径筛选项展示这两个目录
+- 取消一个目录后,结果只剩另一个目录对应数据
+
+## Risks
+
+### Provider Scan Cost
+
+去掉前置 workspace path 白名单后,provider scan 范围会扩大。
+
+缓解:
+
+- 仍严格按时间范围裁剪
+- adapter 先做轻量 metadata 过滤,再读重内容
+- 保持 basic analysis summary-first,不加载全文
+
+### Path Normalization Drift
+
+不同 provider 记录的路径格式可能不完全一致,例如大小写、symlink、Windows 分隔符。
+
+本轮先采用现有绝对路径字符串语义,不引入复杂 canonicalization;如果后续出现实测问题,再单独设计路径归一化策略。
+
+### Empty First Result UX
+
+首次进入时路径筛选项要等第一次分析结果返回后才知道,这比“直接用已打开 workspace 列表”多一步。
+
+这是正确代价,因为路径全集本来就应该来自 provider 历史,而不是 app state。
+
+## Migration Impact
+
+这是一次显式语义重写,不做向后兼容。
+
+影响:
+
+- 前后端 `work analysis` 查询类型全部改成 `workspacePaths`
+- 基础分析结果中的 workspace 维度改成 path 语义
+- 旧测试和旧 seed 数据凡是使用 `workspaceIds` 作为分析筛选输入的,都要更新
+
+不要求保留旧 query record 的读取兼容语义;按新方案统一即可。
+
+## Recommended Implementation Order
+
+1. 改 query types / commands schema 到 `workspacePaths`
+2. 改 service 与 collector contract,去掉 discover-time path 白名单
+3. 改 analyzer/result types,加入 `availableWorkspacePaths`
+4. 改设置页状态机,路径筛选来源切到分析结果
+5. 补齐 server/web/e2e 测试
+
+## Acceptance Criteria
+
+- 用户未打开某个 workspace,但它出现在 provider 日志中时,工作分析仍能发现并展示该路径。
+- 工作分析首次运行时默认汇总全部命中路径,而不是只看当前打开 workspace。
+- 用户可以从结果里的路径列表中多选一个或多个路径重新查看分析结果。
+- 前端和服务端都不再把“已打开 workspace”当成工作分析前置筛选条件。
+- 5 个内置 provider 的工作分析语义保持一致。
diff --git a/docs/superpowers/specs/2026-06-04-workspace-history-design.md b/docs/superpowers/specs/2026-06-04-workspace-history-design.md
new file mode 100644
index 000000000..d87267a33
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-04-workspace-history-design.md
@@ -0,0 +1,264 @@
+# Workspace History Design
+
+## Summary
+
+This design adds a persistent `Recent Workspaces` list to the shared workspace launch panel so users can reopen previously used project folders directly after the app or a workspace has been closed. The history is stored separately from the current open-workspace set so bootstrap routing, active workspace state, and close semantics remain unchanged.
+
+## Problem
+
+The current workspace model only persists workspaces that are still open.
+
+- `workspace.open` creates or returns a live workspace entry.
+- `workspace.close` deletes that workspace entry.
+- `workspace.list` is used during bootstrap to decide whether the app should route to `/workspace` or stay on `/`.
+
+That behavior is correct for active workspaces, but it means a previously opened project disappears completely once it is closed. Users then have to browse the filesystem again from the launch panel to find the same folder.
+
+The missing piece is a separate concept for "recently opened workspaces" that survives workspace closure and server restart without changing the meaning of the existing open-workspace APIs.
+
+## Goals
+
+- Persist a recent workspace list across workspace closure and server restart.
+- Show that recent list inside the existing shared workspace launch panel.
+- Allow clicking a recent entry to immediately reopen that workspace.
+- Keep `workspace.list` scoped to currently open workspaces only.
+- Reuse existing `workspace.open` success and error flows instead of creating a second open path.
+
+## Non-Goals
+
+- Pinning, starring, or manually reordering history entries.
+- Removing invalid history entries before the user clicks them.
+- Adding a separate global workspace manager page.
+- Changing multi-workspace bootstrap or routing behavior.
+- Reusing historical workspace ids across reopen events.
+
+## Approaches Considered
+
+### 1. Browser-local history in `localStorage`
+
+This is the lightest implementation, but it does not fit the product direction.
+
+- history would be tied to one browser profile
+- mobile or second-browser access would not see the same recent list
+- the app already uses server-owned persistence for adjacent workspace activity data such as `workspace.lastViewedTarget`
+
+This approach is rejected.
+
+### 2. Separate recent-workspace history backed by `settingsRepo`
+
+This keeps history independent from live workspace state while using an existing persistence mechanism already meant for global app state.
+
+- survives restart
+- available to all browser clients connected to the same local server
+- does not interfere with `workspace.list`
+- matches the existing `workspace.lastViewedTarget` storage pattern
+
+This is the recommended approach.
+
+### 3. Expand `workspaceRepo` to keep closed workspaces
+
+This approach would mix active and historical concepts in one store.
+
+- closed workspaces would still appear in `workspace.list`
+- bootstrap route decisions would become wrong
+- closing a workspace would no longer mean "remove from active set"
+
+This approach is rejected.
+
+## Recommended Design
+
+### Data Model
+
+Add a shared type in `packages/core`:
+
+```ts
+export interface WorkspaceHistoryEntry {
+ path: string;
+ name: string;
+ lastOpenedAt: number;
+}
+```
+
+Design rules:
+
+- `path` is the stable identity and dedupe key.
+- `workspaceId` is not stored because reopened workspaces may receive a new id after being closed.
+- `name` is derived from `basename(path)` and falls back to `path` if needed.
+- the stored list is sorted by `lastOpenedAt` descending.
+- the list is capped at 20 entries.
+
+### Server Persistence
+
+Recent-workspace history should be stored under a new settings key:
+
+```ts
+workspace.history
+```
+
+The persistence implementation should live in a small server-side helper dedicated to:
+
+- reading the stored history list
+- validating and normalizing entries
+- recording a successful workspace open
+
+The helper should not live inside `WorkspaceRepo`, because `WorkspaceRepo` currently models the active workspace set only.
+
+### Server Behavior
+
+#### Read path
+
+Add a new command:
+
+- `workspace.history.list`
+
+It returns normalized `WorkspaceHistoryEntry[]`.
+
+#### Write path
+
+Do not add a public `workspace.history.record` command.
+
+Instead, record history as part of the existing `workspace.open` server command after `ctx.workspaceMgr.open(...)` succeeds. That keeps all open entry points aligned:
+
+- launch modal open
+- diagnostics retry flows
+- any future caller that uses `workspace.open`
+
+Recording rules:
+
+1. normalize the opened path
+2. remove any existing history entry with the same path
+3. prepend the new entry with current timestamp
+4. trim to the maximum length
+5. write the final list back to `settingsRepo`
+
+### Client Behavior
+
+The shared launch modal should load recent history alongside the current directory browser data.
+
+The browse request and history request should be allowed to resolve independently. A history load failure should degrade the launch panel to its existing directory-browser-only behavior instead of blocking workspace launch entirely.
+
+The action layer in `use-workspace-launch-actions.ts` should gain a reusable helper:
+
+- `openWorkspaceByPath(path: string)`
+
+That helper should contain the existing post-open behavior that is currently coupled to the selected-directory flow:
+
+- dispatch `workspace.open`
+- persist `workspace.lastViewedTarget`
+- update `activeWorkspaceIdAtom`
+- write the workspace into `workspacesAtom`
+- hydrate editor UI state
+- update `workspaceOrderAtom`
+- navigate to `/workspace` when launched from outside the workspace route
+- close the modal on success
+- preserve the existing diagnostics redirect on failure
+
+Both of these launch paths should call the same helper:
+
+- directory selection + `Start Workspace`
+- direct click on a recent history entry
+
+### Launch Panel UI
+
+The feature should be added to the existing shared modal in `workspace-launch-modal.tsx`, because that surface is already reused by:
+
+- welcome page
+- top bar
+- command palette
+- mobile workspace flows
+
+#### Desktop layout
+
+Show a `Recent Workspaces` section in the launch modal before or beside the directory browser content.
+
+Each history row should show:
+
+- workspace name
+- full path
+- optional recency metadata if the existing layout has room
+
+Clicking a history row should immediately open that workspace. It should not require selecting the row and then clicking `Start Workspace`.
+
+#### Mobile layout
+
+Render the recent history block above the directory list inside the same sheet body.
+
+Clicking a history row should also immediately open the workspace on mobile.
+
+### Failure Handling
+
+The first version should reuse the current `workspace.open` failure behavior.
+
+If a history path no longer exists or is no longer accessible:
+
+- `workspace.open` fails
+- the client follows the current diagnostics redirect path with the selected workspace path preserved
+
+This keeps the implementation small and consistent. Invalid-history pruning can be added later if needed.
+
+### Architecture Notes
+
+The design depends on preserving one hard boundary:
+
+- `workspace.list` means "currently open workspaces"
+- `workspace.history.list` means "recently opened workspaces"
+
+Bootstrap code in `useBootstrap.ts` must continue to use only `workspace.list` when deciding whether the app should route to `/workspace` or remain on `/`.
+
+## File Boundaries
+
+Primary files expected to change:
+
+- `packages/core/src/domain/types.ts`
+- `packages/server/src/commands/workspace.ts`
+- `packages/server/src/commands/workspace-activity.ts` or a sibling workspace-history command module
+- `packages/web/src/features/workspace/actions/use-workspace-launch-actions.ts`
+- `packages/web/src/features/workspace/views/shared/workspace-launch-modal.tsx`
+
+Likely new files:
+
+- `packages/server/src/workspace/history-store.ts`
+
+Tests expected to change:
+
+- `packages/server/src/__tests__/workspace-commands.test.ts`
+- `packages/web/src/features/workspace/views/shared/workspace-launch-modal.test.tsx`
+
+Likely locale updates:
+
+- `packages/web/src/locales/en.json`
+- `packages/web/src/locales/zh.json`
+
+## Testing Strategy
+
+### Server tests
+
+Add coverage for:
+
+- `workspace.history.list` returning an empty list by default
+- recording history after a successful `workspace.open`
+- deduping repeated opens of the same path
+- ordering by most recent open time
+- trimming the list to the configured maximum length
+- returning normalized data when malformed entries are present in settings storage
+
+### Client tests
+
+Add coverage for:
+
+- launch modal requesting recent history during mount
+- rendering recent history entries in desktop and mobile launch surfaces
+- clicking a recent history entry dispatching `workspace.open` directly
+- recent-history opens reusing the same post-open state hydration as directory-based opens
+- failed recent-history opens preserving the current diagnostics redirect behavior
+
+## Acceptance Criteria
+
+- opening a workspace records it in recent history
+- closing a workspace does not remove it from recent history
+- reopening the app still shows recent history
+- clicking a recent history row immediately reopens that workspace
+- repeated opens of the same path do not create duplicate history rows
+- `workspace.list` semantics remain unchanged and continue to represent only currently open workspaces
+- bootstrap routing behavior remains unchanged
+- the recent-history UI is available anywhere the shared launch modal is used
diff --git a/docs/superpowers/specs/2026-06-05-workspace-launch-history-timestamp-design.md b/docs/superpowers/specs/2026-06-05-workspace-launch-history-timestamp-design.md
new file mode 100644
index 000000000..e1a992a06
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-05-workspace-launch-history-timestamp-design.md
@@ -0,0 +1,107 @@
+# Workspace Launch History Timestamp Design
+
+Date: 2026-06-05
+Status: Drafted for review
+
+## Summary
+
+Add a visible "last accessed" timestamp to each row in the workspace launch modal's "Recent Workspaces" list.
+
+The server already persists recent workspace history with `WorkspaceHistoryEntry.lastOpenedAt`, so this change is frontend-only. The modal will render an absolute timestamp using the current locale, with the workspace name on the left and the formatted timestamp on the right. The path remains on the second line.
+
+## Goals
+
+- Make recency visible without requiring hover or an extra click.
+- Reuse the existing workspace history payload without protocol changes.
+- Preserve the current compact list density on desktop and mobile.
+
+## Non-Goals
+
+- Renaming history fields or changing persistence semantics.
+- Introducing relative time labels such as "3 minutes ago".
+- Adding sorting or filtering controls to recent workspaces.
+
+## Existing State
+
+- `packages/server/src/workspace/history-store.ts` already stores `lastOpenedAt` for each recent workspace entry.
+- `packages/core/src/domain/types.ts` already exposes `WorkspaceHistoryEntry.lastOpenedAt`.
+- `packages/web/src/features/workspace/views/shared/workspace-launch-modal.tsx` currently renders only the workspace name and path.
+
+## Proposed Design
+
+### Data
+
+Continue using `WorkspaceHistoryEntry.lastOpenedAt` from `workspace.history.list`. No backend or shared type changes are required.
+
+### UI Layout
+
+Each recent workspace row keeps a two-line layout:
+
+1. Top row: workspace name on the left, absolute last-access timestamp on the right.
+2. Bottom row: workspace path in monospace, unchanged from the current design.
+
+The timestamp should visually read as secondary metadata:
+
+- Smaller than the workspace name.
+- Right-aligned within the header row.
+- Styled with secondary or tertiary foreground emphasis.
+
+On narrow widths, the header row may wrap if needed, but the preferred layout remains name-left / time-right.
+
+### Formatting
+
+Use the existing `formatDate(timestamp, locale)` helper from `packages/web/src/lib/i18n.ts`.
+
+Expected output follows the active UI locale:
+
+- `zh` uses `zh-CN`
+- `en` uses `en-US`
+
+No custom timestamp formatting is added in this change.
+
+### Accessibility
+
+- The visible timestamp is informative only and does not change button semantics.
+- The row button's existing accessible name remains the workspace-open action.
+- No new interactive element is introduced.
+
+## Implementation Notes
+
+### Frontend
+
+- Update `workspace-launch-modal.tsx` to render a new row header wrapper for name + timestamp.
+- Read the active locale and format `entry.lastOpenedAt` with `formatDate`.
+- Add small CSS adjustments in `packages/web/src/styles/components.css` for the header row and timestamp styling.
+
+### Tests
+
+Add or update modal tests to verify:
+
+- Recent workspace rows render the formatted timestamp.
+- Existing direct-open behavior from recent workspaces remains unchanged.
+
+No backend tests are required because data persistence is unchanged.
+
+## Alternatives Considered
+
+### Show timestamp on a third line
+
+Rejected because it increases row height and reduces visible recent-workspace density.
+
+### Show relative time instead of absolute time
+
+Rejected because the requested behavior is explicit timestamp visibility and the product already has a locale-aware absolute date formatter.
+
+### Add both relative and absolute time
+
+Rejected for now because it adds noise to a compact picker without a clear decision need.
+
+## Risks
+
+- Locale-formatted timestamps can be wider than expected in some environments, so the row header needs flexible spacing.
+- Very long workspace names and long timestamps may compete for horizontal space; CSS needs to allow graceful shrinking or wrapping.
+
+## Validation
+
+- Unit test coverage for rendered timestamp text in the workspace launch modal.
+- Manual verification on desktop and mobile launch surfaces for spacing and overflow behavior.
diff --git a/docs/superpowers/specs/2026-06-06-system-agent-instructions-design.md b/docs/superpowers/specs/2026-06-06-system-agent-instructions-design.md
new file mode 100644
index 000000000..c008a744d
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-06-system-agent-instructions-design.md
@@ -0,0 +1,324 @@
+# System Agent Instructions Management Design
+
+Date: 2026-06-06
+Status: Draft
+Owner: Codex
+
+## Problem
+
+当前 Agent 面板已经能管理项目级 `.coder-studio/agent.md`。这份文件是 Coder Studio 的 workspace-local 项目说明,保存后会同步到各 provider 在当前项目里实际读取的文件,例如 `AGENTS.md`、`GEMINI.md`、`.claude/CLAUDE.md`。
+
+用户还希望在同一个 Agent 面板里管理每个 Agent 工具自己的用户级全局说明文件。这个文件不属于当前 workspace,通常位于用户 home 目录下,例如 Codex 的 `~/.codex/AGENTS.md` 和 Claude Code 的 `~/.claude/CLAUDE.md`。现有 `file.read` / `file.write` 明确限制在 workspace root 内,不能直接用于这些系统文件。
+
+因此本功能需要在保留 workspace 文件安全边界的前提下,为少量已知 Agent 全局说明文件提供受控编辑入口。
+
+## Goals
+
+- 将现有 Agent 面板中的 `agent.md` 命名调整为 `项目 Agent.md`。
+- 新增 `系统 Agent.md` 分组,列出当前内置 provider 中支持用户级全局说明文件的 Agent。
+- 点击系统 Agent 条目的编辑按钮后,在现有主编辑器中打开对应文件。
+- 文件不存在时创建简短 scaffold 后打开。
+- 保存时支持 baseHash 冲突检测。
+- 后端只允许编辑服务端定义的 provider allowlist 路径,不接受前端传任意绝对路径。
+- 对没有稳定 Markdown 全局说明文件的 provider,展示不可编辑状态,而不是伪造路径。
+- 桌面端和移动端都能看到同一组项目/系统 Agent.md 管理入口。
+
+## Non-Goals
+
+- 不把 `$HOME` 目录挂进文件树。
+- 不支持任意系统文件读写。
+- 不把系统 Agent 文件加入 Git diff、文件搜索、文件树、LSP 或图片预览。
+- 不修改各 Agent CLI 的真实加载规则。
+- 不为 Cursor User Rules 逆向或猜测本地存储路径。
+- 不在一期支持自定义 provider 的全局说明文件配置。
+- 不迁移或合并现有 `.coder-studio/agent.md` 内容到系统文件。
+
+## User Decisions Captured
+
+- “系统 agent.md” 指每个 Agent 工具自己的用户级全局说明文件。
+- 现有项目级 agent.md 继续存在,但 UI 名称改为 `项目 Agent.md`。
+- 新增系统级分组,用户可以直接编辑保存支持的 Agent 全局说明文件。
+- Cursor Agent 如果没有稳定文件入口,可以先显示为不支持直接文件编辑。
+
+## Research Notes
+
+- Codex 官方文档说明全局说明文件位于 Codex home 目录,默认读取 `~/.codex/AGENTS.override.md`,否则读取 `~/.codex/AGENTS.md`。本功能一期编辑稳定基础文件 `~/.codex/AGENTS.md`,不主动创建 override 文件。
+- Claude Code 官方 memory 文档列出用户级 memory 文件为 `~/.claude/CLAUDE.md`。
+- Gemini CLI 文档说明 `/memory add` 会追加到全局 `~/.gemini/GEMINI.md`。
+- OpenCode 官方 rules 文档说明全局规则文件为 `~/.config/opencode/AGENTS.md`。
+- Cursor 官方 rules 文档说明 User Rules 是全局规则,但定义在 Cursor Settings > Rules 中,不是公开稳定的 Markdown 文件路径。
+
+Sources:
+
+- OpenAI Codex AGENTS.md guide: https://developers.openai.com/codex/guides/agents-md
+- Claude Code memory docs: https://docs.claude.com/en/docs/claude-code/memory
+- Gemini CLI context files docs: https://google-gemini.github.io/gemini-cli/docs/cli/gemini-md.html
+- OpenCode rules docs: https://dev.opencode.ai/docs/rules/
+- Cursor rules docs: https://docs.cursor.com/context/rules
+
+## Approaches Considered
+
+### Option A: Reuse workspace file APIs with relative paths
+
+Treat each system file as if it were a workspace file and pass a path like `../../.codex/AGENTS.md`.
+
+Pros:
+
+- Very small frontend change.
+
+Cons:
+
+- Breaks the existing path safety model.
+- Encourages future features to bypass workspace root.
+- Would make a security-sensitive exception inside a general-purpose file API.
+
+Decision: reject.
+
+### Option B: Add a general external file manager
+
+Create a general API that can read/write files outside the workspace, then use it for Agent system files.
+
+Pros:
+
+- Flexible for future system-level config editing.
+
+Cons:
+
+- Much larger product and security surface.
+- Needs permissions, browsing, path validation, audit UI, and likely OS-specific handling.
+- Overbuilds this request.
+
+Decision: reject for this feature.
+
+### Option C: Add provider allowlist system Agent instructions APIs
+
+Create dedicated commands for system Agent instruction files. The frontend passes only `providerId`; the backend maps that provider to a fixed path.
+
+Pros:
+
+- Meets direct edit/save requirement.
+- Keeps existing workspace file API strict.
+- Simple to test because the path matrix is finite.
+- Handles unsupported providers honestly.
+
+Cons:
+
+- Requires editor load/save to understand a small virtual path scheme.
+- Custom providers need a later extension point.
+
+Decision: accept.
+
+## Final Design
+
+### 1. Provider Matrix
+
+一期系统 Agent 文件 allowlist:
+
+| Provider | Display | File | Editable |
+| --- | --- | --- | --- |
+| `codex` | Codex | `~/.codex/AGENTS.md` | yes |
+| `claude` | Claude Code | `~/.claude/CLAUDE.md` | yes |
+| `gemini` | Gemini CLI | `~/.gemini/GEMINI.md` | yes |
+| `opencode` | OpenCode | `~/.config/opencode/AGENTS.md` | yes |
+| `cursor` | Cursor Agent | Cursor Settings > Rules | no |
+
+The backend must derive paths with `os.homedir()` and path joining. The frontend never sends these absolute paths for read/write.
+
+### 2. UI Structure
+
+The current `AgentInstructionsSection` becomes a higher-level Agent instructions panel with two groups.
+
+Group 1: `项目 Agent.md`
+
+- Uses the existing `.coder-studio/agent.md` status, generate, regenerate, and edit flow.
+- Existing behavior stays intact.
+- Copy changes from generic `agent.md` to project-specific wording.
+
+Group 2: `系统 Agent.md`
+
+- Loads provider list/status from a new system status command.
+- Renders one row per built-in provider.
+- Editable rows show provider display name, resolved user-facing path, existence state, and an edit action.
+- Unsupported rows show provider display name, reason, and no edit action.
+- Cursor row copy should say it is managed through Cursor Settings > Rules.
+
+Suggested row states:
+
+- `Ready`: file exists and can be edited.
+- `Missing`: file does not exist; clicking edit will create scaffold.
+- `Unsupported`: provider has no stable file path.
+- `Error`: status/read/write failed.
+
+### 3. Editor Integration
+
+System files open in the existing editor with virtual paths:
+
+- `agent-system:codex`
+- `agent-system:claude`
+- `agent-system:gemini`
+- `agent-system:opencode`
+
+The display label should show the real user-facing path, for example `~/.codex/AGENTS.md`, while internal editor state keeps the virtual path as the stable key.
+
+Editor read/write routing:
+
+- If active path starts with `agent-system:`, `useCodeEditorActions.loadFile` calls `agentInstructions.system.read`.
+- `handleSave` calls `agentInstructions.system.write`.
+- Refresh reconciliation for these paths calls the same system read command.
+- Monaco model path can use the virtual path. No LSP should attach to these files.
+
+The returned payload should stay compatible with text file handling:
+
+```ts
+type SystemAgentInstructionsReadResult = {
+ kind: "text";
+ providerId: string;
+ path: string;
+ displayPath: string;
+ exists: boolean;
+ content: string;
+ baseHash: string;
+ encoding: "utf-8";
+};
+```
+
+The write command returns:
+
+```ts
+type SystemAgentInstructionsWriteResult = {
+ providerId: string;
+ path: string;
+ displayPath: string;
+ newHash: string;
+};
+```
+
+### 4. Backend Commands
+
+Add commands under the existing `agentInstructions` namespace:
+
+- `agentInstructions.system.status`
+- `agentInstructions.system.read`
+- `agentInstructions.system.write`
+
+`status` input:
+
+```ts
+{
+ workspaceId: string;
+}
+```
+
+`read` input:
+
+```ts
+{
+ workspaceId: string;
+ providerId: string;
+}
+```
+
+`write` input:
+
+```ts
+{
+ workspaceId: string;
+ providerId: string;
+ content: string;
+ baseHash?: string;
+}
+```
+
+`workspaceId` is kept for command scoping and client consistency, but the file path does not depend on workspace root.
+
+Implementation details:
+
+- Create a small resolver that returns system instruction metadata by provider id.
+- Only built-in provider ids in the allowlist can resolve to editable files.
+- Unsupported providers return structured metadata from status and throw `agent_system_instructions_unsupported` on read/write.
+- `read` returns empty content and `exists: false` when the file is missing.
+- `write` creates parent directories and writes the file.
+- `write` checks `baseHash` against current file content when provided and throws `conflict` on mismatch.
+- Commands should not emit workspace `fs.dirty`, because these files are outside the workspace tree.
+
+### 5. Scaffold Content
+
+When a supported system file is missing and the user clicks edit, the frontend can either call write first with scaffold content or let read return a scaffold candidate. Keep the implementation consistent with the existing project Agent edit flow by writing scaffold first.
+
+Suggested scaffold:
+
+```md
+# Agent Instructions
+
+## Personal Defaults
+- Add preferences this agent should follow across your projects.
+
+## Working Style
+- Add communication, testing, review, or safety expectations.
+```
+
+Provider-specific heading can be added later.一期不需要不同 provider 生成不同 scaffold。
+
+### 6. State And Persistence
+
+Reuse existing workspace UI state for the project group expansion.
+
+Add one optional UI state field for system group expansion:
+
+```ts
+agentSystemInstructionsExpanded?: boolean;
+```
+
+If this feels too wide for一期, keep the system group always expanded. The preferred implementation adds the field because the current panel already persists expansion for project Agent.md.
+
+### 7. Error Handling
+
+Expected errors:
+
+- `workspace_not_found`: command called without an active workspace.
+- `agent_system_instructions_unsupported`: provider has no editable global file.
+- `agent_system_instructions_unknown_provider`: provider is not in the built-in matrix.
+- `conflict`: file changed since it was opened.
+- filesystem permission errors: show the OS error message in the panel/editor save error.
+
+The UI should not hide unsupported providers. Listing them makes the support boundary clear.
+
+### 8. Testing
+
+Server tests:
+
+- Status lists editable Codex, Claude, Gemini, OpenCode and unsupported Cursor.
+- Read returns `exists: false` for missing editable file.
+- Write creates parent directories and file.
+- Write returns conflict when baseHash is stale.
+- Unsupported provider read/write returns typed error.
+- Unknown provider read/write returns typed error.
+- Commands do not touch workspace file API or emit workspace dirty events.
+
+Web tests:
+
+- Agent panel title changes to `项目 Agent.md`.
+- System group renders provider rows and unsupported Cursor row.
+- Clicking edit on a missing system file writes scaffold then opens virtual path.
+- Clicking edit on an existing system file opens virtual path without scaffold write.
+- Editor loads `agent-system:codex` through system read.
+- Saving `agent-system:codex` calls system write with provider id and baseHash.
+- Existing project Agent generation/regeneration tests continue passing after copy changes.
+
+Manual verification:
+
+- Open Agent panel in desktop workspace and edit Codex global instructions.
+- Save and confirm `~/.codex/AGENTS.md` changes on disk.
+- Repeat for missing-file creation using a temporary HOME in tests where possible.
+- Check mobile file/explorer surface still renders without layout overflow.
+
+## Rollout Notes
+
+This feature changes a personal global Agent configuration file. It should be clear in UI copy that system Agent.md applies across projects for that local user, unlike project Agent.md.
+
+If a user edits Codex `~/.codex/AGENTS.md`, active Codex sessions may not reload it until a new session starts. The UI should avoid promising live reload behavior.
+
+## Open Questions
+
+- Should custom providers later be able to declare a global instructions file path?一期 leaves this out for safety.
+- Should Codex also expose `~/.codex/AGENTS.override.md`?一期 edits only `AGENTS.md` to avoid accidentally creating a higher-precedence override.
diff --git a/docs/superpowers/specs/2026-06-06-work-analysis-hourly-dashboard-redesign-design.md b/docs/superpowers/specs/2026-06-06-work-analysis-hourly-dashboard-redesign-design.md
new file mode 100644
index 000000000..1434e9b8b
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-06-work-analysis-hourly-dashboard-redesign-design.md
@@ -0,0 +1,163 @@
+# Work Analysis Hourly Dashboard Redesign
+
+Date: 2026-06-06
+Status: Approved for first implementation
+Owner: Codex
+
+## Problem
+
+当前工作分析仍然像一个需要手动触发的报告页:
+
+- 用户进入页面时经常没有现成结果,需要先点击运行基础分析。
+- `WorkAnalysisRecord` 以 `queryDigest` 缓存一次性快照,不适合作为长期趋势和自动刷新基础。
+- 页面直接消费复杂 `basicResult/snapshotV2`,前端需要大量兼容映射,信息层级混乱。
+- token 趋势、项目贡献、模型贡献、agent 贡献不够直观。
+
+## Goals
+
+- 工作分析默认展示已有缓存数据,用户不需要先手动运行基础分析。
+- 支持手动刷新和自动刷新两种扫描方式。
+- 自动扫描每小时运行一次,刷新 provider 本地日志索引。
+- 基础扫描以小时维度聚合 token、会话数、活跃时间、项目、模型、agent、provider 等数据。
+- 概览页采用扁平化专业仪表盘布局:
+ - KPI 状态条
+ - Token 趋势独占一整行
+ - 下一行三列展示项目、模型、agent token 贡献排行
+ - 继续展示任务/工具大头、小时热力图、数据质量和扫描状态
+- 不要求兼容旧页面结构;可以推翻当前工作分析 UI。
+
+## Non-Goals
+
+- 不在第一版引入远程服务或云同步。
+- 不在第一版做真实费用金额估算。
+- 不要求所有 provider 立即达到完整 usage coverage。
+- 不把深入分析作为概览页核心依赖。
+- 不在第一版实现无限 drill-down;先保证 dashboard 可用。
+
+## Product Model
+
+`基础分析` 的产品语义改为 `刷新索引`:
+
+- 自动刷新:服务启动后后台调度,每小时运行一次。
+- 手动刷新:用户点击 `立即刷新索引`,强制扫描当前时间范围并更新缓存。
+- 页面读取:`work.analysis.dashboard` 直接返回 dashboard projection。
+- 深入分析:后续基于缓存中的 sessions/events 抽样生成洞察,作为可选二级能力。
+
+## Data Model
+
+第一版采用本地 JSON repo,结构按索引库设计,后续可迁移 SQLite。
+
+```ts
+interface WorkAnalysisDashboardCache {
+ version: 1;
+ scanState: WorkAnalysisScanState;
+ dashboard: WorkAnalysisDashboardProjection;
+}
+
+interface WorkAnalysisScanState {
+ mode: "manual" | "auto";
+ status: "idle" | "running" | "succeeded" | "failed";
+ lastStartedAt?: number;
+ lastCompletedAt?: number;
+ nextScheduledAt?: number;
+ errorMessage?: string;
+ sourceDigest?: string;
+ providerStatuses: WorkAnalysisProviderStatus[];
+}
+
+interface WorkAnalysisDashboardProjection {
+ generatedAt: number;
+ timeRange: ResolvedWorkAnalysisTimeRange;
+ filters: WorkAnalysisDashboardQuery;
+ kpis: WorkAnalysisDashboardKpi[];
+ trends: {
+ tokenHourly: WorkAnalysisTokenTrendPoint[];
+ tokenDaily: WorkAnalysisTokenTrendPoint[];
+ hourHeatmap: WorkAnalysisHourHeatPoint[];
+ };
+ rankings: {
+ projects: WorkAnalysisContributionRank[];
+ models: WorkAnalysisContributionRank[];
+ agents: WorkAnalysisContributionRank[];
+ };
+ breakdowns: {
+ tasks: WorkAnalysisContributionRank[];
+ tools: WorkAnalysisContributionRank[];
+ };
+ quality: WorkAnalysisDataQualitySummary;
+}
+```
+
+Hourly aggregation uses absolute `hourStart` timestamps rather than `0-23` buckets. This lets the UI render real trends across days and still derive the hour heatmap.
+
+## Backend Design
+
+Add a dashboard-oriented service path beside the existing basic/deep commands:
+
+- `work.analysis.dashboard.get`
+ - returns cached dashboard projection when available
+ - if no cache exists, returns an empty projection with scan state
+- `work.analysis.dashboard.refresh`
+ - runs a manual scan for the requested range/filter
+ - updates the dashboard cache
+ - returns the new projection
+
+The first implementation may reuse the existing `workLogCollector.collect()` and `analyzeWorkBasic()` instead of introducing a full normalized fact store immediately. The important contract change is that the service materializes dashboard projection and scan state separately from query snapshot records.
+
+Automatic scanning is owned by `WorkAnalysisService`:
+
+- `startAutoScan()` schedules a scan every hour.
+- The scheduler avoids overlapping scans.
+- The default query is `90d` with all known workspaces.
+- Manual refresh can run independently but should share the same scan lock.
+
+## Frontend Design
+
+Replace the current tab-heavy work analysis page with an overview-first dashboard.
+
+Primary layout:
+
+1. Top status strip: auto scan enabled, last scan, next scan, coverage warnings.
+2. Filter bar: time range, projects, provider, model, agent, metric.
+3. KPI row: total tokens, input/output, sessions, active time, top project share.
+4. Token trend row: full-width chart.
+5. Contribution row: three columns:
+ - project token contribution ranking
+ - model token contribution ranking
+ - agent token contribution ranking
+6. Secondary row: model/agent/task/tool highlights and hour heatmap.
+7. Operational row: scan pipeline and data quality.
+
+The visual direction is flat and professional:
+
+- light background
+- white panels
+- fine borders
+- low shadow
+- table-based rankings
+- restrained blue accent
+
+## Error Handling
+
+- If dashboard cache is absent, show empty state with `立即刷新索引`.
+- If refresh fails, keep the last successful dashboard and show scan error in the status strip.
+- If some providers are partial, show data quality warning without blocking the dashboard.
+- If a selected filter yields no data, show zeroed KPI cards and empty ranking panels.
+
+## Testing
+
+Backend tests:
+
+- dashboard refresh builds token trend and three contribution rankings
+- cached dashboard can be read without rescanning
+- refresh failure updates scan state but does not erase prior dashboard
+
+Frontend tests:
+
+- page renders token trend before contribution rankings
+- contribution section renders project/model/agent rankings as three separate groups
+- refresh button dispatches dashboard refresh command
+
+## First Slice
+
+The first implementation should land a working dashboard path using the existing collector/analyzer data rather than building the full future fact store in one step. This gives users the visible product improvement now while keeping the protocol shaped for hourly indexing.
diff --git a/docs/superpowers/specs/2026-06-06-work-analysis-remove-budget-module-design.md b/docs/superpowers/specs/2026-06-06-work-analysis-remove-budget-module-design.md
new file mode 100644
index 000000000..2f73db881
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-06-work-analysis-remove-budget-module-design.md
@@ -0,0 +1,126 @@
+# Work Analysis Remove Budget Module Design
+
+Date: 2026-06-06
+Status: Draft
+Owner: Codex
+
+## Problem
+
+当前 `工作分析` 的预算能力已经形成一条完整链路:
+
+- 前端存在独立的 `budgets` 页签和预算展示区
+- 前端类型包含预算专用结构
+- 后端协议同时在 `basicResult.budgets` 和 `snapshotV2.delivery.budgets` 暴露预算数据
+- 后端 analyzer 和 metrics 层持续计算预算预测、阈值和目录预算
+- server/web 测试持续维护预算 fixture 和断言
+
+这和当前目标冲突。用户要求把 `工作分析总览额度预算模块功能整个去掉`,因此不能只隐藏页面,也不能保留后端空壳字段。
+
+## Goal
+
+把 `工作分析` 里的预算模块从前后端一起彻底移除,保证:
+
+- UI 不再出现预算页签和预算区块
+- 协议不再输出预算字段
+- 后端不再进行预算计算
+- 测试不再维护预算相关断言
+- 仓库中不再保留工作分析预算模块的活跃实现链路
+
+## Non-Goals
+
+- 不重构 `yield`、`overview`、`compare` 等非预算分析能力
+- 不调整深度分析产品形态
+- 不保留“兼容旧字段但恒为空”的过渡层
+- 不处理与工作分析无关的其他 `budget` 文案或系统预算概念
+
+## Decision
+
+采用硬删除方案,前后端同步收口。
+
+不采用以下替代方案:
+
+1. 只删前端展示
+原因:后端仍会保留无用字段、计算和测试,功能没有真正移除。
+
+2. 保留字段但返回空值
+原因:协议语义变差,后续维护者仍要处理预算分支。
+
+## Scope
+
+本次删除范围包括以下层级。
+
+### Frontend
+
+- 删除 `packages/web/src/features/work-analysis/navigation.ts` 中的 `budgets` tab
+- 删除 `packages/web/src/features/work-analysis/page.tsx` 中预算读取和预算 `TabPanel`
+- 删除 `packages/web/src/features/work-analysis/types.ts` 中预算相关类型
+- 删除中英文 locale 中工作分析预算文案
+
+### Backend
+
+- 删除 `packages/server/src/work-analysis/types.ts` 中预算相关类型和结果字段
+- 删除 `packages/server/src/work-analysis/basic-schema.ts` 中预算 schema
+- 删除 `packages/server/src/work-analysis/basic-analyzer.ts` 中预算汇总、预算投射和 `delivery.budgets` / `basicResult.budgets` 赋值
+- 删除不再使用的 `packages/server/src/work-analysis/metrics/token-budgets.ts` 及其引用
+
+### Tests
+
+- 删除 server analyzer/service tests 中预算 fixture 和预算断言
+- 删除 web page tests 中预算 fixture、tab 断言和预算渲染断言
+
+## Post-Removal Shape
+
+删除后,工作分析页面和协议继续保留这些主域:
+
+- `overview`
+- `tasks`
+- `models`
+- `optimize`
+- `compare`
+- `yield`
+- `capabilities`
+- `dataSources`
+
+以下内容会彻底消失:
+
+- `WORK_ANALYTICS_TABS` 中的 `budgets`
+- 页面里的 `30 天预算预测`、`目标阈值`、`目录预算`
+- `snapshotV2.delivery.budgets`
+- `basicResult.budgets`
+- `WorkAnalysisBudgetSummary`
+- `WorkAnalysisBudgetTarget`
+- `WorkAnalysisBudgetThreshold`
+- 预算计算和预算专用测试数据
+
+## Implementation Order
+
+按以下顺序执行,优先删契约,再删消费方,避免留下半兼容状态。
+
+1. 删除后端类型与 schema 中的预算结构和预算字段
+2. 删除后端 analyzer 中预算计算与装配逻辑
+3. 删除预算 metrics 文件及所有引用
+4. 删除前端导航、页面和类型中的预算消费逻辑
+5. 删除 locale 中预算文案
+6. 更新 server/web tests,去除所有预算相关断言和 fixture
+
+## Validation
+
+完成后需要满足以下结果:
+
+- `/settings?section=analysis` 正常加载
+- 剩余 tab 可以正常切换和渲染
+- 不再存在预算 tab
+- server work-analysis tests 通过
+- web work-analysis page tests 通过
+- 搜索仓库后,不再有工作分析预算模块的实现引用
+
+## Risks
+
+主要风险只有一个:
+
+- 预算字段同时存在于 legacy `basicResult` 与 `snapshotV2.delivery` 两套结构中,如果只删一侧,会造成类型、schema、fixture 与页面消费不同步
+
+应对方式:
+
+- 这次按前后端同步硬删除执行,不保留兼容层
+- 修改后通过类型检查和定向测试验证剩余域仍能正常工作
diff --git a/docs/superpowers/specs/2026-06-07-agent-token-trend-design.md b/docs/superpowers/specs/2026-06-07-agent-token-trend-design.md
new file mode 100644
index 000000000..283e93844
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-07-agent-token-trend-design.md
@@ -0,0 +1,76 @@
+# Agent Panel Token Trend Design
+
+## Goal
+
+Add a compact token consumption trend chart to the AGENT.MD sidebar panel. The chart shows the current project's token consumption over the most recent 24 hours and appears at the top of the expanded panel content.
+
+## Placement
+
+Render the chart as the first content block inside `AgentInstructionsSection` when the AGENT.MD panel is expanded. It appears above the existing "项目 AGENT.MD" status group and above the "系统 AGENT.MD" group.
+
+The outer AGENT.MD header remains unchanged. Collapsing the panel hides the chart together with the rest of the panel body.
+
+## Data Flow
+
+Use the existing work-analysis dashboard command from the web client:
+
+```ts
+dispatch("work.analysis.dashboard.get", {
+ workspacePaths: [workspace.path],
+ timeRange: { preset: "24h" },
+});
+```
+
+Read token trend data from `result.data.dashboard.trends.tokenHourly`. This avoids adding a new server command because the required 24-hour token data already exists.
+
+The chart only runs when `workspace.path` is available. If the workspace cannot be resolved, the chart does not render and does not dispatch.
+
+## UI States
+
+The chart block has four states:
+
+- Loading: show a compact muted skeleton sized like the final chart.
+- Ready with data: render an ECharts line/area chart using hourly token totals.
+- Empty: show "最近 24 小时暂无 token 数据".
+- Error: show a low-emphasis inline message and keep the existing Agent.md controls usable.
+
+The chart header shows:
+
+- Title: `Token 消耗趋势`
+- Subtitle: `当前项目 · 最近 24 小时`
+- Summary metric: total tokens across the 24-hour points
+
+The chart footer shows peak hourly token usage and total session count when data exists.
+
+## Visual Direction
+
+Keep the visual language aligned with the existing sidebar:
+
+- Use `workspace-agent-instructions__*` CSS classes.
+- Use existing theme tokens for text, borders, surfaces, and status colors.
+- Keep the chart compact so it does not dominate the sidebar.
+- Avoid introducing new global visual patterns or new chart dependencies.
+
+## Component Boundaries
+
+Add `agent-instructions-token-trend.tsx` beside `agent-instructions-section.tsx` to keep the existing Agent.md actions readable.
+
+The child component is responsible for:
+
+- Dispatching the 24-hour work-analysis query for the current workspace path.
+- Normalizing hourly points for display.
+- Rendering loading, ready, empty, and error states.
+- Disposing the ECharts instance on unmount.
+
+`AgentInstructionsSection` remains responsible for panel state, Agent.md status, generation, and edit actions.
+
+## Testing
+
+Add focused tests for `agent-instructions-token-trend.tsx` plus one placement assertion in `AgentInstructionsSection` tests. Verify:
+
+- It dispatches `work.analysis.dashboard.get` with the current workspace path and `24h` time range.
+- It renders the chart block before the existing project Agent.md group.
+- It renders the empty state when `tokenHourly` has no token/session data.
+- It renders an error state without hiding existing Agent.md controls.
+
+Run focused web tests for the touched component and a typecheck or broader verification command before handoff.
diff --git a/docs/work-analysis-prototype.html b/docs/work-analysis-prototype.html
new file mode 100644
index 000000000..b882b4f35
--- /dev/null
+++ b/docs/work-analysis-prototype.html
@@ -0,0 +1,1926 @@
+
+
+
+
+
+ 工作分析 - 单页原型
+
+
+
+
+