plumbbob 0.9.5 → 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.
Files changed (142) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +87 -86
  3. package/dist/cli-core.js +8 -8
  4. package/dist/cli.js +1 -1
  5. package/dist/lib/agents.js +26 -26
  6. package/dist/lib/buildlog.js +11 -11
  7. package/dist/lib/buildlogsync.js +8 -8
  8. package/dist/lib/check.js +14 -14
  9. package/dist/lib/commitbody.js +72 -0
  10. package/dist/lib/commitmsg.js +7 -7
  11. package/dist/lib/git.js +44 -15
  12. package/dist/lib/intent.js +20 -19
  13. package/dist/lib/latch.js +16 -16
  14. package/dist/lib/orient.js +36 -17
  15. package/dist/lib/reentry.js +3 -3
  16. package/dist/lib/settings.js +16 -16
  17. package/dist/lib/sidecar.js +54 -54
  18. package/dist/lib/templates.js +1 -1
  19. package/dist/verbs/agent.js +25 -25
  20. package/dist/verbs/build.js +9 -9
  21. package/dist/verbs/check.js +6 -6
  22. package/dist/verbs/checkpoint.js +48 -47
  23. package/dist/verbs/doctor.js +27 -26
  24. package/dist/verbs/finish.js +27 -40
  25. package/dist/verbs/handoff.js +9 -8
  26. package/dist/verbs/init.js +9 -9
  27. package/dist/verbs/park.js +3 -3
  28. package/dist/verbs/recover.js +11 -11
  29. package/dist/verbs/revert.js +12 -12
  30. package/dist/verbs/spike.js +17 -17
  31. package/dist/verbs/start.js +49 -19
  32. package/dist/verbs/status.js +8 -8
  33. package/dist/verbs/turn.js +16 -16
  34. package/dist/verbs/use.js +4 -4
  35. package/node_modules/checkride/.claude-plugin/plugin.json +1 -1
  36. package/node_modules/checkride/CHANGELOG.md +401 -0
  37. package/node_modules/checkride/README.md +27 -9
  38. package/node_modules/checkride/dist/adapters.d.ts +12 -4
  39. package/node_modules/checkride/dist/adapters.d.ts.map +1 -1
  40. package/node_modules/checkride/dist/adapters.js +21 -1
  41. package/node_modules/checkride/dist/adapters.js.map +1 -1
  42. package/node_modules/checkride/dist/agent-setup/cursor.d.ts +9 -1
  43. package/node_modules/checkride/dist/agent-setup/cursor.d.ts.map +1 -1
  44. package/node_modules/checkride/dist/agent-setup/cursor.js +59 -1
  45. package/node_modules/checkride/dist/agent-setup/cursor.js.map +1 -1
  46. package/node_modules/checkride/dist/agent-setup/harness.d.ts +1 -1
  47. package/node_modules/checkride/dist/agent-setup/harness.d.ts.map +1 -1
  48. package/node_modules/checkride/dist/agent-setup/harness.js +10 -1
  49. package/node_modules/checkride/dist/agent-setup/harness.js.map +1 -1
  50. package/node_modules/checkride/dist/agent-setup/scripts.d.ts +23 -0
  51. package/node_modules/checkride/dist/agent-setup/scripts.d.ts.map +1 -1
  52. package/node_modules/checkride/dist/agent-setup/scripts.js +387 -82
  53. package/node_modules/checkride/dist/agent-setup/scripts.js.map +1 -1
  54. package/node_modules/checkride/dist/baseline/fallow.js +2 -2
  55. package/node_modules/checkride/dist/baseline/fingerprint.d.ts +14 -5
  56. package/node_modules/checkride/dist/baseline/fingerprint.d.ts.map +1 -1
  57. package/node_modules/checkride/dist/baseline/fingerprint.js +58 -5
  58. package/node_modules/checkride/dist/baseline/fingerprint.js.map +1 -1
  59. package/node_modules/checkride/dist/baseline/history.d.ts +98 -0
  60. package/node_modules/checkride/dist/baseline/history.d.ts.map +1 -0
  61. package/node_modules/checkride/dist/baseline/history.js +232 -0
  62. package/node_modules/checkride/dist/baseline/history.js.map +1 -0
  63. package/node_modules/checkride/dist/baseline/index.d.ts +4 -2
  64. package/node_modules/checkride/dist/baseline/index.d.ts.map +1 -1
  65. package/node_modules/checkride/dist/baseline/index.js +2 -1
  66. package/node_modules/checkride/dist/baseline/index.js.map +1 -1
  67. package/node_modules/checkride/dist/baseline/store.d.ts +30 -7
  68. package/node_modules/checkride/dist/baseline/store.d.ts.map +1 -1
  69. package/node_modules/checkride/dist/baseline/store.js +50 -16
  70. package/node_modules/checkride/dist/baseline/store.js.map +1 -1
  71. package/node_modules/checkride/dist/cli.d.ts +5 -1
  72. package/node_modules/checkride/dist/cli.d.ts.map +1 -1
  73. package/node_modules/checkride/dist/cli.js +98 -25
  74. package/node_modules/checkride/dist/cli.js.map +1 -1
  75. package/node_modules/checkride/dist/config.d.ts +33 -4
  76. package/node_modules/checkride/dist/config.d.ts.map +1 -1
  77. package/node_modules/checkride/dist/config.js +21 -6
  78. package/node_modules/checkride/dist/config.js.map +1 -1
  79. package/node_modules/checkride/dist/digest/digest.js +1 -1
  80. package/node_modules/checkride/dist/doctor.d.ts +2 -0
  81. package/node_modules/checkride/dist/doctor.d.ts.map +1 -1
  82. package/node_modules/checkride/dist/doctor.js +39 -1
  83. package/node_modules/checkride/dist/doctor.js.map +1 -1
  84. package/node_modules/checkride/dist/gate.d.ts +49 -4
  85. package/node_modules/checkride/dist/gate.d.ts.map +1 -1
  86. package/node_modules/checkride/dist/gate.js +143 -16
  87. package/node_modules/checkride/dist/gate.js.map +1 -1
  88. package/node_modules/checkride/dist/init.d.ts +8 -1
  89. package/node_modules/checkride/dist/init.d.ts.map +1 -1
  90. package/node_modules/checkride/dist/init.js +68 -7
  91. package/node_modules/checkride/dist/init.js.map +1 -1
  92. package/node_modules/checkride/dist/node-pin.d.ts +1 -1
  93. package/node_modules/checkride/dist/node-pin.js +1 -1
  94. package/node_modules/checkride/dist/orchestrator.d.ts +13 -0
  95. package/node_modules/checkride/dist/orchestrator.d.ts.map +1 -1
  96. package/node_modules/checkride/dist/orchestrator.js +118 -13
  97. package/node_modules/checkride/dist/orchestrator.js.map +1 -1
  98. package/node_modules/checkride/dist/pm/index.d.ts +1 -0
  99. package/node_modules/checkride/dist/pm/index.d.ts.map +1 -1
  100. package/node_modules/checkride/dist/pm/index.js.map +1 -1
  101. package/node_modules/checkride/dist/pm/launch.d.ts +23 -2
  102. package/node_modules/checkride/dist/pm/launch.d.ts.map +1 -1
  103. package/node_modules/checkride/dist/pm/launch.js +16 -0
  104. package/node_modules/checkride/dist/pm/launch.js.map +1 -1
  105. package/node_modules/checkride/dist/pm/translate.js +1 -1
  106. package/node_modules/checkride/dist/pm/translate.js.map +1 -1
  107. package/node_modules/checkride/dist/qa/mutation.d.ts +4 -4
  108. package/node_modules/checkride/dist/qa/mutation.js +4 -4
  109. package/node_modules/checkride/dist/recover.d.ts +48 -0
  110. package/node_modules/checkride/dist/recover.d.ts.map +1 -0
  111. package/node_modules/checkride/dist/recover.js +220 -0
  112. package/node_modules/checkride/dist/recover.js.map +1 -0
  113. package/node_modules/checkride/dist/smoke.d.ts +1 -1
  114. package/node_modules/checkride/dist/smoke.js +1 -1
  115. package/node_modules/checkride/dist/snippets.d.ts +1 -1
  116. package/node_modules/checkride/package.json +3 -2
  117. package/node_modules/checkride/schema/checkride.config.schema.json +9 -1
  118. package/node_modules/checkride/skills/check/SKILL.md +9 -1
  119. package/node_modules/checkride/templates/shared/styles/Repo/Drift.yml +41 -0
  120. package/node_modules/checkride/templates/shared/styles/Repo/Latin.yml +14 -0
  121. package/node_modules/checkride/templates/shared/styles/Repo/LyHyphen.yml +28 -0
  122. package/node_modules/checkride/templates/shared/styles/Repo/Minted.yml +33 -0
  123. package/node_modules/checkride/templates/shared/styles/Repo/ThereIs.yml +16 -0
  124. package/node_modules/checkride/templates/shared/styles/Repo/Weasel.yml +26 -0
  125. package/node_modules/checkride/templates/shared/vale.ini +34 -0
  126. package/package.json +10 -9
  127. package/skills/build/SKILL.md +69 -64
  128. package/skills/doctor/SKILL.md +11 -11
  129. package/skills/finish/SKILL.md +20 -19
  130. package/skills/harvest/SKILL.md +13 -12
  131. package/skills/park/SKILL.md +10 -10
  132. package/skills/plan/SKILL.md +63 -54
  133. package/skills/recover/SKILL.md +6 -6
  134. package/skills/refine/SKILL.md +18 -16
  135. package/skills/revert/SKILL.md +6 -6
  136. package/skills/spike/SKILL.md +9 -9
  137. package/skills/status/SKILL.md +5 -5
  138. package/skills/step/SKILL.md +17 -12
  139. package/skills/verify/SKILL.md +45 -40
  140. package/templates/build-log.md +7 -7
  141. package/templates/intent.md +49 -39
  142. package/templates/spike-report.md +10 -10
