cohorte 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,59 @@ short, user-facing, most recent first. One `## <version> — <YYYY-MM-DD>` secti
7
7
  > They are history and are deliberately not rewritten — every command gained a `cohorte-` prefix
8
8
  > in 2.0.0.
9
9
 
10
+ ## 2.3.0 — 2026-08-12
11
+
12
+ - **A bug fix had to pretend to be a feature.** The only way into the pipeline was
13
+ `/cohorte-spec` — a section-by-section interview whose critical part is a frozen §5 contract, with
14
+ a ~300-line budget. For a `500` on an empty cart that is more work than the fix, so in practice you
15
+ left the pipeline and patched by hand: no frozen intent, no review, no trace.
16
+
17
+ `/cohorte-patch` is the bug-fix entry point. A **triage, not an interview** — repro, expected
18
+ behaviour, what must not change — it locates the cause itself and freezes
19
+ `specs/patch-<slug>.md` (`kind: patch`, ~60 lines). The **§4 regression test replaces §5 CONTRACT**
20
+ as the thing the diff is checked against.
21
+
22
+ Then nothing downstream is special-cased: a patch spec *is* a spec, so `/cohorte-build` →
23
+ `/cohorte-review` → `/cohorte-fix` → `/cohorte-ship` consume it unchanged, one `/clear` between
24
+ each — four short sessions instead of one thread re-sending its own triage history at input price
25
+ every turn. Only three places read `kind: patch`: build §1.6/§2 (judge the repro + test, author no
26
+ contract when §5 is `none`), the `review` agent (scope creep becomes a first-class finding), and
27
+ ship (branch off `vcs.patch_branch_prefix`, `patch` bump by default, `fix(<scope>)` commit).
28
+
29
+ A patch may span **several surfaces** — one bug, one repro, one spec. The single hard escalation:
30
+ a fix needing **new** contract surface area is a feature wearing a bug's clothes, and it is routed
31
+ to `/cohorte-spec` rather than letting two surfaces invent a shape independently.
32
+
33
+ - **Telemetry is gone — all of it.** The opt-in usage pings that shipped through 2.2.0 are removed
34
+ wholesale: the `telemetry-send.sh` sender, the per-phase pings in the six funnel commands, the
35
+ consent question in `/cohorte-init-pipeline`, the `telemetry:` block in the config template, the
36
+ `/cohorte-doctor` consent check, the collector contract in `SCHEMA.md`. Cohorte now sends nothing,
37
+ anywhere.
38
+
39
+ **Upgrading removes what is already on disk.** Copy-over never deletes, so an existing install
40
+ would otherwise keep an executable that still POSTs to the collector — all three installers
41
+ (`install.sh`, `install.ps1`, `npx cohorte`) now scrub it, and CI asserts its absence. And
42
+ `/cohorte-update-pipeline` deletes the leftover `telemetry:` block from
43
+ `~/.claude/cohorte.config.yaml`: nothing reads it any more, and an `enabled: true` left sitting in
44
+ a file you may open reads as though data were still leaving the machine.
45
+
46
+ The OpenTelemetry tip (Claude Code's own metrics export, a collector you point at yourself) is
47
+ gone from `SCHEMA.md` §Measuring cost and the token-economy guide too. `/cost` and
48
+ `pipeline-metrics.jsonl` remain — both entirely local.
49
+
50
+ `validate-core.mjs` gained a ratchet: any mention of telemetry or a usage ping under
51
+ `core/{commands,agents,templates,workflows}` now fails CI, so it cannot creep back in by copying an
52
+ old command file. The single exemption is `/cohorte-update-pipeline` — the command that *deletes*
53
+ the leftovers has to name them — and it is itself checked for never naming a ping, a sender or a
54
+ consent flow.
55
+
56
+ - **Kanban:** `/cohorte-patch` with no argument offers the **Ideas** column, `[patch]`-titled cards
57
+ first, and titles its own card `[patch] <title>` (join key `#patch-<slug>`). The `[<kind>]` prefix
58
+ stays a human convention — nothing parses it.
59
+
60
+ - **Profile:** new optional `vcs.patch_branch_prefix` (default `fix/`); `/cohorte-update-pipeline`
61
+ tops it up, and a profile without it falls back to the same default.
62
+
10
63
  ## 2.2.0 — 2026-08-12
11
64
 
12
65
  - **The pipeline was Claude Code or nothing.** The doctrine — frozen spec, stateless surfaces, a
package/README.md CHANGED
@@ -256,6 +256,7 @@ it in `.claude/pipeline/VERSION` and bundled repos in their committed `pipeline.
256
256
  | `/cohorte-init-pipeline` | Detect stack → interview → generate the profile + agents. Run once per project. |
257
257
  | `/cohorte-brainstorm` | Interactive persona panel that pressure-tests a feature idea. |
258
258
  | `/cohorte-spec` | Freeze the feature spec + contract into `specs/<id>.md` (UI features also get a standalone design brief at `specs/design/<id>.md`). Also applies review returns. |
259
+ | `/cohorte-patch [bug]` | Bug-fix entry: triage a bug and freeze a ~60-line patch spec (`specs/patch-<slug>.md`) whose regression test replaces the contract. Then the normal `/cohorte-build → review → ship`. |
259
260
  | `/cohorte-build <id>` | Readiness gate on the frozen spec, then the lead authors the contract and dispatches one implementer per surface in parallel. |
260
261
  | `/cohorte-review <id>` | Read-only review agents (one per touched surface, parallel) audit the diff vs the spec; out-of-scope findings go to the refactor backlog. |
261
262
  | `/cohorte-fix <id>` | Apply a review report: remediation into the spec, re-dispatch only the surfaces with findings. |
@@ -344,15 +345,13 @@ The essentials:
344
345
 
345
346
  Details: `profile/SCHEMA.md` §Workflows.
346
347
 
347
- ## Privacy — opt-in telemetry
348
+ ## Privacy
348
349
 
349
- Cohorte can send **anonymous** usage pings (core version, OS, phase name, duration, per-surface
350
- result counts, and a *hash* of the feature id — never repo names, paths, code, or IPs). It is
351
- **strictly opt-in**: `/cohorte-init-pipeline` asks once per machine, the default is No, and both answers are
352
- recorded so you're never re-asked. Withdraw anytime (`telemetry.enabled: false` in
353
- `~/.claude/cohorte.config.yaml`); erase your history anytime (`/cohorte-doctor` prints your `install_id`,
354
- the collector's `DELETE /v1/install/<id>` drops it). Full spec + GDPR details:
355
- `profile/SCHEMA.md` §Telemetry (including the collector API contract).
350
+ Cohorte sends nothing, anywhere. There is no telemetry, no usage pings, no collector — the
351
+ opt-in stats that shipped through 2.2.0 were removed in 2.3.0, sender included. Everything the
352
+ pipeline records (`pipeline-metrics.jsonl`, `specs/reports/`) stays in your repo, and the only
353
+ network calls are the ones you can see: `git`, `gh`, and whatever MCP providers you wired
354
+ yourself.
356
355
 
357
356
  ## License
358
357
 
@@ -380,7 +379,7 @@ profile/
380
379
  PIPELINE.template.md # the profile skeleton /cohorte-init-pipeline fills
381
380
  SCHEMA.md # field reference
382
381
  cohorte.config.template.yaml # seeds ~/.claude/cohorte.config.yaml (kanban)
383
- scripts/ # worktree-isolation templates + shipped preflight/kanban/telemetry scripts
382
+ scripts/ # worktree-isolation templates + the shipped preflight/kanban scripts
384
383
  dashboard/ # local web cockpit (npx … dashboard) — see dashboard/README.md
385
384
  server/ # dependency-free node runtime (serves the built app + JSON/stream API)
386
385
  app/ # Vite + React source (built to dashboard/dist/ at publish time)
package/bin/cli.js CHANGED
@@ -237,10 +237,10 @@ function copyCore() {
237
237
  // write where this runtime does not look.
238
238
  resolveTemplateConditionals(pipelineDir);
239
239
  // Copy the *.template files AND the shipped executables (kanban-move.sh,
240
- // telemetry-send.sh). Until 1.2.4 this loop took only `.template`, so every
240
+ // preflight.sh). Until 1.2.4 this loop took only `.template`, so every
241
241
  // `npx cohorte install/update` produced a core missing both scripts — and since
242
242
  // every caller chains them with `|| true`, the result was silent: no kanban card
243
- // moves, no telemetry pings, no error. The shell installers named them explicitly
243
+ // moves, no error. The shell installers named them explicitly
244
244
  // and this port drifted. The rule below needs no list to keep in sync: a `<x>.sh`
245
245
  // with an `<x>.sh.template` sibling is rendered per-project by /cohorte-init-pipeline, so
246
246
  // only the template ships; every other `.sh` is a shipped executable.
@@ -255,6 +255,12 @@ function copyCore() {
255
255
  try { fs.chmodSync(target, 0o755); } catch { /* optional */ }
256
256
  }
257
257
  }
258
+ // 2.3.0 removed telemetry. The copy loop above is by-rule, so it simply stops shipping the
259
+ // sender — but copy-over never deletes, and an existing install would keep an executable
260
+ // that still POSTs to the collector. Scrub it. The dead `telemetry:` block in the user's
261
+ // config is not this installer's to parse: /cohorte-update-pipeline deletes it, where an
262
+ // agent can edit the YAML surgically (SCHEMA.md §Reconcile step 5).
263
+ fs.rmSync(path.join(pipelineDir, 'scripts', 'telemetry-send.sh'), { force: true });
258
264
  // The per-surface implementer template is rendered per SURFACE later, by
259
265
  // /cohorte-init-pipeline inside the target repo — but its runtime shape (frontmatter keys,
260
266
  // capability branches, the preamble) is fixed here, at install time. Run it through the
@@ -196,7 +196,7 @@ function stateDir(runtime) {
196
196
  return runtime.id === 'claude' ? '.claude' : '.cohorte';
197
197
  }
198
198
 
199
- // The user-level config (kanban boards, shared vault, telemetry consent). One file per
199
+ // The user-level config (kanban boards, shared vault). One file per
200
200
  // human, not per project or per runtime; the shipped scripts probe the same two paths.
201
201
  function configPath(runtime) {
202
202
  return runtime.id === 'claude' ? '~/.claude/cohorte.config.yaml' : '~/.cohorte/cohorte.config.yaml';
@@ -213,7 +213,7 @@ function preamble(runtime, paths, projectRoot, { kind = 'command' } = {}) {
213
213
  L.push(`> - \`<core>\` = \`${core}\` — the pipeline's shared assets (\`pipeline/scripts/\`, \`pipeline/SCHEMA.md\`, \`templates/\`). Every \`<core>/…\` path below resolves there, and nowhere else.`);
