cohorte 1.5.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +169 -3
  2. package/README.md +65 -57
  3. package/bin/cli.js +31 -15
  4. package/core/agents/implementer.template.md +3 -3
  5. package/core/agents/release.md +1 -1
  6. package/core/agents/review.md +25 -2
  7. package/core/commands/{audit.md → cohorte-audit.md} +11 -3
  8. package/core/commands/{brainstorm.md → cohorte-brainstorm.md} +9 -3
  9. package/core/commands/{build.md → cohorte-build.md} +95 -10
  10. package/core/commands/{doctor.md → cohorte-doctor.md} +22 -9
  11. package/core/commands/{fix.md → cohorte-fix.md} +20 -13
  12. package/core/commands/{init-pipeline.md → cohorte-init-pipeline.md} +1 -1
  13. package/core/commands/cohorte-loop.md +110 -0
  14. package/core/commands/{refactor.md → cohorte-refactor.md} +3 -3
  15. package/core/commands/{review.md → cohorte-review.md} +70 -20
  16. package/core/commands/{ship.md → cohorte-ship.md} +5 -5
  17. package/core/commands/{spec.md → cohorte-spec.md} +32 -12
  18. package/core/commands/{update-pipeline.md → cohorte-update-pipeline.md} +16 -6
  19. package/core/hooks/gate.py +101 -6
  20. package/core/templates/brainstorm-return.md +4 -4
  21. package/core/templates/decisions.template.md +42 -0
  22. package/core/templates/design-brief.md +1 -1
  23. package/core/templates/spec.template.md +8 -6
  24. package/core/templates/steps/init-pipeline/01-detect-stack.md +1 -1
  25. package/core/templates/steps/init-pipeline/02-interview-gaps.md +6 -6
  26. package/core/templates/steps/init-pipeline/03-draft-profile.md +1 -1
  27. package/core/templates/steps/init-pipeline/04-write-render.md +16 -12
  28. package/core/templates/steps/init-pipeline/05-report.md +5 -5
  29. package/core/workflows/audit.js +6 -6
  30. package/core/workflows/refactor.js +14 -14
  31. package/core/workflows/review.js +62 -20
  32. package/dashboard/README.md +2 -2
  33. package/dashboard/dist/assets/{index-dkO8UUVl.css → index-BZ_LQlEj.css} +1 -1
  34. package/dashboard/dist/assets/{index-8owBnqyv.js → index-P1I1JGtj.js} +11 -11
  35. package/dashboard/dist/index.html +2 -2
  36. package/dashboard/server/doctor.js +75 -18
  37. package/dashboard/server/index.js +5 -5
  38. package/dashboard/server/metrics.js +1 -1
  39. package/install.ps1 +31 -14
  40. package/install.sh +31 -14
  41. package/package.json +2 -2
  42. package/profile/PIPELINE.template.md +17 -16
  43. package/profile/SCHEMA.md +199 -48
  44. package/profile/cohorte.config.template.yaml +8 -8
  45. package/scripts/loop-detach.sh +153 -0
  46. package/scripts/loop.sh +202 -25
  47. package/scripts/metrics/collect.mjs +17 -8
  48. package/scripts/new-feature.sh.template +3 -3
  49. package/scripts/preflight.sh +40 -4
  50. package/scripts/remove-feature.sh.template +2 -2
  51. package/scripts/test-dashboard.mjs +34 -7
  52. package/scripts/test-gate.mjs +58 -0
  53. package/scripts/test-loop.mjs +269 -0
  54. package/scripts/test-metrics.mjs +23 -11
  55. package/scripts/test-workflows.mjs +33 -5
  56. package/scripts/validate-core.mjs +46 -9
  57. package/core/commands/loop.md +0 -61
  58. /package/core/commands/{align-ds.md → cohorte-align-ds.md} +0 -0
@@ -1,4 +1,4 @@
1
- # /init-pipeline · 02 Interview the gaps
1
+ # /cohorte-init-pipeline · 02 Interview the gaps
2
2
 
3
3
  ### Phase 2 — Interview the gaps (AskUserQuestion)
4
4
 
@@ -11,7 +11,7 @@ Ask ONLY what you couldn't confidently detect. Batch related questions. Cover:
11
11
  cost), `haiku` for purely mechanical surfaces (scaffolding), `inherit` only for surfaces with real
12
12
  design decisions worth running on the lead's model.
13
13
  - **Specialization (only if Phase 1 flagged a large + cleanly-separable surface)** — offer to split it
14
- into specialized sub-surfaces (e.g. `web-checkout`, `web-billing`) so `/build` runs them in parallel,
14
+ into specialized sub-surfaces (e.g. `web-checkout`, `web-billing`) so `/cohorte-build` runs them in parallel,
15
15
  per SCHEMA.md §Specialization. If the human accepts, apply the rules: **shared code (routing, global
16
16
  state, DS kit/tokens) becomes its own single-owner surface**, and cross-slice shapes go through the
17
17
  contract. Default to NOT splitting when boundaries are tangled or slices are tiny — coarse is fine.
@@ -20,7 +20,7 @@ Ask ONLY what you couldn't confidently detect. Batch related questions. Cover:
20
20
  Recommended option: dot/failures-only reporter (`--reporter=dot` vitest/playwright, `--silent`
21
21
  jest, `-q` pytest, `--quiet` eslint/ruff — whatever the detected runner supports). These land in
22
22
  `test_quiet_cmd`/`lint_quiet_cmd` + `commands.test_quiet`/`lint_quiet` and are what agents and the
23
- `/review` pre-flight actually run (SCHEMA.md §Output discipline). If the human declines or
23
+ `/cohorte-review` pre-flight actually run (SCHEMA.md §Output discipline). If the human declines or
24
24
  the runner has no such flag, leave `""` — consumers then fall back to `<cmd> 2>&1 | tail -40`.
25
25
  - **Contract** — mechanism (`shared-types-zod` / `openapi` / `protobuf` / `json-schema` / `none`) and
26
26
  where feature contracts are authored. If `none`, surfaces sync by the spec prose alone.
@@ -35,11 +35,11 @@ Ask ONLY what you couldn't confidently detect. Batch related questions. Cover:
35
35
  main checkout? If worktrees: DB-per-worktree? port bases? compose file?
36
36
  - **Gate** — confirm the destructive commands to hard-deny and the ones to confirm-first (seed from the
37
37
  detected DB/migration tooling + always git commit/push/merge/rebase/reset).
38
- - **Personas** — keep the default `/brainstorm` panel, or customize members for this domain?
38
+ - **Personas** — keep the default `/cohorte-brainstorm` panel, or customize members for this domain?
39
39
 
40
40
  Prefer sensible defaults from Phase 1 as the first (Recommended) option in each question.
41
41
 
42
- - **Kanban** (optional) — mirror this project's pipeline (`/brainstorm`…`/ship`) onto an Obsidian
42
+ - **Kanban** (optional) — mirror this project's pipeline (`/cohorte-brainstorm`…`/cohorte-ship`) onto an Obsidian
43
43
  Kanban board? If the human says yes: confirm the shared vault path (`obsidian.vault_path` in
44
44
  `~/.claude/cohorte.config.yaml`; ask if empty) and the board's location inside it (default
45
45
  `<ProjectName>/Tasks.md`). Phase 4 creates the board and records the link. Default: no.
@@ -50,7 +50,7 @@ Prefer sensible defaults from Phase 1 as the first (Recommended) option in each
50
50
  - **Telemetry** (optional, machine-scoped — SKIP entirely if `~/.claude/cohorte.config.yaml` already
51
51
  has a `telemetry:` block with a `consent_date`, i.e. the human already answered on this machine).
52
52
  Ask ONE opt-in question, stating exactly: _"Share anonymous usage stats with the cohorte project?
53
- One ping per pipeline phase, `/brainstorm` through `/ship`: core version, OS, phase name, duration,
53
+ One ping per pipeline phase, `/cohorte-brainstorm` through `/cohorte-ship`: core version, OS, phase name, duration,
54
54
  per-surface result counts, and a hash of the feature id — never repo names, paths, code, or IPs.
55
55
  Setup and maintenance commands never ping. Off by default; withdraw anytime
56
56
  (`telemetry.enabled: false`); erase your history anytime (SCHEMA.md §Telemetry). Default: No."_
@@ -1,4 +1,4 @@
1
- # /init-pipeline · 03 Draft the profile
1
+ # /cohorte-init-pipeline · 03 Draft the profile
2
2
 
3
3
  ### Phase 3 — Draft the profile (show, don't write yet)
4
4
 
@@ -1,4 +1,4 @@
1
- # /init-pipeline · 04 Write & render
1
+ # /cohorte-init-pipeline · 04 Write & render
2
2
 
3
3
  ### Phase 4 — Write & render (after go-ahead)
4
4
 
@@ -28,9 +28,9 @@
28
28
  `Bash(git status:*)`, `Bash(git diff:*)`, `Bash(git log:*)`, `Bash(git rev-parse:*)` — plus the
29
29
  shipped pipeline scripts for BOTH cores (`Bash(.claude/pipeline/scripts/:*)` and
30
30
  `Bash(~/.claude/pipeline/scripts/:*)` — preflight, kanban-move, telemetry-send) **plus the
