cohorte 2.1.0 → 2.2.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 (53) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/README.md +41 -32
  3. package/bin/cli.js +316 -26
  4. package/core/adapter/render.js +389 -0
  5. package/core/agents/implementer.template.md +3 -3
  6. package/core/agents/release.md +1 -4
  7. package/core/agents/review.md +10 -2
  8. package/core/commands/cohorte-audit.md +2 -0
  9. package/core/commands/cohorte-brainstorm.md +3 -6
  10. package/core/commands/cohorte-build.md +14 -17
  11. package/core/commands/cohorte-doctor.md +59 -28
  12. package/core/commands/cohorte-fix.md +2 -3
  13. package/core/commands/cohorte-init-pipeline.md +7 -8
  14. package/core/commands/cohorte-refactor.md +5 -2
  15. package/core/commands/cohorte-review.md +20 -16
  16. package/core/commands/cohorte-ship.md +5 -5
  17. package/core/commands/cohorte-spec.md +3 -7
  18. package/core/commands/cohorte-update-pipeline.md +8 -8
  19. package/core/hooks/gate.py +203 -16
  20. package/core/runtimes/claude.json +73 -0
  21. package/core/runtimes/codex.json +82 -0
  22. package/core/runtimes/cursor.json +75 -0
  23. package/core/runtimes/gemini.json +75 -0
  24. package/core/runtimes/opencode.json +72 -0
  25. package/core/templates/spec.template.md +1 -3
  26. package/core/templates/steps/init-pipeline/01-detect-stack.md +1 -1
  27. package/core/templates/steps/init-pipeline/02-interview-gaps.md +2 -2
  28. package/core/templates/steps/init-pipeline/04-write-render.md +23 -17
  29. package/core/templates/steps/init-pipeline/05-report.md +1 -1
  30. package/dashboard/dist/assets/{index-P1I1JGtj.js → index-D1rsbLat.js} +1 -1
  31. package/dashboard/dist/index.html +1 -1
  32. package/dashboard/server/doctor.js +156 -69
  33. package/dashboard/server/index.js +12 -2
  34. package/dashboard/server/metrics.js +13 -6
  35. package/dashboard/server/runtime.js +115 -0
  36. package/dashboard/server/versions.js +12 -1
  37. package/install.ps1 +23 -2
  38. package/install.sh +22 -4
  39. package/package.json +6 -2
  40. package/profile/PIPELINE.template.md +7 -6
  41. package/profile/SCHEMA.md +45 -48
  42. package/scripts/kanban-move.sh +11 -1
  43. package/scripts/metrics/collect.mjs +5 -3
  44. package/scripts/preflight.sh +27 -8
  45. package/scripts/telemetry-send.sh +10 -3
  46. package/scripts/test-adapter.mjs +368 -0
  47. package/scripts/test-dashboard.mjs +70 -0
  48. package/scripts/test-gate.mjs +62 -0
  49. package/scripts/validate-core.mjs +1 -1
  50. package/core/commands/cohorte-loop.md +0 -110
  51. package/scripts/loop-detach.sh +0 -153
  52. package/scripts/loop.sh +0 -399
  53. package/scripts/test-loop.mjs +0 -330
package/install.sh CHANGED
@@ -71,6 +71,27 @@ else
71
71
  fi
72
72
  [ -d "$src/core" ] || { echo "error: pipeline source not found (no core/ in $src)" >&2; exit 1; }
73
73
 
74
+ # --- delegate to the Node CLI ------------------------------------------------
75
+ # Since 2.2.0 the commands in core/ are runtime-NEUTRAL sources: they carry capability
76
+ # conditionals (`<!-- cohorte:if subagents -->`) and path tokens (`<core>`, `<state>`) that
77
+ # the adapter resolves per coding agent. Copying them verbatim, as this script used to,
78
+ # would install prompts full of unresolved markers — an install that looks successful and
79
+ # instructs the model with text meant for a different runtime. There is no shell renderer,
80
+ # so hand the whole job to bin/cli.js, which is the documented route anyway.
81
+ if command -v node >/dev/null 2>&1; then
82
+ set -- install
83
+ [ "$mode" = "update" ] && set -- update
84
+ [ "$scope" = "global" ] && set -- "$@" --global
85
+ [ "$scope" = "project" ] && set -- "$@" "$target"
86
+ exec node "$src/bin/cli.js" "$@"
87
+ fi
88
+ echo "error: cohorte needs Node ≥ 18 to install." >&2
89
+ echo " The pipeline's commands are rendered per coding agent (Claude Code, Codex, Cursor," >&2
90
+ echo " Gemini CLI, OpenCode) at install time; there is no shell equivalent of that step," >&2
91
+ echo " and a raw copy would install prompts this runtime cannot follow." >&2
92
+ echo " Install Node, then: npx cohorte install${scope:+ }$([ "$scope" = global ] && echo --global)" >&2
93
+ exit 1
94
+
74
95
  # --- resolve the destination .claude dir ------------------------------------
75
96
  if [ "$scope" = "global" ]; then
76
97
  dest="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
