cohorte 1.3.3 → 1.4.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 (43) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +4 -7
  3. package/bin/cli.js +49 -4
  4. package/core/agents/implementer.template.md +10 -5
  5. package/core/agents/profile-reader.md +22 -0
  6. package/core/commands/doctor.md +8 -4
  7. package/core/hooks/gate.py +21 -6
  8. package/core/templates/agent-handoff.md +7 -2
  9. package/core/templates/review-feedback.md +7 -4
  10. package/core/templates/spec.template.md +5 -2
  11. package/core/templates/steps/init-pipeline/04-write-render.md +2 -1
  12. package/core/workflows/audit.js +88 -7
  13. package/core/workflows/refactor.js +85 -9
  14. package/core/workflows/review.js +132 -11
  15. package/dashboard/README.md +22 -5
  16. package/dashboard/dist/assets/index-AFQnlfjO.css +1 -0
  17. package/dashboard/dist/assets/{index-BxgA_mz1.js → index-DLBzciIC.js} +12 -11
  18. package/dashboard/dist/index.html +2 -2
  19. package/dashboard/server/doctor.js +68 -20
  20. package/dashboard/server/fleet.js +19 -5
  21. package/dashboard/server/index.js +79 -7
  22. package/dashboard/server/metrics.js +16 -4
  23. package/dashboard/server/versions.js +28 -6
  24. package/dashboard/server/yaml.js +4 -1
  25. package/install.ps1 +4 -0
  26. package/install.sh +19 -1
  27. package/package.json +5 -2
  28. package/profile/SCHEMA.md +23 -26
  29. package/scripts/kanban-move.sh +34 -20
  30. package/scripts/metrics/collect.mjs +495 -0
  31. package/scripts/metrics/prices.json +39 -0
  32. package/scripts/new-feature.sh.template +3 -1
  33. package/scripts/preflight.sh +16 -3
  34. package/scripts/remove-feature.sh.template +2 -1
  35. package/scripts/telemetry-send.sh +15 -1
  36. package/scripts/test-dashboard.mjs +362 -0
  37. package/scripts/test-gate.mjs +273 -0
  38. package/scripts/test-metrics.mjs +135 -0
  39. package/scripts/test-workflows.mjs +321 -0
  40. package/scripts/validate-core.mjs +51 -1
  41. package/core/commands/cycle.md +0 -54
  42. package/core/workflows/cycle.js +0 -407
  43. package/dashboard/dist/assets/index-Cj0SpgEY.css +0 -1
package/profile/SCHEMA.md CHANGED
@@ -162,7 +162,11 @@ Coarse first, specialize on evidence: start with one `frontend` / `backend` surf
162
162
  surface that's proven slow and cleanly separable. The evidence lives in
163
163
  the **main checkout's** `.claude/pipeline-metrics.jsonl` (gitignored) — one JSONL line per phase batch
164
164
  (`ts`/`feature`/`phase`/`seconds`/`surfaces:{key: result}`), appended by `/build`, `/review`, `/fix`