31
- `bash`-prefixed form the `/loop` driver uses** (`Bash(bash .claude/pipeline/scripts/loop.sh:*)`
31
+ `bash`-prefixed form the `/cohorte-loop` driver uses** (`Bash(bash .claude/pipeline/scripts/loop.sh:*)`
32
32
  and `Bash(bash ~/.claude/pipeline/scripts/loop.sh:*)`) — those prefix rules match a command
33
- *starting* with the path, so `bash <path>` needs its own entry or `/loop` stalls on a permission
33
+ *starting* with the path, so `bash <path>` needs its own entry or `/cohorte-loop` stalls on a permission
34
34
  prompt at every launch, and the
35
35
  retrieval provider's MCP tools when wired (e.g. `mcp__serena`). Never allowlist anything matching
36
36
  a `gate.ask`/`gate.deny` pattern. Mention the human can widen it later with
@@ -49,7 +49,7 @@
49
49
  Preserve any existing custom keys.
50
50
  6. **Wire the retrieval provider** (skip if `retrieval.provider: none`):
51
51
  - **serena:** if the `serena` CLI is missing, have the human install it (`uv tool install -p 3.13
52
- serena-agent`) — or set the provider to `none` if they decline, and say `/update-pipeline` can wire
52
+ serena-agent`) — or set the provider to `none` if they decline, and say `/cohorte-update-pipeline` can wire
53
53
  it later. If the binary exists (e.g. `~/.local/bin/serena`) but `command -v serena` fails,
54
54
  recommend the PATH fix (`uv tool update-shell`, or add `~/.local/bin` to the shell profile) for
55
55
  CLI use. Then register at **project scope** (committed `.mcp.json`, portable —
@@ -82,19 +82,23 @@
82
82
  (or, without npm: curl -fsSL https://raw.githubusercontent.com/TheBidouilleAgency/cohorte/main/install.sh | sh -s -- --global;
83
83
  Windows: install.ps1 -Global from the same repo)> " }`.
84
84
  In **global** mode also add, near the top of `CLAUDE.md`, a one-liner:
85
- `> Pipeline: global core — run the installer above if /brainstorm etc. are missing.`
85
+ `> Pipeline: global core — run the installer above if /cohorte-brainstorm etc. are missing.`
86
86
  10. **CI workflow** (if `vcs.host: github` and no existing workflow already runs the profile's
87
87
  checks): with the human's go-ahead, generate `.github/workflows/pipeline-ci.yml` — on
88
88
  `pull_request` to `<default_branch>`: checkout, set up the `package_manager` toolchain,
89
89
  `commands.install`, then `commands.lint` · `commands.typecheck` · `commands.test` (+ per-surface
90
90
  `build_cmd`s that are non-empty). Derive the setup steps from the detected stack — mirror what a
91
- sibling workflow does if one exists. `/ship` watches these checks before the merge.
92
- 11. **Metrics sink & report buffer:** add `.claude/pipeline-metrics.jsonl` to `.gitignore` — `/build`,
93
- `/review` and `/fix` append per-dispatch evidence there (SCHEMA §Specialization reads it).
94
- Also add `specs/reports/` — `/review` stages its last report there so a `/fix` (or
95
- `/spec` Mode B) survives a `/clear`; it's a derived buffer, not a versioned artifact.
96
- 12. **Design system:** if `design.enabled` with a snapshot dir, note that `/align-ds` is active; else the
97
- `/align-ds` command will no-op with a clear message.
91
+ sibling workflow does if one exists. `/cohorte-ship` watches these checks before the merge.
92
+ 11. **Metrics sink, report buffer & preflight stamp:** add `.claude/pipeline-metrics.jsonl` to
93
+ `.gitignore` — `/cohorte-build`, `/cohorte-review` and `/cohorte-fix` append per-dispatch evidence
94
+ there (SCHEMA §Specialization reads it).
95
+ Also add `specs/reports/` `/cohorte-review` stages its last report there so a `/cohorte-fix` (or
96
+ `/cohorte-spec` Mode B) survives a `/clear`; it's a derived buffer, not a versioned artifact.
97
+ And `.claude/preflight.ok` a local, per-checkout freshness stamp. Versioning it breaks the phase
98
+ gate for good (a committed stamp describes the tree *before* its own commit, and it rides into every
99
+ new worktree as a green nobody earned). If it is already tracked: `git rm --cached .claude/preflight.ok`.
100
+ 12. **Design system:** if `design.enabled` with a snapshot dir, note that `/cohorte-align-ds` is active; else the
101
+ `/cohorte-align-ds` command will no-op with a clear message.
98
102
  13. **Kanban** (only if the human opted in at Phase 2): wire it per SCHEMA.md §Kanban, writing into the
99
103
  **global** `~/.claude/cohorte.config.yaml` (never into this repo — the board points at a
100
104
  personal vault). Create the file from `pipeline/cohorte.config.template.yaml` if absent; set
@@ -1,12 +1,12 @@
1
- # /init-pipeline · 05 Report
1
+ # /cohorte-init-pipeline · 05 Report
2
2
 
3
3
  ### Phase 5 — Report
4
4
 
5
5
  Print: the install mode (bundled core under `.claude/` vs global core in `~/.claude/` + the committed
6
6
  `.claude/pipeline.json` pointer), the files written/rendered, the surface→agent mapping, and the
7
7
  tailored workflow line, e.g.
8
- `/brainstorm → /spec → (design) → /build <id> → test → /review → /ship`. Tell the human to sanity-check
9
- `PIPELINE.md`, commit it, and run `/brainstorm` to start a feature. Note that this was the one-time
10
- setup: from now on `/update-pipeline` both refreshes the core AND reconciles the generated files
11
- (SCHEMA.md §Reconcile), and `/build` auto-grows surfaces — re-running `/init-pipeline` is only for deep
8
+ `/cohorte-brainstorm → /cohorte-spec → (design) → /cohorte-build <id> → test → /cohorte-review → /cohorte-ship`. Tell the human to sanity-check
9
+ `PIPELINE.md`, commit it, and run `/cohorte-brainstorm` to start a feature. Note that this was the one-time
10
+ setup: from now on `/cohorte-update-pipeline` both refreshes the core AND reconciles the generated files
11
+ (SCHEMA.md §Reconcile), and `/cohorte-build` auto-grows surfaces — re-running `/cohorte-init-pipeline` is only for deep
12
12
  stack changes (package manager, contract mechanism, surface overhaul).
@@ -1,5 +1,5 @@
1
- // cohorte — /audit as a deterministic workflow (opt-in; the conversational
2
- // /audit command remains the default path and the fallback).
1
+ // cohorte — /cohorte-audit as a deterministic workflow (opt-in; the conversational
2
+ // /cohorte-audit command remains the default path and the fallback).
3
3
  //
4
4
  // Invoke with args = {target: "<path or domain>"} (optional — default whole repo).
5
5
  //
@@ -135,7 +135,7 @@ const surfaces = Array.isArray(profile.surfaces) ? profile.surfaces : []
135
135
  // guard compares against `surfaces` — an empty list makes them all vacuously
136
136
  // pass. Fail loudly here instead of finishing with nothing done.
137
137
  if (!surfaces.length) {
138
- return { error: 'profile has no surfaces — nothing would be audited. the `yaml pipeline-profile` block in PIPELINE.md is empty or unparseable, or the profile-reader mis-returned; run /doctor' }
138
+ return { error: 'profile has no surfaces — nothing would be audited. the `yaml pipeline-profile` block in PIPELINE.md is empty or unparseable, or the profile-reader mis-returned; run /cohorte-doctor' }
139
139
  }
140
140
  const quiet = (q, full) => (q && !String(q).startsWith('<') ? q : full ? `${full} 2>&1 | tail -40` : '')
141
141
  const scope = target || 'the whole repo'
@@ -204,7 +204,7 @@ const written = await agent(
204
204
  `Write EXACTLY this content to specs/refactor-backlog.md (overwrite), then return the single word done:\n<<<BACKLOG\n${body.join('\n')}\nBACKLOG`,
205
205
  { model: 'haiku', label: 'write-backlog', effort: 'low' },
206
206
  )
207
- // Returning `backlog: <path>` when the writer died points /refactor at a file
207
+ // Returning `backlog: <path>` when the writer died points /cohorte-refactor at a file
208
208
  // that does not exist (or, worse, at the PREVIOUS run's stale backlog).
209
209
  const backlogOk = written != null && /done/i.test(String(written))
210
210
 
@@ -220,6 +220,6 @@ return {
220
220
  next: !backlogOk
221
221
  ? 'the backlog was NEVER written (writer died) — the counts above are real but nothing is on disk; re-run the audit'
222
222
  : deadDomains.length
223
- ? `re-audit ${deadDomains.join(', ')} (auditor died — not covered), then /refactor <domain>`
224
- : 'refactor a domain with /refactor <domain> (or the refactor workflow for big domains)',
223
+ ? `re-audit ${deadDomains.join(', ')} (auditor died — not covered), then /cohorte-refactor <domain>`
224
+ : 'refactor a domain with /cohorte-refactor <domain> (or the refactor workflow for big domains)',
225
225
  }
@@ -1,8 +1,8 @@
1
- // cohorte — /refactor as a deterministic workflow (opt-in; the conversational
2
- // /refactor command remains the default path and the fallback).
1
+ // cohorte — /cohorte-refactor as a deterministic workflow (opt-in; the conversational
2
+ // /cohorte-refactor command remains the default path and the fallback).
3
3
  //
4
4
  // BIG domains only: a domain with just a handful of open backlog items is
5
- // cheaper through the conversational /refactor — this script skips it and says
5
+ // cheaper through the conversational /cohorte-refactor — this script skips it and says
6
6
  // so. Invoke with args = {domains: ["backend", …]} or {domains: "all"}.
7
7
  //
8
8
  // Shape (SCHEMA.md §Workflows): profile via profile-reader (phase 0), the open
@@ -13,7 +13,7 @@
13
13
 
14
14
  export const meta = {
15
15
  name: 'cohorte-refactor',
16
- description: 'Apply the /audit refactor backlog for big domains: shared first, then parallel surface implementers, per-domain verify + one retry',
16
+ description: 'Apply the /cohorte-audit refactor backlog for big domains: shared first, then parallel surface implementers, per-domain verify + one retry',
17
17
  whenToUse: 'Only when the human explicitly asks for the refactor workflow on big domains. args = {domains: ["<surface key>", …] | "all"}.',
18
18
  phases: [
19
19
  { title: 'Profile', detail: 'PIPELINE.md → JSON via profile-reader', model: 'haiku' },
@@ -26,7 +26,7 @@ export const meta = {
26
26
  }
27
27
 
28
28
  // A domain below this many open items is not "big" — the conversational
29
- // /refactor handles it with less overhead than a workflow run.
29
+ // /cohorte-refactor handles it with less overhead than a workflow run.
30
30
  const MIN_ITEMS = 5
31
31
 
32
32
  // The Workflow runtime hands `args` to a script verbatim, so a caller that passes a
@@ -136,7 +136,7 @@ const surfaces = Array.isArray(profile.surfaces) ? profile.surfaces : []
136
136
  // guard compares against `surfaces` — an empty list makes them all vacuously
137
137
  // pass. Fail loudly here instead of finishing with nothing done.
138
138
  if (!surfaces.length) {
139
- return { error: 'profile has no surfaces — nothing would be refactored. the `yaml pipeline-profile` block in PIPELINE.md is empty or unparseable, or the profile-reader mis-returned; run /doctor' }
139
+ return { error: 'profile has no surfaces — nothing would be refactored. the `yaml pipeline-profile` block in PIPELINE.md is empty or unparseable, or the profile-reader mis-returned; run /cohorte-doctor' }
140
140
  }
141
141
  const byKey = Object.fromEntries(surfaces.map(s => [s.key, s]))
142
142
  const contractPath = (profile.contract && profile.contract.path) || ''
@@ -153,15 +153,15 @@ const backlog = await agent(
153
153
  { model: 'haiku', label: 'read-backlog', schema: OPEN, effort: 'low' },
154
154
  )
155
155
  // A dead reader is not "the backlog is empty" — reporting it as such sends the
156
- // human to re-run /audit on a backlog that is already there.
156
+ // human to re-run /cohorte-audit on a backlog that is already there.
157
157
  if (!backlog) return { error: 'the backlog-reading agent died — nothing was read; re-run the refactor workflow' }
158
158
  const open = (backlog.domains || []).filter(d => d.items.length)
159
- if (!open.length) return { error: 'no open backlog items for the requested domains — run /audit (or the audit workflow) first' }
159
+ if (!open.length) return { error: 'no open backlog items for the requested domains — run /cohorte-audit (or the audit workflow) first' }
160
160
 
161
161
  const big = open.filter(d => d.items.length >= MIN_ITEMS)
162
162
  const small = open.filter(d => d.items.length < MIN_ITEMS)
163
- for (const d of small) log(`Skipping ${d.key} (${d.items.length} open item(s) < ${MIN_ITEMS}) — use the conversational /refactor ${d.key}, it's cheaper`)
164
- if (!big.length) return { skipped: Object.fromEntries(small.map(d => [d.key, d.items.length])), reason: `every requested domain is below the ${MIN_ITEMS}-item workflow threshold — use /refactor` }
163
+ for (const d of small) log(`Skipping ${d.key} (${d.items.length} open item(s) < ${MIN_ITEMS}) — use the conversational /cohorte-refactor ${d.key}, it's cheaper`)
164
+ if (!big.length) return { skipped: Object.fromEntries(small.map(d => [d.key, d.items.length])), reason: `every requested domain is below the ${MIN_ITEMS}-item workflow threshold — use /cohorte-refactor` }
165
165
 
166
166
  const implementPrompt = d =>
167
167
  'Refactor pass on your surface (no feature spec). Read PIPELINE.md first. Add the missing tests ' +
@@ -243,7 +243,7 @@ if (clearedAll.length) {
243
243
  { model: 'haiku', label: 'tick-backlog', effort: 'low' },
244
244
  )
245
245
  // Reporting items as cleared while the backlog still shows them open means the
246
- // next /refactor re-dispatches work that is already done.
246
+ // next /cohorte-refactor re-dispatches work that is already done.
247
247
  tickedOk = ticked != null && /done/i.test(String(ticked))
248
248
  }
249
249
 
@@ -256,8 +256,8 @@ return {
256
256
  backlogTicked: tickedOk,
257
257
  next: !tickedOk
258
258
  ? `${clearedAll.length} item(s) were cleared in code but NOT ticked off specs/refactor-backlog.md ` +
259
- '(the ticking agent died) — tick them by hand, or the next /refactor re-dispatches finished work'
259
+ '(the ticking agent died) — tick them by hand, or the next /cohorte-refactor re-dispatches finished work'
260
260
  : results.some(r => r.remaining.length || !r.gatesGreen)
261
- ? 'items remain — finish them with the conversational /refactor <domain>'
262
- : 'all dispatched domains clean — optionally close with one final /audit',
261
+ ? 'items remain — finish them with the conversational /cohorte-refactor <domain>'
262
+ : 'all dispatched domains clean — optionally close with one final /cohorte-audit',
263
263
  }
@@ -1,8 +1,8 @@
1
- // cohorte — /review as a deterministic workflow (opt-in; the conversational
2
- // /review command remains the default path and the fallback).
1
+ // cohorte — /cohorte-review as a deterministic workflow (opt-in; the conversational
2
+ // /cohorte-review command remains the default path and the fallback).
3
3
  //
4
4
  // Invoke with args = {feature: "<feature_id>"} (or the bare feature id string).
5
- // Requires Claude Code >= 2.1.154 with workflows enabled — /doctor reports this.
5
+ // Requires Claude Code >= 2.1.154 with workflows enabled — /cohorte-doctor reports this.
6
6
  //
7
7
  // Shape (SCHEMA.md §Workflows): phase 0 reads the profile through the
8
8
  // profile-reader agent (scripts have no filesystem or shell), the deterministic
@@ -131,11 +131,29 @@ const FINDING = {
131
131
  },
132
132
  }
133
133
 
134
+ // A DEFERRED finding is real but out of this feature's scope (pre-existing code the
135
+ // diff never touched). It carries its own out-of-scope reason, counts in no severity
136
+ // row, is never cross-checked, and can never move the verdict — it is routed to
137
+ // specs/refactor-backlog.md so /cohorte-refactor owns it. See core/agents/review.md §Deferred.
138
+ const DEFERRED = {
139
+ type: 'object', required: ['severity', 'file', 'line', 'kind', 'problem', 'fix', 'outOfScope'],
140
+ additionalProperties: false,
141
+ properties: {
142
+ severity: { enum: ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] },
143
+ file: { type: 'string' }, line: { type: 'integer' },
144
+ kind: { enum: ['quality', 'security', 'rule'] },
145
+ problem: { type: 'string', description: 'one line, no code excerpts' },
146
+ fix: { type: 'string', description: 'one concrete change, one line' },
147
+ outOfScope: { type: 'string', description: 'one line: why this feature does not own it' },
148
+ },
149
+ }
150
+
134
151
  const REPORT = {
135
152
  type: 'object', required: ['verdict', 'findings'], additionalProperties: false,
136
153
  properties: {
137
154
  verdict: { enum: ['SHIP', 'REVISE', 'BLOCK'] },
138
155
  findings: { type: 'array', maxItems: 20, items: FINDING },
156
+ deferred: { type: 'array', maxItems: 10, items: DEFERRED },
139
157
  overflow: { type: 'integer', description: 'findings beyond the 20-item cap, if any' },
140
158
  notes: { type: 'string', description: 'RBAC / mobile-first assessment only, when the profile enables them' },
141
159
  },
@@ -165,7 +183,7 @@ const surfaces = Array.isArray(profile.surfaces) ? profile.surfaces : []
165
183
  // guard compares against `surfaces` — an empty list makes them all vacuously
166
184
  // pass. Fail loudly here instead of finishing with nothing done.
167
185
  if (!surfaces.length) {
168
- return { verdict: 'ABORTED', reason: 'profile has no surfaces — nothing would be reviewed. the `yaml pipeline-profile` block in PIPELINE.md is empty or unparseable, or the profile-reader mis-returned; run /doctor' }
186
+ return { verdict: 'ABORTED', reason: 'profile has no surfaces — nothing would be reviewed. the `yaml pipeline-profile` block in PIPELINE.md is empty or unparseable, or the profile-reader mis-returned; run /cohorte-doctor' }
169
187
  }
170
188
  const quiet = (q, full) => (q && !String(q).startsWith('<') ? q : full ? `${full} 2>&1 | tail -40` : '')
171
189
  const checks = [cmds.typecheck, quiet(cmds.lint_quiet, cmds.lint), quiet(cmds.test_quiet, cmds.test)]
@@ -185,7 +203,7 @@ const pre = await agent(
185
203
  if (!pre || !pre.pass) {
186
204
  return {
187
205
  verdict: 'ABORTED',
188
- reason: 'preflight red — fix the mechanical failures (or run /fix) before any review; no reviewer was spawned',
206
+ reason: 'preflight red — fix the mechanical failures (or run /cohorte-fix) before any review; no reviewer was spawned',
189
207
  failures: (pre && pre.tail) || 'preflight agent returned nothing',
190
208
  }
191
209
  }
@@ -208,7 +226,7 @@ if (!staged) {
208
226
  return {
209
227
  verdict: 'ABORTED',
210
228
  reason: 'the diff-staging agent died — no reviewer was spawned and nothing was reviewed',
211
- next: `re-run the review workflow, or /review ${feature} conversationally`,
229
+ next: `re-run the review workflow, or /cohorte-review ${feature} conversationally`,
212
230
  }
213
231
  }
214
232
  const touched = staged.surfaces || []
@@ -222,7 +240,8 @@ const reviewed = await pipeline(
222
240
  s => agent(
223
241
  'Review one feature surface against its frozen spec, per your agent instructions (read the staged ' +
224
242
  'diff FIRST; open a full source file only when a finding demands it; capped shape — max 20 findings, ' +
225
- 'one line each, no code excerpts). Variable slots: ' +
243
+ 'one line each, no code excerpts). Put anything real but OUT of this feature\'s scope in `deferred` ' +
244
+ '(max 10, each with its out-of-scope reason) per your §Deferred rules — never in `findings`. — Variable slots: ' +
226
245
  `feature ${feature} · scope: the ${s.key} surface only · spec: specs/${feature}.md · ` +
227
246
  `staged diff: ${s.diff} · changed files: ${s.files.join(', ')}`,
228
247
  { agentType: 'review', label: `review:${s.key}`, phase: 'Review', schema: REPORT },
@@ -231,7 +250,10 @@ const reviewed = await pipeline(
231
250
  if (!report) return null
232
251
  const hard = report.findings.filter(f => f.severity === 'CRITICAL' || f.kind === 'security')
233
252
  const rest = report.findings.filter(f => !hard.includes(f))
234
- if (!hard.length) return { key: s.key, report, kept: rest, refuted: [] }
253
+ // Deferred items skip the cross-check entirely: refuting one would spend an agent
254
+ // to argue about something that cannot affect the verdict either way.
255
+ const deferred = report.deferred || []
256
+ if (!hard.length) return { key: s.key, report, kept: rest, refuted: [], deferred }
235
257
  const votes = await parallel(hard.map(f => () => agent(
236
258
  'Adversarially verify ONE review finding — your job is to REFUTE it if you can. Read the staged ' +
237
259
  'diff and the exact file:line; refuted=true when the code, a guard, a test, or the spec shows the ' +
@@ -246,6 +268,7 @@ const reviewed = await pipeline(
246
268
  report,
247
269
  kept: rest.concat(checkedVotes.filter(v => !v.refuted).map(v => v.f)),
248
270
  refuted: checkedVotes.filter(v => v.refuted).map(v => ({ ...v.f, reason: v.reason })),
271
+ deferred,
249
272
  }
250
273
  },
251
274
  )
@@ -259,6 +282,9 @@ const unreviewed = touched.filter(s => !results.some(r => r.key === s.key)).map(
259
282
  if (unreviewed.length) log(`Reviewer died on: ${unreviewed.join(', ')} — those surfaces are NOT reviewed`)
260
283
  const kept = results.flatMap(r => r.kept.map(f => ({ ...f, surface: r.key })))
261
284
  const refuted = results.flatMap(r => r.refuted.map(f => ({ ...f, surface: r.key })))
285
+ // Deferred findings are deliberately kept OUT of `counts`, out of `verdict` and out
286
+ // of `clean`: they belong to /cohorte-refactor, not to this feature's fix loop.
287
+ const deferredAll = results.flatMap(r => (r.deferred || []).map(f => ({ ...f, surface: r.key })))
262
288
  const counts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }
263
289
  for (const f of kept) counts[f.severity] = (counts[f.severity] || 0) + 1
264
290
  // Verdict from the findings that SURVIVED the cross-check (a refuted CRITICAL
@@ -269,8 +295,8 @@ const verdict = kept.some(f => f.kind === 'security') ? 'BLOCK'
269
295
  : kept.some(f => f.severity === 'CRITICAL') ? 'REVISE'
270
296
  : unreviewed.length ? 'REVISE' : 'SHIP'
271
297
  // Only a SHIP whose leftovers are all LOW is a clean bill of health. The
272
- // conversational /review routes any surviving CRITICAL/HIGH/security to /fix, so
273
- // this path must not answer "/ship" on a SHIP that still carries HIGH findings.
298
+ // conversational /cohorte-review routes any surviving CRITICAL/HIGH/security to /cohorte-fix, so
299
+ // this path must not answer "/cohorte-ship" on a SHIP that still carries HIGH findings.
274
300
  const clean = verdict === 'SHIP' && kept.every(f => f.severity === 'LOW')
275
301
 
276
302
  // ── Phase 5 — stage the merged report; only the verdict leaves the workflow ──
@@ -283,8 +309,12 @@ const reportBody = [
283
309
  ...['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].map(s => `| ${s} | ${counts[s] || 0} |`), '',
284
310
  `Verdict: ${verdict}`, '', '## Findings', '',
285
311
  kept.length ? kept.map(findingLine).join('\n') : 'None.',
312
+ '', '## Deferred', '',
313
+ deferredAll.length
314
+ ? deferredAll.map(f => `${findingLine(f)} · out of scope: ${f.outOfScope} · deferred:${feature}`).join('\n')
315
+ : 'None.',
286
316
  ...(unreviewed.length ? ['', '## NOT reviewed (reviewer died — no verdict on these)', '',
287
- unreviewed.map(k => `- \`${k}\` — re-run /review ${feature} (or the review workflow)`).join('\n')] : []),
317
+ unreviewed.map(k => `- \`${k}\` — re-run /cohorte-review ${feature} (or the review workflow)`).join('\n')] : []),
288
318
  ...(refuted.length ? ['', '## Refuted by cross-check (no action needed)', '',
289
319
  refuted.map(f => `- ${f.file}:${f.line} · ${f.problem} — refuted: ${f.reason}`).join('\n')] : []),
290
320
  ].join('\n')
@@ -295,13 +325,24 @@ const staging = await agent(
295
325
  `{"ts":"<ISO now>","feature":"${feature}","phase":"review","seconds":0,"surfaces":{${results.map(r => `"${r.key}":"${verdict}:${r.kept.length}"`).join(',')}}}\n` +
296
326
  `3. Chain the opt-in usage ping: <core>/pipeline/scripts/telemetry-send.sh review "${feature}" 0 "${verdict}:${kept.length}" || true ` +
297
327
  '(<core> = .claude if .claude/pipeline/scripts/telemetry-send.sh exists, else ~/.claude; script on neither ⇒ skip the ping).\n' +
298
- // Stamp + tick only when nothing above LOW survived: the conversational /review
328
+ // Deferred findings must land in the backlog on EVERY verdict parked only on a
329
+ // SHIP is parked nowhere the rest of the time, which is the leak this closes.
330
+ (deferredAll.length
331
+ ? `3b. Route the deferred findings to specs/refactor-backlog.md (create it if absent): for each line below, ` +
332
+ `append it under the \`## <domain>\` heading named in its prefix (create that heading if absent) — with \`>>\`, ` +
333
+ `never by rewriting the file, and skip any whose file path + first words already appear there (grep -F first, ` +
334
+ `they may be left from a prior round or an /cohorte-audit):\n` +
335
+ deferredAll.map(f =>
336
+ `${f.surface}||- [ ] ${f.severity} · ${f.file}:${f.line} · ${f.kind} · ${f.fix} · deferred:${feature}`).join('\n') +
337
+ '\n'
338
+ : '') +
339
+ // Stamp + tick only when nothing above LOW survived: the conversational /cohorte-review
299
340
  // keeps the stamp only for LOW findings, and a SHIP verdict here can still carry
300
- // HIGH/MEDIUM ones — certifying those for /ship would ship known defects. A dead
341
+ // HIGH/MEDIUM ones — certifying those for /cohorte-ship would ship known defects. A dead
301
342
  // reviewer already forced the verdict off SHIP, so `clean` covers that too.
302
343
  (clean
303
- ? `4. Stamp the freshness gate in specs/${feature}.md's front-matter, exactly as the conversational /review §3 does ` +
304
- `(so /ship can prove the reviewed code is what ships): BASE=$(git merge-base ${base} HEAD); set reviewed_base: $BASE and ` +
344
+ ? `4. Stamp the freshness gate in specs/${feature}.md's front-matter, exactly as the conversational /cohorte-review §3 does ` +
345
+ `(so /cohorte-ship can prove the reviewed code is what ships): BASE=$(git merge-base ${base} HEAD); set reviewed_base: $BASE and ` +
305
346
  `reviewed_digest: $(git diff $BASE -- . ':(exclude)specs/' | sha256sum | cut -c1-16). ` +
306
347
  '5. Tick the spec DoD boxes this run verified (spec conformance + copy language — review SHIP; tests/lint/typecheck — green preflight); leave the rest unticked.\n'
307
348
  : '') +
@@ -311,13 +352,14 @@ const staging = await agent(
311
352
 
312
353
  // The staging agent writes the report, the metrics line, and (when clean) the
313
354
  // freshness stamp + DoD ticks. If it died, none of that is on disk — returning
314
- // `report: <path>` and "/ship" would point the human at a file that does not
355
+ // `report: <path>` and "/cohorte-ship" would point the human at a file that does not
315
356
  // exist and certify a stamp that was never written.
316
357
  const staged_ok = staging != null && /done/i.test(String(staging))
317
358
 
318
359
  return {
319
360
  verdict,
320
361
  counts,
362
+ deferred: deferredAll.length, // parked in the backlog for /cohorte-refactor — never blocking
321
363
  refutedByCrossCheck: refuted.length,
322
364
  reportStaged: staged_ok,
323
365
  unreviewedSurfaces: unreviewed, // reviewers that died — these carry NO verdict
@@ -325,12 +367,12 @@ return {
325
367
  .map(f => `[${f.surface}] ${f.file}:${f.line} — ${f.problem}`),
326
368
  report: staged_ok ? `specs/reports/${feature}.md` : '(NOT written — the staging agent died)',
327
369
  next: !staged_ok
328
- ? `the report/metrics/freshness stamp were NEVER written (staging agent died) — the verdict above is real, but nothing is on disk: re-run the review workflow, or /review ${feature}`
370
+ ? `the report/metrics/freshness stamp were NEVER written (staging agent died) — the verdict above is real, but nothing is on disk: re-run the review workflow, or /cohorte-review ${feature}`
329
371
  : unreviewed.length
330
372
  ? `re-run the review — no reviewer completed on: ${unreviewed.join(', ')}`
331
373
  : clean
332
- ? `/ship ${feature} (DoD ticked + freshness stamped)`
374
+ ? `/cohorte-ship ${feature} (DoD ticked + freshness stamped)`
333
375
  : verdict === 'SHIP'
334
- ? `/fix ${feature} — SHIP verdict, but ${kept.length} finding(s) above LOW survived; park them in specs/refactor-backlog.md instead if you deliberately defer them`
335
- : `/fix ${feature}`,
376
+ ? `/cohorte-fix ${feature} — SHIP verdict, but ${kept.length} finding(s) above LOW survived; park them in specs/refactor-backlog.md instead if you deliberately defer them`
377
+ : `/cohorte-fix ${feature}`,
336
378
  }
@@ -24,7 +24,7 @@ dashboard/
24
24
  | --- | --- |
25
25
  | `index.js` | HTTP server, routing, static serving (SPA fallback), streamed actions, `--host`/bind |
26
26
  | `versions.js` | installed core vs npm latest (registry fetch → `npm view` fallback, 5-min cache) |
27
- | `doctor.js` | the `/doctor` checks reimplemented in JS → `/api/state` (profile, agents, gate, hooks, …) |
27
+ | `doctor.js` | the `/cohorte-doctor` checks reimplemented in JS → `/api/state` (profile, agents, gate, hooks, …) |
28
28
  | `yaml.js` | minimal block-YAML subset parser (for the `pipeline-profile` block + the config) |
29
29
  | `fleet.js` | tracked-project registry (`~/.claude/cohorte-dashboard.json`) + folder browse |
30
30
  | `kanban.js` | linked Obsidian board → columns/cards; PR enrichment + ship-date sort via `gh` |
@@ -37,7 +37,7 @@ Read: `GET /api/versions`, `/api/state?project=`, `/api/fleet`, `/api/browse?dir
37
37
  `DELETE /api/projects` (remove);
38
38
  `POST /api/action` — `{action:'install'|'update', scope, project}` (spawns the CLI),
39
39
  `{action:'reset', project, purgeSpecs}` (backup+wipe+reinstall), or
40
- `{action:'claude', command:'/init-pipeline'|'/update-pipeline'|'/audit', project}` (headless
40
+ `{action:'claude', command:'/cohorte-init-pipeline'|'/cohorte-update-pipeline'|'/cohorte-audit', project}` (headless
41
41
  `claude -p`).
42
42
  Action responses stream chunked plain text ending in `__EXIT__ <code>`; the client reads the
43
43
  `ReadableStream` (`app/src/api.js` `streamAction`).
@@ -1 +1 @@
1
- :root{--bg: #0e0f13;--panel: #16181f;--panel-2: #1c1f28;--border: #262a35;--text: #e6e8ee;--muted: #8b90a0;--accent: #6ea8fe;--ok: #3fb950;--warn: #d9a441;--bad: #f85149;--dev: #a371f7;--mono: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-family:var(--mono);font-size:14px}.app{min-height:100vh}.topbar{display:flex;align-items:center;justify-content:space-between;padding:12px 20px;border-bottom:1px solid var(--border);position:sticky;top:0;z-index:20;background:#0e0f13e6;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px)}.brand{font-weight:600;letter-spacing:.2px;display:flex;align-items:center;gap:8px}.dot{width:9px;height:9px;border-radius:50%;background:var(--accent);box-shadow:0 0 10px var(--accent)}.muted{color:var(--muted)}.small{font-size:12px}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start;gap:16px;padding:20px;max-width:1100px;margin:0 auto}@media(max-width:720px){.grid{grid-template-columns:1fr}}.panel{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:16px 18px}.panel h2{margin:0 0 12px;font-size:13px;text-transform:uppercase;letter-spacing:1px;color:var(--muted)}.panel.placeholder{opacity:.7}.panel.error{grid-column:1 / -1;border-color:var(--bad);color:var(--bad)}.panel-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}.panel-head h2{margin:0}.badge{font-size:11px;padding:3px 9px;border-radius:999px;border:1px solid var(--border);white-space:nowrap}.badge.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 40%,transparent);background:color-mix(in srgb,var(--ok) 12%,transparent)}.badge.warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 40%,transparent);background:color-mix(in srgb,var(--warn) 12%,transparent)}.badge.bad{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 40%,transparent);background:color-mix(in srgb,var(--bad) 12%,transparent)}.badge.dev{color:var(--dev);border-color:color-mix(in srgb,var(--dev) 40%,transparent);background:color-mix(in srgb,var(--dev) 12%,transparent)}.badge.neutral{color:var(--muted)}.rows{display:flex;flex-direction:column;gap:2px}.row{display:flex;justify-content:space-between;gap:12px;padding:6px 0;border-bottom:1px dashed var(--border)}.row:last-child{border-bottom:none}.row-label{color:var(--muted)}.row-value{text-align:right}.row-value.strong{font-weight:600}.row-value.mono{font-family:var(--mono);font-size:12px}.actions{display:flex;align-items:center;gap:10px;margin-top:16px}.fresh-hint{margin:14px 0 0;padding-top:12px;border-top:1px dashed var(--border)}.fresh-hint strong{color:var(--accent)}button{font-family:var(--mono);font-size:13px;padding:7px 12px;border-radius:8px;border:1px solid var(--border);background:var(--panel-2);color:var(--text);cursor:pointer}button:disabled{opacity:.4;cursor:not-allowed}button.primary{background:color-mix(in srgb,var(--accent) 22%,var(--panel-2));border-color:color-mix(in srgb,var(--accent) 40%,transparent)}button.ghost{background:transparent}button:not(:disabled):hover{border-color:var(--accent)}.span2{grid-column:span 2}@media(max-width:720px){.span2{grid-column:1 / -1}}.summary{display:flex;gap:6px;flex-wrap:wrap}.checks{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}.check{display:flex;gap:10px;padding:9px 0;border-bottom:1px solid var(--border)}.check:last-child{border-bottom:none}.check.skip{opacity:.55}.check-icon{flex:none;width:20px;height:20px;border-radius:6px;display:grid;place-items:center;font-size:12px;font-weight:700;margin-top:1px}.check-icon.ok{color:var(--ok);background:color-mix(in srgb,var(--ok) 15%,transparent)}.check-icon.warn{color:var(--warn);background:color-mix(in srgb,var(--warn) 15%,transparent)}.check-icon.bad{color:var(--bad);background:color-mix(in srgb,var(--bad) 15%,transparent)}.check-icon.skip{color:var(--muted);background:var(--panel-2)}.check-body{flex:1;min-width:0}.check-line{display:flex;gap:10px;justify-content:space-between;flex-wrap:wrap}.check-label{font-weight:600}.check-detail{color:var(--muted);text-align:right}.check-fix{margin-top:4px;font-size:12px;color:var(--muted)}.check-fix code{color:var(--accent);background:var(--panel-2);padding:1px 6px;border-radius:5px}.surfaces{display:flex;flex-direction:column;gap:10px}.surface{border:1px solid var(--border);border-radius:9px;padding:10px 12px;background:var(--panel-2)}.surface-top{display:flex;align-items:center;gap:8px}.surface-key{font-weight:600}.surface-meta{display:flex;justify-content:space-between;gap:10px;margin-top:4px;font-size:12px}.surface-tools{display:flex;flex-wrap:wrap;gap:4px;margin-top:8px}.tool{font-size:11px;color:var(--muted);border:1px solid var(--border);border-radius:5px;padding:1px 6px}.chip{font-size:11px;padding:1px 8px;border-radius:999px;border:1px solid var(--border)}.chip.design{color:var(--dev);border-color:color-mix(in srgb,var(--dev) 40%,transparent)}.chip.model-sonnet{color:var(--accent)}.chip.model-haiku{color:var(--ok)}.chip.model-inherit{color:var(--warn)}.fleet{max-width:1100px;margin:0 auto;padding:20px}.core-banner{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;background:linear-gradient(180deg,var(--panel-2),var(--panel));border:1px solid var(--border);border-radius:12px;padding:14px 18px;margin-bottom:20px}.cb-left{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.cb-title{text-transform:uppercase;letter-spacing:1px;font-size:12px;color:var(--muted)}.cb-version{font-weight:600;font-size:15px}.cb-actions{display:flex;gap:8px}.fleet-head{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:14px;flex-wrap:wrap}.fleet-head h2{margin:0;font-size:14px;display:flex;align-items:center;gap:8px}.add-wrap{flex:1;max-width:560px}.add-form{display:flex;gap:8px}.path-input{flex:1;font-family:var(--mono);font-size:13px;padding:7px 12px;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:8px}.path-input:focus{outline:none;border-color:var(--accent)}.path-input.invalid{border-color:var(--bad)}.add-error{margin-top:6px;font-size:12px;color:var(--bad);word-break:break-word}.fleet-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:14px}.project-card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:14px 16px;cursor:pointer;transition:border-color .12s,transform .12s}.project-card:hover{border-color:var(--accent);transform:translateY(-1px)}.project-card.gone{opacity:.6;cursor:default}.pc-head{display:flex;align-items:center;justify-content:space-between}.pc-name{font-weight:600}.pc-path{margin:2px 0 10px}.pc-badges{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}.pc-health{display:flex;align-items:center;gap:5px}.pc-counts{margin-left:auto}.icon-btn{border:none;background:transparent;color:var(--muted);padding:2px 6px;border-radius:6px;font-size:13px}.icon-btn:hover{color:var(--bad);background:var(--panel-2)}.pill{font-size:11px;min-width:20px;text-align:center;padding:1px 7px;border-radius:999px;font-weight:600}.pill.ok{color:var(--ok);background:color-mix(in srgb,var(--ok) 14%,transparent)}.pill.warn{color:var(--warn);background:color-mix(in srgb,var(--warn) 14%,transparent)}.pill.bad{color:var(--bad);background:color-mix(in srgb,var(--bad) 14%,transparent)}.pill.neutral{color:var(--muted);background:var(--panel-2)}.detail-crumb{grid-column:1 / -1;margin-bottom:-4px;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.detail-tools{display:flex;gap:8px;flex-wrap:wrap}.tool-btn{font-size:12px;padding:5px 10px;border:1px solid color-mix(in srgb,var(--accent) 35%,var(--border));color:var(--accent);background:transparent}.tool-btn:hover{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 10%,transparent)}.confirm-text p{margin:0 0 8px;font-size:13px;line-height:1.6}.confirm-text code{background:var(--panel-2);padding:1px 5px;border-radius:4px;color:var(--accent)}.warn-line{color:var(--warn)!important;background:color-mix(in srgb,var(--warn) 10%,transparent);border-radius:8px;padding:8px 10px}.warn-line code{color:var(--text)!important}.danger-ghost{background:transparent;border:1px solid color-mix(in srgb,var(--bad) 40%,var(--border));color:var(--bad);font-size:12px;padding:5px 10px}.danger-ghost:hover{background:color-mix(in srgb,var(--bad) 12%,transparent);border-color:var(--bad)}button.danger{background:color-mix(in srgb,var(--bad) 20%,var(--panel-2));border-color:color-mix(in srgb,var(--bad) 45%,transparent);color:#ffd7d3}button.danger:hover{border-color:var(--bad)}.reset-list{margin:8px 0;padding-left:18px;font-size:13px;line-height:1.7}.reset-list code{color:var(--accent)}.reset-note{font-size:12.5px;color:var(--muted);background:var(--panel-2);border-radius:8px;padding:10px 12px}.reset-note code{color:var(--text)}.reset-check{display:flex;align-items:center;gap:8px;margin:12px 0 0;font-size:13px}.reset-check code{color:var(--accent)}.board-scroll{max-height:500px;overflow:auto}.board{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}.col{background:var(--bg);border:1px solid var(--border);border-radius:9px;padding:8px;min-width:140px}.col-head{display:flex;justify-content:space-between;font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);padding:2px 4px 8px;position:sticky;top:0;background:var(--bg);z-index:1}.col-shipped{opacity:.85}.col-other{border-color:color-mix(in srgb,var(--warn) 40%,var(--border))}.spec-card{background:var(--panel-2);border:1px solid var(--border);border-radius:7px;padding:8px 9px;margin-bottom:7px}.spec-card.bad{border-color:color-mix(in srgb,var(--warn) 45%,transparent)}.spec-title{font-size:13px;font-weight:600}.spec-meta,.spec-branch{margin-top:3px}@media(max-width:640px){.board{grid-template-columns:repeat(2,1fr)}}.kanban-board{display:flex;gap:10px}.kanban-col{flex:0 0 210px;background:var(--bg);border:1px solid var(--border);border-radius:9px;padding:8px}.kanban-col.empty{opacity:.5}.kanban-card{background:var(--panel-2);border:1px solid var(--border);border-radius:7px;padding:8px 9px;margin-bottom:7px}.kanban-card.done{opacity:.6}.kanban-card.done .kc-text{text-decoration:line-through}.kc-text{font-size:12.5px;line-height:1.4}.kc-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}.kc-tag{font-size:10px;color:var(--accent);background:color-mix(in srgb,var(--accent) 12%,transparent);border-radius:4px;padding:1px 5px}.kc-pr{font-size:10px;border-radius:4px;padding:1px 5px;text-decoration:none;border:1px solid var(--border);color:var(--muted)}a.kc-pr:hover{filter:brightness(1.25)}.kc-pr.state-open{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 40%,transparent);background:color-mix(in srgb,var(--ok) 12%,transparent)}.kc-pr.state-merged{color:var(--dev);border-color:color-mix(in srgb,var(--dev) 40%,transparent);background:color-mix(in srgb,var(--dev) 12%,transparent)}.kc-pr.state-closed{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 40%,transparent);background:color-mix(in srgb,var(--bad) 12%,transparent)}.kc-pr.draft{color:var(--muted);border-color:var(--border);background:var(--panel-2)}.kc-pr.flat{border-style:dashed;cursor:default}.kc-status{font-size:10px;margin-top:5px;color:var(--muted);text-transform:capitalize}.kc-status.state-open{color:var(--ok)}.kc-status.state-merged{color:var(--dev)}.kc-status.state-closed{color:var(--bad)}.metrics-list{display:flex;flex-direction:column;gap:12px}.metric-feature{background:var(--bg);border:1px solid var(--border);border-radius:9px;padding:10px 12px}.mf-head{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:8px}.mf-name{font-weight:600}.mf-badges{display:flex;gap:6px;flex-wrap:wrap}.phase-bars{display:flex;flex-direction:column;gap:4px}.phase-row{display:grid;grid-template-columns:56px 1fr 90px;align-items:center;gap:10px}.phase-label{color:var(--muted);font-size:12px}.phase-track{height:12px;border-radius:4px;background:var(--panel-2);overflow:hidden}.phase-fill{display:block;height:100%;min-width:2px;border-radius:4px;background:color-mix(in srgb,var(--accent) 65%,var(--panel-2))}.phase-value{text-align:right;white-space:nowrap}.surface-table{width:100%;border-collapse:collapse;margin-top:10px;font-size:12px}.surface-table th{text-align:left;color:var(--muted);font-weight:400;text-transform:uppercase;letter-spacing:.6px;font-size:10px;padding:4px 8px 6px 0;border-bottom:1px dashed var(--border)}.surface-table td{padding:5px 8px 5px 0;border-bottom:1px dashed var(--border)}.surface-table tr:last-child td{border-bottom:none}.st-key{font-weight:600}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#0009;display:grid;place-items:center;z-index:50;padding:20px}.modal{background:var(--panel);border:1px solid var(--border);border-radius:12px;width:min(720px,100%);max-height:80vh;display:flex;flex-direction:column;padding:18px}.modal-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}.modal-head h3{margin:0;font-size:15px}.modal-actions{display:flex;gap:8px;margin-top:14px}.cmd{background:#000;color:var(--accent);padding:10px 12px;border-radius:8px;font-size:13px;overflow-x:auto}.run-log{background:#000;color:#d6d9e0;padding:12px;border-radius:8px;font-size:12.5px;line-height:1.5;overflow:auto;white-space:pre-wrap;word-break:break-word;flex:1;min-height:200px;max-height:55vh;margin:0}.picker{max-height:74vh}.picker-path{background:var(--panel-2);border:1px solid var(--border);border-radius:7px;padding:7px 10px;margin-bottom:10px;word-break:break-all}.picker-list{flex:1;overflow:auto;border:1px solid var(--border);border-radius:8px;padding:6px;min-height:220px;max-height:48vh}.picker-row{display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:transparent;border:none;border-radius:6px;padding:7px 9px;color:var(--text);font-size:13px}.picker-row:hover{background:var(--panel-2);border-color:transparent}.picker-row.up{color:var(--muted)}.picker-icon{color:var(--muted);width:14px;flex:none}.picker-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.picker-row .badge.small{font-size:10px;padding:0 6px;margin-left:auto}.picker-empty{padding:12px}.add-form .ghost[type=button]{white-space:nowrap}.usage-list{display:flex;flex-direction:column;gap:2px}.usage-row{display:grid;grid-template-columns:minmax(96px,1.4fr) 44px 62px minmax(90px,1.6fr) 66px 52px 52px 52px;align-items:center;gap:8px;padding:3px 0;font-size:12px}.usage-head{color:var(--muted);font-size:11px;border-bottom:1px solid var(--border);padding-bottom:5px;margin-bottom:3px}.usage-cmd{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}.usage-num,.usage-bar-head{text-align:right;font-variant-numeric:tabular-nums}.usage-bar{position:relative;height:16px;border-radius:4px;background:var(--panel-2);overflow:hidden}.usage-fill{position:absolute;inset:0 auto 0 0;border-radius:4px;background:color-mix(in srgb,var(--accent) 55%,var(--panel-2))}.usage-bar-label{position:relative;display:block;padding-right:6px;text-align:right;line-height:16px;font-variant-numeric:tabular-nums}.usage-chat{color:var(--muted)}.usage-chat .usage-fill{background:var(--panel-2);border:1px solid var(--border)}@media(max-width:720px){.usage-row{grid-template-columns:1fr 40px 58px minmax(70px,1fr)}.usage-row>:nth-child(n+5){display:none}}
1
+ :root{--bg: #0e0f13;--panel: #16181f;--panel-2: #1c1f28;--border: #262a35;--text: #e6e8ee;--muted: #8b90a0;--accent: #6ea8fe;--ok: #3fb950;--warn: #d9a441;--bad: #f85149;--dev: #a371f7;--mono: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-family:var(--mono);font-size:14px}.app{min-height:100vh}.topbar{display:flex;align-items:center;justify-content:space-between;padding:12px 20px;border-bottom:1px solid var(--border);position:sticky;top:0;z-index:20;background:#0e0f13e6;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px)}.brand{font-weight:600;letter-spacing:.2px;display:flex;align-items:center;gap:8px}.dot{width:9px;height:9px;border-radius:50%;background:var(--accent);box-shadow:0 0 10px var(--accent)}.muted{color:var(--muted)}.small{font-size:12px}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start;gap:16px;padding:20px;max-width:1100px;margin:0 auto}@media(max-width:720px){.grid{grid-template-columns:1fr}}.panel{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:16px 18px}.panel h2{margin:0 0 12px;font-size:13px;text-transform:uppercase;letter-spacing:1px;color:var(--muted)}.panel.placeholder{opacity:.7}.panel.error{grid-column:1 / -1;border-color:var(--bad);color:var(--bad)}.panel-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}.panel-head h2{margin:0}.badge{font-size:11px;padding:3px 9px;border-radius:999px;border:1px solid var(--border);white-space:nowrap}.badge.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 40%,transparent);background:color-mix(in srgb,var(--ok) 12%,transparent)}.badge.warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 40%,transparent);background:color-mix(in srgb,var(--warn) 12%,transparent)}.badge.bad{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 40%,transparent);background:color-mix(in srgb,var(--bad) 12%,transparent)}.badge.dev{color:var(--dev);border-color:color-mix(in srgb,var(--dev) 40%,transparent);background:color-mix(in srgb,var(--dev) 12%,transparent)}.badge.neutral{color:var(--muted)}.rows{display:flex;flex-direction:column;gap:2px}.row{display:flex;justify-content:space-between;gap:12px;padding:6px 0;border-bottom:1px dashed var(--border)}.row:last-child{border-bottom:none}.row-label{color:var(--muted)}.row-value{text-align:right}.row-value.strong{font-weight:600}.row-value.mono{font-family:var(--mono);font-size:12px}.actions{display:flex;align-items:center;gap:10px;margin-top:16px}.fresh-hint{margin:14px 0 0;padding-top:12px;border-top:1px dashed var(--border)}.fresh-hint strong{color:var(--accent)}button{font-family:var(--mono);font-size:13px;padding:7px 12px;border-radius:8px;border:1px solid var(--border);background:var(--panel-2);color:var(--text);cursor:pointer}button:disabled{opacity:.4;cursor:not-allowed}button.primary{background:color-mix(in srgb,var(--accent) 22%,var(--panel-2));border-color:color-mix(in srgb,var(--accent) 40%,transparent)}button.ghost{background:transparent}button:not(:disabled):hover{border-color:var(--accent)}.span2{grid-column:span 2}@media(max-width:720px){.span2{grid-column:1 / -1}}.summary{display:flex;gap:6px;flex-wrap:wrap}.checks{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}.check{display:flex;gap:10px;padding:9px 0;border-bottom:1px solid var(--border)}.check:last-child{border-bottom:none}.check.skip{opacity:.55}.check-icon{flex:none;width:20px;height:20px;border-radius:6px;display:grid;place-items:center;font-size:12px;font-weight:700;margin-top:1px}.check-icon.ok{color:var(--ok);background:color-mix(in srgb,var(--ok) 15%,transparent)}.check-icon.warn{color:var(--warn);background:color-mix(in srgb,var(--warn) 15%,transparent)}.check-icon.bad{color:var(--bad);background:color-mix(in srgb,var(--bad) 15%,transparent)}.check-icon.skip{color:var(--muted);background:var(--panel-2)}.check-body{flex:1;min-width:0}.check-line{display:flex;gap:10px;justify-content:space-between;flex-wrap:wrap}.check-label{font-weight:600}.check-detail{color:var(--muted);text-align:right}.check-fix{margin-top:4px;font-size:12px;color:var(--muted)}.check-fix code{color:var(--accent);background:var(--panel-2);padding:1px 6px;border-radius:5px}.surfaces{display:flex;flex-direction:column;gap:10px}.surface{border:1px solid var(--border);border-radius:9px;padding:10px 12px;background:var(--panel-2)}.surface-top{display:flex;align-items:center;gap:8px}.surface-key{font-weight:600}.surface-meta{display:flex;justify-content:space-between;gap:10px;margin-top:4px;font-size:12px}.surface-tools{display:flex;flex-wrap:wrap;gap:4px;margin-top:8px}.tool{font-size:11px;color:var(--muted);border:1px solid var(--border);border-radius:5px;padding:1px 6px}.chip{font-size:11px;padding:1px 8px;border-radius:999px;border:1px solid var(--border)}.chip.design{color:var(--dev);border-color:color-mix(in srgb,var(--dev) 40%,transparent)}.chip.model-sonnet{color:var(--accent)}.chip.model-haiku{color:var(--ok)}.chip.model-inherit{color:var(--warn)}.fleet{max-width:1100px;margin:0 auto;padding:20px}.core-banner{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;background:linear-gradient(180deg,var(--panel-2),var(--panel));border:1px solid var(--border);border-radius:12px;padding:14px 18px;margin-bottom:20px}.cb-left{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.cb-title{text-transform:uppercase;letter-spacing:1px;font-size:12px;color:var(--muted)}.cb-version{font-weight:600;font-size:15px}.cb-actions{display:flex;gap:8px}.fleet-head{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:14px;flex-wrap:wrap}.fleet-head h2{margin:0;font-size:14px;display:flex;align-items:center;gap:8px}.add-wrap{flex:1;max-width:560px}.add-form{display:flex;gap:8px}.path-input{flex:1;font-family:var(--mono);font-size:13px;padding:7px 12px;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:8px}.path-input:focus{outline:none;border-color:var(--accent)}.path-input.invalid{border-color:var(--bad)}.add-error{margin-top:6px;font-size:12px;color:var(--bad);word-break:break-word}.fleet-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:14px}.project-card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:14px 16px;cursor:pointer;transition:border-color .12s,transform .12s}.project-card:hover{border-color:var(--accent);transform:translateY(-1px)}.project-card.gone{opacity:.6;cursor:default}.pc-head{display:flex;align-items:center;justify-content:space-between}.pc-name{font-weight:600}.pc-path{margin:2px 0 10px}.pc-badges{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}.pc-health{display:flex;align-items:center;gap:5px}.pc-counts{margin-left:auto}.icon-btn{border:none;background:transparent;color:var(--muted);padding:2px 6px;border-radius:6px;font-size:13px}.icon-btn:hover{color:var(--bad);background:var(--panel-2)}.pill{font-size:11px;min-width:20px;text-align:center;padding:1px 7px;border-radius:999px;font-weight:600}.pill.ok{color:var(--ok);background:color-mix(in srgb,var(--ok) 14%,transparent)}.pill.warn{color:var(--warn);background:color-mix(in srgb,var(--warn) 14%,transparent)}.pill.bad{color:var(--bad);background:color-mix(in srgb,var(--bad) 14%,transparent)}.pill.neutral{color:var(--muted);background:var(--panel-2)}.detail-crumb{grid-column:1 / -1;margin-bottom:-4px;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.detail-tools{display:flex;gap:8px;flex-wrap:wrap}.tool-btn{font-size:12px;padding:5px 10px;border:1px solid color-mix(in srgb,var(--accent) 35%,var(--border));color:var(--accent);background:transparent}.tool-btn:hover{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 10%,transparent)}.confirm-text p{margin:0 0 8px;font-size:13px;line-height:1.6}.confirm-text code{background:var(--panel-2);padding:1px 5px;border-radius:4px;color:var(--accent)}.warn-line{color:var(--warn)!important;background:color-mix(in srgb,var(--warn) 10%,transparent);border-radius:8px;padding:8px 10px}.warn-line code{color:var(--text)!important}.danger-ghost{background:transparent;border:1px solid color-mix(in srgb,var(--bad) 40%,var(--border));color:var(--bad);font-size:12px;padding:5px 10px}.danger-ghost:hover{background:color-mix(in srgb,var(--bad) 12%,transparent);border-color:var(--bad)}button.danger{background:color-mix(in srgb,var(--bad) 20%,var(--panel-2));border-color:color-mix(in srgb,var(--bad) 45%,transparent);color:#ffd7d3}button.danger:hover{border-color:var(--bad)}.reset-list{margin:8px 0;padding-left:18px;font-size:13px;line-height:1.7}.reset-list code{color:var(--accent)}.reset-note{font-size:12.5px;color:var(--muted);background:var(--panel-2);border-radius:8px;padding:10px 12px}.reset-note code{color:var(--text)}.reset-check{display:flex;align-items:center;gap:8px;margin:12px 0 0;font-size:13px}.reset-check code{color:var(--accent)}.board-scroll{max-height:500px;overflow:auto}.board{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(140px,1fr);gap:10px}.col{background:var(--bg);border:1px solid var(--border);border-radius:9px;padding:8px;min-width:140px}.col-head{display:flex;justify-content:space-between;font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);padding:2px 4px 8px;position:sticky;top:0;background:var(--bg);z-index:1}.col-shipped{opacity:.85}.col-blocked{border-color:color-mix(in srgb,var(--bad) 40%,var(--border))}.col-other{border-color:color-mix(in srgb,var(--warn) 40%,var(--border))}.spec-card{background:var(--panel-2);border:1px solid var(--border);border-radius:7px;padding:8px 9px;margin-bottom:7px}.spec-card.bad{border-color:color-mix(in srgb,var(--warn) 45%,transparent)}.spec-title{font-size:13px;font-weight:600}.spec-meta,.spec-branch,.spec-loop{margin-top:3px}@media(max-width:640px){.board{grid-auto-flow:row;grid-template-columns:repeat(2,1fr)}}.kanban-board{display:flex;gap:10px}.kanban-col{flex:0 0 210px;background:var(--bg);border:1px solid var(--border);border-radius:9px;padding:8px}.kanban-col.empty{opacity:.5}.kanban-card{background:var(--panel-2);border:1px solid var(--border);border-radius:7px;padding:8px 9px;margin-bottom:7px}.kanban-card.done{opacity:.6}.kanban-card.done .kc-text{text-decoration:line-through}.kc-text{font-size:12.5px;line-height:1.4}.kc-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}.kc-tag{font-size:10px;color:var(--accent);background:color-mix(in srgb,var(--accent) 12%,transparent);border-radius:4px;padding:1px 5px}.kc-pr{font-size:10px;border-radius:4px;padding:1px 5px;text-decoration:none;border:1px solid var(--border);color:var(--muted)}a.kc-pr:hover{filter:brightness(1.25)}.kc-pr.state-open{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 40%,transparent);background:color-mix(in srgb,var(--ok) 12%,transparent)}.kc-pr.state-merged{color:var(--dev);border-color:color-mix(in srgb,var(--dev) 40%,transparent);background:color-mix(in srgb,var(--dev) 12%,transparent)}.kc-pr.state-closed{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 40%,transparent);background:color-mix(in srgb,var(--bad) 12%,transparent)}.kc-pr.draft{color:var(--muted);border-color:var(--border);background:var(--panel-2)}.kc-pr.flat{border-style:dashed;cursor:default}.kc-status{font-size:10px;margin-top:5px;color:var(--muted);text-transform:capitalize}.kc-status.state-open{color:var(--ok)}.kc-status.state-merged{color:var(--dev)}.kc-status.state-closed{color:var(--bad)}.metrics-list{display:flex;flex-direction:column;gap:12px}.metric-feature{background:var(--bg);border:1px solid var(--border);border-radius:9px;padding:10px 12px}.mf-head{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:8px}.mf-name{font-weight:600}.mf-badges{display:flex;gap:6px;flex-wrap:wrap}.phase-bars{display:flex;flex-direction:column;gap:4px}.phase-row{display:grid;grid-template-columns:56px 1fr 90px;align-items:center;gap:10px}.phase-label{color:var(--muted);font-size:12px}.phase-track{height:12px;border-radius:4px;background:var(--panel-2);overflow:hidden}.phase-fill{display:block;height:100%;min-width:2px;border-radius:4px;background:color-mix(in srgb,var(--accent) 65%,var(--panel-2))}.phase-value{text-align:right;white-space:nowrap}.surface-table{width:100%;border-collapse:collapse;margin-top:10px;font-size:12px}.surface-table th{text-align:left;color:var(--muted);font-weight:400;text-transform:uppercase;letter-spacing:.6px;font-size:10px;padding:4px 8px 6px 0;border-bottom:1px dashed var(--border)}.surface-table td{padding:5px 8px 5px 0;border-bottom:1px dashed var(--border)}.surface-table tr:last-child td{border-bottom:none}.st-key{font-weight:600}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#0009;display:grid;place-items:center;z-index:50;padding:20px}.modal{background:var(--panel);border:1px solid var(--border);border-radius:12px;width:min(720px,100%);max-height:80vh;display:flex;flex-direction:column;padding:18px}.modal-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}.modal-head h3{margin:0;font-size:15px}.modal-actions{display:flex;gap:8px;margin-top:14px}.cmd{background:#000;color:var(--accent);padding:10px 12px;border-radius:8px;font-size:13px;overflow-x:auto}.run-log{background:#000;color:#d6d9e0;padding:12px;border-radius:8px;font-size:12.5px;line-height:1.5;overflow:auto;white-space:pre-wrap;word-break:break-word;flex:1;min-height:200px;max-height:55vh;margin:0}.picker{max-height:74vh}.picker-path{background:var(--panel-2);border:1px solid var(--border);border-radius:7px;padding:7px 10px;margin-bottom:10px;word-break:break-all}.picker-list{flex:1;overflow:auto;border:1px solid var(--border);border-radius:8px;padding:6px;min-height:220px;max-height:48vh}.picker-row{display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:transparent;border:none;border-radius:6px;padding:7px 9px;color:var(--text);font-size:13px}.picker-row:hover{background:var(--panel-2);border-color:transparent}.picker-row.up{color:var(--muted)}.picker-icon{color:var(--muted);width:14px;flex:none}.picker-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.picker-row .badge.small{font-size:10px;padding:0 6px;margin-left:auto}.picker-empty{padding:12px}.add-form .ghost[type=button]{white-space:nowrap}.usage-list{display:flex;flex-direction:column;gap:2px}.usage-row{display:grid;grid-template-columns:minmax(96px,1.4fr) 44px 62px minmax(90px,1.6fr) 66px 52px 52px 52px;align-items:center;gap:8px;padding:3px 0;font-size:12px}.usage-head{color:var(--muted);font-size:11px;border-bottom:1px solid var(--border);padding-bottom:5px;margin-bottom:3px}.usage-cmd{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}.usage-num,.usage-bar-head{text-align:right;font-variant-numeric:tabular-nums}.usage-bar{position:relative;height:16px;border-radius:4px;background:var(--panel-2);overflow:hidden}.usage-fill{position:absolute;inset:0 auto 0 0;border-radius:4px;background:color-mix(in srgb,var(--accent) 55%,var(--panel-2))}.usage-bar-label{position:relative;display:block;padding-right:6px;text-align:right;line-height:16px;font-variant-numeric:tabular-nums}.usage-chat{color:var(--muted)}.usage-chat .usage-fill{background:var(--panel-2);border:1px solid var(--border)}@media(max-width:720px){.usage-row{grid-template-columns:1fr 40px 58px minmax(70px,1fr)}.usage-row>:nth-child(n+5){display:none}}