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 +53 -0
- package/README.md +8 -9
- package/bin/cli.js +8 -2
- package/core/adapter/render.js +2 -2
- package/core/agents/release.md +6 -2
- package/core/agents/review.md +7 -0
- package/core/commands/cohorte-brainstorm.md +0 -5
- package/core/commands/cohorte-build.md +24 -7
- package/core/commands/cohorte-doctor.md +4 -10
- package/core/commands/cohorte-fix.md +1 -2
- package/core/commands/cohorte-patch.md +113 -0
- package/core/commands/cohorte-review.md +0 -2
- package/core/commands/cohorte-ship.md +8 -9
- package/core/commands/cohorte-spec.md +1 -6
- package/core/commands/cohorte-update-pipeline.md +5 -4
- package/core/templates/patch.template.md +86 -0
- package/core/templates/steps/init-pipeline/02-interview-gaps.md +0 -10
- package/core/templates/steps/init-pipeline/04-write-render.md +2 -2
- package/core/workflows/review.js +1 -3
- package/install.ps1 +3 -1
- package/install.sh +6 -2
- package/package.json +2 -2
- package/profile/PIPELINE.template.md +1 -0
- package/profile/SCHEMA.md +28 -63
- package/profile/cohorte.config.template.yaml +0 -16
- package/scripts/validate-core.mjs +25 -23
- package/scripts/telemetry-send.sh +0 -84
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
|
|
348
|
+
## Privacy
|
|
348
349
|
|
|
349
|
-
Cohorte
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
package/core/adapter/render.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
package/core/agents/release.md
CHANGED
|
@@ -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
|
|
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.
|
|
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`:
|
package/core/agents/review.md
CHANGED
|
@@ -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
|
|
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.
|
|
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`, `
|
|
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
|
|
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. **
|
|
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"`)
|
|
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 `<
|
|
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
|
-
`<
|
|
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`.
|
|
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.
|
|
102
|
-
(
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
package/core/workflows/review.js
CHANGED
|
@@ -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
|
-
? `
|
|
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"
|
|
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.
|
|
4
|
-
"description": "Portable, stack-agnostic multi-agent development pipeline for Claude Code, Codex CLI, Cursor, Gemini CLI and OpenCode
|
|
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
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
for (const
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
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
|