plumbbob 0.11.3 → 0.12.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "plumbbob",
3
3
  "displayName": "PlumbBob",
4
- "version": "0.11.3",
4
+ "version": "0.12.0",
5
5
  "description": "Attention-first build process — driver skills + a CLI that keep you the decider: guidance on the work, a latch on the record.",
6
6
  "author": { "name": "Rob McLarty", "email": "hello@robmclarty.com", "url": "https://robmclarty.com" },
7
7
  "homepage": "https://github.com/robmclarty/plumbbob#readme",
package/README.md CHANGED
@@ -221,7 +221,7 @@ nothing else:
221
221
  | Skill <img alt="" width="110" height="1"> | Does |
222
222
  | ------------------------------------ | ------ |
223
223
  | `/plumbbob:plan` | plan the whole goal: open the session and author intent's Frame, Decisions, Constraints, **and all Steps** |
224
- | `/plumbbob:build` | implement the next planned step, then verify it to the pause: `--auto` self-approves and chains to done; a range like `1-3` self-approves through step 3, then pauses |
224
+ | `/plumbbob:build` | implement the next planned step, then verify it to the pause: `--auto` self-approves and chains to done; a range like `1-3` self-approves through step 3, then stops |
225
225
  | `/plumbbob:finish` | finish up: write the report, make the final commit, clear for a fresh goal |
226
226
 
227
227
  **Plan-shaping moves**: optional, for when the plan needs work mid-flight:
