cohorte 2.2.0 → 2.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.
@@ -31,7 +31,10 @@ You are the **lead**. Ship feature **$ARGUMENTS**. This is the outward-facing ga
31
31
  §`Acceptance criteria / DoD`; if any item is still `- [ ]`, list the open ones and ask the human to
32
32
  confirm shipping anyway (they may be deferred on purpose — e.g. a UI item on a backend-only feature).
33
33
  All `- [x]` ⇒ proceed silently.
34
- - Show `git status` + `git diff --stat`; confirm the branch is `<feature_branch_prefix>$ARGUMENTS`.
34
+ - Show `git status` + `git diff --stat`; confirm the branch is `<prefix>$ARGUMENTS`, where `<prefix>`
35
+ is `vcs.patch_branch_prefix` (falling back to `fix/` on a profile that predates the key) when the
36
+ spec front-matter carries `kind: patch`, and `vcs.feature_branch_prefix` otherwise. Resolve it once
37
+ here and pass the **literal** branch to §3's dispatch — the release agent must not re-derive it.
35
38
  - **Ask the human to confirm** they want to commit, push, and open the PR. Wait for yes.
36
39
  - After the yes: `<core>/pipeline/scripts/kanban-move.sh auto $ARGUMENTS ship`. Report what it
37
40
  printed — `moved #…` or `kanban: <reason>` — never a guess about which happened.
@@ -57,6 +60,9 @@ See SCHEMA.md §Release notes.
57
60
  `release_notes.forbid_levels` (a `0.x` repo forbidding `major` declares the rupture `minor`).
58
61
  - **Ambiguous between two defensible levels?** State your reading in one line and **ask the human to
59
62
  pick** before writing. A wrong bump becomes a published version number.
63
+ - A `kind: patch` spec is a `patch` bump by default — that is what the level means. It is a default,
64
+ not a rule: a bug fix that changes documented behaviour is still a `minor`, and one that removes it
65
+ is still breaking. Say which you chose when it isn't `patch`.
60
66
  - The body describes what changed **for the user**, from the spec §1/§2 — no client names, no internal
61
67
  paths, no exploitable attack vector, no file lists.
62
68
  - If the feature genuinely must move no version, use `release_notes.empty_cmd` instead. Prefer that to
@@ -73,7 +79,7 @@ See SCHEMA.md §Release notes.
73
79
 
74
80
  Spawn one agent (`subagent_type: release`, or the equivalent dispatch for this runtime):
75
81
  "Release feature `$ARGUMENTS` on branch
76
- `<feature_branch_prefix>$ARGUMENTS`. Read `PIPELINE.md` §vcs first. Spec: `specs/$ARGUMENTS.md` (already
82
+ `<the branch §1 resolved>`. Read `PIPELINE.md` §vcs first. Spec: `specs/$ARGUMENTS.md` (already
77
83
  `status: shipped` — stage it). Write conventional commit(s), push (no force), open the PR (use `gh` if
78
84
  `host: github` + available; else emit the compare URL + drafted PR body from `<core>/templates/pr-body.md`).
79
85
  Stage **all** the feature's changes including `specs/$ARGUMENTS.md` and, if `release_notes.enabled`, the
@@ -101,13 +107,6 @@ with a **grep for `#$ARGUMENTS`** on the board it named (with surrounding headin
101
107
  `grep -B20 '#$ARGUMENTS' | grep '^##'`, or an offset-limited Read around the match): exactly one card,
102
108
  under the `shipped` heading — never re-read the whole board into context.
103
109
 
104
- **Telemetry — the usage ping that closes the funnel.** Chain it onto the verify call above
105
- (`/cohorte-build` §4's shared form, `<phase>` = `ship`, `<seconds>` = `0` — the release agent's duration is
106
- not the pipeline's, `<results>` = `pr` when a PR was created / `compare` when only a compare URL was
107
- emitted). Fire it **after** the release agent reports success, never on an aborted ship — a `ship`
108
- event must mean the feature actually left the pipeline. No board ⇒ still ping, in its own `|| true`
109
- call. Silent no-op without consent; never ask about consent here.
110
-
111
110
  ## 5. After the PR — CI gate + teardown
112
111
 
113
112
  - If `host: github` and `gh` is available, watch the PR's checks (`gh pr checks <url> --watch`) and
@@ -51,12 +51,7 @@ Detect the mode from the pasted content:
51
51
  it. This is just so the human isn't surprised when `/cohorte-build` proposes a new agent.
52
52
  5. When the human validates, **freeze**: write `specs/<id>.md` (`status: frozen`, front-matter filled).
53
53
  Create the file — do not ask the human to. **Postcondition:** `grep -q '^status: frozen' specs/<id>.md`
54
- — if it fails the freeze didn't land; fix it before pointing the human at `/cohorte-build`. Chain the
55
- opt-in usage ping onto the postcondition's Bash call (`/cohorte-build` §4's shared form, `<phase>` =
56
- `spec`, `<seconds>` = `0` — interactive time, not pipeline wall-clock, `<results>` = `frozen`).
57
- Ping only on a **landed** freeze, so the funnel counts specs that exist, not attempts. Mode B does
58
- not ping — it re-enters an already-counted spec, and `/cohorte-fix` covers that loop. Silent no-op without
59
- consent; never ask about consent here.
54
+ — if it fails the freeze didn't land; fix it before pointing the human at `/cohorte-build`.
60
55
  5b. **Record the transverse decisions — the journal, not a summary.** Walk what the interview settled