@@ -1,7 +1,7 @@
1
- // `plumbbob init [--uninstall] [--force]` — link plumbbob into Claude Code as an
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
- // — the skills load namespaced (`/plumbbob:*`) and the post-edit hook auto-registers
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 — `--force` overrides
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 — sessions stay per-project via `plumbbob start`. Idempotent + reversible
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 — parent of .claude-plugin/, skills/, hooks/, dist/.
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 — two plugins
68
- * named `plumbbob` collide over the /plumbbob:* namespace — unless `--force`.
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 — repoint it
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` +
@@ -1,6 +1,6 @@
1
- // `plumbbob park "<text>"` — the raw capture path for a thought you don't want
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 — a park line must never vanish silently.
17
+ * section: a park line must never vanish silently.
18
18
  */
19
19
  export function park(cwd, args) {
20
20
  const root = findRepoRoot(cwd);
@@ -1,8 +1,8 @@
1
- // `plumbbob recover` — read the control plane and say, plainly, whether it is
1
+ // `plumbbob recover`: read the control plane and say, plainly, whether it is
2
2
  // telling the truth. Nothing else reads these files as a set: `doctor` checks
3
3
  // the install and the gate, `status` reports what the markers claim without
4
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
5
+ // switched away mid-step can sit in a state no verb detects: the worst being a
6
6
  // cursor pointing at a build that no longer exists, which renders a perfectly
7
7
  // plausible empty dashboard instead of an error.
8
8
  //
@@ -10,7 +10,7 @@
10
10
  // `doctor --migrate` takes. What `--fix` may touch is deliberately narrow: only
11
11
  // the untracked control files this tool owns and can rewrite from what it
12
12
  // already knows. It never touches a tracked artifact (intent, build-log,
13
- // checkpoints, reports), never touches git, and never advances the loop — a
13
+ // checkpoints, reports), never touches git, and never advances the loop: a
14
14
  // recovery verb that could land a step would be a second, quieter checkpoint.
15
15
  //
16
16
  // Spike leftovers are reported and never removed: those worktrees live outside
@@ -26,7 +26,7 @@ import { spikeBranches, spikeWorktrees } from "./spike.js";
26
26
  * ones that can be repaired without judgment.
27
27
  *
28
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
29
+ * fixed), 1 while any problem is still standing, so a scripted caller can tell
30
30
  * "clean" from "you still have to look at this".
31
31
  */
32
32
  export function recover(cwd, args) {
@@ -75,7 +75,7 @@ function summary(standing, fixable, fix) {
75
75
  * Everything that depends on there being a session: the cursor, the phase
76
76
  * markers, and the per-build control files.
77
77
  *
78
- * With no session there is no control plane to be wrong about — only the spike
78
+ * With no session there is no control plane to be wrong about: only the spike
79
79
  * leftovers, which outlive `finish` and are checked separately.
80
80
  */
81
81
  function sessionFindings(root) {
@@ -101,7 +101,7 @@ function sessionFindings(root) {
101
101
  *
102
102
  * This is the quiet one: `status` takes the cursor at its word, so every read
103
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
104
+ * than an error. Repairable only when exactly one real build remains; with
105
105
  * several, which one you meant is a judgment call.
106
106
  */
107
107
  function cursorFinding(root, slug) {
@@ -128,7 +128,7 @@ function cursorFinding(root, slug) {
128
128
  * Contradictions in what phase the build claims to be in.
129
129
  *
130
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
131
+ * is not an impossible state; it is an unreadable one, and `status` resolves
132
132
  * it by showing the spike and hiding the step entirely.
133
133
  */
134
134
  function phaseFindings(root, slug, stepInFlight) {
@@ -175,7 +175,7 @@ function phaseFindings(root, slug, stepInFlight) {
175
175
  */
176
176
  function leftoverFindings(root, slug, stepInFlight) {
177
177
  const findings = [];
178
- // The handoff ledger is step-scoped and cleared by `checkpoint` alone — a
178
+ // The handoff ledger is step-scoped and cleared by `checkpoint` alone; a
179
179
  // revert or an abandoned step leaves it for the next step to inherit.
180
180
  const handoffs = handoffCount(root, slug);
181
181
  if (!stepInFlight && handoffs > 0) {
@@ -209,7 +209,7 @@ function leftoverFindings(root, slug, stepInFlight) {
209
209
  else {
210
210
  findings.push({ name: 'latch tick', ok: true, detail: stepInFlight ? 'stamped for the step in flight' : 'clear' });
211
211
  }
212
- // GRANT is rewritten on every turn tick, so it self-clears — but only while
212
+ // GRANT is rewritten on every turn tick, so it self-clears, but only while
213
213
  // the hook runs. Without a TURN ledger nothing will ever clear it, and the
214
214
  // latch reads a standing grant as self-approval.
215
215
  if (existsSync(grantPath(root)) && readTurn(root) === null) {
@@ -276,7 +276,7 @@ function handoffCount(root, slug) {
276
276
  }
277
277
  }
278
278
  /**
279
- * A control marker's trimmed content, or '?' when it cannot be read — a marker
279
+ * A control marker's trimmed content, or '?' when it cannot be read: a marker
280
280
  * exists to be reported on, so an unreadable one must not throw mid-report.
281
281
  */
282
282
  function readMarker(path) {
@@ -284,7 +284,7 @@ function readMarker(path) {
284
284
  return raw.length > 0 ? raw : '?';
285
285
  }
286
286
  /**
287
- * File content, or '' when absent — recover reads state that may be missing by
287
+ * File content, or '' when absent: recover reads state that may be missing by
288
288
  * definition, so every read degrades rather than throws.
289
289
  */
290
290
  function readOr(path) {
@@ -1,18 +1,18 @@
1
- // `plumbbob revert [--to n]` — git reset --hard to a checkpoint SHA (the most
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 — the tracked `.plumbbob/builds/<slug>/`
5
- // folder holding intent, build-log, checkpoints, and park lines — would not
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 — a rewind must never destroy recorded
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 — or a `pnpm up plumbbob` re-setup — together with the half-done
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 — read the STEP marker before it goes, bump the counter after
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,8 +94,8 @@ 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 — a user's own `.claude/skills/plan/` is
98
- * carried across the reset too. That errs toward keeping work — never destroy —
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
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) {
@@ -106,7 +106,7 @@ function plumbbobOwnedPaths(root) {
106
106
  }
107
107
  }
108
108
  catch {
109
- // Bundled skills dir not resolvable (unexpected) — protect just the sidecar.
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 — the caller then refuses
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 — its edit-grant paths, one per line.
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
  */
@@ -1,15 +1,15 @@
1
- // `plumbbob spike` — the spike lifecycle for a genuine fork the design phase
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 — one of the untracked per-build control files
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 — `spike-NN-<slug>.md` beside
12
- // intent.md in the tracked `builds/<slug>/` folder — so the verdict rides the
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 — a fork defaults to two arms.
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 — its presence
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` — dispatches to open, `report`, or `done`.
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>"` — scaffold a spike report WITHOUT worktrees.
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 — a step in flight is exactly when this runs. Provenance is stamped
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 — reads the STEP marker `build`
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 — a spike is a
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 — but never refuses — when a report's Verdict is still the template
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 — but the
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 — the ones `spike done` nudges on.
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,7 +198,7 @@ function spikeReportsMissingVerdict(root, buildSlug) {
198
198
  });