165
- and `/smoke`. Always the main checkout, never the feature worktree (which dies at teardown while
165
+ and `/smoke`.
166
+ **`surfaces` keys are surface keys, nothing else** — run-level facts go in their own top-level
167
+ fields. Anything put inside `surfaces` is read
168
+ as a surface: the dashboard renders it as a row in the per-surface table and scores a non-`ok`
169
+ value as that surface failing. Always the main checkout, never the feature worktree (which dies at teardown while
166
170
  metrics must accumulate across features) — resolve from anywhere with
167
171
  `$(dirname "$(git rev-parse --git-common-dir)")/.claude/pipeline-metrics.jsonl`. Read it before
168
172
  proposing a split: split the surface that actually dominates wall-clock, not the one that feels big.
@@ -268,7 +272,9 @@ this exact procedure so a surface is always defined the same way. To add surface
268
272
  this feature also changes. The link is self-contained — no stored project id. Build with the code UI
269
273
  kit (the `design_system_project`'s materialization: `@/components/ui/*` + tokens); read a primitive
270
274
  via `get_file` only if it's missing/stale in code. Mobile-first."_
271
- - `<SURFACE_TDD_STEP1>` — _"**Pull the feature design first** (skip if your dispatch's design slot
275
+ - `<SURFACE_TDD_STEP1>` — a **lead-in paragraph** above the TDD list (not a numbered item; it
276
+ renders as nothing for a non-design surface, which is why the list must not start at it):
277
+ _"**Pull the feature design first** (skip if your dispatch's design slot
272
278
  says `none`): `DesignSync get_file(<projectId>, <file>)` for each link in the slot and translate
273
279
  each into the code design system (`@/components/ui/*`, `cn()` + CVA), mobile-first — never ad-hoc
274
280
  CSS. Then:"_
@@ -318,14 +324,13 @@ Workflow runtime executes instead of the lead reasoning out the fan-out turn by
318
324
  `<core>/workflows/review.js`, `audit.js`, `refactor.js` (installed to `.claude/workflows/` bundled or
319
325
  `~/.claude/workflows/` global). The conversational commands (`/review`, `/audit`, `/refactor`)
320
326
  **remain the default path and the fallback** — a workflow runs only when the human explicitly asks
321
- for it ("run the review workflow", or via the `/cycle <id>` launcher command, which resolves
322
- `cycle.js` and invokes the runtime for them), and requires Claude Code ≥ **2.1.154** with workflows
327
+ for it ("run the review workflow"), and requires Claude Code **2.1.154** with workflows
323
328
  enabled.
324
329
  `/doctor` reports which path a session will take. The interactive commands (`/init-pipeline`,
325
330
  `/brainstorm`, `/spec`) and the dispatch-only ones (`/build`, `/ship`) have **no** workflow variant on
326
331
  purpose: they're interviews or already a single parallel dispatch — a script adds nothing.
327
332
 
328
- Shared design, all three scripts:
333
+ Shared design, all four scripts:
329
334
 
330
335
  - **Phase 0 is always `profile-reader`** — workflow scripts have no filesystem or shell access, so a
331
336
  dedicated agent (`core/agents/profile-reader.md`, haiku, read-only) reads `PIPELINE.md` and returns
@@ -336,6 +341,12 @@ Shared design, all three scripts:
336
341
  - **Only the verdict comes back.** Bulk (diffs, reports, backlogs) is staged to the same disk
337
342
  buffers the commands use (`specs/reports/`, `specs/refactor-backlog.md`); the workflow's return is
338
343
  counts + verdict + paths.
344
+ - **A dead agent is never a clean result.** `agent()` resolves to `null` when a subagent dies, and a
345
+ dead *reviewer* returns zero findings — byte-identical to a surface that is genuinely clean. Any
346
+ script that derives a verdict from "how many findings came back" must first subtract the agents
347
+ that never answered: `review.js` names them in `unreviewedSurfaces` and refuses to score
348
+ `SHIP`. `scripts/test-workflows.mjs`
349
+ pins this — it is the one invariant the structural checks in `validate-core.mjs` cannot see.
339
350
  - **`review.js`** — preflight gate (aborts red, zero agents), one `git diff --stat` staged per
340
351
  touched surface, one reviewer per surface in parallel, then an **adversarial cross-check** phase
341
352
  that tries to refute each CRITICAL/security finding before it can trigger a fix loop.
@@ -344,27 +355,9 @@ Shared design, all three scripts:
344
355
  - **`refactor.js`** — big domains only (it skips domains with a handful of open items — the
345
356
  conversational `/refactor` is cheaper there): `shared` first and alone, then the other domains'
346
357
  implementers in parallel, each verified per-domain.
347
- - **`cycle.js`** — the **full dev cycle** on a frozen spec: contract → parallel build → rounds of
348
- [preflight → smoke ∥ review(+cross-check) → fix on the surfaces with findings], looping until
349
- **zero open findings + a PASS smoke** (`maxRounds`, default 5, and the token budget are runaway
350
- protection, not targets). Since a workflow can't ask anything mid-run, the decisions move to the
351
- edges: a **readiness gate** aborts up front if the spec isn't frozen (other gaps ride along as
352
- deferred questions), and everything genuinely human comes back at the END in the result's
353
- `questions` array — empty when `/brainstorm` + `/spec` did their job. Even a finding that implies
354
- a **contract change stays inside the loop**: a lead-equivalent agent re-authors spec §5 + the
355
- contract file (exactly what conversational `/fix` §1 does — implementers still never touch it),
356
- the consuming surfaces re-dispatch, and the loop continues; the re-authorings are reported in the
357
- result's `contractChanges` for the human to review in the diff. A clean exit ticks the DoD and
358
- stamps the freshness gate so `/ship <id>` is a straight shot; a stopped run appends its open
359
- findings to the spec's `## Remediation` so a rerun of the cycle — or a conversational `/fix` —
360
- continues seamlessly. `/ship` itself stays outside on purpose — outward-facing and irreversible,
361
- it keeps its human confirmation.
362
- **Corollary — harden the spec:** the more `/brainstorm` + `/spec` pre-answer (edge cases, error
363
- envelopes, role matrix, design links), the further the cycle runs and the emptier `questions`
364
- comes back; a vague spec just converts into deferred questions.
365
358
  - **No input mid-run.** A workflow runs to completion without questions; anything interactive
