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