@@ -107,11 +128,8 @@ copy_core() {
107
128
  cp "$src/scripts/kanban-move.sh" "$dest/pipeline/scripts/"
108
129
  cp "$src/scripts/telemetry-send.sh" "$dest/pipeline/scripts/"
109
130
  cp "$src/scripts/preflight.sh" "$dest/pipeline/scripts/"
110
- cp "$src/scripts/loop.sh" "$dest/pipeline/scripts/"
111
- cp "$src/scripts/loop-detach.sh" "$dest/pipeline/scripts/"
112
131
  chmod +x "$dest/pipeline/scripts/kanban-move.sh" "$dest/pipeline/scripts/telemetry-send.sh" \
113
- "$dest/pipeline/scripts/preflight.sh" "$dest/pipeline/scripts/loop.sh" \
114
- "$dest/pipeline/scripts/loop-detach.sh" 2>/dev/null || true
132
+ "$dest/pipeline/scripts/preflight.sh" 2>/dev/null || true
115
133
  cp "$src/core/agents/implementer.template.md" "$dest/pipeline/"
116
134
  [ -f "$src/CHANGELOG.md" ] && cp "$src/CHANGELOG.md" "$dest/pipeline/"
117
135
  printf '%s\n' "$ver" > "$dest/pipeline/VERSION"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cohorte",
3
- "version": "2.1.0",
4
- "description": "Portable, stack-agnostic multi-agent development pipeline for Claude Code install the core, run /cohorte-init-pipeline, and it adapts to your project's stack.",
3
+ "version": "2.2.0",
4
+ "description": "Portable, stack-agnostic multi-agent development pipeline for Claude Code, Codex CLI, Cursor, Gemini CLI and OpenCode \u2014 install the core, run /cohorte-init-pipeline, and it adapts to your project's stack.",
5
5
  "bin": {
6
6
  "cohorte": "bin/cli.js"
7
7
  },
@@ -38,6 +38,10 @@
38
38
  "keywords": [
39
39
  "claude",
40
40
  "claude-code",
41
+ "codex",
42
+ "cursor",
43
+ "gemini-cli",
44
+ "opencode",
41
45
  "agents",
42
46
  "pipeline",
43
47
  "multi-agent"
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **Generated by `/cohorte-init-pipeline`.** This is the project profile: the single place the
4
4
  > portable multi-agent pipeline reads for everything stack-specific. The pipeline core
5
- > under `.claude/` is generic and references THIS file by section — keep project facts
5
+ > in the pipeline core is generic and references THIS file by section — keep project facts
6
6
  > here, never hardcoded into agent prompts. Kept current by `/cohorte-update-pipeline` (reconcile:
7
7
  > new fields are topped up, your values never overwritten). Rendered agent files are
8
8
  > regenerated from this profile on every reconcile — customize agents through §Conventions
@@ -52,7 +52,7 @@ surfaces:
52
52
  - key: backend # short id, used as agent name + scope
53
53
  path: apps/api # the ONLY tree this surface's agent may touch
54
54
  label: backend (AdonisJS)
55
- agent: backend # rendered agent file: .claude/agents/backend.md
55
+ agent: backend # rendered as backend.md in this runtime's agents dir
56
56
  tools: [Read, Write, Edit, Bash, Grep, Glob, mcp__serena] # mcp__<provider> mirrors retrieval.provider
57
57
  model: sonnet # frontmatter model tier: sonnet | haiku | inherit
58
58
  # sonnet = default (applies the frozen contract — cheap
@@ -158,7 +158,7 @@ isolation:
158
158
  compose_file: docker-compose.yml
159
159
  registry: .worktrees/slots.tsv
160
160
 
161
- # ── gate (drives .claude/hooks/gate.py + settings.json) ─────────────────────
161
+ # ── gate (drives the pipeline gate hook + permissions) ─────────────────────
162
162
  # Confirm-first or hard-deny on dangerous Bash, inspecting the FULL command
163
163
  # string (so chained `cd x && …` forms are caught).
164
164
  gate:
@@ -184,10 +184,10 @@ gate:
184
184
  - "git rebase"
185
185
  - "git reset"
186
186
  - "docker compose"
187
- # Phase gate: review dispatches require a fresh `.claude/preflight.ok` stamp,
187
+ # Phase gate: review dispatches require a fresh `preflight.ok` stamp,
188
188
  # written by pipeline/scripts/preflight.sh when typecheck+lint+tests are green —
189
189
  # gate.py "ask"s the dispatch when the stamp is missing, stale, or the code changed since.
190
- # The stamp is local: keep `.claude/preflight.ok` gitignored, never committed.
190
+ # The stamp is local: keep `preflight.ok` gitignored, never committed.
191
191
  preflight:
192
192
  enabled: true
193
193
  agents: [review] # subagent_types the stamp gates
@@ -196,7 +196,8 @@ gate:
196
196
  ```
197
197
 
198
198
  > **Note.** The **kanban** board link is **user-scoped** — NOT configured here. Its facts live in the
199
- > consolidated `~/.claude/cohorte.config.yaml`, read at runtime (the kanban board is keyed by this
199
+ > consolidated user config (`~/.claude/cohorte.config.yaml`, or `~/.cohorte/cohorte.config.yaml` on
200
+ > a non-Claude runtime), read at runtime (the kanban board is keyed by this
200
201
  > profile's `name`). This project profile only governs the dev pipeline (`/cohorte-brainstorm…/cohorte-ship`).
201
202
 
202
203
  ---
package/profile/SCHEMA.md CHANGED
@@ -79,7 +79,7 @@ generic pipeline uses it, so a stateless agent can read/regenerate the profile c
79
79
  - **Commands** (`/cohorte-build`, `/cohorte-review`, …) parse the `yaml pipeline-profile` block to know how
80
80
  many surfaces to dispatch, the contract mechanism, the commands, and the capability flags.
81
81
  - **Hook** (`gate.py`) reads `gate.deny`/`gate.ask`/`gate.ask_on_default_branch`/`gate.default_branch`
82
- from a generated `.claude/gate-config.json`. The last two make git + docker free on feature branches
82
+ from a generated `<state>/gate-config.json`. The last two make git + docker free on feature branches
83
83
  but confirm-gated on the default branch (branch resolved at run time via `git rev-parse`).
84
84
  - **Scripts** (`new-feature.sh`) read the `isolation` block (rendered in at init).
85
85
 
@@ -169,7 +169,7 @@ the frozen contract as the only cross-surface channel**. So specialization means
169
169
 
170
170
  Coarse first, specialize on evidence: start with one `frontend` / `backend` surface each; split only a
171
171
  surface that's proven slow and cleanly separable. The evidence lives in
172
- the **main checkout's** `.claude/pipeline-metrics.jsonl` (gitignored) — one JSONL line per phase batch
172
+ the **main checkout's** `<state>/pipeline-metrics.jsonl` (gitignored) — one JSONL line per phase batch
173
173
  (`ts`/`feature`/`phase`/`seconds`/`surfaces:{key: result}`), appended by `/cohorte-build`, `/cohorte-review`
174
174
  and `/cohorte-fix`.
175
175
  **`surfaces` keys are surface keys, nothing else** — run-level facts go in their own top-level
@@ -177,7 +177,7 @@ fields. Anything put inside `surfaces` is read
177
177
  as a surface: the dashboard renders it as a row in the per-surface table and scores a non-`ok`
178
178
  value as that surface failing. Always the main checkout, never the feature worktree (which dies at teardown while
179
179
  metrics must accumulate across features) — resolve from anywhere with
180
- `$(dirname "$(git rev-parse --git-common-dir)")/.claude/pipeline-metrics.jsonl`. Read it before
180
+ `$(dirname "$(git rev-parse --git-common-dir)")/<state>/pipeline-metrics.jsonl`. Read it before
181
181
  proposing a split: split the surface that actually dominates wall-clock, not the one that feels big.
182
182
 
183
183
  ## Measuring cost — what's slow vs what's expensive
@@ -190,7 +190,7 @@ to log it. For what's EXPENSIVE, use Claude Code's own accounting:
190
190
  over the last 24 h / 7 d (e.g. _"Top subagents: frontend 7 %, backend 4 % · Top skills: /cohorte-build 1 %,
191
191
  /cohorte-review 1 %"_). That IS the per-phase ledger — approximate (share-of-total, machine-local, not exact
192
192
  tokens). Read it to see which surface/command actually dominates the bill before you tune a `model` tier.
193
- - **OpenTelemetry** (exact numbers + dashboards) — add an `env` block to `~/.claude/settings.json`:
193
+ - **OpenTelemetry** (exact numbers + dashboards) — add an `env` block to `this runtime's settings file`:
194
194
  `{"env":{"CLAUDE_CODE_ENABLE_TELEMETRY":"1","OTEL_METRICS_EXPORTER":"otlp","OTEL_EXPORTER_OTLP_PROTOCOL":"http/protobuf","OTEL_EXPORTER_OTLP_ENDPOINT":"http://localhost:4318"}}`
195
195
  and point it at a collector. Metrics `claude_code.token.usage` + `claude_code.cost.usage` carry
196
196
  `session.id` + model + type (input/output/cacheRead). Subagent tokens roll into the session total;
@@ -226,28 +226,25 @@ Rules for every consumer (implementers, preflight, `/cohorte-audit` gates, workf
226
226
  `/cohorte-init-pipeline` **asks** for these variants (detected defaults offered first) instead of silently
227
227
  storing a bare `pnpm test` as the thing agents execute; `/cohorte-update-pipeline` tops up older profiles.
228
228
 
229
- ## Spec status — the lifecycle state machine (and the loop's resume state)
229
+ ## Spec status — the lifecycle state machine
230
230
 
231
231
  A spec's front-matter `status` is not a label, it is the pipeline's **state**: every command routes on
232
- it, the dashboard boards on it, the kanban backfill maps it to a column, and `/cohorte-loop --resume` reads it
233
- back to continue an interrupted autonomous run. Six states, and exactly one writer each:
232
+ it, the dashboard boards on it, and the kanban backfill maps it to a column. Six states:
234
233
 
235
234
  | status | meaning | written by | who may build it |
236
235
  | --- | --- | --- | --- |
237
236
  | `draft` | the interview is open, nothing is frozen | `/cohorte-spec` Mode A | no |
238
237
  | `frozen` | the contract is frozen — the handoff to `/cohorte-build` | `/cohorte-spec` Mode A freeze | yes |
239
- | `in-progress` | a `/cohorte-loop` is driving this spec right now (or died doing it) | `scripts/loop.sh`, before each phase | yes |
240
- | `in-review` | reviewed / awaiting the next round or `/cohorte-ship` | `/cohorte-spec` Mode B, `/cohorte-fix`, `loop.sh` on a clean exit | yes |
241
- | `blocked` | a loop gave up here (ceiling, non-convergent, no verdict, not implementable) | `loop.sh` on any non-zero exit | yes, with the reason named |
238
+ | `in-progress` | a round is under way on this spec (or died mid-way) | an automated driver, if any | yes |
239
+ | `in-review` | reviewed / awaiting the next round or `/cohorte-ship` | `/cohorte-spec` Mode B, `/cohorte-fix` | yes |
240
+ | `blocked` | a round gave up here (non-convergent, no verdict, not implementable) | an automated driver, if any | yes, with the reason named |
242
241
  | `shipped` | the PR is open; the status flip is part of the release commit | `/cohorte-ship` | no |
243
242
 
244
- **The resume contract.** Before every phase, `loop.sh` stamps `status: in-progress` plus `loop_pass`
245
- (the review pass it is on) and `loop_phase` (`build`/`review`/`fix`) into the spec deterministically,
246
- with `awk`, spending **no tokens** on state it will need later. On exit it stamps a terminal status:
247
- `in-review` + `loop_phase: done` when clean, `blocked` otherwise. `/cohorte-loop <id> --resume` then continues
248
- at the recorded pass instead of pass 1, so a session killed at pass 3 of 5 does not re-pay passes 1–2.
249
- The build is still skipped or redone by the build stamp alone (`specs/reports/<id>.built`, written only
250
- after a build that finished), so an interrupted *build* correctly rebuilds.
243
+ **`in-progress` and `blocked` are for external drivers.** No shipped command writes them: the
244
+ built-in autonomous driver (`/cohorte-loop`) was retired in 2.2.0, and the human-driven cycle moves
245
+ `frozen` `in-review` `shipped`. They stay valid states because specs in existing repos carry
246
+ them, and because anything automating the cycle from outside needs somewhere to record "a round is
247
+ under way" and "a round gave up". Every reader still routes on them; nothing produces them.
251
248
 
252
249
  Corollaries worth knowing:
253
250
 
@@ -279,8 +276,8 @@ Non-negotiables, in every phase:
279
276
  - **Never speak for a dead agent.** You did not see its work: report what the *tree* says (quiet
280
277
  commands, redirected to a file, grepped), not what a handoff would have said.
281
278
  - **Never let it reach a driver as clean.** `/cohorte-build` writes `dead[]` into
282
- `specs/reports/<id>.build.json`, `/cohorte-review` writes `unreviewed[]` into the verdict; `scripts/loop.sh`
283
- aborts on either with **exit 2** *before* it reads `blocking`, since a dead reviewer makes
279
+ `specs/reports/<id>.build.json`, `/cohorte-review` writes `unreviewed[]` into the verdict; a driver
280
+ must abort on either *before* it reads `blocking`, since a dead reviewer makes
284
281
  `blocking == 0` a statement about code nobody read.
285
282
  - **`unreviewed` is separate from `blocking` on purpose.** Faking a count in `blocking` to force a
286
283
  driver's hand would corrupt the one field the whole contract rests on; a driver reads them as two
@@ -300,8 +297,8 @@ of the step: a spec that cannot be built does not get cheaper by being built on
300
297
  the design gate. Each maps to `NOT-READY` (a surface would have to invent the answer) or
301
298
  `RESERVATIONS` (a surface can proceed on a stated assumption).
302
299
  - **`NOT-READY` aborts the build with no agent spawned** and sends the human to `/cohorte-spec`.
303
- `scripts/loop.sh` reads the same file and exits **4** (`not implementable`) — the one loop outcome
304
- that more passes cannot fix.
300
+ A driver reads the same file and must stop rather than retry it is the one outcome more passes
301
+ cannot fix.
305
302
  - **`RESERVATIONS` never blocks.** Each gap is inlined verbatim into the dispatch of the surface it
306
303
  affects, as an assumption the implementer must apply *and* flag in its handoff. A gate that stalled a
307
304
  sound build on a missing error case would cost more human round-trips than it saves.
@@ -325,7 +322,7 @@ own out-of-scope reason.
325
322
  `## Remediation`, which is what `/cohorte-fix` re-dispatches.
326
323
  - `/cohorte-audit` **carries open `deferred:` items over** when it rewrites the backlog; overwriting them away
327
324
  is the one way they silently vanish.
328
- - The verdict JSON carries `deferred: <n>` (informational, outside `blocking`), so `/cohorte-loop` can name
325
+ - The verdict JSON carries `deferred: <n>` (informational, outside `blocking`), so a driver can name
329
326
  them in its closing line without reading a report.
330
327
 
331
328
  ## Decisions — the transverse decision journal
@@ -359,14 +356,14 @@ variants) with all output redirected to `specs/reports/<id>.preflight.txt`:
359
356
  there: zero agents are spawned.** A reviewer dispatched onto code that doesn't compile burns its
360
357
  whole run rediscovering what `tsc` already printed for free — the failure goes straight to the
361
358
  human (or `/cohorte-fix`) instead.
362
- - **All green** ⇒ the script stamps `.claude/preflight.ok` (`<epoch> <HEAD sha> <tree digest>` —
359
+ - **All green** ⇒ the script stamps `<state>/preflight.ok` (`<epoch> <HEAD sha> <tree digest>` —
363
360
  local and **gitignored**; a versioned stamp describes the tree *before* its own commit and rides
364
361
  into every clone and worktree, which breaks the gate both ways).
365
362
 
366
363
  `hooks/gate.py` enforces the stamp as a **phase gate** (the `preflight` block of `gate-config.json`,
367
364
  generated from `gate.preflight`): a Task dispatch of a listed `subagent_type` (default
368
365
  `review`) with a missing/stale stamp — older than `max_age_minutes`, or the digest no longer
369
- matches the working tree (`.claude` and `specs` excluded, so the pipeline's own writes and a
366
+ matches the working tree (`.claude`, `.cohorte` and `specs` excluded, so the pipeline's own writes and a
370
367
  commit of already-verified code do not invalidate it) — gets an
371
368
  "ask", so a lead can't accidentally skip the gate but a human can consciously override it. The gate
372
369
  hook fires for **every** agent in the session, including subagents spawned by the Workflow runtime
@@ -387,8 +384,9 @@ this exact procedure so a surface is always defined the same way. To add surface
387
384
  scaffolding; `inherit` only when the surface makes real design decisions worth the lead's model),
388
385
  the five `*_cmd`s (derive from the surface's `package.json` / workspace
389
386
  filter, mirroring a sibling surface), and `uses_design`.
390
- 2. **Render the agent file** `.claude/agents/<agent>.md` from `pipeline/implementer.template.md`
391
- (resolve bundled `.claude/` vs global `~/.claude/`), substituting `<SURFACE_AGENT>`, `<SURFACE_LABEL>`,
387
+ 2. **Render the agent file** `<agents>/<agent>.md` from `<core>/pipeline/implementer.template.md`
388
+ the template is already rendered for this runtime, so only the placeholders are yours to fill —
389
+ substituting `<SURFACE_AGENT>`, `<SURFACE_LABEL>`,
392
390
  `<SURFACE_PATH>`, `<SURFACE_TOOLS>`, `<SURFACE_MODEL>`, `<PROJECT_NAME>`, and the surface-specific
393
391
  blocks (`<SURFACE_EXTRA_NEVER>`, `<SURFACE_DESIGN_INPUT>`, `<SURFACE_TDD_STEP1>` — leave the design
394
392
  ones empty unless `uses_design`). Fill `<SURFACE_CONVENTIONS>` with the surface's convention slice
@@ -467,35 +465,35 @@ files automatically. It works because every generated artifact is a **determinis
467
465
  `/cohorte-init-pipeline` Phase 1, and if there is one, ask Phase 2's release-notes question (anchor
468
466
  package, language, bump policy, forbidden levels). No tool found ⇒ top up with `enabled: false`.
469
467
  2. **Re-render agent frontmatter + body.** For each `surfaces[]` entry, re-render
470
- `.claude/agents/<agent>.md` from the current `implementer.template.md` per §Rendering above. Safe by
468
+ `<agents>/<agent>.md` from the current `implementer.template.md` per §Rendering above. Safe by
471
469
  doctrine: rendered agents are regenerable artifacts — hand-written rules belong in `PIPELINE.md`
472
470
  §Conventions (which reconcile never touches), NOT in agent files, where they'd be clobbered here.
473
- 3. **Additive settings patch.** Bring `.claude/settings.json` + `gate-config.json` up to the current
474
- init spec (missing `allow` entries, hooks per install mode) add what's missing, never remove or
475
- rewrite existing/custom keys.
471
+ 3. **Additive settings patch.** Bring `<state>/gate-config.json` and, on a runtime with a settings
472
+ file the pipeline generates, that too up to the current init spec (missing `allow` entries,
473
+ hooks per install mode): add what's missing, never remove or rewrite existing/custom keys.
476
474
  4. **Capability wiring.** If a top-up added a capability needing external setup (e.g. a `retrieval`
477
475
  provider whose MCP server isn't registered yet), run its wiring step from `/cohorte-init-pipeline` Phase 4.
478
476
  Even when nothing new was added, re-run the provider's health check (§Code retrieval) — wiring
479
477
  rots (PATH changes, uninstalls, hand-edits) — and repair whatever fails.
480
- 5. **Global config seed.** If `~/.claude/cohorte.config.yaml` is absent, seed it from the template
478
+ 5. **Global config seed.** If `<config>` is absent, seed it from the template
481
479
  (`profile/cohorte.config.template.yaml`) so the kanban + shared-vault config has a home. Never
482
480
  clobber an existing filled file; report what was seeded.
483
481
  6. **Kanban sync.** Run the §Kanban reconcile: link/create the project's board if configured, verify
484
482
  its columns, and backfill/sync cards from `specs/*.md`. See §Kanban.
485
483
  7. **Spec-template top-up.** `specs/_template.md` is seeded once at install and then **never**
486
484
  refreshed, so a repo keeps whatever front-matter the core shipped the day it was installed (a
487
- pre-1.6 copy has no `loop_pass`/`loop_phase`, and its `status` comment still lists four states).
488
- Top it up the same way as the profile: add the **front-matter fields** the current
489
- `templates/spec.template.md` has and the repo's copy lacks, with their documented defaults, and
490
- refresh the `status:` comment. Never rewrite its body — the section list is the human's to shape,
485
+ pre-1.6 copy's `status` comment still lists four states). Top it up the same way as the profile:
486
+ add the **front-matter fields** the current `templates/spec.template.md` has and the repo's copy
487
+ lacks, with their documented defaults, drop `loop_pass`/`loop_phase` (retired with
488
+ `/cohorte-loop` in 2.2.0), and refresh the `status:` comment. Never rewrite its body — the section list is the human's to shape,
491
489
  and some repos have deliberately trimmed it. Nothing breaks without this (the fields are written on
492
490
  demand when a driver needs them); it just keeps a new spec's front-matter honest about the states
493
491
  the pipeline can put it in.
494
492
 
495
493
  8. **Local-artifact hygiene.** The pipeline's own runtime files must stay out of git:
496
- `.claude/preflight.ok`, `.claude/pipeline-metrics.jsonl`, `specs/reports/`. Add any missing entry to
494
+ `<state>/preflight.ok`, `<state>/pipeline-metrics.jsonl`, `specs/reports/`. Add any missing entry to
497
495
  `.gitignore`, and **untrack** what a pre-2.0.0 install let slip in —
498
- `git rm --cached --ignore-unmatch .claude/preflight.ok` (repeat per stray path). The stamp is the
496
+ `git rm --cached --ignore-unmatch <state>/preflight.ok` (repeat per stray path). The stamp is the
499
497
  one that actively breaks: it records the tree it verified, the commit carrying it moves HEAD past
500
498
  that tree, and the committed copy lands in every clone and worktree — so the phase gate ends up
501
499
  blocking clean trees and greening unchecked ones. Report what was untracked; the human commits it.
@@ -507,8 +505,8 @@ itself changes in ways `/cohorte-build` §1.5 can't auto-grow (e.g. package mana
507
505
 
508
506
  Three phases have a **workflow variant** — a deterministic orchestration script the Claude Code
509
507
  Workflow runtime executes instead of the lead reasoning out the fan-out turn by turn:
510
- `<core>/workflows/review.js`, `audit.js`, `refactor.js` (installed to `.claude/workflows/` bundled or
511
- `~/.claude/workflows/` global). The conversational commands (`/cohorte-review`, `/cohorte-audit`, `/cohorte-refactor`)
508
+ `<core>/workflows/review.js`, `audit.js`, `refactor.js` (installed to `<core>/workflows/` bundled or
509
+ `<core>/workflows/` global). The conversational commands (`/cohorte-review`, `/cohorte-audit`, `/cohorte-refactor`)
512
510
  **remain the default path and the fallback** — a workflow runs only when the human explicitly asks
513
511
  for it ("run the review workflow"), and requires Claude Code ≥ **2.1.154** with workflows
514
512
  enabled.
@@ -557,7 +555,7 @@ Shared design, all four scripts:
557
555
 
558
556
  An **optional, user-scoped** mirror of the dev flow: each pipeline stage moves a card across an
559
557
  [Obsidian Kanban](https://github.com/mgmeyers/obsidian-kanban) board, one board per project. Config
560
- lives in the consolidated global config `~/.claude/cohorte.config.yaml` §`kanban` (NOT in
558
+ lives in the consolidated global config `<config>` §`kanban` (NOT in
561
559
  `PIPELINE.md` — the board path points at the user's personal vault, so it is machine-specific and must
562
560
  not be committed). Everything below **no-ops silently** when the config is absent, `kanban.enabled` is
563
561
  false, no board is configured for the current project, or the board file is missing — the pipeline never
@@ -585,12 +583,12 @@ agent's context (find, dedupe, sub-notes carried along, settings block preserved
585
583
  <core>/pipeline/scripts/kanban-move.sh auto <id> <stage> [--pr <num>] [--title <title>]
586
584
  ```
587
585
 
588
- `<core>` is `~/.claude` (global install) or `.claude` (bundled) — probe with `test -x`. It creates
586
+ It creates
589
587
  the card in the target column when none exists, keeps the first and drops duplicates, and appends
590
588
  ` — PR #<num>` with `--pr`.
591
589
 
592
590
  **`auto` is not a convenience, it is the contract.** It reads `name` from `PIPELINE.md`, then
593
- `kanban.enabled` / `obsidian.vault_path` / `boards[name]` from `~/.claude/cohorte.config.yaml`
591
+ `kanban.enabled` / `obsidian.vault_path` / `boards[name]` from `<config>`
594
592
  (override with `COHORTE_CONFIG`, or skip the profile with `--project <name>`), and it maps the
595
593
  **stage key** (`ideas` · `brainstorm` · `spec` · `ready` · `building` · `review` · `fix` · `ship` ·
596
594
  `shipped`) to that board's heading through `boards[name].columns` → `kanban.columns` → the built-in
@@ -626,15 +624,14 @@ Ideas — so `/cohorte-brainstorm` appends the tag to the picked line before its
626
624
  | `/cohorte-build` | `building` |
627
625
  | `/cohorte-review` | `review` |
628
626
  | `/cohorte-fix` | `fix` |
629
- | a `/cohorte-loop` is driving it (`in-progress`) | the current phase's column |
630
- | a `/cohorte-loop` gave up (`blocked`) | `fix` |
627
+ | a round is under way (`in-progress`) | `building` |
628
+ | a round gave up (`blocked`) | `fix` |
631
629
  | `/cohorte-ship` starts | `ship` |
632
630
  | PR opened (`status: shipped`) | `shipped` (+ `PR #<num>` on the card) |
633
631
 
634
632
  **Backfill / sync from specs (reconcile).** `specs/*.md` is the source of truth. For each spec, read its
635
633
  `feature_id` (front-matter or filename) and `status`, map `status`→column — `frozen`→`ready`,
636
- `in-progress`→the `loop_phase`'s column (`build`→`building`, `review`→`review`, `fix`→`fix`; unset
637
- `building`), `in-review`→`review`, `blocked`→`fix`, `shipped`→`shipped`, anything else / a spec with no
634
+ `in-progress`→`building`, `in-review`→`review`, `blocked`→`fix`, `shipped`→`shipped`, anything else / a spec with no
638
635
  status→`spec` — then **full
639
636
  sync**: card absent ⇒ add it in that column; card present ⇒ **move it** to that column so the board
640
637
  always reflects the specs (this repositions cards the human may have moved by hand). Report cards
@@ -650,7 +647,7 @@ per configured column in pipeline order, and the closing `%% kanban:settings %%`
650
647
  Cohorte can send the maintainers anonymous usage pings so the pipeline improves where it's actually
651
648
  slow. **Nothing is ever sent without explicit consent**: `/cohorte-init-pipeline` (and `/cohorte-update-pipeline` on
652
649
  pre-telemetry installs) ask ONE question, once per machine, default **No**, and record the answer in
653
- `~/.claude/cohorte.config.yaml` §`telemetry` (`enabled`, `install_id`, `consent_date`). The sender —
650
+ `<config>` §`telemetry` (`enabled`, `install_id`, `consent_date`). The sender —
654
651
  `pipeline/scripts/telemetry-send.sh` — is a silent no-op unless `enabled: true` AND `install_id` AND
655
652
  `endpoint` are all set, times out at 2s, and never fails the pipeline. Callers chain it with
656
653
  `|| true`, so a **missing** script is equally silent: `/cohorte-doctor` check 1 verifies `pipeline/scripts/`
@@ -692,7 +689,7 @@ work without revealing what is being built.
692
689
  **GDPR rights, concretely:**
693
690
 
694
691
  - **Consent** — opt-in only, recorded with a date; "No" is also recorded so nothing re-asks.
695
- - **Withdrawal** — set `telemetry.enabled: false` in `~/.claude/cohorte.config.yaml`; effective on
692
+ - **Withdrawal** — set `telemetry.enabled: false` in `<config>`; effective on
696
693
  the next phase, no restart.
697
694
  - **Erasure** — `/cohorte-doctor` prints your `install_id`; send
698
695
  `curl -X DELETE <endpoint-origin>/v1/install/<install_id>` and the collector drops every event
@@ -54,7 +54,17 @@ while [ $# -gt 0 ]; do
54
54
  done
55
55
 
56
56
  TAB=$(printf '\t')
57
- CONFIG="${COHORTE_CONFIG:-$HOME/.claude/cohorte.config.yaml}"
57
+ # One config per human, wherever the runtime that installed it put it: `~/.claude` for a
58
+ # Claude Code install (historical, still authoritative), `~/.cohorte` for every other coding
59
+ # agent. Probe both — a repo driven from two agents must resolve ONE board, not two.
60
+ CONFIG="${COHORTE_CONFIG:-}"
61
+ if [ -z "$CONFIG" ]; then
62
+ for c in "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/cohorte.config.yaml" \
63
+ "$HOME/.cohorte/cohorte.config.yaml"; do
64
+ [ -f "$c" ] && { CONFIG="$c"; break; }
65
+ done
66
+ CONFIG="${CONFIG:-${CLAUDE_CONFIG_DIR:-$HOME/.claude}/cohorte.config.yaml}"
67
+ fi
58
68
 
59
69
  # The nine pipeline stages and their default headings. A stage key given as the
60
70
  # target column is mapped through the config (per-board `columns` first, then the
@@ -62,7 +62,7 @@ function knownCommands() {
62
62
  if (!names.size) {
63
63
  for (const n of ['cohorte-brainstorm', 'cohorte-spec', 'cohorte-build', 'cohorte-review',
64
64
  'cohorte-fix', 'cohorte-ship', 'cohorte-audit', 'cohorte-refactor',
65
- 'cohorte-align-ds', 'cohorte-doctor', 'cohorte-loop',
65
+ 'cohorte-align-ds', 'cohorte-doctor',
66
66
  'cohorte-init-pipeline', 'cohorte-update-pipeline']) names.add(n);
67
67
  }
68
68
  // Retired commands. The list above is read from the shipped core, so a command that is
@@ -73,8 +73,10 @@ function knownCommands() {
73
73
  // 2.0.0 prefixed every command with `cohorte-`, which retires all 13 bare names at once:
74
74
  // months of transcripts say `/build`, and without these they would all reclassify to (chat)
75
75
  // — the largest instance of exactly the bug this list exists to prevent. `drive`/`loop` are
76
- // both here because the driver was `/loop` → `/drive` (1.6.0) → `/cohorte-loop` (2.0.0).
77
- for (const n of ['cycle', 'smoke', 'drive', 'loop', 'brainstorm', 'spec', 'build', 'review',
76
+ // both here because the driver was `/loop` → `/drive` (1.6.0) → `/cohorte-loop` (2.0.0), and
77
+ // `cohorte-loop` joins them now that 2.2.0 retired the driver outright.
78
+ for (const n of ['cycle', 'smoke', 'drive', 'loop', 'cohorte-loop',
79
+ 'brainstorm', 'spec', 'build', 'review',
78
80
  'fix', 'ship', 'audit', 'refactor', 'align-ds', 'doctor',
79
81
  'init-pipeline', 'update-pipeline']) names.add(n);
80
82
  return names;
@@ -58,7 +58,7 @@ sha=$(git rev-parse HEAD 2>/dev/null || echo none)
58
58
  # invalidates the stamp without changing a line of code, while an implementer's edit
59
59
  # changes every line without moving HEAD. A tree id is content-addressed: it survives a
60
60
  # commit of the same content and dies on any real edit (including new untracked files).
61
- # `.claude` (stamps, metrics) and `specs` (DoD ticks, report buffer) are excluded — the
61
+ # `.claude`/`.cohorte` (stamps, metrics) and `specs` (DoD ticks, report buffer) are excluded — the
62
62
  # pipeline writes those itself between the preflight and the dispatch it must not invalidate.
63
63
  # gate.py recomputes this identically; any change here must land there too.
64
64
  digest=none
@@ -69,29 +69,48 @@ if [ -n "$tmpidx" ]; then
69
69
  idx=$(git rev-parse --git-path index 2>/dev/null || echo "")
70
70
  if [ -n "$idx" ] && [ -f "$idx" ]; then
71
71
  cp "$idx" "$tmpidx" 2>/dev/null || true
72
+ # Backdate the copy, for the reason spelled out in gate.py's tree_digest(): git trusts an
73
+ # entry's cached stat data only when its mtime predates the index file's, so a copy stamped
74
+ # `now` makes a file edited in this same second look clean. Both sides must age it by the
75
+ # same window or they compute different trees for the same content. `date -d` is GNU and
76
+ # `date -v` is BSD — try both, and if neither exists just skip the touch (the digest is
77
+ # still correct for anything not edited in the last few seconds).
78
+ stamp=$(date -u -d '5 seconds ago' +%Y%m%d%H%M.%S 2>/dev/null \
79
+ || date -u -v-5S +%Y%m%d%H%M.%S 2>/dev/null || echo "")
80
+ [ -n "$stamp" ] && touch -t "$stamp" "$tmpidx" 2>/dev/null || true
72
81
  else
73
82
  rm -f "$tmpidx" # a 0-byte index is a corrupt index
74
83
  fi
75
84
  # Drop the excluded paths from the throwaway index entirely: an `add` exclude only stops
76
85
  # them being *updated*, so anything already tracked there (a committed stamp, a spec)
77
86
  # would still land in the tree and shift the digest.
78
- GIT_INDEX_FILE="$tmpidx" git rm --cached -r -q --ignore-unmatch -- .claude specs > /dev/null 2>&1 || true
79
- if GIT_INDEX_FILE="$tmpidx" git add -A -- . ':(exclude).claude' ':(exclude)specs' > /dev/null 2>&1; then
87
+ GIT_INDEX_FILE="$tmpidx" git rm --cached -r -q --ignore-unmatch -- .claude .cohorte specs > /dev/null 2>&1 || true
88
+ if GIT_INDEX_FILE="$tmpidx" git add -A -- . ':(exclude).claude' ':(exclude).cohorte' ':(exclude)specs' > /dev/null 2>&1; then
80
89
  digest=$(GIT_INDEX_FILE="$tmpidx" git write-tree 2>/dev/null || echo none)
81
90
  fi
82
91
  rm -f "$tmpidx" "$tmpidx.lock" 2>/dev/null || true
83
92
  fi
84
93
  [ -n "$digest" ] || digest=none
85
- # Stamp BOTH the main checkout and the cwd: gate.py reads CLAUDE_PROJECT_DIR,
86
- # which is the worktree when the session was opened there and the main checkout
87
- # when it wasn't — the two disagree, and either layout is supported.
94
+ # Stamp BOTH the main checkout and the cwd: gate.py reads COHORTE_PROJECT_DIR /
95
+ # CLAUDE_PROJECT_DIR, which is the worktree when the session was opened there and the
96
+ # main checkout when it wasn't — the two disagree, and either layout is supported.
97
+ # Stamp every state dir that EXISTS (`.cohorte` on a non-Claude runtime, `.claude` on a
98
+ # Claude one, both where a repo is driven from both), falling back to `.claude` when the
99
+ # repo has neither yet — gate.py's state_path() probes in that same order.
88
100
  now=$(date +%s)
89
101
  last=""
90
102
  for d in "$proj" "$(pwd)"; do
91
103
  [ "$d" = "$last" ] && continue # same dir twice in the main checkout
92
104
  last="$d"
93
- mkdir -p "$d/.claude" 2>/dev/null || true
94
- printf '%s %s %s\n' "$now" "$sha" "$digest" > "$d/.claude/preflight.ok" 2>/dev/null || true
105
+ wrote=0
106
+ for s in .cohorte .claude; do
107
+ [ -d "$d/$s" ] || continue
108
+ printf '%s %s %s\n' "$now" "$sha" "$digest" > "$d/$s/preflight.ok" 2>/dev/null && wrote=1
109
+ done
110
+ if [ "$wrote" -eq 0 ]; then
111
+ mkdir -p "$d/.claude" 2>/dev/null || true
112
+ printf '%s %s %s\n' "$now" "$sha" "$digest" > "$d/.claude/preflight.ok" 2>/dev/null || true
113
+ fi
95
114
  done
96
115
 
97
116
  echo "PREFLIGHT PASS ($n checks green) — full log: $report"
@@ -16,8 +16,14 @@
16
16
  # - Never blocks or fails the pipeline: 2s timeout, all errors swallowed, exit 0 always.
17
17
  set -u
18
18
 
19
- cfg="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/cohorte.config.yaml"
20
- [ -f "$cfg" ] || exit 0
19
+ # Same two-location probe as kanban-move.sh: consent is recorded once per human, in
20
+ # whichever config the runtime that installed the pipeline created.
21
+ cfg=""
22
+ for c in "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/cohorte.config.yaml" \
23
+ "$HOME/.cohorte/cohorte.config.yaml"; do
24
+ [ -f "$c" ] && { cfg="$c"; break; }
25
+ done
26
+ [ -n "$cfg" ] || exit 0
21
27
 
22
28
  # read keys scoped to the `telemetry:` block only
23
29
  tval() {
@@ -62,7 +68,8 @@ fi
62
68
  # bundled repo report an empty core_version.
63
69
  here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd 2>/dev/null) || here=""
64
70
  ver=""
65
- for v in "$here/../VERSION" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/pipeline/VERSION"; do
71
+ for v in "$here/../VERSION" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/pipeline/VERSION" \
72
+ "$HOME/.cohorte/pipeline/VERSION"; do
66
73
  [ -n "$ver" ] && break
67
74
  ver=$(head -1 "$v" 2>/dev/null | tr -d '"\\')
68
75
  done