214
214
  L.push(`> - \`<state>\` = \`${stateDir(runtime)}/\` in **this repo** — what the pipeline generates for this project (\`gate-config.json\`, \`preflight.ok\`, \`pipeline-metrics.jsonl\`, \`pipeline.json\`). Always project-relative, even when \`<core>\` is global.`);
215
215
  L.push(`> - \`<memory>\` = \`${runtime.memory}\` — this runtime's project-instructions file at the repo root, loaded into every session here. Where the doctrine says to reference or extend it, that is the file.`);
216
- L.push(`> - \`<config>\` = \`${configPath(runtime)}\` — your user-level config (kanban boards, shared vault, telemetry consent). One per human, never committed.`);
216
+ L.push(`> - \`<config>\` = \`${configPath(runtime)}\` — your user-level config (kanban boards, shared vault). One per human, never committed.`);
217
217
 
218
218
  L.push(`> - \`<agents>\` = \`${agentsDir}\` — real subagents. Dispatch: ${runtime.agent.dispatch}.`);
219
219
 
@@ -28,7 +28,9 @@ committing, pushing, and opening the PR. You do **not** write features.
28
28
  ## Your inputs
29
29
 
30
30
  1. The spec path `specs/<id>.md` (title, goal, contract — for the PR body).
31
- 2. `feature_id` and the branch `<vcs.feature_branch_prefix><id>`.
31
+ 2. `feature_id` and the branch — the lead passes it literally in the dispatch; use that, don't
32
+ re-derive it (a `kind: patch` spec branches off `vcs.patch_branch_prefix`, not
33
+ `feature_branch_prefix`).
32
34
  3. If `PIPELINE.md` §`release_notes.enabled`, the already-written note at