61
56
  and keep ONLY the decisions that **outlive this feature**: a rule the next spec would otherwise
62
57
  re-litigate or contradict (auth model, id/naming scheme, where a kind of state lives, an error
@@ -98,10 +98,11 @@ Four of the §Reconcile steps matter specifically here:
98
98
 
99
99
  - **Global config seed** (§Reconcile step 5): if `<config>` is absent, seed it
100
100
  from the template so the kanban + shared-vault config has a home. Never clobber an existing filled
101
- file. Report what was seeded. If the existing file has NO `telemetry:` block with a `consent_date`
102
- (pre-telemetry install), top up the block from the template and ask the ONE opt-in consent
103
- question defined in `templates/steps/init-pipeline/02-interview-gaps.md` §Telemetry record the
104
- answer either way so it is never re-asked. Consent is strictly opt-in; "No" is the default.
101
+ file. Report what was seeded. Then **scrub the retired `telemetry:` block** if the existing file
102
+ still has one (every install seeded before 2.3.0 does) one targeted Edit deleting the block and
103
+ its comment header, nothing else touched. That capability was removed in 2.3.0, sender included,
104
+ so the block is dead config: nothing reads it, and an `enabled: true` left sitting in a file the
105
+ human may open reads as though data were still leaving the machine. Say you removed it.
105
106
  - **Kanban sync** (§Reconcile step 6): resolve this project's board with
106
107
  `<core>/pipeline/scripts/kanban-move.sh --check` — it prints either the board path or the exact
107
108
  missing link. **Not linked** → offer to link/create a board (confirm the vault + `<folder>/Tasks.md`,
@@ -0,0 +1,86 @@
1
+ ---
2
+ feature_id: patch-<slug>
3
+ kind: patch # a bug fix, not a feature — /cohorte-build skips contract authoring, /cohorte-ship commits `fix(…)`
4
+ title: <one line: the bug, from the user's side>
5
+ status: draft # draft → frozen → in-progress → in-review → shipped · blocked (see SCHEMA.md §Spec status)
6
+ severity: <critical | major | minor> # drives nothing mechanical; it is what the human triages on
7
+ branch: <patch_branch_prefix><feature_id>
8
+ created: <YYYY-MM-DD>
9
+ reviewed_base: # merge-base sha at the last SHIP verdict — freshness-gate anchor (written by /cohorte-review)
10
+ reviewed_digest: # sha256 (16 hex) of the reviewed source diff vs reviewed_base, specs excluded — /cohorte-ship re-checks
11
+ design_files: [] # omit unless the fix is itself visual
12
+ ---
13
+
14
+ # <one line: the bug, from the user's side>
15
+
16
+ ## 1. Symptom & repro
17
+
18
+ > What the user sees, and the shortest deterministic path to see it. A repro nobody can run is a
19
+ > hypothesis, not a bug — say so here rather than pretending otherwise.
20
+
21
+ - **Observed:** …
22
+ - **Expected:** …
23
+ - **Repro:** 1. … 2. … 3. …
24
+ - **Since / trigger:** <version, commit, or "unknown">
25
+
26
+ ## 2. Impact
27
+
28
+ > Who is affected, how often, and whether there is a workaround. This is what justifies the patch
29
+ > jumping the feature queue — one or two lines.
30
+
31
+ ## 3. Cause
32
+
33
+ > Confirmed or hypothesis — label which. Name the `file:line` if you have it. If the cause is
34
+ > unknown at freeze, say so: the implementer's first job is then to find it, and the review will
35
+ > check that the stated cause matches the diff.
36
+
37
+ ## 4. Regression test (red first)
38
+
39
+ > The heart of a patch spec — it replaces §5 CONTRACT as the thing the diff is checked against.
40
+ > Name the test file and what it asserts. It must fail on the current code and pass after the fix.
41
+
42
+ - **Test:** `<path/to/test>` · asserts: …
43
+ - **Fails today because:** …
44
+
45
+ ## 5. Contract delta
46
+
47
+ > Usually `none`. A fix that *changes* an existing contract entry describes the change here and
48
+ > `/cohorte-build` propagates it to every surface that names it. A fix that needs **new** contract
49
+ > surface area is not a patch — stop and run `/cohorte-spec` instead.
50
+
51
+ `none`
52
+
53
+ ## 6. Surface tasks
54
+
55
+ > One `###` subsection per surface the fix touches — a patch may legitimately span several (an
56
+ > unvalidated field on the API *and* the form that sends it). Each TDD: the §4 test first.
57
+
58
+ ### <surface.key>
59
+
60
+ - …
61
+
62
+ ## 7. Out of scope
63
+
64
+ > The adjacent things this patch deliberately does NOT fix. This is the section that keeps a bug fix
65
+ > from turning into a refactor — the reviewer reads it before flagging what you left alone. Real but
66
+ > out-of-scope findings belong in `specs/refactor-backlog.md`, not in this diff.
67
+
68
+ - …
69
+
70
+ ## 9. Acceptance criteria / DoD
71
+
72
+ > §9, not §8 — the number is load-bearing, so don't renumber it. `review.md` and
73
+ > `implementer.template.md` both name "contract §5, acceptance §9" verbatim, so a patch spec pins
74
+ > those two sections to the feature spec's numbers and just has no §8 (the feature spec has no §7 —
75
+ > a gap in the sequence is normal here).
76
+
77
+ - [ ] The §4 regression test exists, and failed before the fix
78
+ - [ ] Each touched surface's tests (TDD) green
79
+ - [ ] `PIPELINE.md` commands.lint · typecheck · test green
80
+ - [ ] The repro in §1 no longer reproduces
81
+ - [ ] Nothing outside §6's surfaces changed
82
+
83
+ ## Remediation
84
+
85
+ > Filled by `/cohorte-fix` from a REVIEW REPORT; empty otherwise. Each item:
86
+ > `[ ] <SEVERITY> · <file:line> · <spec-violation|quality|security> · <concrete fix>`
@@ -53,13 +53,3 @@ Prefer sensible defaults from Phase 1 as the first (Recommended) option in each
53
53
  > The **kanban** link is user-scoped (it points at a personal vault, so it never goes in the committed
54
54
  > `PIPELINE.md`), but IS wired here because it is per-project — see Phase 4.
55
55
 
56
- - **Telemetry** (optional, machine-scoped — SKIP entirely if `<config>` already
57
- has a `telemetry:` block with a `consent_date`, i.e. the human already answered on this machine).
58
- Ask ONE opt-in question, stating exactly: _"Share anonymous usage stats with the cohorte project?
59
- One ping per pipeline phase, `/cohorte-brainstorm` through `/cohorte-ship`: core version, OS, phase name, duration,
60
- per-surface result counts, and a hash of the feature id — never repo names, paths, code, or IPs.
61
- Setup and maintenance commands never ping. Off by default; withdraw anytime
62
- (`telemetry.enabled: false`); erase your history anytime (SCHEMA.md §Telemetry). Default: No."_
63
- On **yes**: in the global config set `telemetry.enabled: true`, mint `install_id` (`uuidgen`,
64
- lowercase), set `consent_date` (ISO date). On **no**: set `enabled: false` + `consent_date` (so
65
- no future command re-asks). Never touch `endpoint` — it ships with the template.
@@ -27,8 +27,8 @@
27
27
  per-surface `test_cmd`/`lint_cmd`/`typecheck_cmd`/`build_cmd` **and their `*_quiet_cmd`
28
28
  variants** and repo-wide `commands.*` equivalents as `Bash(<cmd>:*)` rules, plus read-only git —
29
29
  `Bash(git status:*)`, `Bash(git diff:*)`, `Bash(git log:*)`, `Bash(git rev-parse:*)` — plus the
30
- shipped pipeline scripts (`Bash(<core>/pipeline/scripts/:*)` — preflight, kanban-move,
31
- telemetry-send; a prefix rule matches a command *starting* with that path, so a `bash <path>`
30
+ shipped pipeline scripts (`Bash(<core>/pipeline/scripts/:*)` — preflight, kanban-move;
31
+ a prefix rule matches a command *starting* with that path, so a `bash <path>`
32
32
  invocation would need its own entry), and the
33
33
  retrieval provider's MCP tools when wired (e.g. `mcp__serena`). Never allowlist anything matching
34
34
  a `gate.ask`/`gate.deny` pattern. Mention the human can widen it later with
@@ -125,7 +125,7 @@ const FINDING = {
125
125
  properties: {
126
126
  severity: { enum: ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] },
127
127
  file: { type: 'string' }, line: { type: 'integer' },
128
- kind: { enum: ['spec-violation', 'quality', 'security'] },
128
+ kind: { enum: ['spec-violation', 'quality', 'security', 'complexity'] },
129
129
  problem: { type: 'string', description: 'one line, no code excerpts' },
130
130
  fix: { type: 'string', description: 'one concrete change, one line' },
131
131
  },
@@ -141,7 +141,7 @@ const DEFERRED = {
141
141
  properties: {
142
142
  severity: { enum: ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] },
143
143
  file: { type: 'string' }, line: { type: 'integer' },
144
- kind: { enum: ['quality', 'security', 'rule'] },
144
+ kind: { enum: ['quality', 'security', 'rule', 'complexity'] },
145
145
  problem: { type: 'string', description: 'one line, no code excerpts' },
146
146
  fix: { type: 'string', description: 'one concrete change, one line' },
147
147
  outOfScope: { type: 'string', description: 'one line: why this feature does not own it' },
@@ -323,12 +323,10 @@ const staging = await agent(
323
323
  `1. Write EXACTLY this content to specs/reports/${feature}.md (overwrite):\n<<<REPORT\n${reportBody}\nREPORT\n` +
324
324
  `2. Append one line to $(dirname "$(git rev-parse --git-common-dir)")/.claude/pipeline-metrics.jsonl: ` +
325
325
  `{"ts":"<ISO now>","feature":"${feature}","phase":"review","seconds":0,"surfaces":{${results.map(r => `"${r.key}":"${verdict}:${r.kept.length}"`).join(',')}}}\n` +
326
- `3. Chain the opt-in usage ping: <core>/pipeline/scripts/telemetry-send.sh review "${feature}" 0 "${verdict}:${kept.length}" || true ` +
327
- '(<core> = .claude if .claude/pipeline/scripts/telemetry-send.sh exists, else ~/.claude; script on neither ⇒ skip the ping).\n' +
328
326
  // Deferred findings must land in the backlog on EVERY verdict — parked only on a
329
327
  // SHIP is parked nowhere the rest of the time, which is the leak this closes.
330
328
  (deferredAll.length
331
- ? `3b. Route the deferred findings to specs/refactor-backlog.md (create it if absent): for each line below, ` +
329
+ ? `3. Route the deferred findings to specs/refactor-backlog.md (create it if absent): for each line below, ` +
332
330
  `append it under the \`## <domain>\` heading named in its prefix (create that heading if absent) — with \`>>\`, ` +
333
331
  `never by rewriting the file, and skip any whose file path + first words already appear there (grep -F first, ` +
334
332
  `they may be left from a prior round or an /cohorte-audit):\n` +
package/install.ps1 CHANGED
@@ -172,8 +172,10 @@ cohorte needs Node >= 18 to install.
172
172
  Copy-Item (Join-Path $src 'profile\cohorte.config.template.yaml') (Join-Path $dest 'pipeline') -Force
173
173
  Copy-Item (Join-Path $src 'scripts\*.template') (Join-Path $dest 'pipeline\scripts') -Force
174
174
  Copy-Item (Join-Path $src 'scripts\kanban-move.sh') (Join-Path $dest 'pipeline\scripts') -Force
175
- Copy-Item (Join-Path $src 'scripts\telemetry-send.sh') (Join-Path $dest 'pipeline\scripts') -Force
176
175
  Copy-Item (Join-Path $src 'scripts\preflight.sh') (Join-Path $dest 'pipeline\scripts') -Force
176
+ # 2.3.0 removed telemetry; copy-over never deletes, so scrub the sender from existing
177
+ # installs. The dead `telemetry:` config block is deleted by /cohorte-update-pipeline.
178
+ Remove-Item (Join-Path $dest 'pipeline\scripts\telemetry-send.sh') -Force -ErrorAction SilentlyContinue
177
179
  Copy-Item (Join-Path $src 'core\agents\implementer.template.md') (Join-Path $dest 'pipeline') -Force
178
180
  if (Test-Path (Join-Path $src 'CHANGELOG.md')) { Copy-Item (Join-Path $src 'CHANGELOG.md') (Join-Path $dest 'pipeline') -Force }
179
181
  [System.IO.File]::WriteAllText((Join-Path $dest 'pipeline\VERSION'), "$ver`n", [System.Text.UTF8Encoding]::new($false))
package/install.sh CHANGED
@@ -126,10 +126,14 @@ copy_core() {
126
126
  cp "$src/profile/cohorte.config.template.yaml" "$dest/pipeline/"
127
127
  cp "$src"/scripts/*.template "$dest/pipeline/scripts/"
128
128
  cp "$src/scripts/kanban-move.sh" "$dest/pipeline/scripts/"
129
- cp "$src/scripts/telemetry-send.sh" "$dest/pipeline/scripts/"
130
129
  cp "$src/scripts/preflight.sh" "$dest/pipeline/scripts/"
131
- chmod +x "$dest/pipeline/scripts/kanban-move.sh" "$dest/pipeline/scripts/telemetry-send.sh" \
130
+ chmod +x "$dest/pipeline/scripts/kanban-move.sh" \
132
131
  "$dest/pipeline/scripts/preflight.sh" 2>/dev/null || true
132
+ # 2.3.0 removed telemetry. Copy-over never deletes, so an existing install would keep an
133
+ # executable that still POSTs to the collector — scrub the script itself. The dead
134
+ # `telemetry:` block in the user's config is not this installer's to parse; the interactive
135
+ # /cohorte-update-pipeline deletes it (SCHEMA.md §Reconcile step 5).
136
+ rm -f "$dest/pipeline/scripts/telemetry-send.sh"
133
137
  cp "$src/core/agents/implementer.template.md" "$dest/pipeline/"
134
138
  [ -f "$src/CHANGELOG.md" ] && cp "$src/CHANGELOG.md" "$dest/pipeline/"
135
139
  printf '%s\n' "$ver" > "$dest/pipeline/VERSION"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cohorte",
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.",
3
+ "version": "2.4.0",
4
+ "description": "Portable, stack-agnostic multi-agent development pipeline for Claude Code, Codex CLI, Cursor, Gemini CLI and OpenCode 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
  },
@@ -25,6 +25,7 @@ vcs:
25
25
  remote: <owner/repo> # e.g. acme/webapp
26
26
  default_branch: <main>
27
27
  feature_branch_prefix: feature/ # branch = <prefix><feature_id>
28
+ patch_branch_prefix: fix/ # same, for /cohorte-patch specs (kind: patch)
28
29
 
29
30
  # ── repo shape ──────────────────────────────────────────────────────────────
30
31
  repo:
package/profile/SCHEMA.md CHANGED
@@ -16,6 +16,7 @@ generic pipeline uses it, so a stateless agent can read/regenerate the profile c
16
16
  | `vcs.remote` | string | release | `owner/repo` for the PR/compare URL. |
17
17
  | `vcs.default_branch` | string | build, review, release | Base branch for diffs + PRs. |
18
18
  | `vcs.feature_branch_prefix` | string | ship, isolation script | `feature/` → branch `feature/<id>`. |
19
+ | `vcs.patch_branch_prefix` | string | ship | Same, for a `kind: patch` spec: `fix/` → branch `fix/patch-<slug>`. Optional — a profile that predates it falls back to `fix/`. |
19
20
  | `repo.layout` | enum | build, audit | `monorepo` (many surfaces) or `single`. |
20
21
  | `repo.workspace_tool` | enum | audit | `turborepo`/`nx`/`none`. |
21
22
  | `retrieval.provider` | enum | init, update-pipeline, implementer | `serena` (default) / `graphify` / `none` — see §Code retrieval. |
@@ -190,11 +191,6 @@ to log it. For what's EXPENSIVE, use Claude Code's own accounting:
190
191
  over the last 24 h / 7 d (e.g. _"Top subagents: frontend 7 %, backend 4 % · Top skills: /cohorte-build 1 %,
191
192
  /cohorte-review 1 %"_). That IS the per-phase ledger — approximate (share-of-total, machine-local, not exact
192
193
  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 `this runtime's settings file`:
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
- and point it at a collector. Metrics `claude_code.token.usage` + `claude_code.cost.usage` carry
196
- `session.id` + model + type (input/output/cacheRead). Subagent tokens roll into the session total;
197
- per-subagent attribution needs traces (`CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1`, beta).
198
194
 
199
195
  **Lead context discipline — the silent bill.** The lead session's conversation history is re-sent as
200
196
  input on EVERY turn; a session that spans spec→build→review→fix without clearing re-pays the
@@ -246,6 +242,25 @@ built-in autonomous driver (`/cohorte-loop`) was retired in 2.2.0, and the human
246
242
  them, and because anything automating the cycle from outside needs somewhere to record "a round is
247
243
  under way" and "a round gave up". Every reader still routes on them; nothing produces them.
248
244
 
245
+ **`kind` — feature (default) or `patch`.** Orthogonal to `status`, and the only other front-matter
246
+ field commands route on. `/cohorte-patch` freezes `specs/patch-<slug>.md` with `kind: patch` from
247
+ `templates/patch.template.md`: a ~60-line bug spec whose §4 **regression test** replaces §5 CONTRACT
248
+ as the thing the diff is checked against. It moves through the same states and the same commands —
249
+ `/cohorte-build` → `/cohorte-review` → `/cohorte-fix`* → `/cohorte-ship` — which is the whole design:
250
+ a patch is a spec, so nothing downstream is special-cased beyond three lines.
251
+
252
+ | what reads `kind: patch` | what it does differently |
253
+ | --- | --- |
254
+ | `/cohorte-build` §1.6 | judges §1 repro + §4 regression test instead of contract completeness; gap check `repro` |
255
+ | `/cohorte-build` §2 | authors no contract when §5 Contract delta is `none` (the usual case) |
256
+ | `/cohorte-ship` §1/§2b/§3 | branches off `vcs.patch_branch_prefix`; a `patch` bump by default; `fix(<scope>)` commit |
257
+
258
+ A patch may span **several surfaces** — one bug, one repro, one spec. What it may never do is add
259
+ **new** contract surface area: two surfaces agreeing on a shape that doesn't exist yet is what §5 is
260
+ for, so `/cohorte-patch` routes that to `/cohorte-spec` instead. Changing an *existing* contract entry
261
+ is a legitimate delta. The patch template keeps contract on **§5** and acceptance on **§9** — the two
262
+ numbers `review.md` and `implementer.template.md` name verbatim — and simply has no §8.
263
+
249
264
  Corollaries worth knowing:
250
265
 
251
266
  - A spec with no front-matter makes every stamp a **silent no-op** — the state is bookkeeping, and the
@@ -477,7 +492,13 @@ files automatically. It works because every generated artifact is a **determinis
477
492
  rots (PATH changes, uninstalls, hand-edits) — and repair whatever fails.
478
493
  5. **Global config seed.** If `<config>` is absent, seed it from the template
479
494
  (`profile/cohorte.config.template.yaml`) so the kanban + shared-vault config has a home. Never
480
- clobber an existing filled file; report what was seeded.
495
+ clobber an existing filled file; report what was seeded. Then **scrub the retired `telemetry:`
496
+ block** if the file still carries one (every install seeded before 2.3.0 does): delete the block
497
+ and its comment header, leaving the rest byte-identical. It is dead config — the sender is gone
498
+ and nothing reads it — but a `telemetry.enabled: true` sitting in a file the human may open reads
499
+ as "this is still sending", which is the one thing it must not imply. This is the single
500
+ exception to "never rewrite the config": a targeted deletion of a block the core no longer
501
+ defines, never a re-seed.
481
502
  6. **Kanban sync.** Run the §Kanban reconcile: link/create the project's board if configured, verify
482
503
  its columns, and backfill/sync cards from `specs/*.md`. See §Kanban.
483
504
  7. **Spec-template top-up.** `specs/_template.md` is seeded once at install and then **never**
@@ -621,6 +642,7 @@ Ideas — so `/cohorte-brainstorm` appends the tag to the picked line before its
621
642
  | `/cohorte-brainstorm` picks it up | `brainstorm` |
622
643
  | `/cohorte-spec` opens (draft) | `spec` |
623
644
  | `/cohorte-spec` freezes (`status: frozen`) | `ready` |
645
+ | `/cohorte-patch` triages / freezes | `spec` → `ready` (card titled `[patch] <title>`, tag `#patch-<slug>`) |
624
646
  | `/cohorte-build` | `building` |
625
647
  | `/cohorte-review` | `review` |
626
648
  | `/cohorte-fix` | `fix` |
@@ -642,60 +664,3 @@ added vs. moved vs. already-correct.
642
664
  per configured column in pipeline order, and the closing `%% kanban:settings %%` block
643
665
  (`{"kanban-plugin":"board","list-collapse":[false,…]}` with one `false` per column).
644
666
 
645
- ## Telemetry — anonymous usage stats, strictly opt-in (GDPR-first)
646
-
647
- Cohorte can send the maintainers anonymous usage pings so the pipeline improves where it's actually
648
- slow. **Nothing is ever sent without explicit consent**: `/cohorte-init-pipeline` (and `/cohorte-update-pipeline` on
649
- pre-telemetry installs) ask ONE question, once per machine, default **No**, and record the answer in
650
- `<config>` §`telemetry` (`enabled`, `install_id`, `consent_date`). The sender —
651
- `pipeline/scripts/telemetry-send.sh` — is a silent no-op unless `enabled: true` AND `install_id` AND
652
- `endpoint` are all set, times out at 2s, and never fails the pipeline. Callers chain it with
653
- `|| true`, so a **missing** script is equally silent: `/cohorte-doctor` check 1 verifies `pipeline/scripts/`
654
- is fully populated.
655
-
656
- **Which commands ping** — the six that make up the feature funnel, and only those. The point is to
657
- see where features stall, so every stage of `idea → PR` reports and nothing else does:
658
-
659
- | phase | fired when | `seconds` | `results` |
660
- | --- | --- | --- | --- |
661
- | `brainstorm` | the return is staged | `0` | — |
662
- | `spec` | a freeze lands (Mode A only) | `0` | `frozen` |
663
- | `build` | after the batch metrics line | wall-clock | `ok,ok` / `error` |
664
- | `review` | after the merged verdict | wall-clock | `<verdict>:<count>` |
665
- | `fix` | after the batch metrics line | wall-clock | `<fixed>/<found>` |
666
- | `ship` | the release agent succeeded | `0` | `pr` / `compare` |
667
-
668
- > Workflow-variant runs (`review.js`) report `seconds: 0` for their phases — only the
669
- > conversational commands measure wall-clock. `results` is a free-text summary field, so both
670
- > forms are valid — but read the `fix` column knowing which path produced it.
671
-
672
- `seconds: 0` marks a phase whose duration is human thinking time, not pipeline wall-clock — the
673
- funnel signal there is the event, not how long it took. `/cohorte-doctor`, `/cohorte-audit`, `/cohorte-refactor`,
674
- `/cohorte-align-ds`, `/cohorte-init-pipeline` and `/cohorte-update-pipeline` **never** ping: they sit outside the funnel, and
675
- keeping them out is what holds the collected set to what the consent text describes.
676
-
677
- **What one event contains** (strict allowlist, ~200 bytes):
678
-
679
- ```json
680
- {"v":1,"install_id":"<random uuid>","ts":"<ISO>","core_version":"1.2.0","os":"Darwin",
681
- "event":"phase","phase":"build","feature_hash":"<sha256[..12] of the feature id>",
682
- "seconds":412,"results":"ok,ok"}
683
- ```
684
-
685
- **What is NEVER sent:** repo/project names, file paths, code, spec content, prompts, emails,
686
- usernames, IP handling client-side. The feature id is hashed (12 hex chars) so cross-feature counts
687
- work without revealing what is being built.
688
-
689
- **GDPR rights, concretely:**
690
-
691
- - **Consent** — opt-in only, recorded with a date; "No" is also recorded so nothing re-asks.
692
- - **Withdrawal** — set `telemetry.enabled: false` in `<config>`; effective on
693
- the next phase, no restart.
694
- - **Erasure** — `/cohorte-doctor` prints your `install_id`; send
695
- `curl -X DELETE <endpoint-origin>/v1/install/<install_id>` and the collector drops every event
696
- for that id (the deployed collector implements this and stores no IPs).
697
- - **Access/portability** — events are keyed by your `install_id`; ask the operator for an export.
698
-
699
- **Collector contract** (any implementation must honor it):
700
- `POST /v1/events` (one JSON event, allowlisted fields) · `DELETE /v1/install/<id>` (erasure) ·
701
- `GET /healthz`. Operators must not retain IP-bearing access logs for the ingest vhost.
@@ -10,22 +10,6 @@
10
10
 
11
11
  enabled: true # cfg:enabled — master switch; false disables every capability below
12
12
 
13
- # ── telemetry (anonymous usage stats — OPT-IN, GDPR-first) ──────────────────
14
- # Strictly opt-in: nothing is ever sent unless YOU said yes to the consent question
15
- # (/cohorte-init-pipeline or /cohorte-update-pipeline ask once per machine; they record your answer here).
16
- # What is sent when enabled: core version, OS, phase name, wall-clock seconds, per-surface
17
- # result counts, and a SHA-256 HASH of the feature id — never repo names, paths, code, spec
18
- # content, emails, or IPs. One ping per pipeline phase, /cohorte-brainstorm through /cohorte-ship; setup and
19
- # maintenance commands (/cohorte-doctor, /cohorte-init-pipeline, /cohorte-update-pipeline, /cohorte-audit, /cohorte-refactor,
20
- # /cohorte-align-ds) never ping. Sent fire-and-forget (2s timeout, silent on failure) by
21
- # pipeline/scripts/telemetry-send.sh. Withdraw anytime: set enabled: false. Erase your history:
22
- # see SCHEMA.md §Telemetry (DELETE by install_id).
23
- telemetry:
24
- enabled: false # cfg:telemetry_enabled — true ONLY via explicit consent
25
- install_id: "" # cfg:telemetry_install_id — random UUID minted at consent; the erasure key
26
- consent_date: "" # cfg:telemetry_consent_date — ISO date the human said yes
27
- endpoint: "https://telemetry.cohorte.thebidouille.fr/v1/events" # cfg:telemetry_endpoint — collector URL; empty = off even if enabled
28
-
29
13
  # ── obsidian ──────────────────────────────────────────────────────────────────
30
14
  # Local vault used by the kanban mirror — defined here ONCE so no capability ever duplicates
31
15
  # the path. Absolute path to the folder you open in Obsidian.
@@ -25,7 +25,7 @@ const frontmatter = (text) => {
25
25
  const PINNED = ["cohorte-build", "cohorte-review", "cohorte-fix", "cohorte-ship",
26
26
  "cohorte-audit", "cohorte-refactor", "cohorte-doctor", "cohorte-align-ds",
27
27
  "cohorte-update-pipeline"];
28
- const UNPINNED = ["cohorte-brainstorm", "cohorte-spec", "cohorte-init-pipeline"];
28
+ const UNPINNED = ["cohorte-brainstorm", "cohorte-spec", "cohorte-init-pipeline", "cohorte-patch"];
29
29
 
30
30
  // Every command must carry the `cohorte-` prefix. This replaces the old RESERVED
31
31
  // blocklist, which chased collisions one name at a time and always lagged: a command
@@ -129,26 +129,28 @@ const steps = join(root, "core/templates/steps/init-pipeline");
129
129
  if (!existsSync(steps) || readdirSync(steps).length === 0)
130
130
  fail("core/templates/steps/init-pipeline", "router step files missing/empty");
131
131
 
132
- // ── telemetry coverage ──────────────────────────────────────────────────────
133
- // The funnel is only readable if every one of its stages pings — a single missing
134
- // one silently truncates it (that is how /cohorte-review and /cohorte-fix went unreported
135
- // until 1.2.3). The phase list here must match SCHEMA.md §Telemetry's table.
136
- // These are telemetry PHASE names, not command names they stay unprefixed even though
137
- // the commands that emit them are now `/cohorte-*`. The phase is a wire field allowlisted
138
- // in telemetry-send.sh and keyed on by the collector's existing dataset; prefixing it would
139
- // orphan every ping ever sent. Command file = PREFIX + phase.
140
- const FUNNEL = ["brainstorm", "spec", "build", "review", "fix", "ship"];
141
- for (const c of FUNNEL)
142
- if (!/usage ping/i.test(read(`core/commands/${PREFIX}${c}.md`)))
143
- fail(`core/commands/${PREFIX}${c}.md`, "funnel command with no usage ping breaks the telemetry funnel");
144
- // …and nothing outside the funnel may ping (consent text scopes it to the funnel).
145
- for (const f of readdirSync(join(root, "core/commands"))) {
146
- const c = f.replace(/\.md$/, "").replace(new RegExp(`^${PREFIX}`), "");
147
- // `telemetry-send.sh` + an argument = a call site; the bare filename (e.g. /cohorte-doctor
148
- // listing the scripts it checks for) is a mention, not a ping.
149
- if (!FUNNEL.includes(c) && /telemetry-send\.sh +\S|usage ping/i.test(read(`core/commands/${f}`)))
150
- fail(`core/commands/${f}`, "non-funnel command pings telemetry — outside the consented scope");
151
- }
132
+ // ── no telemetry ────────────────────────────────────────────────────────────
133
+ // Telemetry was removed wholesale in 2.3.0: the shipped `telemetry-send.sh`, the
134
+ // per-phase pings, the consent question, the `telemetry:` config block, the collector
135
+ // endpoint. This check is the ratchet it fails if any of it creeps back into the
136
+ // core, which is easy to do by copying an old command file that still chains a ping.
137
+ // Deliberately broad: the whole point is that there is nothing left to send with.
138
+ const NO_TELEMETRY = /telemetry|usage ping/i;
139
+ // One exemption, and it is the opposite of a regression: /cohorte-update-pipeline is what
140
+ // DELETES the leftover `telemetry:` block from configs seeded before 2.3.0, so it is the one
141
+ // file that must still name the thing. Narrow on purpose — a filename, not a pattern.
142
+ const TELEMETRY_SCRUBBER = "core/commands/cohorte-update-pipeline.md";
143
+ const walk = (dir) => readdirSync(join(root, dir), { withFileTypes: true }).flatMap((e) =>
144
+ e.isDirectory() ? walk(`${dir}/${e.name}`) : [`${dir}/${e.name}`]);
145
+ for (const dir of ["core/commands", "core/agents", "core/templates", "core/workflows"])
146
+ for (const rel of walk(dir)) {
147
+ if (!/\.(md|js)$/.test(rel) || rel === TELEMETRY_SCRUBBER) continue;
148
+ if (NO_TELEMETRY.test(read(rel)))
149
+ fail(rel, "mentions telemetry it was removed in 2.3.0; nothing may ping or ask for consent");
150
+ }
151
+ // …and the exempt file may only REMOVE it: naming a send/ping/consent path there is still a bug.
152
+ if (/usage ping|telemetry-send|consent/i.test(read(TELEMETRY_SCRUBBER)))
153
+ fail(TELEMETRY_SCRUBBER, "may reference the retired telemetry block only to delete it — no ping, sender or consent flow");
152
154
 
153
155
  // ── kanban call sites ───────────────────────────────────────────────────────
154
156
  // Every pipeline stage moves a card, and a stage that only *describes* the move
@@ -158,7 +160,7 @@ for (const f of readdirSync(join(root, "core/commands"))) {
158
160
  // having opened neither the config nor PIPELINE.md, and a merged feature's card
159
161
  // stayed in "Ready to build". `kanban-move.sh auto` moved resolution into the
160
162
  // script; this keeps it there. Prose is not a call site — the literal invocation is.
161
- const KANBAN_STAGES = ["brainstorm", "spec", "build", "review", "fix", "ship"];
163
+ const KANBAN_STAGES = ["brainstorm", "spec", "build", "review", "fix", "ship", "patch"];
162
164
  for (const c of KANBAN_STAGES) {
163
165
  const path = `core/commands/${PREFIX}${c}.md`;
164
166
  const text = read(path);
@@ -174,7 +176,7 @@ for (const c of KANBAN_STAGES) {
174
176
  // ── shipped scripts ─────────────────────────────────────────────────────────
175
177
  // Every scripts/*.sh must be copied by BOTH shell installers. Callers chain these
176
178
  // with `|| true`, so one an installer forgets is a silent no-op forever — no kanban
177
- // card moves, no telemetry ping, no error. CI is the only place this is loud.
179
+ // card moves, no error. CI is the only place this is loud.
178
180
  // The third installer, bin/cli.js (what `npx cohorte` runs), copies by rule rather
179
181
  // than by name, so grepping for filenames can't see it — ci.yml dry-runs it into a
180
182
  // scratch HOME and asserts the same postconditions instead. Both are needed: this
@@ -1,84 +0,0 @@
1
- #!/bin/sh
2
- # telemetry-send.sh — fire-and-forget anonymous usage ping (SCHEMA.md §Telemetry).
3
- #
4
- # telemetry-send.sh <phase> <feature_id> <seconds> [results]
5
- # phase brainstorm|spec|build|review|fix|ship — the feature funnel, and only it.
6
- # Setup/maintenance commands never ping (SCHEMA.md §Telemetry).
7
- # feature the feature id — NEVER sent raw; SHA-256-hashed to 12 hex chars
8
- # seconds batch wall-clock
9
- # results optional compact summary, e.g. "ok,ok" or "REVISE:3"
10
- #
11
- # GDPR posture (documented in SCHEMA.md §Telemetry):
12
- # - STRICTLY opt-in: exits silently unless ~/.claude/cohorte.config.yaml has
13
- # telemetry.enabled: true AND a non-empty endpoint AND an install_id
14
- # (all three written only by the explicit consent flow).
15
- # - Data minimization: no repo names, no paths, no code, no IP handling client-side.
16
- # - Never blocks or fails the pipeline: 2s timeout, all errors swallowed, exit 0 always.
17
- set -u
18
-
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
27
-
28
- # read keys scoped to the `telemetry:` block only
29
- tval() {
30
- awk -v key="$1" '
31
- /^telemetry:/ { in_t=1; next }
32
- /^[a-zA-Z]/ { in_t=0 }
33
- in_t && $1 == key":" {
34
- v=$2; gsub(/^"|"$|#.*/,"",v); gsub(/"/,"",v); print v; exit
35
- }' "$cfg"
36
- }
37
-
38
- [ "$(tval enabled)" = "true" ] || exit 0
39
- endpoint="$(tval endpoint)"; [ -n "$endpoint" ] || exit 0
40
- install_id="$(tval install_id)"; [ -n "$install_id" ] || exit 0
41
-
42
- phase="${1:-}"; feature="${2:-}"; seconds="${3:-0}"; results="${4:-}"
43
- [ -n "$phase" ] || exit 0
44
-
45
- # Allowlist the phase here — the collector accepts any string, so a typo in a command
46
- # file would silently pollute the dataset with a phantom phase nobody notices.
47
- case "$phase" in
48
- brainstorm|spec|build|review|fix|ship) ;;
49
- # `smoke` is a RETIRED phase (removed in 1.5.0) — still accepted so a stale install
50
- # pinging it lands in its own bucket instead of being silently dropped.
51
- smoke) ;;
52
- *) exit 0 ;;
53
- esac
54
-
55
- # The payload is hand-built JSON: a non-numeric seconds or a quote in results
56
- # would silently produce an invalid document the collector drops.
57
- case "$seconds" in ''|*[!0-9]*) seconds=0 ;; esac
58
- results=$(printf '%s' "$results" | tr -d '"\\\n\r' | cut -c1-80)
59
-
60
- if command -v shasum >/dev/null 2>&1; then
61
- fhash=$(printf '%s' "$feature" | shasum -a 256 | cut -c1-12)
62
- else
63
- fhash=$(printf '%s' "$feature" | sha256sum | cut -c1-12)
64
- fi
65
- # The core that shipped THIS script is the one whose version we report. Bundled
66
- # installs live at <project>/.claude/pipeline/scripts/, where the global VERSION is
67
- # either absent or a DIFFERENT core — reading only the global path made every
68
- # bundled repo report an empty core_version.
69
- here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd 2>/dev/null) || here=""
70
- ver=""
71
- for v in "$here/../VERSION" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/pipeline/VERSION" \
72
- "$HOME/.cohorte/pipeline/VERSION"; do
73
- [ -n "$ver" ] && break
74
- ver=$(head -1 "$v" 2>/dev/null | tr -d '"\\')
75
- done
76
- os=$(uname -s 2>/dev/null || echo unknown)
77
- ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
78
-
79
- payload=$(printf '{"v":1,"install_id":"%s","ts":"%s","core_version":"%s","os":"%s","event":"phase","phase":"%s","feature_hash":"%s","seconds":%s,"results":"%s"}' \
80
- "$install_id" "$ts" "$ver" "$os" "$phase" "$fhash" "${seconds:-0}" "$results")
81
-
82
- curl -s -m 2 -X POST -H 'content-type: application/json' \
83
- -d "$payload" "$endpoint" >/dev/null 2>&1 || true
84
- exit 0