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.
- package/CHANGELOG.md +169 -3
- package/README.md +65 -57
- package/bin/cli.js +31 -15
- package/core/agents/implementer.template.md +3 -3
- package/core/agents/release.md +1 -1
- package/core/agents/review.md +25 -2
- package/core/commands/{audit.md → cohorte-audit.md} +11 -3
- package/core/commands/{brainstorm.md → cohorte-brainstorm.md} +9 -3
- package/core/commands/{build.md → cohorte-build.md} +95 -10
- package/core/commands/{doctor.md → cohorte-doctor.md} +22 -9
- package/core/commands/{fix.md → cohorte-fix.md} +20 -13
- package/core/commands/{init-pipeline.md → cohorte-init-pipeline.md} +1 -1
- package/core/commands/cohorte-loop.md +110 -0
- package/core/commands/{refactor.md → cohorte-refactor.md} +3 -3
- package/core/commands/{review.md → cohorte-review.md} +70 -20
- package/core/commands/{ship.md → cohorte-ship.md} +5 -5
- package/core/commands/{spec.md → cohorte-spec.md} +32 -12
- package/core/commands/{update-pipeline.md → cohorte-update-pipeline.md} +16 -6
- package/core/hooks/gate.py +101 -6
- package/core/templates/brainstorm-return.md +4 -4
- package/core/templates/decisions.template.md +42 -0
- package/core/templates/design-brief.md +1 -1
- package/core/templates/spec.template.md +8 -6
- package/core/templates/steps/init-pipeline/01-detect-stack.md +1 -1
- package/core/templates/steps/init-pipeline/02-interview-gaps.md +6 -6
- package/core/templates/steps/init-pipeline/03-draft-profile.md +1 -1
- package/core/templates/steps/init-pipeline/04-write-render.md +16 -12
- package/core/templates/steps/init-pipeline/05-report.md +5 -5
- package/core/workflows/audit.js +6 -6
- package/core/workflows/refactor.js +14 -14
- package/core/workflows/review.js +62 -20
- package/dashboard/README.md +2 -2
- package/dashboard/dist/assets/{index-dkO8UUVl.css → index-BZ_LQlEj.css} +1 -1
- package/dashboard/dist/assets/{index-8owBnqyv.js → index-P1I1JGtj.js} +11 -11
- package/dashboard/dist/index.html +2 -2
- package/dashboard/server/doctor.js +75 -18
- package/dashboard/server/index.js +5 -5
- package/dashboard/server/metrics.js +1 -1
- package/install.ps1 +31 -14
- package/install.sh +31 -14
- package/package.json +2 -2
- package/profile/PIPELINE.template.md +17 -16
- package/profile/SCHEMA.md +199 -48
- package/profile/cohorte.config.template.yaml +8 -8
- package/scripts/loop-detach.sh +153 -0
- package/scripts/loop.sh +202 -25
- package/scripts/metrics/collect.mjs +17 -8
- package/scripts/new-feature.sh.template +3 -3
- package/scripts/preflight.sh +40 -4
- package/scripts/remove-feature.sh.template +2 -2
- package/scripts/test-dashboard.mjs +34 -7
- package/scripts/test-gate.mjs +58 -0
- package/scripts/test-loop.mjs +269 -0
- package/scripts/test-metrics.mjs +23 -11
- package/scripts/test-workflows.mjs +33 -5
- package/scripts/validate-core.mjs +46 -9
- package/core/commands/loop.md +0 -61
- /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 · 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 &
|
|
93
|
-
`/review` and `/fix` append per-dispatch evidence
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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).
|
package/core/workflows/audit.js
CHANGED
|
@@ -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
|
}
|
package/core/workflows/review.js
CHANGED
|
@@ -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).
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
}
|
package/dashboard/README.md
CHANGED
|
@@ -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}}
|