33
35
  `<release_notes.dir>/<release_notes.filename>` — stage it with everything else.
34
36
 
@@ -37,7 +39,9 @@ committing, pushing, and opening the PR. You do **not** write features.
37
39
  1. Sanity-check: `git status`, `git diff --stat`. Confirm you're on the feature branch (not the default
38
40
  branch). Confirm no `.env`/secret files staged.
39
41
  2. Stage the feature changes and write **conventional commit(s)**: `feat(<scope>): …` / `fix(<scope>): …`,
40
- body summarizing what shipped, referencing `feature_id`. Scope from the domain. End the commit body with:
42
+ body summarizing what shipped, referencing `feature_id`. Scope from the domain. A spec whose
43
+ front-matter carries `kind: patch` is a bug fix — `fix(<scope>): …`, and the body states the
44
+ symptom it stops, not the code it changed. End the commit body with:
41
45
  `Co-Authored-By: Claude <noreply@anthropic.com>`
42
46
  3. `git push -u origin <branch>` (plain push, no force).
43
47
  4. Open the PR against `vcs.default_branch`:
@@ -47,6 +47,13 @@ be precise and self-contained.
47
47
  1. **Spec conformance (highest priority).** Does the implementation match the frozen contract exactly —
48
48
  every endpoint/interface (method, path, auth, request/response shape, status codes, error cases) and
