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
package/dist/verbs/handoff.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// `plumbbob handoff
|
|
1
|
+
// `plumbbob handoff`: print the standardized build hand-off block: the
|
|
2
2
|
// "state / choice / what's next" the human sees at each step boundary. Read-only,
|
|
3
|
-
// no state change. It derives the moment from the session
|
|
3
|
+
// no state change. It derives the moment from the session: a step in flight ⇒ the
|
|
4
4
|
// pause block (built → looks-good/needs-work → next), none ⇒ the post-checkpoint
|
|
5
|
-
// boundary block
|
|
5
|
+
// boundary block, and renders the next undone step plus its advisory `- model:`
|
|
6
6
|
// recommendation straight from intent.md, the build's plan document. Owning the
|
|
7
7
|
// block here, rather than as prose in the build skill, keeps the skill from
|
|
8
8
|
// drifting out of sync with `status`, which renders the same next-step detail
|
|
@@ -75,7 +75,7 @@ function boundaryBlock(step, title, nextUp) {
|
|
|
75
75
|
* The forward pointer, shared by both blocks: the next undone step, its title,
|
|
76
76
|
* and its advisory `- model:` recommendation.
|
|
77
77
|
*
|
|
78
|
-
* The recommendation renders as just the model token for the `/model` call
|
|
78
|
+
* The recommendation renders as just the model token for the `/model` call;
|
|
79
79
|
* the full rationale lives on the `status` dashboard. No next step ⇒ the
|
|
80
80
|
* finish/step nudge instead.
|
|
81
81
|
*/
|
|
@@ -101,8 +101,9 @@ function titleOf(steps, n) {
|
|
|
101
101
|
return steps.find((s) => s.n === n)?.title ?? null;
|
|
102
102
|
}
|
|
103
103
|
/**
|
|
104
|
-
* The model token from a `- model:` recommendation
|
|
105
|
-
* `model: opus
|
|
104
|
+
* The model token from a `- model:` recommendation: the first word, so a
|
|
105
|
+
* `model: opus` line followed by an em dash and rationale yields `opus` for
|
|
106
|
+
* the `/model` call.
|
|
106
107
|
*
|
|
107
108
|
* Null when there is no recommendation, or it degraded to whitespace (folding
|
|
108
109
|
* the null model into the same guard keeps both branches live).
|
|
@@ -112,7 +113,7 @@ function modelToken(model) {
|
|
|
112
113
|
return first !== undefined && first.length > 0 ? first : null;
|
|
113
114
|
}
|
|
114
115
|
/**
|
|
115
|
-
* Read a file as UTF-8, or return '' when it is missing or unreadable
|
|
116
|
+
* Read a file as UTF-8, or return '' when it is missing or unreadable: an
|
|
116
117
|
* absent sidecar file reads as empty rather than throwing.
|
|
117
118
|
*/
|
|
118
119
|
function readOr(path) {
|
|
@@ -124,7 +125,7 @@ function readOr(path) {
|
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
/**
|
|
127
|
-
* The in-flight step number from the STEP marker
|
|
128
|
+
* The in-flight step number from the STEP marker: the flat one-line file in
|
|
128
129
|
* the build folder that records which step is open. Null when no step is in
|
|
129
130
|
* flight or the file holds anything but a bare number.
|
|
130
131
|
*/
|
package/dist/verbs/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// `plumbbob init [--uninstall] [--force]
|
|
1
|
+
// `plumbbob init [--uninstall] [--force]`: link plumbbob into Claude Code as an
|
|
2
2
|
// in-place skills-directory plugin. It symlinks the installed package into
|
|
3
|
-
// ~/.claude/skills/plumbbob, where Claude Code discovers it as `plumbbob@skills-dir
|
|
4
|
-
//
|
|
3
|
+
// ~/.claude/skills/plumbbob, where Claude Code discovers it as `plumbbob@skills-dir`:
|
|
4
|
+
// the skills load namespaced (`/plumbbob:*`) and the post-edit hook auto-registers
|
|
5
5
|
// from hooks/hooks.json. This non-marketplace install path is first-class by design:
|
|
6
6
|
// it serves an `npm i -g` global, local dev, and clients predating plugins, and it
|
|
7
7
|
// keeps plumbbob usable as a standalone CLI outside any one host rather than
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
// via bin/, so it needs neither `npm i -g` nor `init`). The two are mutually
|
|
11
11
|
// exclusive: both register a plugin named `plumbbob`, and a double-install collides
|
|
12
12
|
// over the /plumbbob:* namespace (skills can drop to flat `/status` names). So init
|
|
13
|
-
// REFUSES when a marketplace plumbbob is already installed
|
|
13
|
+
// REFUSES when a marketplace plumbbob is already installed: `--force` overrides
|
|
14
14
|
// (the dev-install path uses it). Global-only by design: install scope is NOT session
|
|
15
|
-
// scope
|
|
15
|
+
// scope: sessions stay per-project via `plumbbob start`. Idempotent + reversible
|
|
16
16
|
// (`--uninstall` drops the link); it NEVER writes settings.json. Node builtins only.
|
|
17
17
|
import { lstatSync, mkdirSync, readlinkSync, rmSync, symlinkSync } from 'node:fs';
|
|
18
18
|
import { homedir } from 'node:os';
|
|
@@ -20,7 +20,7 @@ import { join } from 'node:path';
|
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
import { marketplacePlumbbob } from "../lib/plugins.js";
|
|
22
22
|
/**
|
|
23
|
-
* The installed package root
|
|
23
|
+
* The installed package root: parent of .claude-plugin/, skills/, hooks/, dist/.
|
|
24
24
|
*
|
|
25
25
|
* Derived from this module's URL: the global install when run as the published
|
|
26
26
|
* bin, the checkout in dev. No trailing slash, so it compares clean against a
|
|
@@ -64,8 +64,8 @@ function symlinkTarget(path) {
|
|
|
64
64
|
/**
|
|
65
65
|
* Link the installed package into ~/.claude/skills/plumbbob, or `--uninstall` it.
|
|
66
66
|
*
|
|
67
|
-
* Refuses when a marketplace plumbbob plugin is already installed
|
|
68
|
-
* named `plumbbob` collide over the /plumbbob:* namespace
|
|
67
|
+
* Refuses when a marketplace plumbbob plugin is already installed (two plugins
|
|
68
|
+
* named `plumbbob` collide over the /plumbbob:* namespace) unless `--force`.
|
|
69
69
|
* Repoints a stale link from an earlier install location; already-linked is a
|
|
70
70
|
* clean exit 0.
|
|
71
71
|
*/
|
|
@@ -95,7 +95,7 @@ export function init(args) {
|
|
|
95
95
|
process.stdout.write(`plumbbob: already linked (${link} → ${target}). Nothing to do.\n`);
|
|
96
96
|
return 0;
|
|
97
97
|
}
|
|
98
|
-
rmSync(link); // a stale link from an earlier install location
|
|
98
|
+
rmSync(link); // a stale link from an earlier install location: repoint it
|
|
99
99
|
}
|
|
100
100
|
symlinkSync(target, link);
|
|
101
101
|
process.stdout.write(`plumbbob: linked ${link} → ${target}.\n` +
|
package/dist/verbs/park.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// `plumbbob park "<text>"
|
|
1
|
+
// `plumbbob park "<text>"`: the raw capture path for a thought you don't want
|
|
2
2
|
// to chase mid-step: append one flat line under the "## Park list" section of
|
|
3
|
-
// build-log.md (the build's human-facing ledger). Capture stays dumb by design
|
|
3
|
+
// build-log.md (the build's human-facing ledger). Capture stays dumb by design:
|
|
4
4
|
// a grep-readable append, no markdown parsing, no model turn (wording the line
|
|
5
5
|
// well is /plumbbob:park's job); triage waits for a step boundary, via harvest.
|
|
6
6
|
// Capture is not a state transition, so it runs in any context (terminal or
|
|
@@ -14,7 +14,7 @@ import { appendToSection } from "../lib/buildlog.js";
|
|
|
14
14
|
*
|
|
15
15
|
* All non-flag args join into a single line. Refuses when there is no active
|
|
16
16
|
* session, when the text is empty, or when build-log.md lacks a "## Park list"
|
|
17
|
-
* section
|
|
17
|
+
* section: a park line must never vanish silently.
|
|
18
18
|
*/
|
|
19
19
|
export function park(cwd, args) {
|
|
20
20
|
const root = findRepoRoot(cwd);
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// `plumbbob recover`: read the control plane and say, plainly, whether it is
|
|
2
|
+
// telling the truth. Nothing else reads these files as a set: `doctor` checks
|
|
3
|
+
// the install and the gate, `status` reports what the markers claim without
|
|
4
|
+
// questioning them. So a session that crashed, lost its context window, or was
|
|
5
|
+
// switched away mid-step can sit in a state no verb detects: the worst being a
|
|
6
|
+
// cursor pointing at a build that no longer exists, which renders a perfectly
|
|
7
|
+
// plausible empty dashboard instead of an error.
|
|
8
|
+
//
|
|
9
|
+
// Diagnosis is free and repair is asked for by name (`--fix`), the same posture
|
|
10
|
+
// `doctor --migrate` takes. What `--fix` may touch is deliberately narrow: only
|
|
11
|
+
// the untracked control files this tool owns and can rewrite from what it
|
|
12
|
+
// already knows. It never touches a tracked artifact (intent, build-log,
|
|
13
|
+
// checkpoints, reports), never touches git, and never advances the loop: a
|
|
14
|
+
// recovery verb that could land a step would be a second, quieter checkpoint.
|
|
15
|
+
//
|
|
16
|
+
// Spike leftovers are reported and never removed: those worktrees live outside
|
|
17
|
+
// the repo root and may hold work nobody copied out, so naming them is help and
|
|
18
|
+
// deleting them is a gamble the human should take by hand.
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
|
+
import { findRepoRoot } from "../lib/git.js";
|
|
21
|
+
import { parseSteps } from "../lib/orient.js";
|
|
22
|
+
import { activeBuild, checkpointsPath, clearHandoff, clearTick, grantPath, handoffPath, hasSession, intentPath, inSpike, listBuilds, readTurn, setActiveBuild, setGrant, stepPath, tickPath, } from "../lib/sidecar.js";
|
|
23
|
+
import { spikeBranches, spikeWorktrees } from "./spike.js";
|
|
24
|
+
/**
|
|
25
|
+
* Report every inconsistency in the control plane, and with `--fix` repair the
|
|
26
|
+
* ones that can be repaired without judgment.
|
|
27
|
+
*
|
|
28
|
+
* Exits 0 when the control plane is consistent (or when every problem found was
|
|
29
|
+
* fixed), 1 while any problem is still standing, so a scripted caller can tell
|
|
30
|
+
* "clean" from "you still have to look at this".
|
|
31
|
+
*/
|
|
32
|
+
export function recover(cwd, args) {
|
|
33
|
+
const root = findRepoRoot(cwd);
|
|
34
|
+
if (root === null) {
|
|
35
|
+
process.stderr.write('plumbbob: not a git repository — recover reads a repo\'s control plane.\n');
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
const fix = args.includes('--fix');
|
|
39
|
+
const findings = [...sessionFindings(root), ...spikeOrphanFindings(root)];
|
|
40
|
+
const lines = ['', 'plumbbob recover — control plane'];
|
|
41
|
+
let standing = 0;
|
|
42
|
+
for (const finding of findings) {
|
|
43
|
+
if (finding.ok) {
|
|
44
|
+
lines.push(` ✓ ${finding.name}: ${finding.detail}`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (fix && finding.repair !== undefined) {
|
|
48
|
+
finding.repair();
|
|
49
|
+
lines.push(` ✓ ${finding.name}: ${finding.repaired ?? finding.detail}`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
standing += 1;
|
|
53
|
+
lines.push(` ✗ ${finding.name}: ${finding.detail}`);
|
|
54
|
+
if (finding.hint !== undefined)
|
|
55
|
+
lines.push(` → ${finding.hint}`);
|
|
56
|
+
}
|
|
57
|
+
const fixable = findings.filter((f) => !f.ok && f.repair !== undefined).length;
|
|
58
|
+
lines.push('', summary(standing, fixable, fix), '');
|
|
59
|
+
process.stdout.write(lines.join('\n'));
|
|
60
|
+
return standing > 0 ? 1 : 0;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The closing line: what is still wrong, and whether `--fix` would help.
|
|
64
|
+
*/
|
|
65
|
+
function summary(standing, fixable, fix) {
|
|
66
|
+
if (standing === 0) {
|
|
67
|
+
return fix ? 'control plane consistent — everything fixable was fixed.' : 'control plane consistent — nothing to recover.';
|
|
68
|
+
}
|
|
69
|
+
const problems = `${standing} problem${standing === 1 ? '' : 's'} still standing`;
|
|
70
|
+
if (fix || fixable === 0)
|
|
71
|
+
return `${problems} — each needs a call only you can make (see the → lines).`;
|
|
72
|
+
return `${problems}; ${fixable} of them recover --fix can repair. Nothing was changed.`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Everything that depends on there being a session: the cursor, the phase
|
|
76
|
+
* markers, and the per-build control files.
|
|
77
|
+
*
|
|
78
|
+
* With no session there is no control plane to be wrong about: only the spike
|
|
79
|
+
* leftovers, which outlive `finish` and are checked separately.
|
|
80
|
+
*/
|
|
81
|
+
function sessionFindings(root) {
|
|
82
|
+
if (!hasSession(root)) {
|
|
83
|
+
return [{ name: 'session', ok: true, detail: 'none open — nothing in flight to reconcile' }];
|
|
84
|
+
}
|
|
85
|
+
const slug = activeBuild(root);
|
|
86
|
+
const findings = [];
|
|
87
|
+
// The cursor first: everything below reads through it, so a cursor pointing
|
|
88
|
+
// nowhere makes the rest meaningless rather than merely wrong.
|
|
89
|
+
const resolvable = slug !== null && existsSync(intentPath(root, slug));
|
|
90
|
+
if (!resolvable) {
|
|
91
|
+
return [cursorFinding(root, slug)];
|
|
92
|
+
}
|
|
93
|
+
findings.push({ name: 'cursor', ok: true, detail: `build "${slug}" resolves` });
|
|
94
|
+
const stepInFlight = existsSync(stepPath(root, slug));
|
|
95
|
+
findings.push(...phaseFindings(root, slug, stepInFlight));
|
|
96
|
+
findings.push(...leftoverFindings(root, slug, stepInFlight));
|
|
97
|
+
return findings;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The cursor names a build that is not there.
|
|
101
|
+
*
|
|
102
|
+
* This is the quiet one: `status` takes the cursor at its word, so every read
|
|
103
|
+
* comes back empty and the dashboard renders as a fresh, untouched build rather
|
|
104
|
+
* than an error. Repairable only when exactly one real build remains; with
|
|
105
|
+
* several, which one you meant is a judgment call.
|
|
106
|
+
*/
|
|
107
|
+
function cursorFinding(root, slug) {
|
|
108
|
+
const named = slug === null ? 'nothing' : `"${slug}"`;
|
|
109
|
+
const builds = listBuilds(root).filter((b) => existsSync(intentPath(root, b)));
|
|
110
|
+
const detail = `points at ${named}, which has no intent.md — status will render an empty dashboard rather than refuse`;
|
|
111
|
+
if (builds.length === 1) {
|
|
112
|
+
const only = builds[0];
|
|
113
|
+
return {
|
|
114
|
+
name: 'cursor',
|
|
115
|
+
ok: false,
|
|
116
|
+
detail,
|
|
117
|
+
hint: `fix: plumbbob recover --fix (re-points the cursor at "${only}", the only build left) — or plumbbob use <slug>`,
|
|
118
|
+
repair: () => setActiveBuild(root, only),
|
|
119
|
+
repaired: `re-pointed at "${only}"`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const hint = builds.length === 0
|
|
123
|
+
? 'no build folder survives here — plumbbob finish closes the session, or start a new one'
|
|
124
|
+
: `pick the one you meant: plumbbob use <slug>. Builds: ${builds.join(', ')}`;
|
|
125
|
+
return { name: 'cursor', ok: false, detail, hint };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Contradictions in what phase the build claims to be in.
|
|
129
|
+
*
|
|
130
|
+
* Phase is derived from the markers rather than stored, so two markers at once
|
|
131
|
+
* is not an impossible state; it is an unreadable one, and `status` resolves
|
|
132
|
+
* it by showing the spike and hiding the step entirely.
|
|
133
|
+
*/
|
|
134
|
+
function phaseFindings(root, slug, stepInFlight) {
|
|
135
|
+
const findings = [];
|
|
136
|
+
const spiking = inSpike(root, slug);
|
|
137
|
+
if (spiking && stepInFlight) {
|
|
138
|
+
findings.push({
|
|
139
|
+
name: 'phase',
|
|
140
|
+
ok: false,
|
|
141
|
+
detail: `both a spike and step ${readMarker(stepPath(root, slug))} are marked in flight — status shows the spike and hides the step`,
|
|
142
|
+
hint: 'close the spike first (plumbbob spike done), which returns you to the step still in flight',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
findings.push({
|
|
147
|
+
name: 'phase',
|
|
148
|
+
ok: true,
|
|
149
|
+
detail: spiking ? 'in a spike' : stepInFlight ? `step ${readMarker(stepPath(root, slug))} in flight` : 'at the boundary',
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
// A STEP the plan no longer contains: `refine` can rewrite `## Steps` under a
|
|
153
|
+
// step that is already in flight, and nothing reconciles the marker with it.
|
|
154
|
+
const marker = Number(readMarker(stepPath(root, slug)));
|
|
155
|
+
if (stepInFlight && Number.isInteger(marker) && marker > 0) {
|
|
156
|
+
const planned = parseSteps(readOr(intentPath(root, slug))).map((s) => s.n);
|
|
157
|
+
if (planned.length > 0 && !planned.includes(marker)) {
|
|
158
|
+
findings.push({
|
|
159
|
+
name: 'step marker',
|
|
160
|
+
ok: false,
|
|
161
|
+
detail: `step ${marker} is in flight but the plan has no step ${marker} (planned: ${planned.join(', ')})`,
|
|
162
|
+
hint: 'the plan was rewritten under the step — sharpen it with /plumbbob:step, or plumbbob revert to drop the step',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return findings;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Control files left behind by a verb that did not get to clean them up.
|
|
170
|
+
*
|
|
171
|
+
* Each of these fails quietly rather than loudly, which is why they are worth a
|
|
172
|
+
* verb: an orphaned handoff ledger feeds a finished step's agent output into
|
|
173
|
+
* the next one, and a stale TICK arms the approval latch against a span that
|
|
174
|
+
* already closed.
|
|
175
|
+
*/
|
|
176
|
+
function leftoverFindings(root, slug, stepInFlight) {
|
|
177
|
+
const findings = [];
|
|
178
|
+
// The handoff ledger is step-scoped and cleared by `checkpoint` alone; a
|
|
179
|
+
// revert or an abandoned step leaves it for the next step to inherit.
|
|
180
|
+
const handoffs = handoffCount(root, slug);
|
|
181
|
+
if (!stepInFlight && handoffs > 0) {
|
|
182
|
+
findings.push({
|
|
183
|
+
name: 'handoff ledger',
|
|
184
|
+
ok: false,
|
|
185
|
+
detail: `${handoffs} agent envelope${handoffs === 1 ? '' : 's'} left over at the boundary — the next step would thread them into its context`,
|
|
186
|
+
hint: 'fix: plumbbob recover --fix (clears the step-scoped ledger)',
|
|
187
|
+
repair: () => clearHandoff(root, slug),
|
|
188
|
+
repaired: `cleared ${handoffs} leftover envelope${handoffs === 1 ? '' : 's'}`,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
findings.push({ name: 'handoff ledger', ok: true, detail: stepInFlight ? 'in scope for the step in flight' : 'empty' });
|
|
193
|
+
}
|
|
194
|
+
// TICK is stamped when a step is entered and when `start` opens the plan
|
|
195
|
+
// pause, and cleared by the checkpoint that lands either one. At the boundary
|
|
196
|
+
// with the plan already committed, neither pause is open, so a TICK left here
|
|
197
|
+
// is measuring a span that closed.
|
|
198
|
+
const planLanded = readOr(checkpointsPath(root, slug)).split('\n').some((l) => l.startsWith('plan '));
|
|
199
|
+
if (!stepInFlight && planLanded && existsSync(tickPath(root, slug))) {
|
|
200
|
+
findings.push({
|
|
201
|
+
name: 'latch tick',
|
|
202
|
+
ok: false,
|
|
203
|
+
detail: 'a TICK stamp survives at the boundary — the approval latch is armed against a pause that already closed',
|
|
204
|
+
hint: 'fix: plumbbob recover --fix (clears the stale stamp; the latch re-arms on the next build)',
|
|
205
|
+
repair: () => clearTick(root, slug),
|
|
206
|
+
repaired: 'cleared the stale stamp',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
findings.push({ name: 'latch tick', ok: true, detail: stepInFlight ? 'stamped for the step in flight' : 'clear' });
|
|
211
|
+
}
|
|
212
|
+
// GRANT is rewritten on every turn tick, so it self-clears, but only while
|
|
213
|
+
// the hook runs. Without a TURN ledger nothing will ever clear it, and the
|
|
214
|
+
// latch reads a standing grant as self-approval.
|
|
215
|
+
if (existsSync(grantPath(root)) && readTurn(root) === null) {
|
|
216
|
+
findings.push({
|
|
217
|
+
name: 'self-approval grant',
|
|
218
|
+
ok: false,
|
|
219
|
+
detail: 'a GRANT survives with no turn ledger to clear it — the latch would honor it as self-approval',
|
|
220
|
+
hint: 'fix: plumbbob recover --fix (drops the grant; type /plumbbob:build --auto to mint a fresh one)',
|
|
221
|
+
repair: () => setGrant(root, null),
|
|
222
|
+
repaired: 'dropped the stranded grant',
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return findings;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Spike worktrees and branches with no open spike to own them.
|
|
229
|
+
*
|
|
230
|
+
* `spike done` is the only cleanup and it refuses without the marker, so a
|
|
231
|
+
* spike interrupted by `finish`, a cursor switch, or a half-failed open strands
|
|
232
|
+
* these where no verb can reach them. Reported with the exact commands and
|
|
233
|
+
* never removed: the directories sit outside the repo and may hold the only
|
|
234
|
+
* copy of what the spike learned.
|
|
235
|
+
*/
|
|
236
|
+
function spikeOrphanFindings(root) {
|
|
237
|
+
if (inSpike(root, activeBuild(root))) {
|
|
238
|
+
return [{ name: 'spike leftovers', ok: true, detail: 'a spike is open — plumbbob spike done owns its cleanup' }];
|
|
239
|
+
}
|
|
240
|
+
const worktrees = spikeWorktrees(root);
|
|
241
|
+
const branches = spikeBranches(root);
|
|
242
|
+
if (worktrees.length === 0 && branches.length === 0) {
|
|
243
|
+
return [{ name: 'spike leftovers', ok: true, detail: 'none' }];
|
|
244
|
+
}
|
|
245
|
+
const parts = [
|
|
246
|
+
worktrees.length > 0 ? `${worktrees.length} worktree${worktrees.length === 1 ? '' : 's'} (${worktrees.join(', ')})` : '',
|
|
247
|
+
branches.length > 0 ? `${branches.length} branch${branches.length === 1 ? '' : 'es'} (${branches.join(', ')})` : '',
|
|
248
|
+
].filter((p) => p.length > 0);
|
|
249
|
+
const commands = [
|
|
250
|
+
...worktrees.map((path) => `git worktree remove --force ${path}`),
|
|
251
|
+
...(branches.length > 0 ? [`git branch -D ${branches.join(' ')}`] : []),
|
|
252
|
+
];
|
|
253
|
+
return [
|
|
254
|
+
{
|
|
255
|
+
name: 'spike leftovers',
|
|
256
|
+
ok: false,
|
|
257
|
+
detail: `${parts.join(' and ')} survive with no spike open — plumbbob spike done cannot reach them`,
|
|
258
|
+
hint: `remove by hand once you have salvaged anything you want: ${commands.join(' && ')}`,
|
|
259
|
+
},
|
|
260
|
+
];
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* How many agent envelopes the step-scoped handoff ledger holds; 0 when it is
|
|
264
|
+
* absent or unreadable.
|
|
265
|
+
*/
|
|
266
|
+
function handoffCount(root, slug) {
|
|
267
|
+
const raw = readOr(handoffPath(root, slug));
|
|
268
|
+
if (raw.trim().length === 0)
|
|
269
|
+
return 0;
|
|
270
|
+
try {
|
|
271
|
+
const parsed = JSON.parse(raw);
|
|
272
|
+
return Array.isArray(parsed) ? parsed.length : 1;
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return 1;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* A control marker's trimmed content, or '?' when it cannot be read: a marker
|
|
280
|
+
* exists to be reported on, so an unreadable one must not throw mid-report.
|
|
281
|
+
*/
|
|
282
|
+
function readMarker(path) {
|
|
283
|
+
const raw = readOr(path).trim();
|
|
284
|
+
return raw.length > 0 ? raw : '?';
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* File content, or '' when absent: recover reads state that may be missing by
|
|
288
|
+
* definition, so every read degrades rather than throws.
|
|
289
|
+
*/
|
|
290
|
+
function readOr(path) {
|
|
291
|
+
try {
|
|
292
|
+
return readFileSync(path, 'utf8');
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return '';
|
|
296
|
+
}
|
|
297
|
+
}
|
package/dist/verbs/revert.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
// `plumbbob revert [--to n]
|
|
1
|
+
// `plumbbob revert [--to n]`: git reset --hard to a checkpoint SHA (the most
|
|
2
2
|
// recent step, or `--to n`, with the baseline as fallback), then remove untracked
|
|
3
3
|
// files under the SEAM only (the seam is the in-flight step's flat list of
|
|
4
|
-
// granted edit paths). The artifact plane
|
|
5
|
-
// folder holding intent, build-log, checkpoints, and park lines
|
|
4
|
+
// granted edit paths). The artifact plane (the tracked `.plumbbob/builds/<slug>/`
|
|
5
|
+
// folder holding intent, build-log, checkpoints, and park lines) would not
|
|
6
6
|
// survive a bare reset: it would discard park lines and intent edits, or, when
|
|
7
7
|
// reverting to a baseline that predates the build folder, delete the folder
|
|
8
8
|
// wholesale. So revert snapshots the sidecar to temp and restores it as
|
|
9
|
-
// uncommitted changes after the reset
|
|
9
|
+
// uncommitted changes after the reset: a rewind must never destroy recorded
|
|
10
10
|
// work, in either case. The untracked cleanup additionally whitelists the
|
|
11
11
|
// artifact plane, so no seam pattern can ever sweep away a build's own files.
|
|
12
12
|
//
|
|
13
13
|
// Plumbbob also installs its driver skills INTO the repo (.claude/skills/<driver>/
|
|
14
14
|
// for a self-contained install), so a blunt reset would discard an out-of-seam
|
|
15
|
-
// skill edit
|
|
15
|
+
// skill edit (or a `pnpm up plumbbob` re-setup) together with the half-done
|
|
16
16
|
// step. revert discards the step's WORK, never plumbbob's own machinery, so those
|
|
17
17
|
// paths are carried across the reset unchanged.
|
|
18
18
|
import { cpSync, existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
@@ -66,7 +66,7 @@ export function revert(cwd, args) {
|
|
|
66
66
|
const seam = readSeamTokens(root, slug);
|
|
67
67
|
const toRemove = untrackedPaths(root).filter((p) => matchesSeam(p, seam) && !isArtifactPath(p));
|
|
68
68
|
// A revert against an in-flight step is a datapoint the finish report's stats
|
|
69
|
-
// table wants
|
|
69
|
+
// table wants: read the STEP marker before it goes, bump the counter after
|
|
70
70
|
// the reset (the sidecar is preserved through it, so the write survives).
|
|
71
71
|
const inFlight = readInFlightStep(root, slug);
|
|
72
72
|
resetPreserving(root, sha, plumbbobOwnedPaths(root));
|
|
@@ -79,7 +79,7 @@ export function revert(cwd, args) {
|
|
|
79
79
|
bumpStepStat(root, slug, inFlight, 'reverts');
|
|
80
80
|
}
|
|
81
81
|
// The step is abandoned: the build-log's Current-step line returns to the
|
|
82
|
-
// boundary and its Steps mirror re-renders from the preserved intent.md
|
|
82
|
+
// boundary and its Steps mirror re-renders from the preserved intent.md:
|
|
83
83
|
// revert keeps intent edits, so intent's checkboxes stay the truth to reflect.
|
|
84
84
|
// Best-effort, like every build-log write.
|
|
85
85
|
syncBuildLogState(root, slug, AT_BOUNDARY);
|
|
@@ -94,9 +94,9 @@ export function revert(cwd, args) {
|
|
|
94
94
|
* under .claude/skills/, named from plumbbob's own bundled `skills/` dir.
|
|
95
95
|
*
|
|
96
96
|
* The match is by name alone, and since the skills dropped their `pb-` prefix
|
|
97
|
-
* those names are ordinary words
|
|
98
|
-
* carried across the reset too. That errs toward keeping work (
|
|
99
|
-
* the safe direction to be wrong in. Only paths that currently exist are returned.
|
|
97
|
+
* those names are ordinary words; a user's own `.claude/skills/plan/` is
|
|
98
|
+
* carried across the reset too. That errs toward keeping work (never destroy),
|
|
99
|
+
* so it is the safe direction to be wrong in. Only paths that currently exist are returned.
|
|
100
100
|
*/
|
|
101
101
|
function plumbbobOwnedPaths(root) {
|
|
102
102
|
const paths = [sidecarDir(root)];
|
|
@@ -106,7 +106,7 @@ function plumbbobOwnedPaths(root) {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
catch {
|
|
109
|
-
// Bundled skills dir not resolvable (unexpected)
|
|
109
|
+
// Bundled skills dir not resolvable (unexpected): protect just the sidecar.
|
|
110
110
|
}
|
|
111
111
|
return paths.filter((p) => existsSync(p));
|
|
112
112
|
}
|
|
@@ -146,7 +146,7 @@ function parseTo(args) {
|
|
|
146
146
|
/**
|
|
147
147
|
* Parse the build's CHECKPOINTS record into a baseline sha and per-step shas.
|
|
148
148
|
*
|
|
149
|
-
* A missing or unreadable file yields an empty record
|
|
149
|
+
* A missing or unreadable file yields an empty record: the caller then refuses
|
|
150
150
|
* with its own message rather than crashing here.
|
|
151
151
|
*/
|
|
152
152
|
function readCheckpoints(root, slug) {
|
|
@@ -173,7 +173,7 @@ function readCheckpoints(root, slug) {
|
|
|
173
173
|
return { baseline, steps };
|
|
174
174
|
}
|
|
175
175
|
/**
|
|
176
|
-
* Read the in-flight step's seam
|
|
176
|
+
* Read the in-flight step's seam: its edit-grant paths, one per line.
|
|
177
177
|
*
|
|
178
178
|
* A missing or unreadable SEAM contributes nothing: no tokens, never an error.
|
|
179
179
|
*/
|
package/dist/verbs/spike.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
// `plumbbob spike
|
|
1
|
+
// `plumbbob spike`: the spike lifecycle for a genuine fork the design phase
|
|
2
2
|
// couldn't settle. `spike "<slug>" [opt…]` creates a sibling git worktree and a
|
|
3
3
|
// `spike/<slug>-<opt>` branch per option OUTSIDE the repo root (default options
|
|
4
|
-
// a/b) and drops the SPIKE marker
|
|
4
|
+
// a/b) and drops the SPIKE marker: one of the untracked per-build control files
|
|
5
5
|
// under `.plumbbob/` that record what's in flight. The main tree stays put while
|
|
6
6
|
// you experiment in the worktrees, which are hook-dormant by construction: the
|
|
7
7
|
// untracked control files don't exist in a fresh checkout, so the hooks find no
|
|
8
8
|
// STATE (the session sentinel) there. `spike done` removes every spike worktree
|
|
9
9
|
// and branch and clears the marker.
|
|
10
10
|
//
|
|
11
|
-
// Every spike also leaves a durable report
|
|
12
|
-
// intent.md in the tracked `builds/<slug>/` folder
|
|
11
|
+
// Every spike also leaves a durable report (`spike-NN-<slug>.md` beside
|
|
12
|
+
// intent.md in the tracked `builds/<slug>/` folder) so the verdict rides the
|
|
13
13
|
// branch into the PR instead of evaporating with the throwaway worktrees.
|
|
14
14
|
//
|
|
15
15
|
// Worktree git calls run directly here rather than via lib/git.ts (which holds the
|
|
@@ -21,15 +21,15 @@ import { basename, dirname, join, relative } from 'node:path';
|
|
|
21
21
|
import { findRepoRoot } from "../lib/git.js";
|
|
22
22
|
import { buildFolder, clearSpike, hasSession, inSpike, listSpikeReports, markSpike, nextSpikeReportPath, resolveBuild, stepPath, } from "../lib/sidecar.js";
|
|
23
23
|
import { readTemplate, stampTemplate } from "../lib/templates.js";
|
|
24
|
-
// Worktree/branch names when the caller lists none
|
|
24
|
+
// Worktree/branch names when the caller lists none: a fork defaults to two arms.
|
|
25
25
|
const DEFAULT_OPTIONS = ['a', 'b'];
|
|
26
26
|
/**
|
|
27
|
-
* The verbatim Verdict placeholder from templates/spike-report.md
|
|
27
|
+
* The verbatim Verdict placeholder from templates/spike-report.md: its presence
|
|
28
28
|
* means the spike's call was never recorded, which `spike done` nudges on.
|
|
29
29
|
*/
|
|
30
30
|
const VERDICT_PLACEHOLDER = '*(viable | not viable | partial';
|
|
31
31
|
/**
|
|
32
|
-
* Entry point for `plumbbob spike
|
|
32
|
+
* Entry point for `plumbbob spike`: dispatches to open, `report`, or `done`.
|
|
33
33
|
*
|
|
34
34
|
* Requires an active session (the STATE sentinel) and resolves which build the
|
|
35
35
|
* spike belongs to before dispatching.
|
|
@@ -51,11 +51,11 @@ export function spike(cwd, args) {
|
|
|
51
51
|
return spikeStart(root, buildSlug, positionals);
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
|
-
* `spike report "<slug>"
|
|
54
|
+
* `spike report "<slug>"`: scaffold a spike report WITHOUT worktrees.
|
|
55
55
|
*
|
|
56
56
|
* Serves the spike-as-step case: a planned step titled `spike: …`, where the
|
|
57
57
|
* increment itself is the experiment. No boundary requirement and no SPIKE
|
|
58
|
-
* marker
|
|
58
|
+
* marker: a step in flight is exactly when this runs. Provenance is stamped
|
|
59
59
|
* `step <n>` when a step is in flight, else `/plumbbob:spike`.
|
|
60
60
|
*/
|
|
61
61
|
function spikeReport(root, buildSlug, positionals) {
|
|
@@ -75,7 +75,7 @@ function spikeReport(root, buildSlug, positionals) {
|
|
|
75
75
|
* Write a fresh spike report from the template at the next free
|
|
76
76
|
* `spike-NN-<slug>.md` in the build folder, and return its path.
|
|
77
77
|
*
|
|
78
|
-
* Shared by the worktree-opening spike and the spike-as-step `spike report
|
|
78
|
+
* Shared by the worktree-opening spike and the spike-as-step `spike report`:
|
|
79
79
|
* one artifact, two entry points. The CLI owns the numbering; the human never
|
|
80
80
|
* creates or numbers a report.
|
|
81
81
|
*/
|
|
@@ -86,7 +86,7 @@ function scaffoldSpikeReport(root, buildSlug, spikeSlug, via) {
|
|
|
86
86
|
return path;
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
|
-
* The build's in-flight step number, or null
|
|
89
|
+
* The build's in-flight step number, or null: reads the STEP marker `build`
|
|
90
90
|
* writes.
|
|
91
91
|
*
|
|
92
92
|
* Used to stamp a spike-as-step report's provenance as `step <n>`.
|
|
@@ -103,7 +103,7 @@ function readInFlightStep(root, buildSlug) {
|
|
|
103
103
|
/**
|
|
104
104
|
* Open a spike: one throwaway worktree + `spike/<slug>-<opt>` branch per option.
|
|
105
105
|
*
|
|
106
|
-
* Refuses when a spike is already open or a step is in flight
|
|
106
|
+
* Refuses when a spike is already open or a step is in flight: a spike is a
|
|
107
107
|
* deliberate fork from a settled boundary, so the current step must checkpoint
|
|
108
108
|
* or revert first. Marks the SPIKE control file and scaffolds the report before
|
|
109
109
|
* returning.
|
|
@@ -147,7 +147,7 @@ function spikeStart(root, buildSlug, positionals) {
|
|
|
147
147
|
/**
|
|
148
148
|
* Close the spike: remove every spike worktree and branch, clear the marker.
|
|
149
149
|
*
|
|
150
|
-
* Nudges
|
|
150
|
+
* Nudges (but never refuses) when a report's Verdict is still the template
|
|
151
151
|
* placeholder: guidance, not a gate.
|
|
152
152
|
*/
|
|
153
153
|
function spikeDone(root, buildSlug) {
|
|
@@ -156,7 +156,7 @@ function spikeDone(root, buildSlug) {
|
|
|
156
156
|
return 1;
|
|
157
157
|
}
|
|
158
158
|
// Check for an unrecorded verdict BEFORE teardown. The reports live in the
|
|
159
|
-
// build folder, not the spike worktrees, so they survive the removal
|
|
159
|
+
// build folder, not the spike worktrees, so they survive the removal, but the
|
|
160
160
|
// learning that fills them does not, so this is the moment to nudge.
|
|
161
161
|
const unfilled = spikeReportsMissingVerdict(root, buildSlug);
|
|
162
162
|
for (const path of spikeWorktrees(root)) {
|
|
@@ -181,7 +181,7 @@ function spikeDone(root, buildSlug) {
|
|
|
181
181
|
}
|
|
182
182
|
/**
|
|
183
183
|
* The spike-report filenames in the build folder whose Verdict is still the
|
|
184
|
-
* template placeholder
|
|
184
|
+
* template placeholder: the ones `spike done` nudges on.
|
|
185
185
|
*
|
|
186
186
|
* Best-effort per file: an unreadable report is skipped rather than blocking
|
|
187
187
|
* the close.
|
|
@@ -198,11 +198,15 @@ function spikeReportsMissingVerdict(root, buildSlug) {
|
|
|
198
198
|
});
|
|
199
199
|
}
|
|
200
200
|
/**
|
|
201
|
-
* Worktree paths whose checked-out branch is under spike
|
|
201
|
+
* Worktree paths whose checked-out branch is under spike/: parsed from the
|
|
202
202
|
* porcelain output (blank-line-separated `worktree <path>` / `branch <ref>`
|
|
203
203
|
* blocks).
|
|
204
|
+
*
|
|
205
|
+
* Exported for `recover`, which reports the ones this verb can no longer reach
|
|
206
|
+
* (a spike interrupted before `spike done` strands them). Sharing the reader
|
|
207
|
+
* keeps one definition of what counts as a spike worktree.
|
|
204
208
|
*/
|
|
205
|
-
function spikeWorktrees(root) {
|
|
209
|
+
export function spikeWorktrees(root) {
|
|
206
210
|
const out = git(root, ['worktree', 'list', '--porcelain']);
|
|
207
211
|
const paths = [];
|
|
208
212
|
let current = null;
|
|
@@ -218,13 +222,15 @@ function spikeWorktrees(root) {
|
|
|
218
222
|
}
|
|
219
223
|
/**
|
|
220
224
|
* List the local `spike/<slug>` branch names.
|
|
225
|
+
*
|
|
226
|
+
* Exported alongside spikeWorktrees, for the same reason.
|
|
221
227
|
*/
|
|
222
|
-
function spikeBranches(root) {
|
|
228
|
+
export function spikeBranches(root) {
|
|
223
229
|
const out = git(root, ['for-each-ref', '--format=%(refname:short)', 'refs/heads/spike/']);
|
|
224
230
|
return out.length === 0 ? [] : out.split('\n').filter((b) => b.length > 0);
|
|
225
231
|
}
|
|
226
232
|
/**
|
|
227
|
-
* Slugify a raw spike name
|
|
233
|
+
* Slugify a raw spike name: lowercase, non-alphanumerics collapsed to dashes.
|
|
228
234
|
*/
|
|
229
235
|
function sanitize(raw) {
|
|
230
236
|
return raw
|