plumbbob 0.9.3 → 0.10.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/.claude-plugin/plugin.json +1 -1
- package/README.md +92 -89
- package/dist/cli-core.js +25 -8
- package/dist/cli.js +1 -1
- package/dist/lib/agents.js +26 -26
- package/dist/lib/buildlog.js +11 -11
- package/dist/lib/buildlogsync.js +8 -8
- package/dist/lib/check.js +55 -22
- package/dist/lib/commitbody.js +72 -0
- package/dist/lib/commitmsg.js +7 -7
- package/dist/lib/git.js +44 -15
- package/dist/lib/intent.js +21 -20
- package/dist/lib/latch.js +19 -19
- package/dist/lib/orient.js +36 -17
- package/dist/lib/reentry.js +70 -0
- package/dist/lib/settings.js +16 -16
- package/dist/lib/sidecar.js +54 -54
- package/dist/lib/templates.js +1 -1
- package/dist/verbs/agent.js +27 -27
- package/dist/verbs/build.js +9 -9
- package/dist/verbs/check.js +6 -6
- package/dist/verbs/checkpoint.js +48 -47
- package/dist/verbs/doctor.js +31 -30
- package/dist/verbs/finish.js +28 -41
- package/dist/verbs/handoff.js +9 -8
- package/dist/verbs/init.js +9 -9
- package/dist/verbs/park.js +3 -3
- package/dist/verbs/recover.js +297 -0
- package/dist/verbs/revert.js +13 -13
- package/dist/verbs/spike.js +25 -19
- package/dist/verbs/start.js +49 -19
- package/dist/verbs/status.js +8 -8
- package/dist/verbs/turn.js +16 -16
- package/dist/verbs/use.js +4 -4
- package/hooks/post-edit.sh +4 -4
- package/hooks/pre-bash-commit.sh +3 -3
- package/node_modules/checkride/.claude-plugin/plugin.json +11 -0
- package/node_modules/checkride/CHANGELOG.md +1404 -0
- package/node_modules/checkride/LICENSE +201 -21
- package/node_modules/checkride/README.md +197 -38
- package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.d.ts +15 -0
- package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.d.ts.map +1 -0
- package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.js +17 -0
- package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.js.map +1 -0
- package/node_modules/checkride/dist/adapters.d.ts +30 -10
- package/node_modules/checkride/dist/adapters.d.ts.map +1 -1
- package/node_modules/checkride/dist/adapters.js +62 -23
- package/node_modules/checkride/dist/adapters.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/claude.d.ts +88 -0
- package/node_modules/checkride/dist/agent-setup/claude.d.ts.map +1 -0
- package/node_modules/checkride/dist/agent-setup/claude.js +305 -0
- package/node_modules/checkride/dist/agent-setup/claude.js.map +1 -0
- package/node_modules/checkride/dist/agent-setup/command.d.ts +43 -0
- package/node_modules/checkride/dist/agent-setup/command.d.ts.map +1 -0
- package/node_modules/checkride/dist/agent-setup/command.js +77 -0
- package/node_modules/checkride/dist/agent-setup/command.js.map +1 -0
- package/node_modules/checkride/dist/agent-setup/cursor.d.ts +69 -0
- package/node_modules/checkride/dist/agent-setup/cursor.d.ts.map +1 -0
- package/node_modules/checkride/dist/agent-setup/cursor.js +247 -0
- package/node_modules/checkride/dist/agent-setup/cursor.js.map +1 -0
- package/node_modules/checkride/dist/agent-setup/files.d.ts +50 -0
- package/node_modules/checkride/dist/agent-setup/files.d.ts.map +1 -0
- package/node_modules/checkride/dist/agent-setup/files.js +80 -0
- package/node_modules/checkride/dist/agent-setup/files.js.map +1 -0
- package/node_modules/checkride/dist/agent-setup/harness.d.ts +101 -0
- package/node_modules/checkride/dist/agent-setup/harness.d.ts.map +1 -0
- package/node_modules/checkride/dist/agent-setup/harness.js +93 -0
- package/node_modules/checkride/dist/agent-setup/harness.js.map +1 -0
- package/node_modules/checkride/dist/agent-setup/hook.d.ts +45 -52
- package/node_modules/checkride/dist/agent-setup/hook.d.ts.map +1 -1
- package/node_modules/checkride/dist/agent-setup/hook.js +64 -95
- package/node_modules/checkride/dist/agent-setup/hook.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/index.d.ts +10 -4
- package/node_modules/checkride/dist/agent-setup/index.d.ts.map +1 -1
- package/node_modules/checkride/dist/agent-setup/index.js +9 -4
- package/node_modules/checkride/dist/agent-setup/index.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/scripts.d.ts +81 -0
- package/node_modules/checkride/dist/agent-setup/scripts.d.ts.map +1 -0
- package/node_modules/checkride/dist/agent-setup/scripts.js +604 -0
- package/node_modules/checkride/dist/agent-setup/scripts.js.map +1 -0
- package/node_modules/checkride/dist/agent-setup/skills.d.ts +31 -0
- package/node_modules/checkride/dist/agent-setup/skills.d.ts.map +1 -0
- package/node_modules/checkride/dist/agent-setup/skills.js +56 -0
- package/node_modules/checkride/dist/agent-setup/skills.js.map +1 -0
- package/node_modules/checkride/dist/artifacts/configured.d.ts +30 -0
- package/node_modules/checkride/dist/artifacts/configured.d.ts.map +1 -0
- package/node_modules/checkride/dist/artifacts/configured.js +45 -0
- package/node_modules/checkride/dist/artifacts/configured.js.map +1 -0
- package/node_modules/checkride/dist/artifacts/format.d.ts +33 -0
- package/node_modules/checkride/dist/artifacts/format.d.ts.map +1 -0
- package/node_modules/checkride/dist/artifacts/format.js +56 -0
- package/node_modules/checkride/dist/artifacts/format.js.map +1 -0
- package/node_modules/checkride/dist/artifacts/freshness.d.ts +32 -0
- package/node_modules/checkride/dist/artifacts/freshness.d.ts.map +1 -0
- package/node_modules/checkride/dist/artifacts/freshness.js +37 -0
- package/node_modules/checkride/dist/artifacts/freshness.js.map +1 -0
- package/node_modules/checkride/dist/artifacts/index.d.ts +22 -0
- package/node_modules/checkride/dist/artifacts/index.d.ts.map +1 -0
- package/node_modules/checkride/dist/artifacts/index.js +19 -0
- package/node_modules/checkride/dist/artifacts/index.js.map +1 -0
- package/node_modules/checkride/dist/artifacts/json.d.ts +31 -0
- package/node_modules/checkride/dist/artifacts/json.d.ts.map +1 -0
- package/node_modules/checkride/dist/artifacts/json.js +47 -0
- package/node_modules/checkride/dist/artifacts/json.js.map +1 -0
- package/node_modules/checkride/dist/artifacts/raw.d.ts +60 -0
- package/node_modules/checkride/dist/artifacts/raw.d.ts.map +1 -0
- package/node_modules/checkride/dist/artifacts/raw.js +91 -0
- package/node_modules/checkride/dist/artifacts/raw.js.map +1 -0
- package/node_modules/checkride/dist/artifacts/summary.d.ts +69 -0
- package/node_modules/checkride/dist/artifacts/summary.d.ts.map +1 -0
- package/node_modules/checkride/dist/artifacts/summary.js +108 -0
- package/node_modules/checkride/dist/artifacts/summary.js.map +1 -0
- package/node_modules/checkride/dist/atomic.d.ts +16 -1
- package/node_modules/checkride/dist/atomic.d.ts.map +1 -1
- package/node_modules/checkride/dist/atomic.js +25 -3
- package/node_modules/checkride/dist/atomic.js.map +1 -1
- package/node_modules/checkride/dist/baseline/fallow.d.ts +9 -5
- package/node_modules/checkride/dist/baseline/fallow.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/fallow.js +163 -60
- package/node_modules/checkride/dist/baseline/fallow.js.map +1 -1
- package/node_modules/checkride/dist/baseline/fingerprint.d.ts +20 -11
- package/node_modules/checkride/dist/baseline/fingerprint.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/fingerprint.js +73 -23
- package/node_modules/checkride/dist/baseline/fingerprint.js.map +1 -1
- package/node_modules/checkride/dist/baseline/history.d.ts +98 -0
- package/node_modules/checkride/dist/baseline/history.d.ts.map +1 -0
- package/node_modules/checkride/dist/baseline/history.js +232 -0
- package/node_modules/checkride/dist/baseline/history.js.map +1 -0
- package/node_modules/checkride/dist/baseline/index.d.ts +7 -5
- package/node_modules/checkride/dist/baseline/index.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/index.js +5 -4
- package/node_modules/checkride/dist/baseline/index.js.map +1 -1
- package/node_modules/checkride/dist/baseline/store.d.ts +41 -10
- package/node_modules/checkride/dist/baseline/store.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/store.js +65 -24
- package/node_modules/checkride/dist/baseline/store.js.map +1 -1
- package/node_modules/checkride/dist/baseline-command.d.ts +5 -5
- package/node_modules/checkride/dist/baseline-command.js +7 -7
- package/node_modules/checkride/dist/baseline-command.js.map +1 -1
- package/node_modules/checkride/dist/cli.d.ts +21 -4
- package/node_modules/checkride/dist/cli.d.ts.map +1 -1
- package/node_modules/checkride/dist/cli.js +350 -36
- package/node_modules/checkride/dist/cli.js.map +1 -1
- package/node_modules/checkride/dist/config.d.ts +114 -9
- package/node_modules/checkride/dist/config.d.ts.map +1 -1
- package/node_modules/checkride/dist/config.js +142 -23
- package/node_modules/checkride/dist/config.js.map +1 -1
- package/node_modules/checkride/dist/digest/digest.d.ts +5 -5
- package/node_modules/checkride/dist/digest/digest.js +6 -6
- package/node_modules/checkride/dist/digest/index.d.ts +2 -2
- package/node_modules/checkride/dist/digest/index.js +2 -2
- package/node_modules/checkride/dist/doctor.d.ts +27 -6
- package/node_modules/checkride/dist/doctor.d.ts.map +1 -1
- package/node_modules/checkride/dist/doctor.js +231 -27
- package/node_modules/checkride/dist/doctor.js.map +1 -1
- package/node_modules/checkride/dist/gate.d.ts +180 -0
- package/node_modules/checkride/dist/gate.d.ts.map +1 -0
- package/node_modules/checkride/dist/gate.js +617 -0
- package/node_modules/checkride/dist/gate.js.map +1 -0
- package/node_modules/checkride/dist/init.d.ts +79 -12
- package/node_modules/checkride/dist/init.d.ts.map +1 -1
- package/node_modules/checkride/dist/init.js +385 -81
- package/node_modules/checkride/dist/init.js.map +1 -1
- package/node_modules/checkride/dist/json.d.ts +21 -0
- package/node_modules/checkride/dist/json.d.ts.map +1 -0
- package/node_modules/checkride/dist/json.js +21 -0
- package/node_modules/checkride/dist/json.js.map +1 -0
- package/node_modules/checkride/dist/links.d.ts +31 -5
- package/node_modules/checkride/dist/links.d.ts.map +1 -1
- package/node_modules/checkride/dist/links.js +141 -32
- package/node_modules/checkride/dist/links.js.map +1 -1
- package/node_modules/checkride/dist/node-pin.d.ts +129 -0
- package/node_modules/checkride/dist/node-pin.d.ts.map +1 -0
- package/node_modules/checkride/dist/node-pin.js +211 -0
- package/node_modules/checkride/dist/node-pin.js.map +1 -0
- package/node_modules/checkride/dist/orchestrator.d.ts +53 -10
- package/node_modules/checkride/dist/orchestrator.d.ts.map +1 -1
- package/node_modules/checkride/dist/orchestrator.js +312 -113
- package/node_modules/checkride/dist/orchestrator.js.map +1 -1
- package/node_modules/checkride/dist/pack.d.ts +8 -9
- package/node_modules/checkride/dist/pack.d.ts.map +1 -1
- package/node_modules/checkride/dist/pack.js +53 -17
- package/node_modules/checkride/dist/pack.js.map +1 -1
- package/node_modules/checkride/dist/pm/detect.d.ts +6 -1
- package/node_modules/checkride/dist/pm/detect.d.ts.map +1 -1
- package/node_modules/checkride/dist/pm/detect.js +6 -1
- package/node_modules/checkride/dist/pm/detect.js.map +1 -1
- package/node_modules/checkride/dist/pm/index.d.ts +7 -2
- package/node_modules/checkride/dist/pm/index.d.ts.map +1 -1
- package/node_modules/checkride/dist/pm/index.js +6 -2
- package/node_modules/checkride/dist/pm/index.js.map +1 -1
- package/node_modules/checkride/dist/pm/launch.d.ts +72 -0
- package/node_modules/checkride/dist/pm/launch.d.ts.map +1 -0
- package/node_modules/checkride/dist/pm/launch.js +108 -0
- package/node_modules/checkride/dist/pm/launch.js.map +1 -0
- package/node_modules/checkride/dist/pm/tools.d.ts +73 -0
- package/node_modules/checkride/dist/pm/tools.d.ts.map +1 -0
- package/node_modules/checkride/dist/pm/tools.js +124 -0
- package/node_modules/checkride/dist/pm/tools.js.map +1 -0
- package/node_modules/checkride/dist/pm/translate.d.ts +35 -9
- package/node_modules/checkride/dist/pm/translate.d.ts.map +1 -1
- package/node_modules/checkride/dist/pm/translate.js +78 -18
- package/node_modules/checkride/dist/pm/translate.js.map +1 -1
- package/node_modules/checkride/dist/proc.d.ts +30 -0
- package/node_modules/checkride/dist/proc.d.ts.map +1 -0
- package/node_modules/checkride/dist/proc.js +42 -0
- package/node_modules/checkride/dist/proc.js.map +1 -0
- package/node_modules/checkride/dist/qa/cli.d.ts +20 -0
- package/node_modules/checkride/dist/qa/cli.d.ts.map +1 -0
- package/node_modules/checkride/dist/qa/cli.js +24 -0
- package/node_modules/checkride/dist/qa/cli.js.map +1 -0
- package/node_modules/checkride/dist/qa/dead.d.ts +39 -0
- package/node_modules/checkride/dist/qa/dead.d.ts.map +1 -0
- package/node_modules/checkride/dist/qa/dead.js +88 -0
- package/node_modules/checkride/dist/qa/dead.js.map +1 -0
- package/node_modules/checkride/dist/qa/dupes.d.ts +45 -0
- package/node_modules/checkride/dist/qa/dupes.d.ts.map +1 -0
- package/node_modules/checkride/dist/qa/dupes.js +62 -0
- package/node_modules/checkride/dist/qa/dupes.js.map +1 -0
- package/node_modules/checkride/dist/qa/health.d.ts +85 -0
- package/node_modules/checkride/dist/qa/health.d.ts.map +1 -0
- package/node_modules/checkride/dist/qa/health.js +109 -0
- package/node_modules/checkride/dist/qa/health.js.map +1 -0
- package/node_modules/checkride/dist/qa/index.d.ts +23 -0
- package/node_modules/checkride/dist/qa/index.d.ts.map +1 -0
- package/node_modules/checkride/dist/qa/index.js +22 -0
- package/node_modules/checkride/dist/qa/index.js.map +1 -0
- package/node_modules/checkride/dist/qa/mutation.d.ts +80 -0
- package/node_modules/checkride/dist/qa/mutation.d.ts.map +1 -0
- package/node_modules/checkride/dist/qa/mutation.js +195 -0
- package/node_modules/checkride/dist/qa/mutation.js.map +1 -0
- package/node_modules/checkride/dist/qa/qa.d.ts +71 -0
- package/node_modules/checkride/dist/qa/qa.d.ts.map +1 -0
- package/node_modules/checkride/dist/qa/qa.js +158 -0
- package/node_modules/checkride/dist/qa/qa.js.map +1 -0
- package/node_modules/checkride/dist/qa/read.d.ts +40 -0
- package/node_modules/checkride/dist/qa/read.d.ts.map +1 -0
- package/node_modules/checkride/dist/qa/read.js +47 -0
- package/node_modules/checkride/dist/qa/read.js.map +1 -0
- package/node_modules/checkride/dist/qa/render.d.ts +26 -0
- package/node_modules/checkride/dist/qa/render.d.ts.map +1 -0
- package/node_modules/checkride/dist/qa/render.js +310 -0
- package/node_modules/checkride/dist/qa/render.js.map +1 -0
- package/node_modules/checkride/dist/recover.d.ts +48 -0
- package/node_modules/checkride/dist/recover.d.ts.map +1 -0
- package/node_modules/checkride/dist/recover.js +220 -0
- package/node_modules/checkride/dist/recover.js.map +1 -0
- package/node_modules/checkride/dist/security.d.ts +45 -0
- package/node_modules/checkride/dist/security.d.ts.map +1 -0
- package/node_modules/checkride/dist/security.js +86 -0
- package/node_modules/checkride/dist/security.js.map +1 -0
- package/node_modules/checkride/dist/smoke.d.ts +6 -6
- package/node_modules/checkride/dist/smoke.js +6 -6
- package/node_modules/checkride/dist/snippets.d.ts +27 -30
- package/node_modules/checkride/dist/snippets.d.ts.map +1 -1
- package/node_modules/checkride/dist/snippets.js +26 -30
- package/node_modules/checkride/dist/snippets.js.map +1 -1
- package/node_modules/checkride/dist/tool-json.d.ts +33 -0
- package/node_modules/checkride/dist/tool-json.d.ts.map +1 -0
- package/node_modules/checkride/dist/tool-json.js +58 -0
- package/node_modules/checkride/dist/tool-json.js.map +1 -0
- package/node_modules/checkride/dist/triage/cli.d.ts +21 -0
- package/node_modules/checkride/dist/triage/cli.d.ts.map +1 -0
- package/node_modules/checkride/dist/triage/cli.js +26 -0
- package/node_modules/checkride/dist/triage/cli.js.map +1 -0
- package/node_modules/checkride/dist/triage/doctor-fold.d.ts +51 -0
- package/node_modules/checkride/dist/triage/doctor-fold.d.ts.map +1 -0
- package/node_modules/checkride/dist/triage/doctor-fold.js +88 -0
- package/node_modules/checkride/dist/triage/doctor-fold.js.map +1 -0
- package/node_modules/checkride/dist/triage/env.d.ts +54 -0
- package/node_modules/checkride/dist/triage/env.d.ts.map +1 -0
- package/node_modules/checkride/dist/triage/env.js +81 -0
- package/node_modules/checkride/dist/triage/env.js.map +1 -0
- package/node_modules/checkride/dist/triage/gate.d.ts +61 -0
- package/node_modules/checkride/dist/triage/gate.d.ts.map +1 -0
- package/node_modules/checkride/dist/triage/gate.js +121 -0
- package/node_modules/checkride/dist/triage/gate.js.map +1 -0
- package/node_modules/checkride/dist/triage/index.d.ts +22 -0
- package/node_modules/checkride/dist/triage/index.d.ts.map +1 -0
- package/node_modules/checkride/dist/triage/index.js +20 -0
- package/node_modules/checkride/dist/triage/index.js.map +1 -0
- package/node_modules/checkride/dist/triage/render.d.ts +19 -0
- package/node_modules/checkride/dist/triage/render.d.ts.map +1 -0
- package/node_modules/checkride/dist/triage/render.js +419 -0
- package/node_modules/checkride/dist/triage/render.js.map +1 -0
- package/node_modules/checkride/dist/triage/triage.d.ts +83 -0
- package/node_modules/checkride/dist/triage/triage.d.ts.map +1 -0
- package/node_modules/checkride/dist/triage/triage.js +131 -0
- package/node_modules/checkride/dist/triage/triage.js.map +1 -0
- package/node_modules/checkride/package.json +18 -15
- package/node_modules/checkride/schema/checkride.config.schema.json +70 -5
- package/node_modules/checkride/schema/checkride.summary.schema.json +1 -1
- package/node_modules/checkride/skills/check/SKILL.md +265 -0
- package/node_modules/checkride/skills/qa/SKILL.md +231 -0
- package/node_modules/checkride/templates/shared/cspell.json +1 -0
- package/node_modules/checkride/templates/shared/oxlintrc.json +1 -1
- package/node_modules/checkride/templates/shared/styles/Repo/Drift.yml +41 -0
- package/node_modules/checkride/templates/shared/styles/Repo/Latin.yml +14 -0
- package/node_modules/checkride/templates/shared/styles/Repo/LyHyphen.yml +28 -0
- package/node_modules/checkride/templates/shared/styles/Repo/Minted.yml +33 -0
- package/node_modules/checkride/templates/shared/styles/Repo/ThereIs.yml +16 -0
- package/node_modules/checkride/templates/shared/styles/Repo/Weasel.yml +26 -0
- package/node_modules/checkride/templates/shared/vale.ini +34 -0
- package/package.json +10 -9
- package/skills/build/SKILL.md +85 -76
- package/skills/doctor/SKILL.md +11 -11
- package/skills/finish/SKILL.md +21 -18
- package/skills/harvest/SKILL.md +13 -11
- package/skills/park/SKILL.md +10 -10
- package/skills/plan/SKILL.md +69 -59
- package/skills/recover/SKILL.md +28 -0
- package/skills/refine/SKILL.md +21 -18
- package/skills/revert/SKILL.md +6 -6
- package/skills/spike/SKILL.md +9 -9
- package/skills/status/SKILL.md +5 -5
- package/skills/step/SKILL.md +20 -15
- package/skills/verify/SKILL.md +56 -46
- package/templates/build-log.md +7 -7
- package/templates/intent.md +52 -41
- package/templates/spike-report.md +10 -10
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Cursor hook writer — `.cursor/hooks.json` plus the scripts its entries
|
|
3
|
+
* invoke.
|
|
4
|
+
*
|
|
5
|
+
* Same contract as the Claude writer (`./claude.ts`): sentinel-keyed identity,
|
|
6
|
+
* in-place refresh, every unrelated key preserved, re-applying is a no-op. The
|
|
7
|
+
* schema is what differs, and it differs enough that sharing the merge would
|
|
8
|
+
* cost more than it saved:
|
|
9
|
+
*
|
|
10
|
+
* - Cursor requires a top-level `"version": 1`.
|
|
11
|
+
* - An event maps to a flat list of `{command, matcher?, timeout?}` entries —
|
|
12
|
+
* there is no group wrapper and no `type` discriminator, so a hook is one
|
|
13
|
+
* object where Claude Code needs two levels.
|
|
14
|
+
* - The events are lowercase and differently cut: `stop`, `afterFileEdit`
|
|
15
|
+
* (purpose-built, no matcher) and `preToolUse` (matched against Cursor's own
|
|
16
|
+
* tool names — `Shell`, `Read`, `Write`, `Grep`, `Delete`, `Task`, `MCP:*`).
|
|
17
|
+
* - Cursor cuts the shell out as its own pair of events, `beforeShellExecution`
|
|
18
|
+
* and `afterShellExecution`, whose matchers run against the **command string**
|
|
19
|
+
* rather than a tool name. Both guards take a second entry there to cover what
|
|
20
|
+
* the file tools never see; the gate does not, since it is not per-call.
|
|
21
|
+
*
|
|
22
|
+
* A hook name therefore maps to one *or more* entries, on different events. They
|
|
23
|
+
* share a name because they are one guard — `--remove-hook protect` takes out
|
|
24
|
+
* both — and share a script, since the script branches on `hook_event_name`.
|
|
25
|
+
*
|
|
26
|
+
* The gate entry carries three fields the Claude writer has no equivalent for —
|
|
27
|
+
* `timeout`, `loop_limit` and `failClosed` — because Cursor's defaults for all
|
|
28
|
+
* three end a turn that checkride would keep blocking. See {@link hookSpecs}.
|
|
29
|
+
*/
|
|
30
|
+
import { CURSOR_HOOKS_FILE } from '../gate.js';
|
|
31
|
+
import { GATE_TIMEOUT_SECONDS, HOOK_NAMES, writeHarnessHooks } from './harness.js';
|
|
32
|
+
/**
|
|
33
|
+
* Project-shared Cursor hook config (committed), relative to the repo root.
|
|
34
|
+
* Declared in `../gate.js`, which reads it to detect a native Cursor gate, and
|
|
35
|
+
* re-exported here so this module stays the one place a caller looks for it.
|
|
36
|
+
*/
|
|
37
|
+
export { CURSOR_HOOKS_FILE };
|
|
38
|
+
const GATE_SCRIPT_FILE = '.cursor/hooks/checkride-gate.sh';
|
|
39
|
+
const DIRTY_SCRIPT_FILE = '.cursor/hooks/checkride-dirty.sh';
|
|
40
|
+
const PROTECT_SCRIPT_FILE = '.cursor/hooks/checkride-protect.cjs';
|
|
41
|
+
const HARNESS = 'cursor';
|
|
42
|
+
/** The schema version Cursor's loader expects at the top of the file. */
|
|
43
|
+
const SCHEMA_VERSION = 1;
|
|
44
|
+
/**
|
|
45
|
+
* `sh` (not the exec bit) runs the script so a clone that lost file modes still
|
|
46
|
+
* gates; `CURSOR_PROJECT_DIR` anchors it when the session cwd is a subdirectory.
|
|
47
|
+
*
|
|
48
|
+
* **Unverified against a live Cursor.** Cursor documents `command` as "a shell
|
|
49
|
+
* string, an absolute path, or a relative path", but every documented example is
|
|
50
|
+
* a bare path, and it states separately that project hooks already run from the
|
|
51
|
+
* project root. If Cursor ever spawns these without a shell, `${…}` stays
|
|
52
|
+
* literal, the spawn fails, and Cursor's fail-open default ends the turn with no
|
|
53
|
+
* signal at all — the one failure mode `failClosed` cannot cover, because the
|
|
54
|
+
* hook it would guard never started. `failClosed` is set regardless; if that
|
|
55
|
+
* turns out not to catch it, drop both the interpreter and the variable and
|
|
56
|
+
* write the bare `.cursor/hooks/checkride-gate.sh` the docs show, accepting the
|
|
57
|
+
* dependence on the exec bit. See docs/cursor.md.
|
|
58
|
+
*/
|
|
59
|
+
const run = (interpreter, file) => `${interpreter} "\${CURSOR_PROJECT_DIR:-.}/${file}"`;
|
|
60
|
+
/**
|
|
61
|
+
* Cursor's file-mutating tool names. Cursor has no separate `Edit` tool — `Write`
|
|
62
|
+
* covers both creating and modifying — and `Delete` can remove an accounting
|
|
63
|
+
* file just as effectively as a write can overwrite it.
|
|
64
|
+
*/
|
|
65
|
+
const EDIT_TOOLS = 'Write|Delete';
|
|
66
|
+
/**
|
|
67
|
+
* Commands `protect` bothers to parse: the ones that name an accounting path at
|
|
68
|
+
* all. `beforeShellExecution` matches its `matcher` against the **full command
|
|
69
|
+
* string**, and that is the whole reason a shell guard is affordable here.
|
|
70
|
+
*
|
|
71
|
+
* checkride's standing objection to matching a shell tool is that a guard firing
|
|
72
|
+
* on a wrong parse is worse than a known hole. Filtering first turns the blast
|
|
73
|
+
* radius from "every command the agent runs" into "commands that already mention
|
|
74
|
+
* `checkride.baseline.json` or `.check`" — and inside that set the script still
|
|
75
|
+
* denies only on demonstrated write intent. Reads land here too and are allowed;
|
|
76
|
+
* they have to be, because triage reads these files.
|
|
77
|
+
*/
|
|
78
|
+
const ACCOUNTING_PATHS = 'checkride\\.baseline\\.json|\\.check\\b';
|
|
79
|
+
/**
|
|
80
|
+
* Commands `dirty` reads as having written a file: a `>`/`>>` redirect, or one of
|
|
81
|
+
* the mutating verbs. The negative lookahead on `&` is load-bearing — without it
|
|
82
|
+
* the `2>&1` on the end of half the agent's commands marks every turn dirty and
|
|
83
|
+
* the gate stops skipping anything.
|
|
84
|
+
*
|
|
85
|
+
* Heuristic on purpose, and biased toward matching: a false positive costs one
|
|
86
|
+
* pipeline run on a turn that changed nothing, while a false negative is the
|
|
87
|
+
* status quo — a shell-written file that no gate ever sees. Only the first of
|
|
88
|
+
* those is recoverable by running the gate.
|
|
89
|
+
*/
|
|
90
|
+
const SHELL_WRITES = '>>?\\s*[^&\\s>]' +
|
|
91
|
+
'|(^|[\\s;&|(])(rm|mv|cp|tee|sed|truncate|dd|patch|install|shred|ln)(\\s|$)' +
|
|
92
|
+
'|(^|[\\s;&|(])git\\s+(apply|checkout|restore|stash|clean|reset|revert|switch|merge|rebase|pull|cherry-pick)(\\s|$)';
|
|
93
|
+
/**
|
|
94
|
+
* Cursor's defaults are tuned for observers; the gate is not one, so all three
|
|
95
|
+
* of its non-command fields are set against the default rather than with it:
|
|
96
|
+
*
|
|
97
|
+
* - `timeout` — a full pipeline run is minutes, and the platform default is not.
|
|
98
|
+
* - `loop_limit: null` — the default of **5** stops the auto-followup after five
|
|
99
|
+
* turns, at which point a red repo finishes anyway. Claude Code re-blocks
|
|
100
|
+
* indefinitely; without this the two harnesses are not the same gate, and the
|
|
101
|
+
* Cursor one degrades into five nudges.
|
|
102
|
+
* - `failClosed: true` — Cursor's default is fail-*open*: a hook that crashes,
|
|
103
|
+
* times out, or emits unparseable JSON lets the turn end silently. That is the
|
|
104
|
+
* vacuous green the whole contract exists to prevent (docs/contract.md).
|
|
105
|
+
*
|
|
106
|
+
* The two guards keep Cursor's fail-open default on purpose — see `dirty` and
|
|
107
|
+
* `protect` in `./scripts.ts` for why each must never block an edit.
|
|
108
|
+
*/
|
|
109
|
+
function hookSpecs() {
|
|
110
|
+
return [
|
|
111
|
+
{
|
|
112
|
+
name: 'gate',
|
|
113
|
+
event: 'stop',
|
|
114
|
+
timeout: GATE_TIMEOUT_SECONDS,
|
|
115
|
+
loopLimit: null,
|
|
116
|
+
failClosed: true,
|
|
117
|
+
sentinels: [GATE_SCRIPT_FILE],
|
|
118
|
+
command: run('sh', GATE_SCRIPT_FILE),
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
// `afterFileEdit` rather than `postToolUse`: it is purpose-built for "a
|
|
122
|
+
// file changed", needs no matcher, and so cannot drift when Cursor renames
|
|
123
|
+
// a tool.
|
|
124
|
+
name: 'dirty',
|
|
125
|
+
event: 'afterFileEdit',
|
|
126
|
+
sentinels: [DIRTY_SCRIPT_FILE],
|
|
127
|
+
command: run('sh', DIRTY_SCRIPT_FILE),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
// The shell half of the edit marker. `afterFileEdit` sees only the file
|
|
131
|
+
// tools, so a turn that wrote through the shell alone used to skip the
|
|
132
|
+
// gate entirely. The matcher does all the deciding here — the script is
|
|
133
|
+
// the same unconditional `touch` either event reaches it through.
|
|
134
|
+
name: 'dirty',
|
|
135
|
+
event: 'afterShellExecution',
|
|
136
|
+
matcher: SHELL_WRITES,
|
|
137
|
+
sentinels: [DIRTY_SCRIPT_FILE],
|
|
138
|
+
command: run('sh', DIRTY_SCRIPT_FILE),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'protect',
|
|
142
|
+
event: 'preToolUse',
|
|
143
|
+
matcher: EDIT_TOOLS,
|
|
144
|
+
sentinels: [PROTECT_SCRIPT_FILE],
|
|
145
|
+
command: run('node', PROTECT_SCRIPT_FILE),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
// The shell half of the same guard. A tool-name matcher cannot see
|
|
149
|
+
// `echo … > checkride.baseline.json`, because that is a shell call and not
|
|
150
|
+
// a `Write`; `beforeShellExecution` is where it becomes visible. Same
|
|
151
|
+
// script, same fail-open default — see `protectScript` in `./scripts.ts`
|
|
152
|
+
// for why the parse behind it refuses to guess.
|
|
153
|
+
name: 'protect',
|
|
154
|
+
event: 'beforeShellExecution',
|
|
155
|
+
matcher: ACCOUNTING_PATHS,
|
|
156
|
+
sentinels: [PROTECT_SCRIPT_FILE],
|
|
157
|
+
command: run('node', PROTECT_SCRIPT_FILE),
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
/** True when an entry belongs to `spec` (matched by any of its sentinels). */
|
|
162
|
+
function isSpecHook(spec, hook) {
|
|
163
|
+
const command = hook.command;
|
|
164
|
+
return typeof command === 'string' && spec.sentinels.some((s) => command.includes(s));
|
|
165
|
+
}
|
|
166
|
+
/** The event's entry list, tolerating a malformed (non-array) config value. */
|
|
167
|
+
function eventHooks(config, event) {
|
|
168
|
+
const entries = config.hooks?.[event];
|
|
169
|
+
return Array.isArray(entries) ? entries.map((h) => ({ ...h })) : [];
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The canonical fields for `spec`, omitting the optional ones it doesn't set.
|
|
173
|
+
* `loopLimit` is compared against `undefined` rather than truth-tested, because
|
|
174
|
+
* the value checkride writes is `null`.
|
|
175
|
+
*/
|
|
176
|
+
function specFields(spec) {
|
|
177
|
+
return {
|
|
178
|
+
command: spec.command,
|
|
179
|
+
...(spec.matcher !== undefined ? { matcher: spec.matcher } : {}),
|
|
180
|
+
...(spec.timeout !== undefined ? { timeout: spec.timeout } : {}),
|
|
181
|
+
...(spec.loopLimit !== undefined ? { loop_limit: spec.loopLimit } : {}),
|
|
182
|
+
...(spec.failClosed !== undefined ? { failClosed: spec.failClosed } : {}),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Merge one hook into a parsed config, idempotently. An existing entry (found by
|
|
187
|
+
* sentinel) has its fields refreshed in place, keeping any key checkride does not
|
|
188
|
+
* own; otherwise a new entry is appended.
|
|
189
|
+
*/
|
|
190
|
+
function applyHook(config, spec) {
|
|
191
|
+
const hooks = { ...config.hooks };
|
|
192
|
+
const entries = eventHooks(config, spec.event);
|
|
193
|
+
const idx = entries.findIndex((h) => isSpecHook(spec, h));
|
|
194
|
+
if (idx >= 0) {
|
|
195
|
+
entries[idx] = { ...entries[idx], ...specFields(spec) };
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
entries.push(specFields(spec));
|
|
199
|
+
}
|
|
200
|
+
return { ...config, hooks: { ...hooks, [spec.event]: entries } };
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Merge the selected hooks into a parsed config, seeding `version` when absent.
|
|
204
|
+
* An existing `version` is never overwritten: if a future Cursor bumps it, the
|
|
205
|
+
* repo's value is the one that matches the rest of the file.
|
|
206
|
+
*/
|
|
207
|
+
export function applyCursorHooks(config = {}, names = HOOK_NAMES) {
|
|
208
|
+
const seeded = config.version === undefined ? { version: SCHEMA_VERSION, ...config } : config;
|
|
209
|
+
return hookSpecs()
|
|
210
|
+
.filter((spec) => names.includes(spec.name))
|
|
211
|
+
.reduce((acc, spec) => applyHook(acc, spec), seeded);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Strip one hook from a parsed config. An event left with no entries loses its
|
|
215
|
+
* key; `version` and every unrelated entry stay. Cursor's schema is flat, so
|
|
216
|
+
* there is no empty group to prune — the Claude writer's extra step has no
|
|
217
|
+
* counterpart here.
|
|
218
|
+
*/
|
|
219
|
+
function removeHook(config, spec) {
|
|
220
|
+
const hooks = { ...config.hooks };
|
|
221
|
+
const entries = eventHooks(config, spec.event).filter((h) => !isSpecHook(spec, h));
|
|
222
|
+
if (entries.length > 0)
|
|
223
|
+
hooks[spec.event] = entries;
|
|
224
|
+
else
|
|
225
|
+
delete hooks[spec.event];
|
|
226
|
+
return { ...config, hooks };
|
|
227
|
+
}
|
|
228
|
+
/** Remove the named hooks from a parsed config — the inverse of {@link applyCursorHooks}. */
|
|
229
|
+
export function removeCursorHooks(config = {}, names = HOOK_NAMES) {
|
|
230
|
+
return hookSpecs()
|
|
231
|
+
.filter((spec) => names.includes(spec.name))
|
|
232
|
+
.reduce((acc, spec) => removeHook(acc, spec), config);
|
|
233
|
+
}
|
|
234
|
+
const SPEC = {
|
|
235
|
+
name: HARNESS,
|
|
236
|
+
configFile: CURSOR_HOOKS_FILE,
|
|
237
|
+
// Cursor's config has no field that renders the gate's command in its UI, so
|
|
238
|
+
// the package manager reaches its hooks only through the generated script.
|
|
239
|
+
apply: applyCursorHooks,
|
|
240
|
+
scripts: { gate: GATE_SCRIPT_FILE, dirty: DIRTY_SCRIPT_FILE, protect: PROTECT_SCRIPT_FILE },
|
|
241
|
+
remove: removeCursorHooks,
|
|
242
|
+
};
|
|
243
|
+
/** Write or refresh the selected Cursor hooks in `cwd`. */
|
|
244
|
+
export function writeCursorHooks(cwd, opts) {
|
|
245
|
+
return writeHarnessHooks(cwd, SPEC, opts);
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/agent-setup/cursor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem helpers shared by the per-harness hook writers.
|
|
3
|
+
*
|
|
4
|
+
* Every write `agent-setup` makes is idempotent — a second run must report
|
|
5
|
+
* `changed: false` for every file — so the writers never write blind: they read
|
|
6
|
+
* first, compare, and only then touch the disk. `dryRun` computes the same
|
|
7
|
+
* result without writing anything.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* One file a hook writer produced, and whether this run actually changed it.
|
|
11
|
+
* `removed` marks a deletion, so a caller reporting the run can tell a file that
|
|
12
|
+
* was written from one that was taken away — both are changes.
|
|
13
|
+
*/
|
|
14
|
+
export type HookFile = {
|
|
15
|
+
path: string;
|
|
16
|
+
changed: boolean;
|
|
17
|
+
removed?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare function readIfExists(path: string): Promise<string | null>;
|
|
20
|
+
/** Write `content` to `cwd/rel` (creating directories) unless it already matches. */
|
|
21
|
+
export declare function putFile(cwd: string, rel: string, content: string, opts: {
|
|
22
|
+
dryRun: boolean;
|
|
23
|
+
executable?: boolean;
|
|
24
|
+
}): Promise<HookFile>;
|
|
25
|
+
/**
|
|
26
|
+
* Delete `cwd/rel` if it is there. Reports `changed: true` only when a file was
|
|
27
|
+
* actually removed, so a second removal is the no-op every other write here is.
|
|
28
|
+
*/
|
|
29
|
+
export declare function dropFile(cwd: string, rel: string, opts: {
|
|
30
|
+
dryRun: boolean;
|
|
31
|
+
}): Promise<HookFile>;
|
|
32
|
+
/**
|
|
33
|
+
* Merge `next` into the JSON config at `cwd/rel` and write it back when it
|
|
34
|
+
* changed. `merge` receives the parsed file — or `undefined` when there is none
|
|
35
|
+
* — and returns the merged value; it must be idempotent.
|
|
36
|
+
*
|
|
37
|
+
* `skipIfMissing` suppresses creating the file at all: a run that only *removes*
|
|
38
|
+
* hooks has nothing to say to a repo that never had that harness configured, and
|
|
39
|
+
* writing `{}` into a `.claude/settings.json` that did not exist would be a
|
|
40
|
+
* confusing way to say "nothing to remove".
|
|
41
|
+
*
|
|
42
|
+
* A malformed file is named in the error, so a consumer sees
|
|
43
|
+
* `invalid .claude/settings.json: <reason>` instead of a bare `SyntaxError`
|
|
44
|
+
* stack (mirrors `invalidConfig` in `config.ts`).
|
|
45
|
+
*/
|
|
46
|
+
export declare function putJson<T>(cwd: string, rel: string, merge: (current?: T) => T, opts: {
|
|
47
|
+
dryRun: boolean;
|
|
48
|
+
skipIfMissing?: boolean;
|
|
49
|
+
}): Promise<HookFile>;
|
|
50
|
+
//# sourceMappingURL=files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/agent-setup/files.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH;;;;GAIG;AACH,MAAM,MAAM,QAAQ,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE7E,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMvE;AAED,qFAAqF;AACrF,wBAAsB,OAAO,CAC3B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9C,OAAO,CAAC,QAAQ,CAAC,CAUnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GACxB,OAAO,CAAC,QAAQ,CAAC,CAKnB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,OAAO,CAAC,CAAC,EAC7B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,EACzB,IAAI,EAAE;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,GACjD,OAAO,CAAC,QAAQ,CAAC,CAoBnB"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem helpers shared by the per-harness hook writers.
|
|
3
|
+
*
|
|
4
|
+
* Every write `agent-setup` makes is idempotent — a second run must report
|
|
5
|
+
* `changed: false` for every file — so the writers never write blind: they read
|
|
6
|
+
* first, compare, and only then touch the disk. `dryRun` computes the same
|
|
7
|
+
* result without writing anything.
|
|
8
|
+
*/
|
|
9
|
+
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
export async function readIfExists(path) {
|
|
12
|
+
try {
|
|
13
|
+
return await readFile(path, 'utf8');
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Write `content` to `cwd/rel` (creating directories) unless it already matches. */
|
|
20
|
+
export async function putFile(cwd, rel, content, opts) {
|
|
21
|
+
const path = join(cwd, rel);
|
|
22
|
+
const raw = await readIfExists(path);
|
|
23
|
+
const changed = raw !== content;
|
|
24
|
+
if (changed && !opts.dryRun) {
|
|
25
|
+
await mkdir(dirname(path), { recursive: true });
|
|
26
|
+
await writeFile(path, content);
|
|
27
|
+
if (opts.executable)
|
|
28
|
+
await chmod(path, 0o755);
|
|
29
|
+
}
|
|
30
|
+
return { path: rel, changed };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Delete `cwd/rel` if it is there. Reports `changed: true` only when a file was
|
|
34
|
+
* actually removed, so a second removal is the no-op every other write here is.
|
|
35
|
+
*/
|
|
36
|
+
export async function dropFile(cwd, rel, opts) {
|
|
37
|
+
const path = join(cwd, rel);
|
|
38
|
+
const changed = (await readIfExists(path)) !== null;
|
|
39
|
+
if (changed && !opts.dryRun)
|
|
40
|
+
await rm(path, { force: true });
|
|
41
|
+
return { path: rel, changed, removed: true };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Merge `next` into the JSON config at `cwd/rel` and write it back when it
|
|
45
|
+
* changed. `merge` receives the parsed file — or `undefined` when there is none
|
|
46
|
+
* — and returns the merged value; it must be idempotent.
|
|
47
|
+
*
|
|
48
|
+
* `skipIfMissing` suppresses creating the file at all: a run that only *removes*
|
|
49
|
+
* hooks has nothing to say to a repo that never had that harness configured, and
|
|
50
|
+
* writing `{}` into a `.claude/settings.json` that did not exist would be a
|
|
51
|
+
* confusing way to say "nothing to remove".
|
|
52
|
+
*
|
|
53
|
+
* A malformed file is named in the error, so a consumer sees
|
|
54
|
+
* `invalid .claude/settings.json: <reason>` instead of a bare `SyntaxError`
|
|
55
|
+
* stack (mirrors `invalidConfig` in `config.ts`).
|
|
56
|
+
*/
|
|
57
|
+
export async function putJson(cwd, rel, merge, opts) {
|
|
58
|
+
const path = join(cwd, rel);
|
|
59
|
+
const raw = await readIfExists(path);
|
|
60
|
+
if (raw === null && opts.skipIfMissing === true)
|
|
61
|
+
return { path: rel, changed: false };
|
|
62
|
+
let current;
|
|
63
|
+
if (raw !== null) {
|
|
64
|
+
try {
|
|
65
|
+
current = JSON.parse(raw);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
69
|
+
throw new Error(`invalid ${rel}: ${reason}`, { cause: err });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const nextRaw = `${JSON.stringify(merge(current), null, 2)}\n`;
|
|
73
|
+
const changed = raw !== nextRaw;
|
|
74
|
+
if (changed && !opts.dryRun) {
|
|
75
|
+
await mkdir(dirname(path), { recursive: true });
|
|
76
|
+
await writeFile(path, nextRaw);
|
|
77
|
+
}
|
|
78
|
+
return { path: rel, changed };
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/agent-setup/files.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape a harness writer fills in, and the one routine that drives it.
|
|
3
|
+
*
|
|
4
|
+
* Every harness needs the same two writes — merge its hook entries into its
|
|
5
|
+
* config file, then drop whichever scripts those entries invoke — and differs
|
|
6
|
+
* only in *where* each lands, *how* its config merges, and how much of the work
|
|
7
|
+
* its config can do without a script at all. Keeping that common body here is
|
|
8
|
+
* what makes a new harness a description rather than a copy: a `HarnessSpec`
|
|
9
|
+
* plus a merge function, and nothing else.
|
|
10
|
+
*/
|
|
11
|
+
import { type HarnessName } from '../gate.js';
|
|
12
|
+
import { type PackageManager } from '../pm/index.js';
|
|
13
|
+
import type { HookFile } from './files.js';
|
|
14
|
+
/**
|
|
15
|
+
* Every hook `agent-setup` can write; `--hook <a,b>` selects a subset. It lives
|
|
16
|
+
* here rather than in `./hook.ts` so the per-harness writers can name it without
|
|
17
|
+
* importing the module that imports them.
|
|
18
|
+
*/
|
|
19
|
+
export declare const HOOK_NAMES: readonly ["gate", "dirty", "protect"];
|
|
20
|
+
export type HookName = (typeof HOOK_NAMES)[number];
|
|
21
|
+
/**
|
|
22
|
+
* Ceiling for the gate run, in seconds, for any harness whose config carries
|
|
23
|
+
* one. Generous on purpose: it exists to break a hang, not to police a slow
|
|
24
|
+
* repo, and checkride's own per-check timeouts fire long before it does.
|
|
25
|
+
*
|
|
26
|
+
* Both harnesses default lower than a real pipeline takes — Cursor's default is
|
|
27
|
+
* short, and Claude Code's is 600s — and both read a timed-out stop hook as a
|
|
28
|
+
* *broken* one, which under Claude Code means the turn simply ends. A gate that
|
|
29
|
+
* quietly stops gating on the slowest repos is the vacuous green the contract
|
|
30
|
+
* exists to prevent, so neither default is left in place.
|
|
31
|
+
*/
|
|
32
|
+
export declare const GATE_TIMEOUT_SECONDS = 900;
|
|
33
|
+
/**
|
|
34
|
+
* The spinner text a harness shows while the gate runs.
|
|
35
|
+
*
|
|
36
|
+
* Without it the only feedback during a full pipeline run is the harness's
|
|
37
|
+
* default spinner, which is indistinguishable from a hung model — the user is
|
|
38
|
+
* left guessing whether anything is happening at all. Naming the command that
|
|
39
|
+
* is actually running answers that in the one place they are already looking.
|
|
40
|
+
*/
|
|
41
|
+
export declare function gateStatusMessage(pm: PackageManager): string;
|
|
42
|
+
/**
|
|
43
|
+
* Where one harness keeps its hooks, and how to merge into its config.
|
|
44
|
+
*
|
|
45
|
+
* `T` is that harness's config shape, so each writer keeps its own types all
|
|
46
|
+
* the way through — `apply` is never handed a config it does not understand.
|
|
47
|
+
*/
|
|
48
|
+
export type HarnessSpec<T> = {
|
|
49
|
+
name: HarnessName;
|
|
50
|
+
/** The harness's hook config, relative to the repo root. */
|
|
51
|
+
configFile: string;
|
|
52
|
+
/**
|
|
53
|
+
* Where each hook's script lives, relative to the repo root — for the hooks
|
|
54
|
+
* that still need one.
|
|
55
|
+
*
|
|
56
|
+
* A hook is absent here when the harness can express it as *configuration*
|
|
57
|
+
* instead: Claude Code's `protect` is a `permissions.deny` rule and its
|
|
58
|
+
* `dirty` is an inline one-liner, so neither writes a file. Generated code in
|
|
59
|
+
* a consumer's repo is a cost — a file to review, to keep in sync, and to
|
|
60
|
+
* wonder about — and it should be paid only where config cannot do the job.
|
|
61
|
+
*/
|
|
62
|
+
scripts: Partial<Record<HookName, string>>;
|
|
63
|
+
/**
|
|
64
|
+
* Scripts an earlier checkride wrote for this harness that this one does not.
|
|
65
|
+
* Deleted whenever their hook is written or removed, so a repo upgrading into
|
|
66
|
+
* the config-only form does not keep a dead file it will never run again.
|
|
67
|
+
*/
|
|
68
|
+
retired?: Partial<Record<HookName, string>>;
|
|
69
|
+
/**
|
|
70
|
+
* Merge the named hooks into a parsed config. Must be idempotent. `pm` is the
|
|
71
|
+
* repo's package manager, which a harness that renders the gate's command in
|
|
72
|
+
* its own UI (Claude Code's `statusMessage`) needs in the config, not just in
|
|
73
|
+
* the script.
|
|
74
|
+
*/
|
|
75
|
+
apply: (config: T | undefined, names: readonly string[], pm: PackageManager) => T;
|
|
76
|
+
/** Strip the named hooks from a parsed config. Must be idempotent. */
|
|
77
|
+
remove: (config: T | undefined, names: readonly string[]) => T;
|
|
78
|
+
};
|
|
79
|
+
export type WriteOptions = {
|
|
80
|
+
pm?: PackageManager;
|
|
81
|
+
dryRun?: boolean;
|
|
82
|
+
hooks: readonly string[];
|
|
83
|
+
/**
|
|
84
|
+
* Hooks to tear out: their config entries are stripped and their scripts
|
|
85
|
+
* deleted. Removal wins over `hooks`, so a name in both is removed — the
|
|
86
|
+
* caller-facing flags (`--hook`/`--remove-hook`) reject that combination
|
|
87
|
+
* before it gets here, and this is the safe reading if one ever slips through.
|
|
88
|
+
*/
|
|
89
|
+
remove?: readonly string[];
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Write or refresh `spec`'s hooks in `cwd` and tear out the ones marked for
|
|
93
|
+
* removal: the config entries first, then only the scripts the selected hooks
|
|
94
|
+
* actually invoke. Every write is idempotent, and `dryRun` computes the result
|
|
95
|
+
* without touching the disk.
|
|
96
|
+
*
|
|
97
|
+
* A run that only removes never *creates* the harness's config file — a repo
|
|
98
|
+
* that was never wired for this harness has nothing to un-wire.
|
|
99
|
+
*/
|
|
100
|
+
export declare function writeHarnessHooks<T extends object>(cwd: string, spec: HarnessSpec<T>, opts: WriteOptions): Promise<HookFile[]>;
|
|
101
|
+
//# sourceMappingURL=harness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../src/agent-setup/harness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAwB,KAAK,cAAc,EAAa,MAAM,gBAAgB,CAAC;AACtF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAI3C;;;;GAIG;AACH,eAAO,MAAM,UAAU,uCAAwC,CAAC;AAChE,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,cAAc,GAAG,MAAM,CAE5D;AAED;;;;;GAKG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC3B,IAAI,EAAE,WAAW,CAAC;IAClB,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;OASG;IACH,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3C;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5C;;;;;OAKG;IACH,KAAK,EAAE,CAAC,MAAM,EAAE,CAAC,GAAG,SAAS,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,EAAE,cAAc,KAAK,CAAC,CAAC;IAClF,sEAAsE;IACtE,MAAM,EAAE,CAAC,MAAM,EAAE,CAAC,GAAG,SAAS,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,KAAK,CAAC,CAAC;CAChE,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,CAAC,EAAE,cAAc,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,SAAS,MAAM,EACtD,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,EACpB,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,QAAQ,EAAE,CAAC,CAyDrB"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape a harness writer fills in, and the one routine that drives it.
|
|
3
|
+
*
|
|
4
|
+
* Every harness needs the same two writes — merge its hook entries into its
|
|
5
|
+
* config file, then drop whichever scripts those entries invoke — and differs
|
|
6
|
+
* only in *where* each lands, *how* its config merges, and how much of the work
|
|
7
|
+
* its config can do without a script at all. Keeping that common body here is
|
|
8
|
+
* what makes a new harness a description rather than a copy: a `HarnessSpec`
|
|
9
|
+
* plus a merge function, and nothing else.
|
|
10
|
+
*/
|
|
11
|
+
import { gatePreflight } from '../gate.js';
|
|
12
|
+
import { detectPackageManager, runScript } from '../pm/index.js';
|
|
13
|
+
import { dropFile, putFile, putJson } from './files.js';
|
|
14
|
+
import { dirtyScript, gateScript, protectScript } from './scripts.js';
|
|
15
|
+
/**
|
|
16
|
+
* Every hook `agent-setup` can write; `--hook <a,b>` selects a subset. It lives
|
|
17
|
+
* here rather than in `./hook.ts` so the per-harness writers can name it without
|
|
18
|
+
* importing the module that imports them.
|
|
19
|
+
*/
|
|
20
|
+
export const HOOK_NAMES = ['gate', 'dirty', 'protect'];
|
|
21
|
+
/**
|
|
22
|
+
* Ceiling for the gate run, in seconds, for any harness whose config carries
|
|
23
|
+
* one. Generous on purpose: it exists to break a hang, not to police a slow
|
|
24
|
+
* repo, and checkride's own per-check timeouts fire long before it does.
|
|
25
|
+
*
|
|
26
|
+
* Both harnesses default lower than a real pipeline takes — Cursor's default is
|
|
27
|
+
* short, and Claude Code's is 600s — and both read a timed-out stop hook as a
|
|
28
|
+
* *broken* one, which under Claude Code means the turn simply ends. A gate that
|
|
29
|
+
* quietly stops gating on the slowest repos is the vacuous green the contract
|
|
30
|
+
* exists to prevent, so neither default is left in place.
|
|
31
|
+
*/
|
|
32
|
+
export const GATE_TIMEOUT_SECONDS = 900;
|
|
33
|
+
/**
|
|
34
|
+
* The spinner text a harness shows while the gate runs.
|
|
35
|
+
*
|
|
36
|
+
* Without it the only feedback during a full pipeline run is the harness's
|
|
37
|
+
* default spinner, which is indistinguishable from a hung model — the user is
|
|
38
|
+
* left guessing whether anything is happening at all. Naming the command that
|
|
39
|
+
* is actually running answers that in the one place they are already looking.
|
|
40
|
+
*/
|
|
41
|
+
export function gateStatusMessage(pm) {
|
|
42
|
+
return `checkride gate — running \`${runScript(pm, 'check')}\``;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Write or refresh `spec`'s hooks in `cwd` and tear out the ones marked for
|
|
46
|
+
* removal: the config entries first, then only the scripts the selected hooks
|
|
47
|
+
* actually invoke. Every write is idempotent, and `dryRun` computes the result
|
|
48
|
+
* without touching the disk.
|
|
49
|
+
*
|
|
50
|
+
* A run that only removes never *creates* the harness's config file — a repo
|
|
51
|
+
* that was never wired for this harness has nothing to un-wire.
|
|
52
|
+
*/
|
|
53
|
+
export async function writeHarnessHooks(cwd, spec, opts) {
|
|
54
|
+
const pm = opts.pm ?? detectPackageManager({ cwd });
|
|
55
|
+
const dryRun = opts.dryRun ?? false;
|
|
56
|
+
const gone = opts.remove ?? [];
|
|
57
|
+
const names = opts.hooks.filter((name) => !gone.includes(name));
|
|
58
|
+
// Read on every write, which is what makes the preflight the one seam that
|
|
59
|
+
// survives a refresh: `hooks add` rewrites the script and re-points the
|
|
60
|
+
// harness config, and re-derives this from the repo's own config each time.
|
|
61
|
+
const preflight = gatePreflight(cwd);
|
|
62
|
+
const bodies = {
|
|
63
|
+
// The gate guards on the edit marker only when the hook that sets it is
|
|
64
|
+
// written too; alone, the guard would disarm the gate entirely. Removing
|
|
65
|
+
// `dirty` therefore rewrites a surviving gate unguarded, which is why the
|
|
66
|
+
// config merge and the script bodies both read the post-removal selection.
|
|
67
|
+
gate: () => gateScript(pm, {
|
|
68
|
+
harness: spec.name,
|
|
69
|
+
dirtyGuard: names.includes('dirty'),
|
|
70
|
+
...(preflight === null ? {} : { preflight }),
|
|
71
|
+
}),
|
|
72
|
+
dirty: dirtyScript,
|
|
73
|
+
protect: protectScript,
|
|
74
|
+
};
|
|
75
|
+
const config = await putJson(cwd, spec.configFile, (c) => spec.apply(spec.remove(c, gone), names, pm), { dryRun, skipIfMissing: names.length === 0 });
|
|
76
|
+
// A hook this harness expresses as config has no script to write, and may have
|
|
77
|
+
// a retired one to clear away. `touched` is both directions at once, because a
|
|
78
|
+
// dead file is worth deleting whether the hook is arriving or leaving.
|
|
79
|
+
const touched = HOOK_NAMES.filter((name) => names.includes(name) || gone.includes(name));
|
|
80
|
+
const [written, removed, swept] = await Promise.all([
|
|
81
|
+
Promise.all(HOOK_NAMES.filter((name) => names.includes(name) && spec.scripts[name] !== undefined).map((name) => putFile(cwd, spec.scripts[name] ?? '', bodies[name](), { dryRun, executable: true }))),
|
|
82
|
+
Promise.all(HOOK_NAMES.filter((name) => gone.includes(name) && spec.scripts[name] !== undefined).map((name) => dropFile(cwd, spec.scripts[name] ?? '', { dryRun }))),
|
|
83
|
+
Promise.all(touched
|
|
84
|
+
.map((name) => spec.retired?.[name])
|
|
85
|
+
.filter((path) => path !== undefined)
|
|
86
|
+
.map((path) => dropFile(cwd, path, { dryRun }))),
|
|
87
|
+
]);
|
|
88
|
+
// Only report a retired script that was actually there. Every repo set up
|
|
89
|
+
// after these became configuration has none, and listing files that never
|
|
90
|
+
// existed as "absent" would make the common run read like a cleanup.
|
|
91
|
+
return [config, ...written, ...removed, ...swept.filter((f) => f.changed)];
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=harness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness.js","sourceRoot":"","sources":["../../src/agent-setup/harness.ts"],"names":[],"mappings":"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"}
|