49
49
  every acceptance criterion? Any deviation is a finding. Cross-surface calls must match the contract.
50
+ **On a `kind: patch` spec** (front-matter) the contract is usually `none` and this check reads
51
+ differently: the diff must (a) contain the §4 regression test, asserting what §4 says it asserts,
52
+ (b) actually address the §1 repro through the §3 cause — a fix that suppresses the symptom
53
+ elsewhere is a finding, not a fix — and (c) stay inside §7 Out of scope. **Scope creep is a
54
+ first-class finding on a patch**: a correct, tidy improvement the spec did not ask for still
55
+ widens the blast radius of a change that is shipping fast, so report it rather than waving it
56
+ through. Genuinely-good-but-out-of-scope work is a **deferred** finding, exactly as elsewhere.
50
57
  2. **Correctness.** Logic bugs, unhandled errors, validation gaps, auth holes, data exposure.
51
58
  3. **Security.** Authz on every entry point, input validation, no secret/PII leakage, no injection.
52
59
  A security vulnerability ⇒ verdict **BLOCK**.
@@ -64,8 +64,3 @@ downstream. Then, in this order:
64
64
  2. `<core>/pipeline/scripts/kanban-move.sh auto <feature_id> brainstorm --title "<human title>"` —
65
65
  which moves the (now tagged) card, or creates one under `--title` if the human typed a fresh
66
66
  idea. Read its output: `moved #…` or `kanban: <reason>`. Never assume either.
67
-
68
- **Telemetry:** chain the opt-in usage ping onto that same Bash call — `/cohorte-build` §4's shared form,
69
- `<phase>` = `brainstorm`, `<seconds>` = `0` (this phase is human thinking time, not pipeline
70
- wall-clock — the funnel signal is the event, not its duration), `<results>` = `""`. No board ⇒ still
71
- ping, in its own `|| true` call. Silent no-op without consent; never ask about consent here.
@@ -26,6 +26,11 @@ You are the **lead**. Build feature **$ARGUMENTS** from its frozen spec.
26
26
  `/cohorte-spec` first. Only then read the body, selectively: front-matter, §5 contract, the surface