package/dist/lib/latch.js CHANGED
@@ -112,7 +112,7 @@ export function checkLatch(root, step) {
112
112
  /**
113
113
  * The current GRANT, or null when the file is absent or unparseable.
114
114
  */
115
- function readGrant(root) {
115
+ export function readGrant(root) {
116
116
  try {
117
117
  return parseGrant(readFileSync(grantPath(root), 'utf8'));
118
118
  }
@@ -741,6 +741,16 @@ export function parseSections(detail) {
741
741
  export function parseRecommendation(detail) {
742
742
  return sectionProse(detail, /^##\s+recommendation\s*$/i);
743
743
  }
744
+ /**
745
+ * The step `.plumbbob/detail.md` was written for, read off its
746
+ * `# Detail · Step <n> · <title>` header, or null when the first line is not a
747
+ * step header (a plan pause's detail, or a file the model wrote without one).
748
+ */
749
+ export function parseDetailStep(detail) {
750
+ const first = detail.trimStart().split('\n', 1)[0] ?? '';
751
+ const m = /^#\s+Detail\b.*?\bStep\s+(\d+)\b/.exec(first);
752
+ return m === null ? null : Number(m[1]);
753
+ }
744
754
  /**
745
755
  * Render an Orientation as the plain-text dashboard `status` prints.
746
756
  */
@@ -10,14 +10,16 @@ import { hasSession, intentPath, resolveBuild, seamPath, stampStepStat, stampTic
10
10
  import { parseStepSeam } from "../lib/intent.js";
11
11
  import { parseSteps } from "../lib/orient.js";
12
12
  import { stepLabel, syncBuildLogState } from "../lib/buildlogsync.js";
13
+ import { readGrant } from "../lib/latch.js";
13
14
  import { notice } from "../lib/notice.js";
14
15
  /**
15
16
  * Enter step n (or the next undone step) and write its SEAM/STEP markers.
16
17
  *
17
18
  * Refuses a missing session, a malformed step argument, an `N-M` range (a
18
19
  * `/plumbbob:build` skill feature, not a CLI one), and a seam that fails to parse.
19
- * On entry it stamps the turn ledger and the step's start time, and re-renders
20
- * the build-log's Current step line.
20
+ * On entry it stamps the turn ledger and the step's start time, re-renders the
21
+ * build-log's Current step line, and says so when the step is the top of a
22
+ * range the human typed.
21
23
  */
22
24
  export function build(cwd, args) {
23
25
  const root = findRepoRoot(cwd);
@@ -28,12 +30,13 @@ export function build(cwd, args) {
28
30
  const { build: slug, rest } = resolveBuild(root, args);
29
31
  const raw = rest.find((a) => !a.startsWith('--'));
30
32
  // A step range like `1-3` is a `/plumbbob:build` skill affordance (auto-approve
31
- // through the range, then pause), not a CLI capability: the CLI records one
32
- // in-flight step at a time. Name it rather than bounce off the generic usage.
33
+ // every step through the range's top, then stop), not a CLI capability: the CLI
34
+ // records one in-flight step at a time. Name it rather than bounce off the
35
+ // generic usage.
33
36
  if (raw !== undefined && /^\d+-\d*$/.test(raw)) {
34
37
  process.stderr.write(notice({
35
38
  fact: 'build takes one step number',
36
- detail: [`\`${raw}\` is a /plumbbob:build range, which auto-approves through it and then pauses`],
39
+ detail: [`\`${raw}\` is a /plumbbob:build range, which auto-approves through its top and then stops`],
37
40
  remedy: `plumbbob build ${raw.split('-')[0]}`,
38
41
  }));
39
42
  return 1;
@@ -94,9 +97,22 @@ export function build(cwd, args) {
94
97
  : skipped > 0
95
98
  ? ['explicitly requested', `skips ${skipped} undone step${skipped === 1 ? '' : 's'}`]
96
99
  : [];
97
- // Two lines, one colon each, then the seam as a plain readout beneath the
98
- // notice that frames it: the paths are a list, and a list is not a one-liner.
100
+ // The top step of a typed range lands like every step under it, and the turn
101
+ // ends on its checkpoint. Saying so as the step is entered puts that fact in
102
+ // front of the model before it chooses between landing the step and holding
103
+ // it at a pause, which is the choice a range kept getting wrong.
104
+ const grant = readGrant(root);
105
+ const top = grant?.kind === 'range' && grant.ceiling === step
106
+ ? notice({
107
+ fact: `step ${step} is the top of the range you granted`,
108
+ remedy: 'land it on green, then stop at the boundary',
109
+ })
110
+ : '';
111
+ // Two lines (three at a range's top), one colon each, then the seam as a
112
+ // plain readout beneath the notice that frames it: the paths are a list, and
113
+ // a list is not a one-liner.
99
114
  process.stdout.write(notice({ fact: `building step ${step}`, detail: picked }) +
115
+ top +
100
116
  notice({
101
117
  fact: 'the seam is orientation, not a lock',
102
118
  detail: [`${parsed.seam.length} path${parsed.seam.length === 1 ? '' : 's'}`],
@@ -16,7 +16,7 @@ import { activeBuild, buildFolder, buildLogPath, buildScope, bumpStepStat, check
16
16
  import { runCheck } from "../lib/check.js";
17
17
  import { readCommitBody } from "../lib/commitbody.js";
18
18
  import { checkLatch } from "../lib/latch.js";
19
- import { markStepDone, parseSteps } from "../lib/orient.js";
19
+ import { markStepDone, parseDetailStep, parseSteps } from "../lib/orient.js";
20
20
  import { parseBuildScope, parseStepSeam, scopeDrift } from "../lib/intent.js";
21
21
  import { conventionalSubject, subjectFromTitle, withMarker } from "../lib/commitmsg.js";
22
22
  import { appendToSection, checkpointLogLine, logEntry, planLogLine } from "../lib/buildlog.js";
@@ -87,7 +87,7 @@ export async function checkpoint(cwd, args) {
87
87
  // tracked ledger is the archive, and it rides the branch where a commit body
88
88
  // would not survive the squash); the commit body keeps its marker and the
89
89
  // lead prose only.
90
- const record = stepRecord(root, activeBuild(root), step, readDetail(root) ?? '');
90
+ const record = stepRecord(root, activeBuild(root), step, readDetail(root, step) ?? '');
91
91
  let sha;
92
92
  // The drift advisory is read off the index (staged, before the commit clears
93
93
  // it) but printed inside the ending below, where the fixed order puts it: the
@@ -165,7 +165,7 @@ function checkpointPlan(root, args) {
165
165
  appendFileSync(checkpointsPath(root), `plan ${sha}\n`);
166
166
  // The cold read that approved the plan is the Log's first entry, beneath a
167
167
  // dated plan line; the detail file is then spent, the same as at a step.
168
- const pointer = logPlan(root, sha, planRecord(readDetail(root) ?? ''));
168
+ const pointer = logPlan(root, sha, planRecord(readDetail(root, null) ?? ''));
169
169
  clearDetail(root);
170
170
  // Landing the plan consumes `start`'s entry stamp: a later hand-built diff
171
171
  // (no `build <n>`) must find no stale TICK and stay guidance-governed.
@@ -444,16 +444,23 @@ function fallbackBody(root, step) {
444
444
  }
445
445
  /**
446
446
  * The in-flight step's detail from `.plumbbob/detail.md`, or null when the file
447
- * is absent or empty.
447
+ * is absent, empty, or written for another step.
448
448
  *
449
449
  * The model overwrites it before every pause; checkpoint reads it once here for
450
450
  * the Log's record, and the caller clears it afterward. A missing or blank file
451
- * records nothing.
451
+ * records nothing, and so does one whose header names a different step: that
452
+ * file is a leftover, written after its own step had already landed, and
453
+ * recording it would file one step's story under another. The plan commit
454
+ * passes no step and takes the file as it is.
452
455
  */
453
- function readDetail(root) {
456
+ function readDetail(root, step) {
454
457
  try {
455
458
  const raw = readFileSync(detailPath(root), 'utf8').trim();
456
- return raw.length > 0 ? raw : null;
459
+ if (raw.length === 0) {
460
+ return null;
461
+ }
462
+ const written = step === null ? null : parseDetailStep(raw);
463
+ return written === null || written === step ? raw : null;
457
464
  }
458
465
  catch {
459
466
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plumbbob",
3
- "version": "0.11.3",
3
+ "version": "0.12.0",
4
4
  "description": "Attention-first build process: 14 skills + a CLI that keep you the decider — guidance on the work, a latch on the record.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: build
3
- description: "The default engine: read the next planned step from intent, implement it (its done-when, seam, Decisions, Constraints), then verify it through to the approval pause. Swappable: build by hand/vibed/another harness and run /plumbbob:verify instead. `--auto` self-approves and chains to done; a step range like `1-3` self-approves through step 3, then pauses."
3
+ description: "The default engine: read the next planned step from intent, implement it (its done-when, seam, Decisions, Constraints), then verify it through to the approval pause. Swappable: build by hand/vibed/another harness and run /plumbbob:verify instead. `--auto` self-approves and chains to done; a step range like `1-3` self-approves through step 3, then stops."
4
4
  argument-hint: "[step-number | step-range] [--auto]"
5
5
  disable-model-invocation: true
6
6
  allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob build:*), Bash(plumbbob handoff:*), Bash(plumbbob check:*), Bash(plumbbob checkpoint:*), Bash(plumbbob park:*), Bash(plumbbob agent:*), Bash(plumbbob spike:*), Bash(git diff:*)
@@ -36,8 +36,8 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
36
36
 
37
37
  1. **Pick the step.** Use the number you were invoked with (for example `/plumbbob:build 4`), or,
38
38
  if you were given a range like `/plumbbob:build 1-3`, start at the first number and treat
39
- the second as the auto-approve ceiling (see the range note under `--auto`). With no
40
- argument, don't resolve the next step yourself; the CLI does: bare `plumbbob build`
39
+ the second as the last step you approve and land yourself (see the range note under
40
+ `--auto`). With no argument, don't resolve the next step yourself; the CLI does: bare `plumbbob build`
41
41
  (step 2) enters the next undone step and refuses with a `/plumbbob:step` nudge when every
42
42
  step is checkpointed.
43
43
  2. **Enter the step.** Run `plumbbob build <n>`, or bare `plumbbob build` to enter the
@@ -87,7 +87,9 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
87
87
  <<'BODY'`). Do **not** pass it as an argument value (`--body "$(cat <<'BODY'…)"`):
88
88
  `--body` ignores an argument and always reads stdin. Under an agent harness that
89
89
  stdin is a socket, and `--body` now refuses rather than blocking on one; the
90
- refusal names this exact heredoc form.
90
+ refusal names this exact heredoc form. End that line at `<<'BODY'`, with no `2>&1`
91
+ after it: Claude Code's permission checker cannot analyze the extra redirect, so it
92
+ blocks the command or stops to ask instead of letting this skill's allowance cover it.
91
93
  - **A drifted subject is reconciled at the pause, never silently.** The planned
92
94
  title *is* the checkpoint subject ([D68 (conventional-subjects)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d68)). If the diff drifted
93
95
  from it, **present** a corrected subject at the pause (`planned title → proposed
@@ -256,30 +258,42 @@ and approves in the human's place**, and it **chains**:
256
258
  self-review finds no done-when / Decision / Constraint mismatch, checkpoint** and move
257
259
  straight on to the next planned step. Repeat. `--auto` adds no new machinery; the
258
260
  `after`-agent output simply feeds the *existing* self-review halt condition.
259
- - **Stop and hand back to the human** the moment any of these is true: the check is red,
260
- the self-review finds a mismatch (surface exactly what, and do not checkpoint it), a
261
- bound agent returns `blocked` or `drift` (unblock-and-re-run, or `/plumbbob:refine`; an
262
- agent cannot advance the loop), a new decision is needed, no planned steps
263
- remain, or the top of a requested range is reached. **When `--auto` halts back to the
264
- human, end on the same turn the default pause does**: write the detail file, run
265
- `plumbbob handoff`, and paste its block: the step just completed, the next undone step,
266
- and that next step's `- model:` recommendation if it has one, so a fresh context window
267
- knows which `/model` to select.
261
+ - **Stop and hand back to the human** the moment a halt arrives, and end the turn by the
262
+ kind of halt it is
263
+ ([D85 (range-top-lands)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d85)):
264
+ - **A halt on trouble leaves a step unlanded.** The check is red, the self-review finds
265
+ a mismatch (surface exactly what, and do not checkpoint it), a bound agent returns
266
+ `blocked` or `drift` (unblock-and-re-run, or `/plumbbob:refine`; an agent cannot
267
+ advance the loop), or a new decision is needed. The step is still in flight and the
268
+ human has a call to make about it, so end on the same turn the default pause does:
269
+ write the detail file, run `plumbbob handoff`, and paste its block.
270
+ - **A clean halt lands its step first.** The top of a requested range is reached, or no
271
+ planned steps remain. That step checkpoints like every step before it, and its
272
+ checkpoint's block is the whole ending: relay it with nothing written around it, then
273
+ end the turn. Write no detail file and run no `handoff`, because nothing is pending.
274
+ The block already carries the Verdict the step earned and a Next Up pointing at what
275
+ comes next (the next undone step and its `- model:` recommendation, or the finish),
276
+ which is what a fresh context window needs to pick the right `/model`.
268
277
 
269
278
  `--auto` and a step range are the only paths that checkpoint without a human pause, and
270
- only because the human asked for it by name; a range re-imposes the pause at its top.
271
- The default (no flag, no range) always ends at the pause.
279
+ only because the human asked for it by name. A range hands the clock back once its top
280
+ step has landed: the step after it waits for the human's next run. The default (no flag,
281
+ no range) always ends at the pause.
272
282
 
273
283
  ### A step range (`N-M`) is a bounded `--auto`
274
284
 
275
- `/plumbbob:build 1-3` self-approves steps 1 through 3 exactly as `--auto` does, then **pauses
276
- after step 3** instead of chaining to done. The range *is* the opt-in; you do not also
277
- pass `--auto`. Run it as the `--auto` loop with one extra halt: **stop before building
278
- any next step whose plan number is past the top of the range.** It adds no machinery:
279
- just the one more entry already in the halt list above.
280
-
281
- - **`N-M` with N ≤ M**: build N…M, self-approving and checkpointing each, then pause at
282
- M. `N-N` is just the single step `N` (which already ends at the pause).
285
+ `/plumbbob:build 1-3` self-approves steps 1 through 3 exactly as `--auto` does, then **stops
286
+ once step 3 has landed** instead of chaining to done. The range *is* the opt-in; you do not
287
+ also pass `--auto`. Run it as the `--auto` loop with one extra halt: **stop before building
288
+ any next step whose plan number is past the top of the range.** It adds no machinery: just
289
+ the one more entry already in the halt list above, and it is a clean halt, so the top step
290
+ is never held back for approval. The human approved it by typing the range, and
291
+ `plumbbob build` says so as it enters that step:
292
+ `step 3 is the top of the range you granted`.
293
+
294
+ - **`N-M` with N ≤ M**: build N…M, self-approving and checkpointing each, M included, then
295
+ relay M's checkpoint block and end the turn. `N-N` is a range like any other: it builds
296
+ and lands the single step `N`. Only a bare `/plumbbob:build N` ends at the pause.
283
297
  - **N > M** (for example `3-1`): that is not a range you can walk; report it and stop rather
284
298
  than guess the intent.
285
299
  - **M past the last planned step** (for example `1-9` with three steps): build through the
@@ -302,7 +316,7 @@ just the one more entry already in the halt list above.
302
316
  - **Default ends at the pause.** Implement → verify → wait for approval; never
303
317
  checkpoint without it. Only an explicit `--auto` or a step range lets the agent approve
304
318
  in your place, and it still halts on a red check or any mismatch; a range also stops
305
- at its top.
319
+ once its top step lands.
306
320
  - **Agents feed the beat; they never advance it**. `before` loads context,
307
321
  `build` writes the diff, `after` is advisory; none can checkpoint, flip a step, or
308
322
  chain. `blocked` → unblock and re-run; `drift` → `/plumbbob:refine`. You are still the one
@@ -53,8 +53,11 @@ record.
53
53
  Redirect the heredoc *into* the command (`--body <<'BODY'`). Do **not** pass it as
54
54
  an argument value (`--body "$(cat <<'BODY'…)"`): `--body` ignores an argument and
55
55
  always reads stdin. Under an agent harness that stdin is a socket, and `--body` now
56
- refuses rather than blocking on one; the refusal names this exact heredoc form. Omit
57
- `--body` entirely and the commit carries the `plumbbob finish` marker subject only.
56
+ refuses rather than blocking on one; the refusal names this exact heredoc form. End
57
+ that line at `<<'BODY'`, with no `2>&1` after it: Claude Code's permission checker
58
+ cannot analyze the extra redirect, so it blocks the command or stops to ask instead of
59
+ letting this skill's allowance cover it. Omit `--body` entirely and the commit carries
60
+ the `plumbbob finish` marker subject only.
58
61
  3. **Point at the next goal.** Relay `plumbbob finish`'s output verbatim, blank lines
59
62
  included: its lead line, which names the branch the tracked folder now rides into the
60
63
  PR, any advisory beneath it, and `**Next Up**: Nothing planned - /plumbbob:plan`. That
@@ -142,7 +142,8 @@ an agent can follow with `/plumbbob:build`. The argument only seeds how you get
142
142
  a `plan <sha>` line in
143
143
  `checkpoints`. This keeps the first step's diff clean, so history reads
144
144
  baseline → plan → steps. Pass a proportional `--body` (the single-quoted stdin
145
- heredoc) when the rationale is worth carrying; skip it for a small plan. Do this
145
+ heredoc, with nothing after `<<'BODY'` on its line, not even `2>&1`) when the
146
+ rationale is worth carrying; skip it for a small plan. Do this
146
147
  only on the human's approval; the plan is their convergence.
147
148
  - **The plan pause is a decision turn.** Present the framed plan for the human's call,
148
149
  then give it the cold read (**§ The cold read**), write the result into
@@ -163,7 +163,10 @@ this skill verifies it the same way: **it reads the diff, not the author**
163
163
  Redirect the heredoc *into* the command (`--body <<'BODY'`). Do **not** pass it as
164
164
  an argument value (`--body "$(cat <<'BODY'…)"`): `--body` ignores an argument and
165
165
  always reads stdin. Under an agent harness that stdin is a socket, and `--body` now
166
- refuses rather than blocking on one; the refusal names this exact heredoc form.
166
+ refuses rather than blocking on one; the refusal names this exact heredoc form. End
167
+ that line at `<<'BODY'`, with no `2>&1` after it: Claude Code's permission checker
168
+ cannot analyze the extra redirect, so it blocks the command or stops to ask instead of
169
+ letting this skill's allowance cover it.
167
170
 
168
171
  Do **not** run a TIL scan or reach for a separate commit skill; the body is yours to
169
172
  write in one breath. Omit `--body` entirely and the CLI writes a deterministic body