cohorte 1.3.0 → 1.3.2
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 +35 -0
- package/README.md +4 -0
- package/bin/cli.js +32 -14
- package/core/commands/cycle.md +54 -0
- package/core/commands/doctor.md +1 -1
- package/install.ps1 +18 -10
- package/install.sh +26 -11
- package/package.json +1 -1
- package/profile/SCHEMA.md +3 -1
- package/scripts/assert-gate-hook.mjs +75 -0
- package/scripts/validate-core.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,41 @@
|
|
|
3
3
|
Entries are shown by `/update-pipeline` ("What's new") after a core refresh. Keep them short,
|
|
4
4
|
user-facing, most recent first. One `## <version> — <YYYY-MM-DD>` section per release.
|
|
5
5
|
|
|
6
|
+
## 1.3.2 — 2026-07-30
|
|
7
|
+
|
|
8
|
+
> **Re-run `npx cohorte@latest update --global` (or `update`).** This release repairs the gate
|
|
9
|
+
> hook registration in place — updating is what applies it.
|
|
10
|
+
|
|
11
|
+
- **1.3.0's preflight phase gate never fired on any install.** `gate.py` gates review/smoke
|
|
12
|
+
dispatches on `tool_name == "Task"`, but all three installers registered the hook with
|
|
13
|
+
`matcher: "Bash"` — a Task call never reached it. The `preflight` block in `gate-config.json`
|
|
14
|
+
and `gate.preflight` in `PIPELINE.md` were both dead config. The matcher is now `Bash|Task`.
|
|
15
|
+
- **Re-installing duplicated the hook, every time.** The "already registered?" test was
|
|
16
|
+
`command.endswith("gate.py")`, which is false for the Windows form `py "C:\…\gate.py"` because
|
|
17
|
+
of the trailing quote — so `install.sh` and `bin/cli.js` appended another copy on each run, and
|
|
18
|
+
`gate.py` ran once per copy on every Bash call (four copies seen in the wild). Registration is
|
|
19
|
+
now a **reconcile**: it drops every existing `gate.py` entry and writes exactly one. Idempotent,
|
|
20
|
+
it collapses the duplicates you already have, and it upgrades the stale matcher — an
|
|
21
|
+
append-if-absent would have found the stale entry and skipped, pinning the bug forever.
|
|
22
|
+
Unrelated hooks and every other settings key are untouched.
|
|
23
|
+
- **`npx cohorte update` never touched the hook at all**, so neither fix above could have reached
|
|
24
|
+
you through the command you actually run to get fixes — only a full re-install rewrote it.
|
|
25
|
+
`install.sh` and `install.ps1` always registered on update; this port had drifted (the same
|
|
26
|
+
class of drift as 1.2.4 and 1.2.6). It now registers on both paths.
|
|
27
|
+
- CI installs **twice** before asserting the hook, via a new `scripts/assert-gate-hook.mjs`:
|
|
28
|
+
exactly one registration, matcher covering both Bash and Task. A single install could never
|
|
29
|
+
surface the duplication — which is precisely why CI stayed green while it shipped.
|
|
30
|
+
|
|
31
|
+
## 1.3.1 — 2026-07-30
|
|
32
|
+
|
|
33
|
+
- **`/cycle <feature_id> [max_rounds]`** — a launcher command for the full dev-cycle workflow,
|
|
34
|
+
so you don't have to phrase the request in prose. It resolves `workflows/cycle.js`
|
|
35
|
+
(bundled or global), checks the runtime is available (missing ⇒ it hands you the
|
|
36
|
+
conversational `/build` → `/smoke` → `/review` path instead), sanity-checks the spec is
|
|
37
|
+
frozen, launches the workflow in the background, then relays the verdict: outcome,
|
|
38
|
+
contract re-authorings to eyeball, the `questions` array verbatim, and the next step
|
|
39
|
+
(`/ship` on SHIP-READY, rerun `/cycle` or `/fix` otherwise). Kanban card moves included.
|
|
40
|
+
|
|
6
41
|
## 1.3.0 — 2026-07-30
|
|
7
42
|
|
|
8
43
|
**Token economy — immediate wins, no workflow needed:**
|
package/README.md
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
[](https://github.com/TheBidouilleAgency/cohorte/actions/workflows/publish.yml)
|
|
8
8
|
[](https://nodejs.org)
|
|
9
9
|
[](LICENSE)
|
|
10
|
+
[](https://thebidouilleagency.github.io/cohorte/)
|
|
11
|
+
|
|
12
|
+
**[📖 Full documentation](https://thebidouilleagency.github.io/cohorte/)** — guides (feature cycle, workflows, token economy, parallel features) + complete reference (commands, agents, profile, gate, scripts).
|
|
10
13
|
|
|
11
14
|
</div>
|
|
12
15
|
|
|
@@ -214,6 +217,7 @@ it in `.claude/pipeline/VERSION` and bundled repos in their committed `pipeline.
|
|
|
214
217
|
| `/brainstorm` | Interactive persona panel that pressure-tests a feature idea. |
|
|
215
218
|
| `/spec` | Freeze the feature spec + contract into `specs/<id>.md` (UI features also get a standalone design brief at `specs/design/<id>.md`). Also applies review returns. |
|
|
216
219
|
| `/build <id>` | Lead authors the contract, then dispatches one implementer per surface in parallel. |
|
|
220
|
+
| `/cycle <id>` | Launch the full dev-cycle **workflow** on a frozen spec: build → smoke ∥ review → fix until zero findings; deferred questions in the output. Needs workflows enabled (falls back to the conversational path). |
|
|
217
221
|
| `/smoke <id>` | Run the feature for real: infra up, contract endpoints, UI flows, design conformance. |
|
|
218
222
|
| `/review <id>` | Read-only review agents (one per touched surface, parallel) audit the diff vs the spec. |
|
|
219
223
|
| `/fix <id>` | Apply a review/smoke report: remediation into the spec, re-dispatch only the surfaces with findings. |
|
package/bin/cli.js
CHANGED
|
@@ -21,6 +21,13 @@ const VERSION = pkg.version;
|
|
|
21
21
|
|
|
22
22
|
const REPO_URL = 'https://github.com/TheBidouilleAgency/cohorte';
|
|
23
23
|
|
|
24
|
+
// PreToolUse matcher for the gate hook. MUST cover Task as well as Bash:
|
|
25
|
+
// gate.py's preflight phase gate keys off tool_name === "Task" (the `preflight`
|
|
26
|
+
// block in a repo's gate-config.json). A Bash-only matcher never delivers a Task
|
|
27
|
+
// dispatch to the hook, so that gate silently never fires — it was dead code
|
|
28
|
+
// from 1.3.0 to 1.3.1. Keep in lockstep with install.sh and install.ps1.
|
|
29
|
+
const GATE_MATCHER = 'Bash|Task';
|
|
30
|
+
|
|
24
31
|
function usage(code) {
|
|
25
32
|
console.log(`cohorte v${VERSION}
|
|
26
33
|
|
|
@@ -274,19 +281,24 @@ function registerGlobalHook() {
|
|
|
274
281
|
} catch { /* absent or invalid → start fresh */ }
|
|
275
282
|
if (!data.hooks || typeof data.hooks !== 'object') data.hooks = {};
|
|
276
283
|
if (!Array.isArray(data.hooks.PreToolUse)) data.hooks.PreToolUse = [];
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
284
|
+
|
|
285
|
+
const file = path.join(dest, 'hooks', 'gate.py');
|
|
286
|
+
const base = path.basename(file);
|
|
287
|
+
// Trailing-quote tolerant: the Windows form is `py "C:\...\gate.py"`, and a
|
|
288
|
+
// bare .endsWith() missed it — which is how repeat `npx cohorte install`
|
|
289
|
+
// runs accumulated a duplicate registration every time (gate.py then ran
|
|
290
|
+
// once per copy on every Bash call).
|
|
291
|
+
const isGate = entry => (entry.hooks || []).some(
|
|
292
|
+
h => typeof h.command === 'string' && h.command.trim().replace(/"+$/, '').endsWith(base));
|
|
293
|
+
const cmd = process.platform === 'win32' ? `${python} "${file}"` : `${python} ${file}`;
|
|
294
|
+
|
|
295
|
+
// Reconcile rather than append-if-absent: drop every existing gate.py
|
|
296
|
+
// registration, then add exactly one. Idempotent, collapses duplicates older
|
|
297
|
+
// installers left behind, and upgrades a stale "Bash"-only matcher in place —
|
|
298
|
+
// an append-if-absent would find the stale entry and skip, pinning the bug.
|
|
299
|
+
data.hooks.PreToolUse = data.hooks.PreToolUse.filter(e => !isGate(e));
|
|
300
|
+
data.hooks.PreToolUse.push({ matcher: GATE_MATCHER, hooks: [{ type: 'command', command: cmd }] });
|
|
301
|
+
|
|
290
302
|
fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2) + '\n');
|
|
291
303
|
return 'ok';
|
|
292
304
|
}
|
|
@@ -311,7 +323,13 @@ if (scope === 'global') {
|
|
|
311
323
|
: `→ updating pipeline core GLOBALLY in ${dest} (keeping global settings.json)`);
|
|
312
324
|
copyFixedAgents();
|
|
313
325
|
copyCore();
|
|
314
|
-
|
|
326
|
+
// Register on UPDATE too — install.sh and install.ps1 always have, and this
|
|
327
|
+
// port skipping it is why a duplicated or stale-matcher registration could
|
|
328
|
+
// never be repaired by `npx cohorte update`: the only route that rewrites it
|
|
329
|
+
// was a full re-install, which is not what anyone runs to get a fix. Safe to
|
|
330
|
+
// run every time — registration reconciles only gate.py entries and leaves
|
|
331
|
+
// every other hook and settings key untouched.
|
|
332
|
+
const hookState = registerGlobalHook();
|
|
315
333
|
await seedConfig();
|
|
316
334
|
console.log(`
|
|
317
335
|
✓ pipeline core installed globally into ${dest} (version ${VERSION})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
model: sonnet
|
|
3
|
+
description: Launch the full dev-cycle workflow (contract → build → smoke ∥ review → fix, until zero findings) for a frozen spec; relay its verdict + deferred questions.
|
|
4
|
+
argument-hint: <feature_id> [max_rounds]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are the **lead**. Launch the full dev-cycle **workflow** for feature **$ARGUMENTS** — the
|
|
8
|
+
deterministic script does the orchestration (SCHEMA.md §Workflows, `cycle.js`); your job is only to
|
|
9
|
+
start it and relay its result. Do NOT run the phases yourself here — that's the conversational path
|
|
10
|
+
(`/build` → `/smoke` → `/review` → `/fix`), which remains the fallback below.
|
|
11
|
+
|
|
12
|
+
> **Kanban** (SCHEMA.md §Kanban): move card `#<feature_id>` → **Building** at launch. No-op silently
|
|
13
|
+
> if no board.
|
|
14
|
+
|
|
15
|
+
## 1. Resolve & check (fail fast, before spending anything)
|
|
16
|
+
|
|
17
|
+
- Parse `$ARGUMENTS`: the first token is `<feature_id>`, an optional second numeric token is
|
|
18
|
+
`<max_rounds>` (the workflow defaults to 5).
|
|
19
|
+
- Resolve the script: `.claude/workflows/cycle.js` if it exists, else `~/.claude/workflows/cycle.js`
|
|
20
|
+
(`test -f`). **Missing both** ⇒ the core predates 1.3.0 or is half-copied: tell the human to run
|
|
21
|
+
`/update-pipeline`, and stop.
|
|
22
|
+
- **Workflow runtime available?** If the `Workflow` tool is not in your toolset (Claude Code
|
|
23
|
+
< 2.1.154 or workflows disabled), say so and hand over the conversational path instead:
|
|
24
|
+
`/build <feature_id>` → `/smoke` → `/review` → `/fix` — same phases, interactive. Stop.
|
|
25
|
+
- Quick spec sanity (the workflow re-checks properly — this just saves a doomed launch):
|
|
26
|
+
`grep '^status:' specs/<feature_id>.md` must say `frozen` or `in-review`; otherwise tell the human
|
|
27
|
+
to run `/spec` first, and stop.
|
|
28
|
+
|
|
29
|
+
## 2. Launch
|
|
30
|
+
|
|
31
|
+
Call the `Workflow` tool: `scriptPath: <resolved cycle.js path>`,
|
|
32
|
+
`args: {"feature": "<feature_id>", "maxRounds": <max_rounds, omit if not given>}`.
|
|
33
|
+
It runs in the background — tell the human it's off and what it will do (build, then smoke ∥ review
|
|
34
|
+
→ fix rounds until zero findings + PASS; no questions mid-run), and that `/workflows` shows live
|
|
35
|
+
progress. Then END YOUR TURN — never poll, never sleep; the completion notification re-wakes you.
|
|
36
|
+
|
|
37
|
+
## 3. Relay the result (when the task notification arrives)
|
|
38
|
+
|
|
39
|
+
The workflow returns only a verdict object — the bulk is already on disk
|
|
40
|
+
(`specs/reports/<feature_id>.md`, spec `## Remediation`). Print, without re-reading any of it into
|
|
41
|
+
context:
|
|
42
|
+
|
|
43
|
+
- `outcome` · rounds used · review verdict · smoke result.
|
|
44
|
+
- `contractChanges` if any — flag them explicitly: the loop re-authored the frozen contract
|
|
45
|
+
lead-style; the human should eyeball those hunks in the diff.
|
|
46
|
+
- **The `questions` array, verbatim** — this is the human's inbox from the run (empty when the spec
|
|
47
|
+
pre-answered everything). Each one is a decision to make, usually by sharpening the spec.
|
|
48
|
+
- The `next` line: **SHIP-READY** ⇒ `/ship <feature_id>` (DoD ticked + freshness stamped — ship is a
|
|
49
|
+
straight shot, its human confirmation stays). **STOPPED** ⇒ answer the questions, then rerun
|
|
50
|
+
`/cycle <feature_id>` (it picks up from the spec's Remediation) or finish conversationally with
|
|
51
|
+
`/fix <feature_id>` + `/review <feature_id>`.
|
|
52
|
+
- **Kanban:** outcome SHIP-READY ⇒ move card → **Review** (the cycle's last verdict is a review);
|
|
53
|
+
otherwise → **Fix**. No-op silently if no board.
|
|
54
|
+
- **Recommend a `/clear`** — everything the next command needs is on disk.
|
package/core/commands/doctor.md
CHANGED
|
@@ -37,7 +37,7 @@ fix only with the human's go-ahead (or hand them the command).
|
|
|
37
37
|
(`review.md`, `release.md`, `smoke.md`, `profile-reader.md` — repo or `~/.claude/agents/`) must
|
|
38
38
|
each carry their `model:` line too (sonnet/haiku/sonnet/haiku). **Command pins:** every mechanical command file
|
|
39
39
|
(`build`, `review`, `fix`, `smoke`, `ship`, `audit`, `refactor`, `doctor`, `align-ds`,
|
|
40
|
-
`update-pipeline` — in `.claude/commands/` or `~/.claude/commands/`) carries `model: sonnet` in
|
|
40
|
+
`update-pipeline`, `cycle` — in `.claude/commands/` or `~/.claude/commands/`) carries `model: sonnet` in
|
|
41
41
|
its frontmatter — ⚠️ if missing (the lead's orchestration turn then bills at the session model,
|
|
42
42
|
e.g. Opus/Fable). `brainstorm`, `spec`, and `init-pipeline` are intentionally unpinned
|
|
43
43
|
(interactive — they inherit the session model).
|
package/install.ps1
CHANGED
|
@@ -251,23 +251,31 @@ try {
|
|
|
251
251
|
$data.hooks | Add-Member -NotePropertyName PreToolUse -NotePropertyValue @()
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
|
|
254
|
+
# Reconcile rather than append-if-absent: drop every existing gate.py
|
|
255
|
+
# registration, then add exactly one. Idempotent, collapses duplicates older
|
|
256
|
+
# installers left behind, and upgrades a stale "Bash"-only matcher in place —
|
|
257
|
+
# an append-if-absent would find the stale entry and skip, pinning the bug.
|
|
258
|
+
$kept = @()
|
|
255
259
|
foreach ($entry in @($data.hooks.PreToolUse)) {
|
|
260
|
+
$isGate = $false
|
|
256
261
|
foreach ($h in @($entry.hooks)) {
|
|
257
262
|
if ($h -and $h.command -and "$($h.command)".Trim().TrimEnd('"').EndsWith('gate.py')) {
|
|
258
|
-
$
|
|
263
|
+
$isGate = $true
|
|
259
264
|
}
|
|
260
265
|
}
|
|
266
|
+
if (-not $isGate) { $kept += $entry }
|
|
261
267
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
268
|
+
# The matcher MUST cover Task as well as Bash: gate.py's preflight phase gate
|
|
269
|
+
# keys off tool_name == "Task" (the `preflight` block in a repo's
|
|
270
|
+
# gate-config.json). A Bash-only matcher never delivers a Task dispatch to the
|
|
271
|
+
# hook, so that gate silently never fires. Keep in lockstep with install.sh
|
|
272
|
+
# and bin/cli.js.
|
|
273
|
+
$data.hooks.PreToolUse = @($kept) + [pscustomobject]@{
|
|
274
|
+
matcher = 'Bash|Task'
|
|
275
|
+
hooks = @([pscustomobject]@{ type = 'command'; command = $cmd })
|
|
269
276
|
}
|
|
270
|
-
|
|
277
|
+
Write-JsonFile $settingsPath $data
|
|
278
|
+
return 'ok'
|
|
271
279
|
}
|
|
272
280
|
|
|
273
281
|
# Bump only the core_version in a repo's committed .claude/pipeline.json (bundled mode).
|
package/install.sh
CHANGED
|
@@ -185,8 +185,24 @@ register_global_hook() {
|
|
|
185
185
|
python3 - "$dest/settings.json" "$dest/hooks/gate.py" <<'PY'
|
|
186
186
|
import json, sys
|
|
187
187
|
settings, gate = sys.argv[1], sys.argv[2]
|
|
188
|
-
#
|
|
189
|
-
|
|
188
|
+
# The matcher MUST cover Task as well as Bash: gate.py's preflight phase gate
|
|
189
|
+
# keys off tool_name == "Task" (the `preflight` block in a repo's
|
|
190
|
+
# gate-config.json). A Bash-only matcher never delivers a Task dispatch to the
|
|
191
|
+
# hook, so that gate silently never fires — it was dead code from 1.3.0 to 1.3.1.
|
|
192
|
+
MATCHER = "Bash|Task"
|
|
193
|
+
base = gate.rsplit("/", 1)[-1]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def is_gate(entry):
|
|
197
|
+
# Trailing-quote tolerant: the Windows form is `py "C:\...\gate.py"`, and a
|
|
198
|
+
# bare .endswith() missed it — which is how repeat installs accumulated a
|
|
199
|
+
# duplicate registration every time (gate.py then ran once per copy).
|
|
200
|
+
return any(
|
|
201
|
+
(h.get("command") or "").strip().rstrip('"').endswith(base)
|
|
202
|
+
for h in entry.get("hooks", [])
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
190
206
|
try:
|
|
191
207
|
with open(settings) as fh:
|
|
192
208
|
data = json.load(fh)
|
|
@@ -195,15 +211,14 @@ try:
|
|
|
195
211
|
except Exception:
|
|
196
212
|
data = {}
|
|
197
213
|
pre = data.setdefault("hooks", {}).setdefault("PreToolUse", [])
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
"hooks": [{"type": "command", "command": "python3 " + path}]})
|
|
214
|
+
# Reconcile rather than append-if-absent: drop every existing gate.py
|
|
215
|
+
# registration, then add exactly one. Idempotent, collapses duplicates older
|
|
216
|
+
# installers left behind, and upgrades a stale "Bash"-only matcher in place —
|
|
217
|
+
# an append-if-absent would find the stale entry and skip, pinning the bug.
|
|
218
|
+
kept = [e for e in pre if not is_gate(e)]
|
|
219
|
+
kept.append({"matcher": MATCHER,
|
|
220
|
+
"hooks": [{"type": "command", "command": "python3 " + gate}]})
|
|
221
|
+
data["hooks"]["PreToolUse"] = kept
|
|
207
222
|
with open(settings, "w") as fh:
|
|
208
223
|
json.dump(data, fh, indent=2)
|
|
209
224
|
fh.write("\n")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cohorte",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "Portable, stack-agnostic multi-agent development pipeline for Claude Code — install the core, run /init-pipeline, and it adapts to your project's stack.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cohorte": "bin/cli.js"
|
package/profile/SCHEMA.md
CHANGED
|
@@ -318,7 +318,9 @@ Workflow runtime executes instead of the lead reasoning out the fan-out turn by
|
|
|
318
318
|
`<core>/workflows/review.js`, `audit.js`, `refactor.js` (installed to `.claude/workflows/` bundled or
|
|
319
319
|
`~/.claude/workflows/` global). The conversational commands (`/review`, `/audit`, `/refactor`)
|
|
320
320
|
**remain the default path and the fallback** — a workflow runs only when the human explicitly asks
|
|
321
|
-
for it ("run the review workflow"
|
|
321
|
+
for it ("run the review workflow", or via the `/cycle <id>` launcher command, which resolves
|
|
322
|
+
`cycle.js` and invokes the runtime for them), and requires Claude Code ≥ **2.1.154** with workflows
|
|
323
|
+
enabled.
|
|
322
324
|
`/doctor` reports which path a session will take. The interactive commands (`/init-pipeline`,
|
|
323
325
|
`/brainstorm`, `/spec`) and the dispatch-only ones (`/build`, `/ship`) have **no** workflow variant on
|
|
324
326
|
purpose: they're interviews or already a single parallel dispatch — a script adds nothing.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
//
|
|
3
|
+
// assert-gate-hook.mjs — post-install assertion on a settings.json's gate hook.
|
|
4
|
+
//
|
|
5
|
+
// node scripts/assert-gate-hook.mjs <path-to-settings.json>
|
|
6
|
+
//
|
|
7
|
+
// Two invariants, both regressions that shipped to users:
|
|
8
|
+
//
|
|
9
|
+
// 1. EXACTLY ONE registration. Until 1.3.2 the installers appended
|
|
10
|
+
// if-absent, and their "already registered?" test was a bare
|
|
11
|
+
// `.endsWith("gate.py")` — false for the Windows form `py "C:\...\gate.py"`
|
|
12
|
+
// because of the trailing quote. Every re-install appended another copy
|
|
13
|
+
// (four seen in the wild), so gate.py ran once per copy on every Bash call.
|
|
14
|
+
// CI installed only once, so it never noticed. Callers must install TWICE
|
|
15
|
+
// before running this.
|
|
16
|
+
//
|
|
17
|
+
// 2. THE MATCHER COVERS Task. gate.py's preflight phase gate dispatches on
|
|
18
|
+
// tool_name === "Task" (the `preflight` block in a repo's
|
|
19
|
+
// gate-config.json). A Bash-only matcher never delivers a Task call to the
|
|
20
|
+
// hook, so the gate was dead code from 1.3.0 (which introduced it) to 1.3.1.
|
|
21
|
+
//
|
|
22
|
+
// Exits non-zero with a specific message on failure.
|
|
23
|
+
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
|
|
26
|
+
const settingsPath = process.argv[2];
|
|
27
|
+
if (!settingsPath) {
|
|
28
|
+
console.error('usage: assert-gate-hook.mjs <path-to-settings.json>');
|
|
29
|
+
process.exit(2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let data;
|
|
33
|
+
try {
|
|
34
|
+
data = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error(`assert-gate-hook: cannot read ${settingsPath}: ${err.message}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const pre = (data.hooks || {}).PreToolUse || [];
|
|
41
|
+
const isGate = (entry) =>
|
|
42
|
+
(entry.hooks || []).some(
|
|
43
|
+
(h) => typeof h.command === 'string' && h.command.trim().replace(/"+$/, '').endsWith('gate.py')
|
|
44
|
+
);
|
|
45
|
+
const gate = pre.filter(isGate);
|
|
46
|
+
|
|
47
|
+
if (gate.length !== 1) {
|
|
48
|
+
console.error(
|
|
49
|
+
`assert-gate-hook: expected exactly 1 gate.py registration, found ${gate.length}.\n` +
|
|
50
|
+
(gate.length > 1
|
|
51
|
+
? ' Registration is not idempotent — a re-install duplicated the hook.'
|
|
52
|
+
: ' The installer did not register the gate hook.') +
|
|
53
|
+
`\n PreToolUse: ${JSON.stringify(pre, null, 2)}`
|
|
54
|
+
);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const matcher = gate[0].matcher || '';
|
|
59
|
+
if (!/\bTask\b/.test(matcher)) {
|
|
60
|
+
console.error(
|
|
61
|
+
`assert-gate-hook: gate matcher must cover Task, got ${JSON.stringify(matcher)}.\n` +
|
|
62
|
+
" gate.py's preflight phase gate keys off tool_name === 'Task'; without it\n" +
|
|
63
|
+
' the `preflight` block in gate-config.json is dead code.'
|
|
64
|
+
);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
if (!/\bBash\b/.test(matcher)) {
|
|
68
|
+
console.error(
|
|
69
|
+
`assert-gate-hook: gate matcher must cover Bash, got ${JSON.stringify(matcher)}.\n` +
|
|
70
|
+
' The deny/ask command gating runs on Bash tool calls.'
|
|
71
|
+
);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(`gate hook ok — 1 registration, matcher ${JSON.stringify(matcher)}`);
|
|
@@ -23,7 +23,7 @@ const frontmatter = (text) => {
|
|
|
23
23
|
// orchestration turn silently bills at the session model — Opus/Fable).
|
|
24
24
|
// Interactive commands must stay unpinned (they inherit on purpose).
|
|
25
25
|
const PINNED = ["build", "review", "fix", "smoke", "ship", "audit",
|
|
26
|
-
"refactor", "doctor", "align-ds", "update-pipeline"];
|
|
26
|
+
"refactor", "doctor", "align-ds", "update-pipeline", "cycle"];
|
|
27
27
|
const UNPINNED = ["brainstorm", "spec", "init-pipeline"];
|
|
28
28
|
|
|
29
29
|
for (const f of readdirSync(join(root, "core/commands"))) {
|