cohorte 2.0.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +148 -0
- package/README.md +41 -32
- package/bin/cli.js +316 -26
- package/core/adapter/render.js +389 -0
- package/core/agents/implementer.template.md +3 -3
- package/core/agents/release.md +7 -4
- package/core/agents/review.md +10 -2
- package/core/commands/cohorte-audit.md +2 -0
- package/core/commands/cohorte-brainstorm.md +3 -6
- package/core/commands/cohorte-build.md +14 -17
- package/core/commands/cohorte-doctor.md +59 -28
- package/core/commands/cohorte-fix.md +2 -3
- package/core/commands/cohorte-init-pipeline.md +7 -8
- package/core/commands/cohorte-refactor.md +5 -2
- package/core/commands/cohorte-review.md +20 -16
- package/core/commands/cohorte-ship.md +44 -9
- package/core/commands/cohorte-spec.md +3 -7
- package/core/commands/cohorte-update-pipeline.md +8 -8
- 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/spec.template.md +1 -3
- package/core/templates/steps/init-pipeline/01-detect-stack.md +7 -3
- package/core/templates/steps/init-pipeline/02-interview-gaps.md +8 -2
- package/core/templates/steps/init-pipeline/04-write-render.md +23 -17
- package/core/templates/steps/init-pipeline/05-report.md +1 -1
- 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 +23 -2
- package/install.sh +22 -4
- package/package.json +6 -2
- package/profile/PIPELINE.template.md +27 -6
- package/profile/SCHEMA.md +88 -49
- package/scripts/kanban-move.sh +11 -1
- package/scripts/metrics/collect.mjs +5 -3
- package/scripts/preflight.sh +27 -8
- package/scripts/telemetry-send.sh +10 -3
- 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 +1 -1
- package/core/commands/cohorte-loop.md +0 -110
- package/scripts/loop-detach.sh +0 -153
- package/scripts/loop.sh +0 -399
- package/scripts/test-loop.mjs +0 -330
package/install.sh
CHANGED
|
@@ -71,6 +71,27 @@ else
|
|
|
71
71
|
fi
|
|
72
72
|
[ -d "$src/core" ] || { echo "error: pipeline source not found (no core/ in $src)" >&2; exit 1; }
|
|
73
73
|
|
|
74
|
+
# --- delegate to the Node CLI ------------------------------------------------
|
|
75
|
+
# Since 2.2.0 the commands in core/ are runtime-NEUTRAL sources: they carry capability
|
|
76
|
+
# conditionals (`<!-- cohorte:if subagents -->`) and path tokens (`<core>`, `<state>`) that
|
|
77
|
+
# the adapter resolves per coding agent. Copying them verbatim, as this script used to,
|
|
78
|
+
# would install prompts full of unresolved markers — an install that looks successful and
|
|
79
|
+
# instructs the model with text meant for a different runtime. There is no shell renderer,
|
|
80
|
+
# so hand the whole job to bin/cli.js, which is the documented route anyway.
|
|
81
|
+
if command -v node >/dev/null 2>&1; then
|
|
82
|
+
set -- install
|
|
83
|
+
[ "$mode" = "update" ] && set -- update
|
|
84
|
+
[ "$scope" = "global" ] && set -- "$@" --global
|
|
85
|
+
[ "$scope" = "project" ] && set -- "$@" "$target"
|
|
86
|
+
exec node "$src/bin/cli.js" "$@"
|
|
87
|
+
fi
|
|
88
|
+
echo "error: cohorte needs Node ≥ 18 to install." >&2
|
|
89
|
+
echo " The pipeline's commands are rendered per coding agent (Claude Code, Codex, Cursor," >&2
|
|
90
|
+
echo " Gemini CLI, OpenCode) at install time; there is no shell equivalent of that step," >&2
|
|
91
|
+
echo " and a raw copy would install prompts this runtime cannot follow." >&2
|
|
92
|
+
echo " Install Node, then: npx cohorte install${scope:+ }$([ "$scope" = global ] && echo --global)" >&2
|
|
93
|
+
exit 1
|
|
94
|
+
|
|
74
95
|
# --- resolve the destination .claude dir ------------------------------------
|
|
75
96
|
if [ "$scope" = "global" ]; then
|
|
76
97
|
dest="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
@@ -107,11 +128,8 @@ copy_core() {
|
|
|
107
128
|
cp "$src/scripts/kanban-move.sh" "$dest/pipeline/scripts/"
|
|
108
129
|
cp "$src/scripts/telemetry-send.sh" "$dest/pipeline/scripts/"
|
|
109
130
|
cp "$src/scripts/preflight.sh" "$dest/pipeline/scripts/"
|
|
110
|
-
cp "$src/scripts/loop.sh" "$dest/pipeline/scripts/"
|
|
111
|
-
cp "$src/scripts/loop-detach.sh" "$dest/pipeline/scripts/"
|
|
112
131
|
chmod +x "$dest/pipeline/scripts/kanban-move.sh" "$dest/pipeline/scripts/telemetry-send.sh" \
|
|
113
|
-
"$dest/pipeline/scripts/preflight.sh"
|
|
114
|
-
"$dest/pipeline/scripts/loop-detach.sh" 2>/dev/null || true
|
|
132
|
+
"$dest/pipeline/scripts/preflight.sh" 2>/dev/null || true
|
|
115
133
|
cp "$src/core/agents/implementer.template.md" "$dest/pipeline/"
|
|
116
134
|
[ -f "$src/CHANGELOG.md" ] && cp "$src/CHANGELOG.md" "$dest/pipeline/"
|
|
117
135
|
printf '%s\n' "$ver" > "$dest/pipeline/VERSION"
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cohorte",
|
|
3
|
-
"version": "2.0
|
|
4
|
-
"description": "Portable, stack-agnostic multi-agent development pipeline for Claude Code
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "Portable, stack-agnostic multi-agent development pipeline for Claude Code, Codex CLI, Cursor, Gemini CLI and OpenCode \u2014 install the core, run /cohorte-init-pipeline, and it adapts to your project's stack.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cohorte": "bin/cli.js"
|
|
7
7
|
},
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
"keywords": [
|
|
39
39
|
"claude",
|
|
40
40
|
"claude-code",
|
|
41
|
+
"codex",
|
|
42
|
+
"cursor",
|
|
43
|
+
"gemini-cli",
|
|
44
|
+
"opencode",
|
|
41
45
|
"agents",
|
|
42
46
|
"pipeline",
|
|
43
47
|
"multi-agent"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **Generated by `/cohorte-init-pipeline`.** This is the project profile: the single place the
|
|
4
4
|
> portable multi-agent pipeline reads for everything stack-specific. The pipeline core
|
|
5
|
-
>
|
|
5
|
+
> in the pipeline core is generic and references THIS file by section — keep project facts
|
|
6
6
|
> here, never hardcoded into agent prompts. Kept current by `/cohorte-update-pipeline` (reconcile:
|
|
7
7
|
> new fields are topped up, your values never overwritten). Rendered agent files are
|
|
8
8
|
> regenerated from this profile on every reconcile — customize agents through §Conventions
|
|
@@ -52,7 +52,7 @@ surfaces:
|
|
|
52
52
|
- key: backend # short id, used as agent name + scope
|
|
53
53
|
path: apps/api # the ONLY tree this surface's agent may touch
|
|
54
54
|
label: backend (AdonisJS)
|
|
55
|
-
agent: backend # rendered
|
|
55
|
+
agent: backend # rendered as backend.md in this runtime's agents dir
|
|
56
56
|
tools: [Read, Write, Edit, Bash, Grep, Glob, mcp__serena] # mcp__<provider> mirrors retrieval.provider
|
|
57
57
|
model: sonnet # frontmatter model tier: sonnet | haiku | inherit
|
|
58
58
|
# sonnet = default (applies the frozen contract — cheap
|
|
@@ -97,6 +97,26 @@ contract:
|
|
|
97
97
|
index: packages/shared-types/src/index.ts # barrel to export from, or "" if none
|
|
98
98
|
authored_by: lead # NEVER the implementer agents
|
|
99
99
|
|
|
100
|
+
# ── release notes (optional) ────────────────────────────────────────────────
|
|
101
|
+
# A per-feature release note the project's versioning tool consumes. When
|
|
102
|
+
# enabled, /cohorte-ship authors it BEFORE dispatching the release agent, so it
|
|
103
|
+
# ships inside the release commit. Repos whose CI fails a PR that lacks one
|
|
104
|
+
# (Changesets' `changeset` job) MUST set this — otherwise every ship goes red.
|
|
105
|
+
release_notes:
|
|
106
|
+
enabled: <true | false> # false ⇒ /cohorte-ship §2b is a no-op
|
|
107
|
+
tool: <changesets | none> # what consumes the file
|
|
108
|
+
dir: .changeset # where the note is authored
|
|
109
|
+
filename: "<feature_id>.md" # one file per feature
|
|
110
|
+
anchor_package: shared-types # sole key in the front-matter; a lockstep/fixed group propagates the bump
|
|
111
|
+
language: <ui_language | English> # language of the note's prose
|
|
112
|
+
forbid_levels: [] # bumps project policy refuses, e.g. [major] while 0.x
|
|
113
|
+
empty_cmd: "pnpm changeset --empty" # when no version should move; "" if the tool has no such escape hatch
|
|
114
|
+
ci_job: changeset # CI job that fails when the note is missing (name it so /cohorte-ship §5 recognizes the red)
|
|
115
|
+
guidance: | # rules the lead follows when writing the note (project policy)
|
|
116
|
+
major = the deploy needs an operator action; minor = a user-visible capability;
|
|
117
|
+
patch = fix/perf/refactor with no change of use. No client names, no internal
|
|
118
|
+
paths, no exploitable attack vector.
|
|
119
|
+
|
|
100
120
|
# ── repo-wide commands (root unless noted) ──────────────────────────────────
|
|
101
121
|
commands:
|
|
102
122
|
install: pnpm install
|
|
@@ -138,7 +158,7 @@ isolation:
|
|
|
138
158
|
compose_file: docker-compose.yml
|
|
139
159
|
registry: .worktrees/slots.tsv
|
|
140
160
|
|
|
141
|
-
# ── gate (drives
|
|
161
|
+
# ── gate (drives the pipeline gate hook + permissions) ─────────────────────
|
|
142
162
|
# Confirm-first or hard-deny on dangerous Bash, inspecting the FULL command
|
|
143
163
|
# string (so chained `cd x && …` forms are caught).
|
|
144
164
|
gate:
|
|
@@ -164,10 +184,10 @@ gate:
|
|
|
164
184
|
- "git rebase"
|
|
165
185
|
- "git reset"
|
|
166
186
|
- "docker compose"
|
|
167
|
-
# Phase gate: review dispatches require a fresh
|
|
187
|
+
# Phase gate: review dispatches require a fresh `preflight.ok` stamp,
|
|
168
188
|
# written by pipeline/scripts/preflight.sh when typecheck+lint+tests are green —
|
|
169
189
|
# gate.py "ask"s the dispatch when the stamp is missing, stale, or the code changed since.
|
|
170
|
-
# The stamp is local: keep
|
|
190
|
+
# The stamp is local: keep `preflight.ok` gitignored, never committed.
|
|
171
191
|
preflight:
|
|
172
192
|
enabled: true
|
|
173
193
|
agents: [review] # subagent_types the stamp gates
|
|
@@ -176,7 +196,8 @@ gate:
|
|
|
176
196
|
```
|
|
177
197
|
|
|
178
198
|
> **Note.** The **kanban** board link is **user-scoped** — NOT configured here. Its facts live in the
|
|
179
|
-
> consolidated `~/.claude/cohorte.config.yaml`,
|
|
199
|
+
> consolidated user config (`~/.claude/cohorte.config.yaml`, or `~/.cohorte/cohorte.config.yaml` on
|
|
200
|
+
> a non-Claude runtime), read at runtime (the kanban board is keyed by this
|
|
180
201
|
> profile's `name`). This project profile only governs the dev pipeline (`/cohorte-brainstorm…/cohorte-ship`).
|
|
181
202
|
|
|
182
203
|
---
|
package/profile/SCHEMA.md
CHANGED
|
@@ -33,6 +33,15 @@ generic pipeline uses it, so a stateless agent can read/regenerate the profile c
|
|
|
33
33
|
| `contract.mechanism` | enum | build, lead | `shared-types-zod`/`openapi`/`protobuf`/`json-schema`/`none`. |
|
|
34
34
|
| `contract.path` `.ext` `.index` | string | build | Where `<feature_id>` contract is authored + barrel. |
|
|
35
35
|
| `contract.authored_by` | const `lead` | build | Implementers import it read-only, never edit. |
|
|
36
|
+
| `release_notes.enabled` | bool | ship | `false` ⇒ skip note authoring (§2b of /cohorte-ship). See §Release notes. |
|
|
37
|
+
| `release_notes.tool` | enum | ship | `changesets`/`none` — what consumes the file. |
|
|
38
|
+
| `release_notes.dir` `.filename` | string | ship | Where the per-feature note is authored, e.g. `.changeset/<feature_id>.md`. |
|
|
39
|
+
| `release_notes.anchor_package` | string | ship | Sole key in the note's front-matter; a lockstep/`fixed` group propagates the bump. |
|
|
40
|
+
| `release_notes.language` | string | ship | Language of the note's prose — usually `ui_language`. |
|
|
41
|
+
| `release_notes.forbid_levels` | list | ship | Bump levels project policy refuses, e.g. `[major]` while `0.x`. |
|
|
42
|
+
| `release_notes.empty_cmd` | string | ship | Escape hatch when no version should move; `""` if none. |
|
|
43
|
+
| `release_notes.ci_job` | string | ship | CI job that fails on a missing note — lets §5 name the red check. |
|
|
44
|
+
| `release_notes.guidance` | string | ship | Project policy the lead follows when picking the bump + writing the prose. |
|
|
36
45
|
| `commands.*` | string | all | Repo-wide install/dev/lint/format/typecheck/test + migrate. |
|
|
37
46
|
| `commands.test_quiet` `.lint_quiet` | string | review, audit, workflows | Repo-wide bridled variants — what the `/cohorte-review` pre-flight runs. Same fallback as the per-surface ones. |
|
|
38
47
|
| `rbac.enabled` | bool | brainstorm, review | Toggle RBAC personas + authz audit. |
|
|
@@ -70,7 +79,7 @@ generic pipeline uses it, so a stateless agent can read/regenerate the profile c
|
|
|
70
79
|
- **Commands** (`/cohorte-build`, `/cohorte-review`, …) parse the `yaml pipeline-profile` block to know how
|
|
71
80
|
many surfaces to dispatch, the contract mechanism, the commands, and the capability flags.
|
|
72
81
|
- **Hook** (`gate.py`) reads `gate.deny`/`gate.ask`/`gate.ask_on_default_branch`/`gate.default_branch`
|
|
73
|
-
from a generated
|
|
82
|
+
from a generated `<state>/gate-config.json`. The last two make git + docker free on feature branches
|
|
74
83
|
but confirm-gated on the default branch (branch resolved at run time via `git rev-parse`).
|
|
75
84
|
- **Scripts** (`new-feature.sh`) read the `isolation` block (rendered in at init).
|
|
76
85
|
|
|
@@ -160,7 +169,7 @@ the frozen contract as the only cross-surface channel**. So specialization means
|
|
|
160
169
|
|
|
161
170
|
Coarse first, specialize on evidence: start with one `frontend` / `backend` surface each; split only a
|
|
162
171
|
surface that's proven slow and cleanly separable. The evidence lives in
|
|
163
|
-
the **main checkout's**
|
|
172
|
+
the **main checkout's** `<state>/pipeline-metrics.jsonl` (gitignored) — one JSONL line per phase batch
|
|
164
173
|
(`ts`/`feature`/`phase`/`seconds`/`surfaces:{key: result}`), appended by `/cohorte-build`, `/cohorte-review`
|
|
165
174
|
and `/cohorte-fix`.
|
|
166
175
|
**`surfaces` keys are surface keys, nothing else** — run-level facts go in their own top-level
|
|
@@ -168,7 +177,7 @@ fields. Anything put inside `surfaces` is read
|
|
|
168
177
|
as a surface: the dashboard renders it as a row in the per-surface table and scores a non-`ok`
|
|
169
178
|
value as that surface failing. Always the main checkout, never the feature worktree (which dies at teardown while
|
|
170
179
|
metrics must accumulate across features) — resolve from anywhere with
|
|
171
|
-
`$(dirname "$(git rev-parse --git-common-dir)")
|
|
180
|
+
`$(dirname "$(git rev-parse --git-common-dir)")/<state>/pipeline-metrics.jsonl`. Read it before
|
|
172
181
|
proposing a split: split the surface that actually dominates wall-clock, not the one that feels big.
|
|
173
182
|
|
|
174
183
|
## Measuring cost — what's slow vs what's expensive
|
|
@@ -181,7 +190,7 @@ to log it. For what's EXPENSIVE, use Claude Code's own accounting:
|
|
|
181
190
|
over the last 24 h / 7 d (e.g. _"Top subagents: frontend 7 %, backend 4 % · Top skills: /cohorte-build 1 %,
|
|
182
191
|
/cohorte-review 1 %"_). That IS the per-phase ledger — approximate (share-of-total, machine-local, not exact
|
|
183
192
|
tokens). Read it to see which surface/command actually dominates the bill before you tune a `model` tier.
|
|
184
|
-
- **OpenTelemetry** (exact numbers + dashboards) — add an `env` block to
|
|
193
|
+
- **OpenTelemetry** (exact numbers + dashboards) — add an `env` block to `this runtime's settings file`:
|
|
185
194
|
`{"env":{"CLAUDE_CODE_ENABLE_TELEMETRY":"1","OTEL_METRICS_EXPORTER":"otlp","OTEL_EXPORTER_OTLP_PROTOCOL":"http/protobuf","OTEL_EXPORTER_OTLP_ENDPOINT":"http://localhost:4318"}}`
|
|
186
195
|
and point it at a collector. Metrics `claude_code.token.usage` + `claude_code.cost.usage` carry
|
|
187
196
|
`session.id` + model + type (input/output/cacheRead). Subagent tokens roll into the session total;
|
|
@@ -217,28 +226,25 @@ Rules for every consumer (implementers, preflight, `/cohorte-audit` gates, workf
|
|
|
217
226
|
`/cohorte-init-pipeline` **asks** for these variants (detected defaults offered first) instead of silently
|
|
218
227
|
storing a bare `pnpm test` as the thing agents execute; `/cohorte-update-pipeline` tops up older profiles.
|
|
219
228
|
|
|
220
|
-
## Spec status — the lifecycle state machine
|
|
229
|
+
## Spec status — the lifecycle state machine
|
|
221
230
|
|
|
222
231
|
A spec's front-matter `status` is not a label, it is the pipeline's **state**: every command routes on
|
|
223
|
-
it, the dashboard boards on it, the kanban backfill maps it to a column
|
|
224
|
-
back to continue an interrupted autonomous run. Six states, and exactly one writer each:
|
|
232
|
+
it, the dashboard boards on it, and the kanban backfill maps it to a column. Six states:
|
|
225
233
|
|
|
226
234
|
| status | meaning | written by | who may build it |
|
|
227
235
|
| --- | --- | --- | --- |
|
|
228
236
|
| `draft` | the interview is open, nothing is frozen | `/cohorte-spec` Mode A | no |
|
|
229
237
|
| `frozen` | the contract is frozen — the handoff to `/cohorte-build` | `/cohorte-spec` Mode A freeze | yes |
|
|
230
|
-
| `in-progress` | a
|
|
231
|
-
| `in-review` | reviewed / awaiting the next round or `/cohorte-ship` | `/cohorte-spec` Mode B, `/cohorte-fix
|
|
232
|
-
| `blocked` | a
|
|
238
|
+
| `in-progress` | a round is under way on this spec (or died mid-way) | an automated driver, if any | yes |
|
|
239
|
+
| `in-review` | reviewed / awaiting the next round or `/cohorte-ship` | `/cohorte-spec` Mode B, `/cohorte-fix` | yes |
|
|
240
|
+
| `blocked` | a round gave up here (non-convergent, no verdict, not implementable) | an automated driver, if any | yes, with the reason named |
|
|
233
241
|
| `shipped` | the PR is open; the status flip is part of the release commit | `/cohorte-ship` | no |
|
|
234
242
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
The build is still skipped or redone by the build stamp alone (`specs/reports/<id>.built`, written only
|
|
241
|
-
after a build that finished), so an interrupted *build* correctly rebuilds.
|
|
243
|
+
**`in-progress` and `blocked` are for external drivers.** No shipped command writes them: the
|
|
244
|
+
built-in autonomous driver (`/cohorte-loop`) was retired in 2.2.0, and the human-driven cycle moves
|
|
245
|
+
`frozen` → `in-review` → `shipped`. They stay valid states because specs in existing repos carry
|
|
246
|
+
them, and because anything automating the cycle from outside needs somewhere to record "a round is
|
|
247
|
+
under way" and "a round gave up". Every reader still routes on them; nothing produces them.
|
|
242
248
|
|
|
243
249
|
Corollaries worth knowing:
|
|
244
250
|
|
|
@@ -270,8 +276,8 @@ Non-negotiables, in every phase:
|
|
|
270
276
|
- **Never speak for a dead agent.** You did not see its work: report what the *tree* says (quiet
|
|
271
277
|
commands, redirected to a file, grepped), not what a handoff would have said.
|
|
272
278
|
- **Never let it reach a driver as clean.** `/cohorte-build` writes `dead[]` into
|
|
273
|
-
`specs/reports/<id>.build.json`, `/cohorte-review` writes `unreviewed[]` into the verdict;
|
|
274
|
-
|
|
279
|
+
`specs/reports/<id>.build.json`, `/cohorte-review` writes `unreviewed[]` into the verdict; a driver
|
|
280
|
+
must abort on either *before* it reads `blocking`, since a dead reviewer makes
|
|
275
281
|
`blocking == 0` a statement about code nobody read.
|
|
276
282
|
- **`unreviewed` is separate from `blocking` on purpose.** Faking a count in `blocking` to force a
|
|
277
283
|
driver's hand would corrupt the one field the whole contract rests on; a driver reads them as two
|
|
@@ -291,8 +297,8 @@ of the step: a spec that cannot be built does not get cheaper by being built on
|
|
|
291
297
|
the design gate. Each maps to `NOT-READY` (a surface would have to invent the answer) or
|
|
292
298
|
`RESERVATIONS` (a surface can proceed on a stated assumption).
|
|
293
299
|
- **`NOT-READY` aborts the build with no agent spawned** and sends the human to `/cohorte-spec`.
|
|
294
|
-
|
|
295
|
-
|
|
300
|
+
A driver reads the same file and must stop rather than retry — it is the one outcome more passes
|
|
301
|
+
cannot fix.
|
|
296
302
|
- **`RESERVATIONS` never blocks.** Each gap is inlined verbatim into the dispatch of the surface it
|
|
297
303
|
affects, as an assumption the implementer must apply *and* flag in its handoff. A gate that stalled a
|
|
298
304
|
sound build on a missing error case would cost more human round-trips than it saves.
|
|
@@ -316,7 +322,7 @@ own out-of-scope reason.
|
|
|
316
322
|
`## Remediation`, which is what `/cohorte-fix` re-dispatches.
|
|
317
323
|
- `/cohorte-audit` **carries open `deferred:` items over** when it rewrites the backlog; overwriting them away
|
|
318
324
|
is the one way they silently vanish.
|
|
319
|
-
- The verdict JSON carries `deferred: <n>` (informational, outside `blocking`), so
|
|
325
|
+
- The verdict JSON carries `deferred: <n>` (informational, outside `blocking`), so a driver can name
|
|
320
326
|
them in its closing line without reading a report.
|
|
321
327
|
|
|
322
328
|
## Decisions — the transverse decision journal
|
|
@@ -350,14 +356,14 @@ variants) with all output redirected to `specs/reports/<id>.preflight.txt`:
|
|
|
350
356
|
there: zero agents are spawned.** A reviewer dispatched onto code that doesn't compile burns its
|
|
351
357
|
whole run rediscovering what `tsc` already printed for free — the failure goes straight to the
|
|
352
358
|
human (or `/cohorte-fix`) instead.
|
|
353
|
-
- **All green** ⇒ the script stamps
|
|
359
|
+
- **All green** ⇒ the script stamps `<state>/preflight.ok` (`<epoch> <HEAD sha> <tree digest>` —
|
|
354
360
|
local and **gitignored**; a versioned stamp describes the tree *before* its own commit and rides
|
|
355
361
|
into every clone and worktree, which breaks the gate both ways).
|
|
356
362
|
|
|
357
363
|
`hooks/gate.py` enforces the stamp as a **phase gate** (the `preflight` block of `gate-config.json`,
|
|
358
364
|
generated from `gate.preflight`): a Task dispatch of a listed `subagent_type` (default
|
|
359
365
|
`review`) with a missing/stale stamp — older than `max_age_minutes`, or the digest no longer
|
|
360
|
-
matches the working tree (`.claude` and `specs` excluded, so the pipeline's own writes and a
|
|
366
|
+
matches the working tree (`.claude`, `.cohorte` and `specs` excluded, so the pipeline's own writes and a
|
|
361
367
|
commit of already-verified code do not invalidate it) — gets an
|
|
362
368
|
"ask", so a lead can't accidentally skip the gate but a human can consciously override it. The gate
|
|
363
369
|
hook fires for **every** agent in the session, including subagents spawned by the Workflow runtime
|
|
@@ -378,8 +384,9 @@ this exact procedure so a surface is always defined the same way. To add surface
|
|
|
378
384
|
scaffolding; `inherit` only when the surface makes real design decisions worth the lead's model),
|
|
379
385
|
the five `*_cmd`s (derive from the surface's `package.json` / workspace
|
|
380
386
|
filter, mirroring a sibling surface), and `uses_design`.
|
|
381
|
-
2. **Render the agent file**
|
|
382
|
-
|
|
387
|
+
2. **Render the agent file** `<agents>/<agent>.md` from `<core>/pipeline/implementer.template.md`
|
|
388
|
+
— the template is already rendered for this runtime, so only the placeholders are yours to fill —
|
|
389
|
+
substituting `<SURFACE_AGENT>`, `<SURFACE_LABEL>`,
|
|
383
390
|
`<SURFACE_PATH>`, `<SURFACE_TOOLS>`, `<SURFACE_MODEL>`, `<PROJECT_NAME>`, and the surface-specific
|
|
384
391
|
blocks (`<SURFACE_EXTRA_NEVER>`, `<SURFACE_DESIGN_INPUT>`, `<SURFACE_TDD_STEP1>` — leave the design
|
|
385
392
|
ones empty unless `uses_design`). Fill `<SURFACE_CONVENTIONS>` with the surface's convention slice
|
|
@@ -412,6 +419,36 @@ this exact procedure so a surface is always defined the same way. To add surface
|
|
|
412
419
|
Removing/merging a surface is the reverse: drop the `surfaces[]` entry, delete its agent file, fold its
|
|
413
420
|
conventions. Never leave an agent file with no matching `surfaces[]` entry (orphan) or vice-versa.
|
|
414
421
|
|
|
422
|
+
## Release notes — the per-feature note the versioning tool consumes
|
|
423
|
+
|
|
424
|
+
Many repos gate merges on a **per-feature release note**: Changesets' `changeset` CI job fails any PR
|
|
425
|
+
that touches product code without a `.changeset/*.md`. That file is **not** something the release
|
|
426
|
+
agent can invent — picking the bump level is project policy (semver over an API vs. over a product,
|
|
427
|
+
a `0.x` rule forbidding `major`, what counts as user-visible), and the prose is outward-facing copy.
|
|
428
|
+
It is the **lead's** to write, exactly like the contract.
|
|
429
|
+
|
|
430
|
+
The failure mode this block exists to prevent is silent and reproducible: the requirement lives in the
|
|
431
|
+
project's `CLAUDE.md`, which the ship flow never reads, so `/cohorte-ship` completes, opens the PR,
|
|
432
|
+
moves the kanban card to **Shipped** — and CI goes red on a job nobody looked at. The feature reads as
|
|
433
|
+
shipped while being unmergeable. Encoding it in the profile is what makes the step survive a stateless
|
|
434
|
+
lead.
|
|
435
|
+
|
|
436
|
+
- **Authored before the dispatch**, next to the `status: shipped` flip (`/cohorte-ship` §2b), so the note
|
|
437
|
+
lands **inside** the release commit. Written after the fact it needs a second commit, and the PR is
|
|
438
|
+
already open and red.
|
|
439
|
+
- **One key in the front-matter** — `anchor_package`. Lockstep/`fixed` version groups propagate the bump
|
|
440
|
+
from it to every other workspace; listing more is how a repo ends up with a package bumped twice.
|
|
441
|
+
- **`forbid_levels`** encodes policy the tool itself may not enforce. The common one: while the product
|
|
442
|
+
is `0.y.z`, a `major` changeset makes Changesets jump to `1.0.0` with no human deciding it — so `0.x`
|
|
443
|
+
repos forbid `major` and declare a breaking change as `minor`.
|
|
444
|
+
- **Ask, don't guess, on an ambiguous bump.** Between two defensible levels (a refactor that also changes
|
|
445
|
+
what the user sees), state the reading and let the human pick — a wrong bump is a published version
|
|
446
|
+
number, not a fixable draft.
|
|
447
|
+
- **`empty_cmd`** covers the honest no-op: a PR that touches product code but must move no version.
|
|
448
|
+
Prefer it to skipping the step; the CI job wants a file, not a version.
|
|
449
|
+
|
|
450
|
+
`enabled: false` (or `tool: none`) ⇒ §2b is a no-op and nothing below applies.
|
|
451
|
+
|
|
415
452
|
## Reconcile — bringing generated files up to the current core
|
|
416
453
|
|
|
417
454
|
`/cohorte-init-pipeline` is **one-time per project**. Afterwards, `/cohorte-update-pipeline` runs this procedure so a
|
|
@@ -423,37 +460,40 @@ files automatically. It works because every generated artifact is a **determinis
|
|
|
423
460
|
`pipeline/PIPELINE.template.md`: every block/field the template has and the profile lacks is added
|
|
424
461
|
with its documented default (e.g. `surfaces[].model: sonnet`, `retrieval.provider: serena`).
|
|
425
462
|
**Ask only when a new field is a genuine human decision** (batch into ONE question set); never
|
|
426
|
-
change a value the profile already sets; never rewrite the prose sections.
|
|
463
|
+
change a value the profile already sets; never rewrite the prose sections. `release_notes` is one
|
|
464
|
+
such decision and must not be defaulted blind: detect a versioning tool / note-enforcing CI job per
|
|
465
|
+
`/cohorte-init-pipeline` Phase 1, and if there is one, ask Phase 2's release-notes question (anchor
|
|
466
|
+
package, language, bump policy, forbidden levels). No tool found ⇒ top up with `enabled: false`.
|
|
427
467
|
2. **Re-render agent frontmatter + body.** For each `surfaces[]` entry, re-render
|
|
428
|
-
|
|
468
|
+
`<agents>/<agent>.md` from the current `implementer.template.md` per §Rendering above. Safe by
|
|
429
469
|
doctrine: rendered agents are regenerable artifacts — hand-written rules belong in `PIPELINE.md`
|
|
430
470
|
§Conventions (which reconcile never touches), NOT in agent files, where they'd be clobbered here.
|
|
431
|
-
3. **Additive settings patch.** Bring
|
|
432
|
-
|
|
433
|
-
rewrite existing/custom keys.
|
|
471
|
+
3. **Additive settings patch.** Bring `<state>/gate-config.json` — and, on a runtime with a settings
|
|
472
|
+
file the pipeline generates, that too — up to the current init spec (missing `allow` entries,
|
|
473
|
+
hooks per install mode): add what's missing, never remove or rewrite existing/custom keys.
|
|
434
474
|
4. **Capability wiring.** If a top-up added a capability needing external setup (e.g. a `retrieval`
|
|
435
475
|
provider whose MCP server isn't registered yet), run its wiring step from `/cohorte-init-pipeline` Phase 4.
|
|
436
476
|
Even when nothing new was added, re-run the provider's health check (§Code retrieval) — wiring
|
|
437
477
|
rots (PATH changes, uninstalls, hand-edits) — and repair whatever fails.
|
|
438
|
-
5. **Global config seed.** If
|
|
478
|
+
5. **Global config seed.** If `<config>` is absent, seed it from the template
|
|
439
479
|
(`profile/cohorte.config.template.yaml`) so the kanban + shared-vault config has a home. Never
|
|
440
480
|
clobber an existing filled file; report what was seeded.
|
|
441
481
|
6. **Kanban sync.** Run the §Kanban reconcile: link/create the project's board if configured, verify
|
|
442
482
|
its columns, and backfill/sync cards from `specs/*.md`. See §Kanban.
|
|
443
483
|
7. **Spec-template top-up.** `specs/_template.md` is seeded once at install and then **never**
|
|
444
484
|
refreshed, so a repo keeps whatever front-matter the core shipped the day it was installed (a
|
|
445
|
-
pre-1.6 copy
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
refresh the `status:` comment. Never rewrite its body — the section list is the human's to shape,
|
|
485
|
+
pre-1.6 copy's `status` comment still lists four states). Top it up the same way as the profile:
|
|
486
|
+
add the **front-matter fields** the current `templates/spec.template.md` has and the repo's copy
|
|
487
|
+
lacks, with their documented defaults, drop `loop_pass`/`loop_phase` (retired with
|
|
488
|
+
`/cohorte-loop` in 2.2.0), and refresh the `status:` comment. Never rewrite its body — the section list is the human's to shape,
|
|
449
489
|
and some repos have deliberately trimmed it. Nothing breaks without this (the fields are written on
|
|
450
490
|
demand when a driver needs them); it just keeps a new spec's front-matter honest about the states
|
|
451
491
|
the pipeline can put it in.
|
|
452
492
|
|
|
453
493
|
8. **Local-artifact hygiene.** The pipeline's own runtime files must stay out of git:
|
|
454
|
-
|
|
494
|
+
`<state>/preflight.ok`, `<state>/pipeline-metrics.jsonl`, `specs/reports/`. Add any missing entry to
|
|
455
495
|
`.gitignore`, and **untrack** what a pre-2.0.0 install let slip in —
|
|
456
|
-
`git rm --cached --ignore-unmatch
|
|
496
|
+
`git rm --cached --ignore-unmatch <state>/preflight.ok` (repeat per stray path). The stamp is the
|
|
457
497
|
one that actively breaks: it records the tree it verified, the commit carrying it moves HEAD past
|
|
458
498
|
that tree, and the committed copy lands in every clone and worktree — so the phase gate ends up
|
|
459
499
|
blocking clean trees and greening unchecked ones. Report what was untracked; the human commits it.
|
|
@@ -465,8 +505,8 @@ itself changes in ways `/cohorte-build` §1.5 can't auto-grow (e.g. package mana
|
|
|
465
505
|
|
|
466
506
|
Three phases have a **workflow variant** — a deterministic orchestration script the Claude Code
|
|
467
507
|
Workflow runtime executes instead of the lead reasoning out the fan-out turn by turn:
|
|
468
|
-
`<core>/workflows/review.js`, `audit.js`, `refactor.js` (installed to
|
|
469
|
-
|
|
508
|
+
`<core>/workflows/review.js`, `audit.js`, `refactor.js` (installed to `<core>/workflows/` bundled or
|
|
509
|
+
`<core>/workflows/` global). The conversational commands (`/cohorte-review`, `/cohorte-audit`, `/cohorte-refactor`)
|
|
470
510
|
**remain the default path and the fallback** — a workflow runs only when the human explicitly asks
|
|
471
511
|
for it ("run the review workflow"), and requires Claude Code ≥ **2.1.154** with workflows
|
|
472
512
|
enabled.
|
|
@@ -515,7 +555,7 @@ Shared design, all four scripts:
|
|
|
515
555
|
|
|
516
556
|
An **optional, user-scoped** mirror of the dev flow: each pipeline stage moves a card across an
|
|
517
557
|
[Obsidian Kanban](https://github.com/mgmeyers/obsidian-kanban) board, one board per project. Config
|
|
518
|
-
lives in the consolidated global config
|
|
558
|
+
lives in the consolidated global config `<config>` §`kanban` (NOT in
|
|
519
559
|
`PIPELINE.md` — the board path points at the user's personal vault, so it is machine-specific and must
|
|
520
560
|
not be committed). Everything below **no-ops silently** when the config is absent, `kanban.enabled` is
|
|
521
561
|
false, no board is configured for the current project, or the board file is missing — the pipeline never
|
|
@@ -543,12 +583,12 @@ agent's context (find, dedupe, sub-notes carried along, settings block preserved
|
|
|
543
583
|
<core>/pipeline/scripts/kanban-move.sh auto <id> <stage> [--pr <num>] [--title <title>]
|
|
544
584
|
```
|
|
545
585
|
|
|
546
|
-
|
|
586
|
+
It creates
|
|
547
587
|
the card in the target column when none exists, keeps the first and drops duplicates, and appends
|
|
548
588
|
` — PR #<num>` with `--pr`.
|
|
549
589
|
|
|
550
590
|
**`auto` is not a convenience, it is the contract.** It reads `name` from `PIPELINE.md`, then
|
|
551
|
-
`kanban.enabled` / `obsidian.vault_path` / `boards[name]` from
|
|
591
|
+
`kanban.enabled` / `obsidian.vault_path` / `boards[name]` from `<config>`
|
|
552
592
|
(override with `COHORTE_CONFIG`, or skip the profile with `--project <name>`), and it maps the
|
|
553
593
|
**stage key** (`ideas` · `brainstorm` · `spec` · `ready` · `building` · `review` · `fix` · `ship` ·
|
|
554
594
|
`shipped`) to that board's heading through `boards[name].columns` → `kanban.columns` → the built-in
|
|
@@ -584,15 +624,14 @@ Ideas — so `/cohorte-brainstorm` appends the tag to the picked line before its
|
|
|
584
624
|
| `/cohorte-build` | `building` |
|
|
585
625
|
| `/cohorte-review` | `review` |
|
|
586
626
|
| `/cohorte-fix` | `fix` |
|
|
587
|
-
| a
|
|
588
|
-
| a
|
|
627
|
+
| a round is under way (`in-progress`) | `building` |
|
|
628
|
+
| a round gave up (`blocked`) | `fix` |
|
|
589
629
|
| `/cohorte-ship` starts | `ship` |
|
|
590
630
|
| PR opened (`status: shipped`) | `shipped` (+ `PR #<num>` on the card) |
|
|
591
631
|
|
|
592
632
|
**Backfill / sync from specs (reconcile).** `specs/*.md` is the source of truth. For each spec, read its
|
|
593
633
|
`feature_id` (front-matter or filename) and `status`, map `status`→column — `frozen`→`ready`,
|
|
594
|
-
`in-progress
|
|
595
|
-
`building`), `in-review`→`review`, `blocked`→`fix`, `shipped`→`shipped`, anything else / a spec with no
|
|
634
|
+
`in-progress`→`building`, `in-review`→`review`, `blocked`→`fix`, `shipped`→`shipped`, anything else / a spec with no
|
|
596
635
|
status→`spec` — then **full
|
|
597
636
|
sync**: card absent ⇒ add it in that column; card present ⇒ **move it** to that column so the board
|
|
598
637
|
always reflects the specs (this repositions cards the human may have moved by hand). Report cards
|
|
@@ -608,7 +647,7 @@ per configured column in pipeline order, and the closing `%% kanban:settings %%`
|
|
|
608
647
|
Cohorte can send the maintainers anonymous usage pings so the pipeline improves where it's actually
|
|
609
648
|
slow. **Nothing is ever sent without explicit consent**: `/cohorte-init-pipeline` (and `/cohorte-update-pipeline` on
|
|
610
649
|
pre-telemetry installs) ask ONE question, once per machine, default **No**, and record the answer in
|
|
611
|
-
|
|
650
|
+
`<config>` §`telemetry` (`enabled`, `install_id`, `consent_date`). The sender —
|
|
612
651
|
`pipeline/scripts/telemetry-send.sh` — is a silent no-op unless `enabled: true` AND `install_id` AND
|
|
613
652
|
`endpoint` are all set, times out at 2s, and never fails the pipeline. Callers chain it with
|
|
614
653
|
`|| true`, so a **missing** script is equally silent: `/cohorte-doctor` check 1 verifies `pipeline/scripts/`
|
|
@@ -650,7 +689,7 @@ work without revealing what is being built.
|
|
|
650
689
|
**GDPR rights, concretely:**
|
|
651
690
|
|
|
652
691
|
- **Consent** — opt-in only, recorded with a date; "No" is also recorded so nothing re-asks.
|
|
653
|
-
- **Withdrawal** — set `telemetry.enabled: false` in
|
|
692
|
+
- **Withdrawal** — set `telemetry.enabled: false` in `<config>`; effective on
|
|
654
693
|
the next phase, no restart.
|
|
655
694
|
- **Erasure** — `/cohorte-doctor` prints your `install_id`; send
|
|
656
695
|
`curl -X DELETE <endpoint-origin>/v1/install/<install_id>` and the collector drops every event
|
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"
|
|
@@ -16,8 +16,14 @@
|
|
|
16
16
|
# - Never blocks or fails the pipeline: 2s timeout, all errors swallowed, exit 0 always.
|
|
17
17
|
set -u
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
# Same two-location probe as kanban-move.sh: consent is recorded once per human, in
|
|
20
|
+
# whichever config the runtime that installed the pipeline created.
|
|
21
|
+
cfg=""
|
|
22
|
+
for c in "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/cohorte.config.yaml" \
|
|
23
|
+
"$HOME/.cohorte/cohorte.config.yaml"; do
|
|
24
|
+
[ -f "$c" ] && { cfg="$c"; break; }
|
|
25
|
+
done
|
|
26
|
+
[ -n "$cfg" ] || exit 0
|
|
21
27
|
|
|
22
28
|
# read keys scoped to the `telemetry:` block only
|
|
23
29
|
tval() {
|
|
@@ -62,7 +68,8 @@ fi
|
|
|
62
68
|
# bundled repo report an empty core_version.
|
|
63
69
|
here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd 2>/dev/null) || here=""
|
|
64
70
|
ver=""
|
|
65
|
-
for v in "$here/../VERSION" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/pipeline/VERSION"
|
|
71
|
+
for v in "$here/../VERSION" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/pipeline/VERSION" \
|
|
72
|
+
"$HOME/.cohorte/pipeline/VERSION"; do
|
|
66
73
|
[ -n "$ver" ] && break
|
|
67
74
|
ver=$(head -1 "$v" 2>/dev/null | tr -d '"\\')
|
|
68
75
|
done
|