27
27
  task sections, and `## Remediation` (fall back to a full read if the spec doesn't follow the
28
28
  template's headings).
29
+ - **`kind: patch` in the front-matter ⇒ this is a bug fix** frozen by `/cohorte-patch`, and it is
30
+ built by this command like any other spec, with two differences called out where they apply: §1.6
31
+ judges it against its regression test instead of a contract, and §2 authors no contract unless §5
32
+ carries a delta. Everything else — surface mapping, parallel dispatch, roll call, metrics — is
33
+ identical, and `kind` absent means feature, so nothing changes for existing specs.
29
34
  - **Route check** — if `## Remediation` has open `- [ ]` items and none requires a contract change,
30
35
  stop and tell the human to run `/cohorte-fix $ARGUMENTS` instead: it re-dispatches only the surfaces with
31
36
  findings. A full build with open items is only right when the contract change ripples into clean
@@ -79,6 +84,13 @@ good idea (that was `/cohorte-brainstorm`), never by re-reading files you don't
79
84
  request fields with types + validation, the success shape, and its error cases. A missing
80
85
  **request or success shape** ⇒ `NOT-READY` (an implementer would invent it, and the other surface
81
86
  would invent a different one). A missing **error case** ⇒ `RESERVATIONS`.
87
+ **On a `kind: patch` spec this check is replaced, not skipped** — a patch has no contract to be
88
+ complete, so judge §1 Symptom & repro + §4 Regression test instead: no stated expected behaviour,
89
+ or a §4 that names no test and no reason the fix would be verifiable ⇒ `NOT-READY` (an implementer
90
+ would fix whatever it guessed the bug was, and nothing would catch a wrong guess). A repro
91
+ explicitly frozen as a hypothesis, or a cause left to the implementer to find, is
92
+ `RESERVATIONS` — normal for a patch, never a blocker. Then judge §5 as above **only** if it
93
+ carries a delta rather than `none`.
82
94
  2. **Surface coverage** — every §6 task maps to a surface in the reconciled list, and every contract
83
95
  entry has an owner **on each side it names** (producer and consumer). A contract entry no surface
84
96
  owns ⇒ `NOT-READY`.
@@ -103,7 +115,8 @@ this gate and any automated driver, which parses no prose:
103
115
  ```
104
116
 
105
117
  - **`gaps`** — one normalized string per gap, `<check>|<where>|<what is missing>`: `<check>` is
106
- `contract` · `coverage` · `dependency` · `ambiguity` · `design`; `<where>` is the contract entry,
118
+ `contract` · `coverage` · `dependency` · `ambiguity` · `design` plus `repro` on a `kind: patch`
119
+ spec, for a gap check 1 raised against §1/§4; `<where>` is the contract entry,
107
120
  surface key or dependency name (no `:line` — it shifts on every edit); `<what>` is the gap, not the
108
121
  fix. `READY` ⇒ `[]`.
109
122
  - **`NOT-READY` ⇒ STOP: author no contract and spawn NO agent.** Print the gaps and send the human to
@@ -117,6 +130,15 @@ this gate and any automated driver, which parses no prose:
117
130
 
118
131
  ## 2. Author the contract (lead-only — the single sync channel)
119
132
 
133
+ _Skipped entirely on a `kind: patch` spec whose §5 Contract delta is `none`_ — which is the usual
134
+ case: a bug fix corrects behaviour behind a shape that already exists, and re-authoring that shape
135
+ would put the contract file in the diff for nothing. Say you skipped it and why, in one line. A patch
136
+ whose §5 **does** carry a delta is authored exactly as below, from the delta, against the existing
137
+ file — never rewritten from scratch. (A patch needing *new* contract surface area never reaches here:
138
+ `/cohorte-patch` §3 routes it to `/cohorte-spec`.) When you skip, still run `date +%s` on its own —
139
+ the skipped postcondition is where §4's wall-clock start comes from, and a build with no start epoch
140
+ writes a metrics line with no duration.
141
+
120
142
  _Only if `contract.enabled`._ From §5 of the spec, write/update the feature's contract file at
121
143
  `<contract.path>/$ARGUMENTS.<contract.ext>` in the profile's `mechanism` (e.g. Zod v4 schemas + inferred
122
144
  types for `shared-types-zod`). Export it from `contract.index` if set. This is the ONLY file the agents
@@ -186,12 +208,7 @@ anything went wrong. In the same call write the machine-readable batch result to
186
208
  `specs/reports/$ARGUMENTS.build.json` (overwrite) — the channel an automated driver reads, since it
187
209
  never sees your chat:
188
210
  `{"id":"$ARGUMENTS","phase":"build","ts":"<ISO>","surfaces":{"<key>":"ok|error|dead",…},"dead":["<key>",…]}`
189
- — this is the evidence SCHEMA.md §Specialization asks for before splitting a surface. In the same
190
- Bash call, chain the opt-in usage ping — **the shared form every phase command reuses**:
191
- `<core>/pipeline/scripts/telemetry-send.sh <phase> "$ARGUMENTS" <seconds> "<results>" || true` — a silent no-op unless the human explicitly consented (SCHEMA.md §Telemetry);
192
- never ask about consent here. `/cohorte-review` and `/cohorte-fix` chain the same line with their own
193
- phase + results. The `|| true` swallows a **missing** script too, so a half-copied core goes
194
- silent rather than loud — `/cohorte-doctor` check 1 is what catches that.
211
+ — this is the evidence SCHEMA.md §Specialization asks for before splitting a surface.
195
212
  Then tell the human: exercise the feature by hand if it's worth it, then run `/cohorte-review $ARGUMENTS` —
196
213
  unless a surface is dead, in which case say so first and let them decide whether to re-run `/cohorte-build`
197
214
  (a dead surface has no findings, so `/cohorte-fix` has nothing to re-dispatch).
@@ -30,10 +30,10 @@ fix only with the human's go-ahead (or hand them the command).
30
30
  commands' step files are present — `templates/steps/init-pipeline/` non-empty (a router whose
31
31
  `templates/steps/<cmd>/` dir is missing is a partial/stale install ⇒
32
32
  re-run install/update). **Shipped scripts present and executable** in `<core>/pipeline/scripts/`:
33
- `kanban-move.sh`, `telemetry-send.sh`, `preflight.sh`,
33
+ `kanban-move.sh`, `preflight.sh`,
34
34
  `new-feature.sh.template`, `remove-feature.sh.template` — ❌ any missing one.
35
35
  Every caller chains these with `|| true`, so an absent script is a **silent**
36
- no-op (no kanban card moves, no telemetry ping, no error anywhere) — this check is the only thing
36
+ no-op (no kanban card moves, no error anywhere) — this check is the only thing
37
37
  that sees it. Also flag ❌ a `VERSION` **newer than** the other `pipeline/` files (compare mtimes):
38
38
  a version bumped without a full re-copy is a half-done update ⇒ re-run install/update.
39
39
  2. **Profile.** `PIPELINE.md` exists and its `yaml pipeline-profile` block parses. Every
@@ -51,7 +51,7 @@ fix only with the human's go-ahead (or hand them the command).
51
51
  (`build`, `review`, `fix`, `ship`, `audit`, `refactor`, `doctor`, `align-ds`,
52
52
  `update-pipeline` — in `<commands>/`) carries `model: sonnet` in
53
53
  its frontmatter — ⚠️ if missing (the lead's orchestration turn then bills at the session model,
54
- e.g. Opus/Fable). `brainstorm`, `spec`, and `init-pipeline` are intentionally unpinned
54
+ e.g. Opus/Fable). `brainstorm`, `spec`, `patch`, and `init-pipeline` are intentionally unpinned
55
55
  (interactive — they inherit the session model).
56
56
  <!-- cohorte:endif -->
57
57
 
@@ -96,13 +96,7 @@ fix only with the human's go-ahead (or hand them the command).
96
96
  When ≥2 slots are live, print the parallel-feature table (feature · worktree · ports · db ·
97
97
  branch behind main by N commits) — a worktree far behind main means its next review will diff
98
98
  against stale code ⇒ suggest rebasing it.
99
- 7. **Telemetry** (consent hygieneread `<config>` §`telemetry`). Report the
100
- status in one line: `disabled` / `enabled since <consent_date> · install_id <id> · endpoint <url>`
101
- (the install_id is the human's GDPR erasure key — see SCHEMA.md §Telemetry). Flag ❌ any
102
- incoherent state: `enabled: true` with no `install_id` or no `consent_date` (sending without
103
- recorded consent — fix: set `enabled: false` until the consent question is re-run), or a
104
- `telemetry:` block missing entirely on a current core (top up via `/cohorte-update-pipeline`).
105
- 7b. **Kanban** (the board mirror — SCHEMA.md §Kanban). Run
99
+ 7. **Kanban** (the board mirror SCHEMA.md §Kanban). Run
106
100
  `<core>/pipeline/scripts/kanban-move.sh --check` and report its one line verbatim: the resolved
107
101
  board path, or the exact link that is missing. A board mirror is optional, so "not configured" is
108
102
  ℹ️, never ❌ — but it must be **stated**, because the whole class of bug here is a card that
@@ -72,8 +72,7 @@ When the agents return:
72
72
  handoff content — and append ONE metrics line for the batch to the **main checkout's**
73
73
  `$(dirname "$(git rev-parse --git-common-dir)")/<state>/pipeline-metrics.jsonl` (never a bare
74
74
  relative path: from a feature worktree that writes a stray sink whose lines die at teardown)
75
- (rules in `/cohorte-build` §4, `phase: "fix"`), chaining the opt-in usage ping in the same Bash call
76
- (results = items fixed over items found across surfaces, e.g. `"5/6"`).
75
+ (rules in `/cohorte-build` §4, `phase: "fix"`).
77
76
  - Tell the human: re-test by hand if the failures were runtime ones, and `/cohorte-review $ARGUMENTS` for the
78
77
  re-verdict — the re-review is what *verifies* the ticked items actually hold (a regression simply
79
78
  reappears as a new finding in the next round). **Recommend a `/clear`** — all state (spec,
@@ -0,0 +1,113 @@
1
+ ---
2
+ description: Triage a bug and freeze a minimal patch spec — the cheap entry into the pipeline for a fix, no brainstorm, no contract.
3
+ argument-hint: [bug description / stack trace / issue text — or empty to pick from the kanban]
4
+ ---
5
+
6
+ You run the **patch triage** in the main thread — interactive, with the human. This is the bug-fix
7
+ entry point: it produces `specs/patch-<slug>.md` and stops. Everything after it is the normal
8
+ pipeline (`/cohorte-build` → `/cohorte-review` → `/cohorte-fix`* → `/cohorte-ship`), unchanged — the
9
+ patch spec is a spec, so those commands consume it as-is. Splitting it this way is the point: each
10
+ phase is a fresh session with the artifact on disk, instead of one long thread paying for the whole
11
+ cycle at input price on every turn.
12
+
13
+ Bug (may be empty): **$ARGUMENTS**
14
+
15
+ > Read `PIPELINE.md` §`pipeline-profile` first: `surfaces` (paths — this is what you map the bug onto)
16
+ > and `vcs`. _Skip the re-read if it's already in your context this session and unmodified since._
17
+ > Do NOT read `specs/_decisions.md`: a bug fix decides nothing transverse, and the journal is for
18
+ > decisions that outlive a feature.
19
+ >
20
+ > **Kanban** (SCHEMA.md §Kanban): two moves, and their **order is load-bearing** — §1 settles the
21
+ > slug and moves the card to `spec`, §4 moves it to `ready` at freeze. Both are one call to
22
+ > `<core>/pipeline/scripts/kanban-move.sh auto patch-<slug> <stage> [--title "[patch] <human title>"]`.
23
+ > The slug is settled first precisely because the move needs it: a card cannot be joined on an id
24
+ > that doesn't exist yet. `auto` resolves the board from the config itself and exits 0
25
+ > with a `kanban: <reason>` line when there is none — so **never decide "no board is configured"
26
+ > without running it**. Reading the Ideas column at §1 still needs the board path: get it from a
27
+ > `kanban-move.sh` run, or grep the config for `boards[<PIPELINE name>]`.
28
+
29
+ ## 1. Get the bug
30
+
31
+ If `$ARGUMENTS` is non-empty, restate it in one line and confirm you've got it.
32
+
33
+ If it is empty: when a board is configured and its **Ideas** column has cards, list them (with any
34
+ sub-bullet notes as context) — **cards titled `[patch]` first**, since those are the ones a human
35
+ filed as bugs — and let the human pick one. Otherwise ask **"What's broken?"**. Either way, wait.
36
+
37
+ Then **settle the id, before anything moves.** Derive `<slug>` (kebab-case, from the symptom —
38
+ `500-on-empty-cart`, not `bug-42`) and confirm it. The `feature_id` is **`patch-<slug>`**, prefix
39
+ included: it is the join key for the kanban card, the spec filename, the branch and every later
40
+ command, so the prefix is part of the id itself, not decoration on the card.
41
+
42
+ **Kanban, in this order:**
43
+
44
+ 1. **If the human picked an Ideas card, tag it FIRST.** Ideas cards are free text with no
45
+ `#<feature_id>`, and the move script joins on that tag: move before tagging and it finds nothing,
46
+ creates a second card, and strands the untagged original in Ideas forever. One targeted Edit
47
+ appending ` #patch-<slug>` to that line, located by `grep -n` — never a full board read.
48
+ 2. `<core>/pipeline/scripts/kanban-move.sh auto patch-<slug> spec --title "[patch] <human title>"` —
49
+ which moves the (now tagged) card, or creates one under `--title` if the human typed a fresh bug.
50
+ Report what it printed — `moved #…` or `kanban: <reason>` — never a guess about which happened.
51
+
52
+ ## 2. Triage — three questions, not an interview
53
+
54
+ `/cohorte-spec` walks a template section by section because a feature has to be *designed*. A bug is
55
+ already specified by reality; your job is to pin it down, not to explore it. Ask only what you
56
+ genuinely cannot infer from the input, batched into ONE message:
57
+
58
+ 1. **Repro** — the shortest deterministic path to the symptom. No repro ⇒ ask whether they want you
59
+ to go find one first (a diagnosis session, no spec) or to freeze it as a hypothesis and let the
60
+ implementer confirm. Never invent a repro to fill the section.
61
+ 2. **Expected behaviour** — often the whole spec. "It should 404, not 500" is a complete contract.
62
+ 3. **Blast radius** — what must NOT change. This becomes §7 Out of scope, and it is what stops a fix
63
+ from becoming a refactor.
64
+
65
+ Then locate it yourself — do not make the human do it. Use the retrieval provider if one is wired,
66
+ else grep for the symptom's strings/identifiers. Read only the files the trail actually names.
67
+ Report the suspected `file:line` in one line and let them confirm or correct it.
68
+
69
+ ## 3. Map it onto surfaces — as many as it takes
70
+
71
+ Match the suspected cause and the fix's blast radius against `surfaces[].path`. **A patch is not
72
+ capped at one surface**: a bug that spans an API validator and the form feeding it is one bug with
73
+ one repro, and splitting it into two specs would give each half a contract it doesn't have. List the
74
+ surfaces you're claiming, one line each, with why.
75
+
76
+ **The one escalation that is not a judgment call:** if the fix needs **new** contract surface area —
77
+ a new endpoint, a new shared type, a new field crossing surfaces — stop. That is a feature wearing a
78
+ bug's clothes, and §5 is the only channel that keeps two surfaces in agreement about a shape that
79
+ doesn't exist yet. Say so plainly and send the human to `/cohorte-spec`. Changing an **existing**
80
+ contract entry is fine: describe the delta in §5 and continue.
81
+
82
+ ## 4. Freeze the patch spec
83
+
84
+ Write `specs/patch-<slug>.md` from `<core>/templates/patch.template.md` with `status: frozen`, filled
85
+ from §§1–3. **Create the file — do not ask the human to.** Budget: **~60 lines**. A patch spec that
86
+ wants 200 is a feature or a refactor — say which and route it (`/cohorte-spec`, or an item on
87
+ `specs/refactor-backlog.md`).
88
+
89
+ Two sections carry the weight, and both are cheap to get wrong:
90
+
91
+ - **§4 Regression test** — name the test file and what it asserts. It replaces §5 as the thing the
92
+ reviewer checks the diff against, so "add a test" is not enough: say what fails today and why. A
93
+ patch whose test can only be written after the cause is found says exactly that.
94
+ - **§5/§9 keep the feature spec's numbers** (contract delta, acceptance). `review.md` and
95
+ `implementer.template.md` name "contract §5, acceptance §9" verbatim — renumbering them here would
96
+ silently point both agents at the wrong section. The template already does this; don't "fix" it.
97
+
98
+ **Postcondition:** `grep -q '^status: frozen' specs/patch-<slug>.md` — if it fails the freeze didn't
99
+ land; fix it before pointing the human at `/cohorte-build`. Chain the second kanban move onto that
100
+ same Bash call — `kanban-move.sh auto patch-<slug> ready` (the card is already tagged and on the
101
+ board from §1, so this one needs no `--title`) — and report what it printed: `moved #…` or
102
+ `kanban: <reason>`, never a guess.
103
+
104
+ ## 5. Hand off
105
+
106
+ Print the spec path, the surfaces, and the branch to cut: `<prefix>patch-<slug>`, where `<prefix>` is
107
+ `vcs.patch_branch_prefix` — falling back to `fix/` on a profile that predates the key. If `isolation.enabled`
108
+ and the fix is big enough to want its own worktree, mention `scripts/new-feature.sh patch-<slug>`;
109
+ most patches don't need it.
110
+
111
+ Then: `/cohorte-build patch-<slug>` — and **recommend a `/clear` first**. The spec is on disk; the
112
+ lead's triage history is re-sent at input price on every turn it survives, and `/cohorte-build`
113
+ re-reads everything it needs.
@@ -118,8 +118,6 @@ re-ordered by severity, counts summed, duplicates collapsed, verdict = the worst
118
118
  routes them. Append ONE metrics line for the batch to the **main checkout's**
119
119
  `$(dirname "$(git rev-parse --git-common-dir)")/<state>/pipeline-metrics.jsonl` (rules in
120
120
  `/cohorte-build` §4; never a bare relative path — from a worktree that strands the lines): `{"ts":"<ISO>","feature":"$ARGUMENTS","phase":"review","seconds":<wall-clock>,"surfaces":{"<key>":"<verdict>:<finding count>",…}}`.
121
- In the same Bash call, chain the opt-in usage ping (`/cohorte-build` §4, `phase: "review"`, results = the
122
- merged verdict + total finding count, e.g. `"REVISE:3"`).
123
121
  **Stage the full report to `specs/reports/$ARGUMENTS.md`** (overwrite) — a gitignored buffer so a
124
122
  `/cohorte-fix` after a `/clear` can still read the findings; the `specs/reports/` subfolder is skipped by the
125
123
  non-recursive `specs/*.md` glob, so it's never mistaken for a spec (no phantom card, no bogus stage).
@@ -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
@@ -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.3.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