366
- (contract changes, human decisions) belongs to the conversational path or, for `cycle.js`, to
367
- the `questions` array of its result. The gate hook still fires on workflow subagents (see
359
+ (contract changes, human decisions) belongs to the conversational path. The gate hook still
360
+ fires on workflow subagents (see
368
361
  §Preflight) — in unattended runs its asks become denies.
369
362
  - **Permissions:** `/init-pipeline` and `/update-pipeline` extend the generated `settings.json`
370
363
  `allow` list with what workflow agents need (the quiet commands, the shipped
@@ -416,7 +409,7 @@ card created in the target column if missing.
416
409
  | `/spec` opens (draft) | `spec` |
417
410
  | `/spec` freezes (`status: frozen`) | `ready` |
418
411
  | `/build` | `building` |
419
- | `/smoke` · `/review` | `review` |
412
+ | `/review` (owns the move — `/smoke` never moves the card) | `review` |
420
413
  | `/fix` | `fix` |
421
414
  | `/ship` starts | `ship` |
422
415
  | PR opened (`status: shipped`) | `shipped` (+ `PR #<num>` on the card) |
@@ -457,6 +450,10 @@ see where features stall, so every stage of `idea → PR` reports and nothing el
457
450
  | `fix` | after the batch metrics line | wall-clock | `<fixed>/<found>` |
458
451
  | `ship` | the release agent succeeded | `0` | `pr` / `compare` |
459
452
 
453
+ > Workflow-variant runs (`review.js`) report `seconds: 0` for their phases — only the
454
+ > conversational commands measure wall-clock. `results` is a free-text summary field, so both
455
+ > forms are valid — but read the `fix` column knowing which path produced it.
456
+
460
457
  `seconds: 0` marks a phase whose duration is human thinking time, not pipeline wall-clock — the
461
458
  funnel signal there is the event, not how long it took. `/doctor`, `/audit`, `/refactor`,
462
459
  `/align-ds`, `/init-pipeline` and `/update-pipeline` **never** ping: they sit outside the funnel, and
@@ -28,28 +28,44 @@ done
28
28
 
29
29
  tmp="${board}.kanban-move.$$"
30
30
  ID="$id" COL="$col" PR="$pr" TITLE="${title:-$id}" awk '
31
+ # Emit one line, collapsing runs of blank lines to a single one. Without this
32
+ # every move netted +1 blank line in the target column (the card is inserted
33
+ # with its own separator, right after the blank that already preceded the next
34
+ # heading) — ten moves of one card padded a board with fifteen blank lines.
35
+ function emit(s) {
36
+ if (s == "") { if (lastblank) return; lastblank = 1 } else lastblank = 0
37
+ print s
38
+ }
39
+ function put_card( k) {
40
+ emit(card)
41
+ for (k = 1; k <= sn; k++) emit(snote[k])
42
+ }
31
43
  BEGIN {
32
44
  id = ENVIRON["ID"]; col = tolower(ENVIRON["COL"])
33
45
  pr = ENVIRON["PR"]; title = ENVIRON["TITLE"]
34
- n = 0; card = ""; incard = 0; found = 0; colseen = 0
46
+ n = 0; card = ""; found = 0; colseen = 0; sn = 0; lastblank = 0
35
47
  }
36
48
  { lines[++n] = $0 }
37
49
  END {
38
50
  # pass 1: extract the FIRST card block tagged #id; mark every block for removal
39
51
  for (i = 1; i <= n; i++) {
40
52
  l = lines[i]
41
- if (l ~ /^- \[.\] / && index(l, "#" id) > 0) {
42
- # word-boundary check: char after the tag must not extend the id
43
- rest = substr(l, index(l, "#" id) + length(id) + 1, 1)
44
- if (rest != "" && rest ~ /[A-Za-z0-9_-]/) continue
45
- del[i] = 1
46
- if (!found) { found = 1; card = l }
47
- for (j = i + 1; j <= n && lines[j] ~ /^[ \t]/; j++) {
48
- del[j] = 1
49
- if (found && card == lines[i]) sub_notes = sub_notes lines[j] "\n"
50
- }
51
- i = j - 1
53
+ if (l !~ /^- \[.\] /) continue
54
+ p = index(l, "#" id)
55
+ if (p == 0) continue
56
+ # word-boundary check: char after the tag must not extend the id
57
+ rest = substr(l, p + length(id) + 1, 1)
58
+ if (rest != "" && rest ~ /[A-Za-z0-9_-]/) continue
59
+ # Capture the sub-notes of the FIRST match only. Testing `card == lines[i]`
60
+ # instead also captured a later duplicate whose text happened to match.
61
+ isfirst = 0
62
+ if (!found) { found = 1; card = l; isfirst = 1 }
63
+ del[i] = 1
64
+ for (j = i + 1; j <= n && lines[j] ~ /^[ \t]/; j++) {
65
+ del[j] = 1
66
+ if (isfirst) snote[++sn] = lines[j]
52
67
  }
68
+ i = j - 1
53
69
  }
54
70
  if (!found) card = "- [ ] " title " #" id
55
71
  if (pr != "" && index(card, "PR #") == 0) card = card " — PR #" pr
@@ -63,22 +79,20 @@ ID="$id" COL="$col" PR="$pr" TITLE="${title:-$id}" awk '
63
79
  atheading = (l ~ /^## /)
64
80
  atsettings = (l ~ /^%% kanban:settings/)
65
81
  if (intarget && (atheading || atsettings)) {
66
- # back up over trailing blank lines already printed is not possible;
67
- # instead print card just before this boundary
68
- print card
69
- if (sub_notes != "") printf "%s", sub_notes
70
- print ""
82
+ # can not back up over already-printed lines; place the card just before
83
+ # this boundary instead
84
+ put_card()
85
+ emit("")
71
86
  intarget = 0; placed = 1
72
87
  }
73
88
  if (atheading) {
74
89
  h = tolower(l); sub(/^## +/, "", h); gsub(/ +$/, "", h)
75
90
  if (h == col) { intarget = 1; colseen = 1 }
76
91
  }
77
- print l
92
+ emit(l)
78
93
  }
79
94
  if (intarget && !placed) {
80
- print card
81
- if (sub_notes != "") printf "%s", sub_notes
95
+ put_card()
82
96
  placed = 1
83
97
  }
84
98
  if (!colseen) exit 3