199
199
  }
200
200
  /**
201
- * Worktree paths whose checked-out branch is under spike/ — parsed from the
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
204
  *
@@ -230,7 +230,7 @@ export function spikeBranches(root) {
230
230
  return out.length === 0 ? [] : out.split('\n').filter((b) => b.length > 0);
231
231
  }
232
232
  /**
233
- * Slugify a raw spike name — lowercase, non-alphanumerics collapsed to dashes.
233
+ * Slugify a raw spike name: lowercase, non-alphanumerics collapsed to dashes.
234
234
  */
235
235
  function sanitize(raw) {
236
236
  return raw
@@ -1,4 +1,4 @@
1
- // `plumbbob start "<title>"` — open a session: scaffold the sidecar (the
1
+ // `plumbbob start "<title>"`: open a session: scaffold the sidecar (the
2
2
  // `.plumbbob/` tree of build artifacts and control files), record the baseline
3
3
  // commit, and point the session at a new build. Refuses on a dirty tree (a
4
4
  // clean baseline is the contract; `--allow-dirty` overrides), an existing
@@ -7,7 +7,7 @@ import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
8
  import { detectGate } from "../lib/check.js";
9
9
  import { readTemplate, stampTemplate } from "../lib/templates.js";
10
- import { findRepoRoot, hasCommit, headSha, isDirty } from "../lib/git.js";
10
+ import { findRepoRoot, hasCommit, headSha, isDirty, isIgnored } from "../lib/git.js";
11
11
  import { sidecarDir, buildDir, listBuilds, slugify, checkpointsPath, intentPath, buildLogPath, beginSession, hasSession, excludeControl, excludeSidecar, setGrant, stampTick, } from "../lib/sidecar.js";
12
12
  import { settingsPath } from "../lib/settings.js";
13
13
  /**
@@ -15,7 +15,7 @@ import { settingsPath } from "../lib/settings.js";
15
15
  * {{CHECK}} line.
16
16
  *
17
17
  * The gate is checkride unless a `check` setting overrides it, so settings.json
18
- * seeds with no `check` key at all — absence IS the default; this line says
18
+ * seeds with no `check` key at all; absence IS the default; this line says
19
19
  * that in prose where the templates need it.
20
20
  */
21
21
  const CHECK_ECHO = 'checkride (set a "check" key in .plumbbob/settings.json to override)';
@@ -71,13 +71,13 @@ export async function start(cwd, args) {
71
71
  }
72
72
  const sha = headSha(root);
73
73
  mkdirSync(sidecarDir(root), { recursive: true });
74
- // beginSession writes STATE — one file that is both the session sentinel (its
74
+ // beginSession writes STATE: one file that is both the session sentinel (its
75
75
  // existence means a session is live) and the active-build cursor (its single
76
- // line names the build every verb acts on). Point it at this build — null
76
+ // line names the build every verb acts on). Point it at this build: null
77
77
  // under --local, which has no cursor. The cursor lives in STATE, not
78
78
  // settings.local.json, so that personal overlay stays purely human-owned.
79
79
  beginSession(root, local ? null : slug);
80
- // Scaffold the tracked settings.json ONLY when absent, and seed it EMPTY — the
80
+ // Scaffold the tracked settings.json ONLY when absent, and seed it EMPTY: the
81
81
  // human owns this file once it exists (their `check` gate lives here), so a
82
82
  // re-start must never touch it. Inject no opinions even on first create:
83
83
  // absence of `check` already means checkride and absence of `auto` already
@@ -87,11 +87,11 @@ export async function start(cwd, args) {
87
87
  if (!existsSync(settingsPath(root))) {
88
88
  writeFileSync(settingsPath(root), `${JSON.stringify({}, null, 2)}\n`);
89
89
  }
90
- // `--local` keeps a fully-untracked flat layout (the whole `.plumbbob/`
91
- // git-excluded); the default plants a tracked `builds/<slug>/` folder — the
92
- // build's record (intent, build-log, checkpoints) rides its branch into the
93
- // PR — and git-excludes only the per-worktree control files. The slug is
94
- // date-prefixed when derived (see datedSlug) and validated unique above —
90
+ // `--local` keeps a fully untracked flat layout (the whole `.plumbbob/`
91
+ // git-excluded); the default plants a tracked `builds/<slug>/` folder (the
92
+ // build's record [intent, build-log, checkpoints] rides its branch into the
93
+ // PR) and git-excludes only the per-worktree control files. The slug is
94
+ // date-prefixed when derived (see datedSlug) and validated unique above;
95
95
  // the CLI refuses a collision, never auto-suffixes.
96
96
  let intentLocation;
97
97
  if (local) {
@@ -112,18 +112,32 @@ export async function start(cwd, args) {
112
112
  }
113
113
  // The plan's entry stamp for the checkpoint latch: set TICK to the current
114
114
  // TURN, so `checkpoint --plan` only lands once the harness ledger records a
115
- // human turn after this moment. Skipped when TURN is absent — a hookless host
115
+ // human turn after this moment. Skipped when TURN is absent: a hookless host
116
116
  // grows no ledger and the latch stays dormant, which also covers a first
117
117
  // session where the hook has never ticked; that one plan commit stays
118
118
  // guidance-governed. Any GRANT file lying around predates this session (a
119
- // legitimate one is minted by a `/build` prompt, which never runs `start`)
120
- // — clear it so a stale grant can't self-approve this session's plan; a
119
+ // legitimate one is minted by a `/build` prompt, which never runs `start`);
120
+ // clear it so a stale grant can't self-approve this session's plan; a
121
121
  // grant's lifetime is one turn.
122
122
  setGrant(root, null);
123
123
  stampTick(root, local ? null : slug);
124
124
  process.stdout.write(`plumbbob: started "${title}" — baseline ${sha.slice(0, 9)}. Frame and decide in ${intentLocation}; \`build\` a step once the decisions are made.\n`);
125
+ // The tracked layout plants `builds/<slug>/` to ride the branch into the PR,
126
+ // but a repo whose own `.gitignore` excludes `.plumbbob/` overrides that, and
127
+ // an explicit `git add` of an ignored folder is a hard error. Detect it here,
128
+ // once, so the record-only mode (plan/finish commits land empty; their bodies
129
+ // carry the record, the files stay untracked) is a known mode from the start
130
+ // rather than a surprise at the first `checkpoint --plan`. `--local` already
131
+ // excludes the whole sidecar by design and says so, so skip the probe there.
132
+ // Guidance only, and the session is already open: a probe hiccup stays silent
133
+ // rather than half-crashing a scaffolded session.
134
+ if (!local && sidecarIsIgnored(root, slug)) {
135
+ process.stderr.write('plumbbob: heads-up — this repo gitignores .plumbbob/, so the tracked build folder cannot ride the branch.\n' +
136
+ ' Plan and finish commits will be record-only (empty commits; the plan and report ride the commit message).\n' +
137
+ ' To keep the record in-tree, unignore .plumbbob/builds/ (or the whole sidecar) before the first checkpoint.\n');
138
+ }
125
139
  // The plan-time gate probe: if checkride sees no code checks here, say so
126
- // NOW — while the human is still deciding — instead of at the first
140
+ // NOW (while the human is still deciding) instead of at the first
127
141
  // checkpoint, where the gate either refuses a vacuous run or, worse, greens
128
142
  // on the always-on repo checks alone. Guidance only: it never changes the
129
143
  // exit code, and a configured `check` or a probe hiccup stays silent.
@@ -139,8 +153,8 @@ export async function start(cwd, args) {
139
153
  * Derive a `YYYY-MM-DD-<slug>` build slug from the title (local time).
140
154
  *
141
155
  * The date prefix makes `builds/` sort chronologically under `listBuilds`'
142
- * plain lexical sort — ordering by construction, not by titling convention. An
143
- * explicit `--slug` bypasses this and stays verbatim — the CLI never rewrites
156
+ * plain lexical sort: ordering by construction, not by titling convention. An
157
+ * explicit `--slug` bypasses this and stays verbatim: the CLI never rewrites
144
158
  * what the caller chose. An untitleable title yields `''` so the empty-slug
145
159
  * guard fires instead of minting a date-only slug.
146
160
  */
@@ -153,7 +167,23 @@ function datedSlug(title) {
153
167
  return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${base}`;
154
168
  }
155
169
  /**
156
- * Read the value that follows a `--flag` in argv (e.g. `--slug my-build`), or
170
+ * Whether the tracked build folder is gitignored: the record-only heads-up's
171
+ * predicate, probing exactly the path `checkpoint --plan` will try to stage.
172
+ *
173
+ * Best-effort: a probe that throws (a fatal `git check-ignore`) reads as
174
+ * not-ignored so the heads-up stays silent rather than crashing a session that
175
+ * is already open.
176
+ */
177
+ function sidecarIsIgnored(root, slug) {
178
+ try {
179
+ return isIgnored(root, buildDir(root, slug));
180
+ }
181
+ catch {
182
+ return false;
183
+ }
184
+ }
185
+ /**
186
+ * Read the value that follows a `--flag` in argv (for example `--slug my-build`), or
157
187
  * undefined when the flag is absent or trails with no value.
158
188
  */
159
189
  function flagValue(args, flag) {
@@ -161,7 +191,7 @@ function flagValue(args, flag) {
161
191
  return i >= 0 ? args[i + 1] : undefined;
162
192
  }
163
193
  /**
164
- * Stamp a build template's `{{TITLE}}`/`{{CHECK}}` — the check is always the
194
+ * Stamp a build template's `{{TITLE}}`/`{{CHECK}}`: the check is always the
165
195
  * echo constant, since an absent `check` setting IS the checkride default.
166
196
  */
167
197
  function stampBuild(template, title) {
@@ -1,6 +1,6 @@
1
- // `plumbbob status` — the orientation dashboard, or NO ACTIVE SESSION. It
2
- // parses the live session — intent.md, the build-log, the checkpoints ledger,
3
- // the in-flight STEP marker — into a where-am-I view that suggests one primary
1
+ // `plumbbob status`: the orientation dashboard, or NO ACTIVE SESSION. It
2
+ // parses the live session (intent.md, the build-log, the checkpoints ledger,
3
+ // the in-flight STEP marker) into a where-am-I view that suggests one primary
4
4
  // next move while printing the full picture, so you can always override.
5
5
  // Read-only, always exits 0. Skills pre-inject this output to gate their own
6
6
  // behavior, so the `NO ACTIVE SESSION` sentinel is kept exact.
@@ -25,7 +25,7 @@ function readOr(path) {
25
25
  * Print the orientation dashboard for the active build, plus its harness bindings.
26
26
  *
27
27
  * No session prints the exact `NO ACTIVE SESSION` sentinel; a session with
28
- * builds but no active one lists them and points at `use`. Always exits 0 —
28
+ * builds but no active one lists them and points at `use`. Always exits 0:
29
29
  * status is a report, never a gate.
30
30
  */
31
31
  export function status(cwd, args = []) {
@@ -46,9 +46,9 @@ export function status(cwd, args = []) {
46
46
  }
47
47
  }
48
48
  const inFlightRaw = readOr(stepPath(root, slug)).trim();
49
- // Out-of-band commits — ones that landed outside the checkpoints ledger — are
49
+ // Out-of-band commits (ones that landed outside the checkpoints ledger) are
50
50
  // surfaced here, never blocked. Counting them needs git (orient is pure), so
51
- // status measures it: commits on HEAD since the last ledger line's SHA —
51
+ // status measures it: commits on HEAD since the last ledger line's SHA:
52
52
  // baseline, plan, or step, so a commit landing before the first step
53
53
  // checkpoint still surfaces. No ledger yet ⇒ nothing to reconcile against ⇒ 0.
54
54
  const checkpoints = readOr(checkpointsPath(root, slug));
@@ -70,7 +70,7 @@ export function status(cwd, args = []) {
70
70
  * `defaults` and per step.
71
71
  *
72
72
  * Bindings live in `builds/<slug>/harness.json` and status is the surface that
73
- * reports them — no separate `agent check` verb. Any bound agent that does not
73
+ * reports them: no separate `agent check` verb. Any bound agent that does not
74
74
  * resolve gets a warning, not an error: a teammate can lack a personal agent
75
75
  * and the loop still works. Returns [] when the build has no harness.json (a
76
76
  * clean no-op) or the file binds nothing, so the dashboard stays uncluttered
@@ -100,7 +100,7 @@ function harnessSection(root, slug) {
100
100
  /**
101
101
  * Render a SlotBindings as `slot: name1, name2` fragments, in slot order.
102
102
  *
103
- * A slot bound to no agents (an explicit "override to none") is omitted —
103
+ * A slot bound to no agents (an explicit "override to none") is omitted:
104
104
  * there is nothing to run and nothing to name.
105
105
  */
106
106
  function slotFragments(bindings) {