plumbbob 0.4.8 → 0.4.11

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.4.8",
4
+ "version": "0.4.11",
5
5
  "description": "Attention-first build process — driver skills + a CLI that keep you the decider; guidance, not enforcement.",
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
@@ -142,8 +142,8 @@ Calibration is the skill. When in doubt, smaller.
142
142
  pass that injects file-scoped lint into the model's context so it self-corrects in
143
143
  flow. (v1's pre-edit muzzle, seam-guard, and bash-guard are gone — guidance, not
144
144
  enforcement.)
145
- - A `.plumbbob/` sidecar of flat files: `STATE` (orientation only), `intent.md`,
146
- `build-log.md`, `checkpoints`, and `archive/`.
145
+ - A `.plumbbob/` sidecar of flat files: `STATE` (the session sentinel — its presence
146
+ means a session is live), `intent.md`, `build-log.md`, `checkpoints`, and `archive/`.
147
147
 
148
148
  ## Gates — two tiers, different jobs
149
149
 
@@ -155,13 +155,15 @@ Calibration is the skill. When in doubt, smaller.
155
155
  checkpoint while red. The hard gate lives on the deliberate boundary, not the
156
156
  keystroke.
157
157
 
158
- ## STATE is orientation, not a gate
158
+ ## Position is derived, not stored
159
159
 
160
- The current position lives in one word in `.plumbbob/STATE` — `DESIGN`, `BUILD`, or
161
- `SPIKE`. It no longer gates anything. It is read by `/plumbbob:pb-status` to tell you where
162
- you are and what to do next; a wrong state is a mislabeled position on a map, not a
163
- locked door. The post-edit hook is session-gated: a repo with no `.plumbbob/STATE`
164
- behaves exactly like plain Claude Code.
160
+ There is no stored state machine. The dashboard's phase — `DESIGN`, `BUILD`, or `SPIKE` —
161
+ is *derived* from what's on disk: a `STEP` file means a step is in flight (BUILD), the
162
+ `SPIKE` marker means a fork is open (SPIKE), otherwise you're at a boundary (DESIGN). The
163
+ position gates nothing it's a label on a map, read by `/plumbbob:pb-status` to tell you
164
+ where you are and what to do next, never a locked door. `.plumbbob/STATE` is just the
165
+ session sentinel: its presence means a session is live, and the post-edit hook is gated on
166
+ it, so a repo with no `.plumbbob/STATE` behaves exactly like plain Claude Code.
165
167
 
166
168
  ## Git footprint — additive only
167
169
 
@@ -175,9 +177,10 @@ archives plain markdown under `.plumbbob/archive/` and never touches git.
175
177
 
176
178
  ```text
177
179
  .plumbbob/
178
- STATE # one word: DESIGN | BUILD | SPIKE orientation, not a gate
180
+ STATE # session sentinel its presence means a session is live
179
181
  SEAM # the in-flight step's declared paths (awareness, not a lock)
180
- STEP # the in-flight step number
182
+ STEP # the in-flight step number (its presence is the BUILD phase)
183
+ SPIKE # marker — present while a spike fork is open
181
184
  config # key=value; check=<heavy-check command> (defaults to pnpm run check)
182
185
  checkpoints # "baseline <sha>" then "step N <sha>", one per verified step
183
186
  intent.md # canonical intent
@@ -236,8 +239,10 @@ plumbbob doctor
236
239
 
237
240
  `doctor` works for either install path: it confirms a marketplace plugin, or checks that the
238
241
  skills-dir link resolves to the plugin manifest, the skills, and the hook — flagging a
239
- double-install collision — and prints the exact fix for anything broken. Run it first if a
240
- `/plumbbob:*` skill ever opens with an empty dashboard;
242
+ double-install collision — and prints the exact fix for anything broken. The terminal command
243
+ above is for the **global / skills-dir** install; a **marketplace** plugin puts the CLI on
244
+ PATH only *inside a Claude Code session*, so run **`/plumbbob:pb-doctor`** there instead. Run
245
+ it first if a `/plumbbob:*` skill ever opens with an empty dashboard;
241
246
  [`docs/troubleshooting.md`](docs/troubleshooting.md) covers the rest.
242
247
 
243
248
  ## Development
package/bin/pb CHANGED
@@ -1,6 +1,15 @@
1
1
  #!/bin/sh
2
- # pb — short alias for `plumbbob` (see bin/plumbbob for the full note). Same PATH
3
- # shim: resolves the bundled CLI relative to this file so the plugin works on PATH
4
- # without a global install.
5
- here=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
2
+ # pb — short alias for `plumbbob` (see bin/plumbbob for the full note). Same dual
3
+ # role: the npm `bin` entry for `pb` (so npm ships it 0755) and the plugin PATH shim.
4
+ # Resolves any symlink chain so it works on PATH, globally linked, or as a dep.
5
+ src=$0
6
+ while [ -h "$src" ]; do
7
+ dir=$(CDPATH='' cd -- "$(dirname -- "$src")" && pwd)
8
+ link=$(readlink -- "$src")
9
+ case $link in
10
+ /*) src=$link ;;
11
+ *) src=$dir/$link ;;
12
+ esac
13
+ done
14
+ here=$(CDPATH='' cd -- "$(dirname -- "$src")" && pwd)
6
15
  exec node "$here/../dist/cli.js" "$@"
package/bin/plumbbob CHANGED
@@ -1,9 +1,26 @@
1
1
  #!/bin/sh
2
- # plumbbob — PATH shim for the marketplace / plugin install. Claude Code adds this
3
- # bin/ dir to the Bash tool's PATH while the plugin is enabled (the plugins `bin/`
4
- # convention), so the driver skills can shell a bare `plumbbob` without a global
5
- # `npm i -g`. The CLI is resolved relative to this file, so it works from the
6
- # plugin's install/cache dir wherever that lands. Requires node >=22 on PATH (the
7
- # same requirement as the published `bin`/`dist/cli.js` shebang).
8
- here=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
2
+ # plumbbob — the package's `bin` entry AND the marketplace/plugin PATH shim.
3
+ #
4
+ # Pointing the npm `bin` at THIS file (not dist/cli.js) is deliberate: npm/pacote
5
+ # rewrites every packed file to 0644 except the ones named in `bin`, which it stamps
6
+ # 0755 regardless of the mode in git. Claude Code adds this bin/ dir to the Bash
7
+ # tool's PATH while the plugin is enabled, so the shim must arrive executable; naming
8
+ # it in `bin` is what carries the +x bit through an npm-sourced plugin install.
9
+ # (dist/cli.js no longer needs its own bit — it is always run as `node` below.)
10
+ #
11
+ # As the npm bin, this file is also symlinked into a bin dir by `npm i -g` and by a
12
+ # local node_modules/.bin dep, so $0 can reach us through one or more symlinks.
13
+ # Resolve the chain to the real file, then run the bundled CLI relative to it — so it
14
+ # works on PATH (plugin), globally linked, and as a dep. Requires node >=22 on PATH
15
+ # (same as dist/cli.js's shebang).
16
+ src=$0
17
+ while [ -h "$src" ]; do
18
+ dir=$(CDPATH='' cd -- "$(dirname -- "$src")" && pwd)
19
+ link=$(readlink -- "$src")
20
+ case $link in
21
+ /*) src=$link ;;
22
+ *) src=$dir/$link ;;
23
+ esac
24
+ done
25
+ here=$(CDPATH='' cd -- "$(dirname -- "$src")" && pwd)
9
26
  exec node "$here/../dist/cli.js" "$@"
package/dist/cli-core.js CHANGED
@@ -16,15 +16,15 @@ import { wrap } from "./verbs/wrap.js";
16
16
  import { init } from "./verbs/init.js";
17
17
  import { doctor } from "./verbs/doctor.js";
18
18
  const VERBS = [
19
- { name: 'start', summary: 'scaffold .plumbbob/; STATE=DESIGN; record the baseline commit' },
20
- { name: 'status', summary: 'print the session state, or NO ACTIVE SESSION' },
21
- { name: 'build', summary: 'build <n>: write the seam from step n; STATE=BUILD (orientation, not a lock)' },
19
+ { name: 'start', summary: 'scaffold .plumbbob/; open the session; record the baseline commit' },
20
+ { name: 'status', summary: 'print the orientation dashboard, or NO ACTIVE SESSION' },
21
+ { name: 'build', summary: 'build <n>: write the seam from step n (in-flight, not a lock)' },
22
22
  { name: 'check', summary: 'run the heavy check and report; no state change' },
23
- { name: 'checkpoint', summary: 'checkpoint [<n>]: gate on green, commit/record SHA, mark step done, STATE=DESIGN (executor-agnostic)' },
24
- { name: 'revert', summary: 'revert [--to n]: git reset --hard to a checkpoint SHA; STATE=DESIGN' },
23
+ { name: 'checkpoint', summary: 'checkpoint [<n>]: gate on green, commit/record SHA, mark step done (executor-agnostic)' },
24
+ { name: 'revert', summary: 'revert [--to n]: git reset --hard to a checkpoint SHA' },
25
25
  { name: 'park', summary: 'park "<text>": append a raw line to the park list' },
26
26
  { name: 'spike', summary: 'spike "<slug>" | spike done: throwaway worktree experiment' },
27
- { name: 'wrap', summary: 'v2 close-out: archive intent+log+report (no gate), clear the sidecar, STATE off' },
27
+ { name: 'wrap', summary: 'v2 close-out: archive intent+log+report (no gate), clear the sidecar, close the session' },
28
28
  { name: 'init', summary: 'link plumbbob into Claude Code as an in-place plugin (~/.claude/skills/plumbbob); --uninstall to undo' },
29
29
  { name: 'doctor', summary: 'diagnose the plugin link (manifest, skills, hook) and print the fix for anything broken' },
30
30
  ];
package/dist/cli.js CHANGED
File without changes
@@ -0,0 +1,34 @@
1
+ // Mutations to the active build-log.md — the human-facing ledger. `park` appends to
2
+ // its Park list; `checkpoint` appends to its Log so the build's history accrues
3
+ // line-by-line *as it happens* rather than being reconstructed at wrap (D-history).
4
+ // Pure string transforms (no fs, no classes — C1/C2); callers own the IO so these
5
+ // stay trivially testable.
6
+ // Append `line` after the last non-blank line of the `## <heading>` section — i.e.
7
+ // just before the next `## ` heading or EOF. Returns null when the section is absent
8
+ // so the caller can report rather than silently corrupt the doc.
9
+ export function appendToSection(content, heading, line) {
10
+ const lines = content.split('\n');
11
+ const headingIdx = lines.findIndex((l) => l.trim() === `## ${heading}`);
12
+ if (headingIdx === -1) {
13
+ return null;
14
+ }
15
+ let nextSection = lines.findIndex((l, i) => i > headingIdx && l.startsWith('## '));
16
+ if (nextSection === -1) {
17
+ nextSection = lines.length;
18
+ }
19
+ let insertAt = headingIdx + 1;
20
+ for (let i = headingIdx + 1; i < nextSection; i++) {
21
+ if ((lines[i] ?? '').trim() !== '') {
22
+ insertAt = i + 1;
23
+ }
24
+ }
25
+ lines.splice(insertAt, 0, line);
26
+ return lines.join('\n');
27
+ }
28
+ // The Log line `checkpoint` writes for a landed step: dated, names the step (its
29
+ // title when intent.md still carries it), and carries the short SHA. One line of the
30
+ // build's history — `/plumbbob:pb-wrap` reads these instead of re-narrating the build.
31
+ export function checkpointLogLine(date, step, sha, title) {
32
+ const head = `- ${date} — step ${step} checkpointed · ${sha.slice(0, 9)}`;
33
+ return title !== null && title.length > 0 ? `${head} — ${title}` : head;
34
+ }
@@ -93,52 +93,45 @@ export function parseLastCheckpoint(checkpoints) {
93
93
  return last;
94
94
  }
95
95
  // The single primary next move (D15). It suggests; the dashboard prints the full
96
- // list + counts so the human can always override.
97
- function nextMove(state, steps, inFlight, parked) {
98
- switch (state) {
99
- case 'SPIKE':
100
- return 'close the spike — `plumbbob spike done`';
101
- case 'FINISH':
102
- return 'wrap up — `/plumbbob:pb-wrap`';
103
- case 'REVIEW':
104
- return 'read the diff cold against intent, then `/plumbbob:pb-verify`';
105
- case 'BUILD': {
106
- const n = inFlight ?? steps.find((s) => !s.done)?.n;
107
- return n === undefined
108
- ? 'finish the step in flight — `/plumbbob:pb-verify`'
109
- : `finish step ${n} — \`/plumbbob:pb-verify\` (or keep editing, then \`/plumbbob:pb-verify\`)`;
110
- }
111
- default: {
112
- // DESIGN (and any unknown state): you are at the boundary.
113
- const nextUndone = steps.find((s) => !s.done);
114
- if (nextUndone === undefined) {
115
- if (steps.length === 0) {
116
- return 'plan the first step — `/plumbbob:pb-step`';
117
- }
118
- // Batch-default: the steps were planned up front, so finishing them usually
119
- // means "wrap up" — but `/plumbbob:pb-step` can still add an increment if reality grew.
120
- const harvest = parked > 0 ? `harvest ${parked} parked idea${parked === 1 ? '' : 's'} — \`/plumbbob:pb-harvest\`; then ` : '';
121
- return `${harvest}wrap up — \`/plumbbob:pb-wrap\` (or \`/plumbbob:pb-step\` to add another increment)`;
122
- }
123
- return nextUndone.planned
124
- ? `build step ${nextUndone.n} — \`/plumbbob:pb-build\` (or \`/plumbbob:pb-step\` to revise it first)`
125
- : `plan step ${nextUndone.n} — \`/plumbbob:pb-step\``;
96
+ // list + counts so the human can always override. The phase is derived: a spike
97
+ // in progress and an in-flight step each have one obvious next move; otherwise you
98
+ // are at the boundary and the move follows from the steps.
99
+ function nextMove(spiking, steps, inFlight, parked) {
100
+ if (spiking) {
101
+ return 'close the spike — `plumbbob spike done`';
102
+ }
103
+ if (inFlight !== null) {
104
+ return `finish step ${inFlight} \`/plumbbob:pb-verify\` (or keep editing, then \`/plumbbob:pb-verify\`)`;
105
+ }
106
+ // At the boundary (DESIGN): the move follows from the steps.
107
+ const nextUndone = steps.find((s) => !s.done);
108
+ if (nextUndone === undefined) {
109
+ if (steps.length === 0) {
110
+ return 'plan the first step — `/plumbbob:pb-step`';
126
111
  }
112
+ // Batch-default: the steps were planned up front, so finishing them usually
113
+ // means "wrap up" — but `/plumbbob:pb-step` can still add an increment if reality grew.
114
+ const harvest = parked > 0 ? `harvest ${parked} parked idea${parked === 1 ? '' : 's'} — \`/plumbbob:pb-harvest\`; then ` : '';
115
+ return `${harvest}wrap up — \`/plumbbob:pb-wrap\` (or \`/plumbbob:pb-step\` to add another increment)`;
127
116
  }
117
+ return nextUndone.planned
118
+ ? `build step ${nextUndone.n} — \`/plumbbob:pb-build\` (or \`/plumbbob:pb-step\` to revise it first)`
119
+ : `plan step ${nextUndone.n} — \`/plumbbob:pb-step\``;
128
120
  }
129
121
  export function orient(input) {
130
122
  const steps = parseSteps(input.intent);
131
123
  const parked = parseParked(input.buildLog);
132
124
  const nextUndone = steps.find((s) => !s.done);
133
125
  const seamParse = nextUndone === undefined ? null : parseStepSeam(input.intent, nextUndone.n);
126
+ const phase = input.spiking ? 'SPIKE' : input.inFlight !== null ? 'BUILD' : 'DESIGN';
134
127
  return {
135
128
  title: parseTitle(input.intent),
136
- state: input.state,
129
+ phase,
137
130
  steps,
138
131
  lastCheckpoint: parseLastCheckpoint(input.checkpoints),
139
132
  parked,
140
133
  openQuestions: parseOpenQuestions(input.intent),
141
- next: nextMove(input.state, steps, input.inFlight, parked),
134
+ next: nextMove(input.spiking, steps, input.inFlight, parked),
142
135
  nextDoneWhen: nextUndone?.doneWhen ?? null,
143
136
  nextSeam: seamParse !== null && seamParse.ok ? seamParse.seam : [],
144
137
  };
@@ -168,7 +161,7 @@ export function formatOrientation(o) {
168
161
  const cp = o.lastCheckpoint;
169
162
  const cpLine = cp === null ? 'last checkpoint none yet' : `last checkpoint step ${cp.n} · ${cp.sha.slice(0, 7)}`;
170
163
  return [
171
- `Plumbbob — ${o.title ?? '(untitled)'} [${o.state}]`,
164
+ `Plumbbob — ${o.title ?? '(untitled)'} [${o.phase}]`,
172
165
  '',
173
166
  stepsBlock,
174
167
  '',
@@ -1,7 +1,12 @@
1
1
  // The .plumbbob/ sidecar: control state lives in flat files so the hooks can
2
2
  // read it with a grep and no markdown parsing (D7). Functional/procedural,
3
3
  // node builtins only (C1/C2).
4
- import { existsSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs';
4
+ //
5
+ // STATE is a pure session sentinel: its EXISTENCE means "a session is active",
6
+ // and nothing reads its content. The phase the dashboard shows (DESIGN/BUILD/
7
+ // SPIKE) is derived, not stored — BUILD ⇔ a STEP is in flight, SPIKE ⇔ the SPIKE
8
+ // marker is present, otherwise DESIGN.
9
+ import { existsSync, readFileSync, writeFileSync, appendFileSync, rmSync } from 'node:fs';
5
10
  import { join } from 'node:path';
6
11
  import { gitDir } from "./git.js";
7
12
  const DIRNAME = '.plumbbob';
@@ -11,6 +16,12 @@ export function sidecarDir(root) {
11
16
  function statePath(root) {
12
17
  return join(root, DIRNAME, 'STATE');
13
18
  }
19
+ // The SPIKE marker (a single-purpose presence flag, like SEAM/STEP): written by
20
+ // `spike` on open, removed on `spike done`. Its existence is the one signal that
21
+ // the dashboard and the spike gates read to know "a spike is active".
22
+ export function spikePath(root) {
23
+ return join(root, DIRNAME, 'SPIKE');
24
+ }
14
25
  // SEAM and STEP carry the in-flight step (D4/D7): a plain path list and a bare
15
26
  // number, so the hooks read them with a grep and no markdown parsing.
16
27
  export function seamPath(root) {
@@ -31,21 +42,24 @@ export function intentPath(root) {
31
42
  export function buildLogPath(root) {
32
43
  return join(root, DIRNAME, 'build-log.md');
33
44
  }
34
- // A session exists iff STATE exists. Deleting STATE (at finish) is what switches
35
- // the muzzle off — so it is the single source of truth for "is there a session".
45
+ // A session exists iff STATE exists. Deleting STATE (at wrap) is what flips the
46
+ // repo back to "no session" — so it is the single source of truth for "is there
47
+ // a session". `start` calls beginSession; `wrap` removes the file.
36
48
  export function hasSession(root) {
37
49
  return existsSync(statePath(root));
38
50
  }
39
- export function readState(root) {
40
- try {
41
- return readFileSync(statePath(root), 'utf8').trim();
42
- }
43
- catch {
44
- return null;
45
- }
51
+ export function beginSession(root) {
52
+ writeFileSync(statePath(root), 'active\n');
53
+ }
54
+ // SPIKE marker helpers — existence is the whole signal (content is irrelevant).
55
+ export function inSpike(root) {
56
+ return existsSync(spikePath(root));
57
+ }
58
+ export function markSpike(root) {
59
+ writeFileSync(spikePath(root), 'active\n');
46
60
  }
47
- export function writeState(root, state) {
48
- writeFileSync(statePath(root), `${state}\n`);
61
+ export function clearSpike(root) {
62
+ rmSync(spikePath(root), { force: true });
49
63
  }
50
64
  // D17: keep the sidecar untracked by appending `.plumbbob/` to the repo's
51
65
  // git/info/exclude. Idempotent — a re-`start` after finish must not double-add.
@@ -1,9 +1,9 @@
1
1
  // `plumbbob build <n>` — read step n's seam from intent.md, write the normalized
2
- // SEAM + STEP, and enter BUILD. Re-entering from REVIEW just re-derives the same
3
- // seam and flips back to BUILD; it never checkpoints (only `done` commits).
2
+ // SEAM + STEP. The STEP file is the in-flight signal (the dashboard derives the
3
+ // BUILD phase from it); it never checkpoints (only `checkpoint` commits).
4
4
  import { readFileSync, writeFileSync } from 'node:fs';
5
5
  import { findRepoRoot } from "../lib/git.js";
6
- import { hasSession, intentPath, seamPath, stepPath, writeState } from "../lib/sidecar.js";
6
+ import { hasSession, intentPath, seamPath, stepPath } from "../lib/sidecar.js";
7
7
  import { parseStepSeam } from "../lib/intent.js";
8
8
  export function build(cwd, args) {
9
9
  const root = findRepoRoot(cwd);
@@ -24,7 +24,6 @@ export function build(cwd, args) {
24
24
  }
25
25
  writeFileSync(seamPath(root), `${parsed.seam.join('\n')}\n`);
26
26
  writeFileSync(stepPath(root), `${step}\n`);
27
- writeState(root, 'BUILD');
28
- process.stdout.write(`plumbbob: building step ${step} — STATE=BUILD. Seam (for orientation; not a lock in v2):\n${parsed.seam.map((p) => ` ${p}`).join('\n')}\n`);
27
+ process.stdout.write(`plumbbob: building step ${step}. Seam (for orientation; not a lock in v2):\n${parsed.seam.map((p) => ` ${p}`).join('\n')}\n`);
29
28
  return 0;
30
29
  }
@@ -1,16 +1,18 @@
1
1
  // `plumbbob checkpoint [<n>] [-m <msg>]` — the executor-agnostic commit tick (D3).
2
- // Unlike v1 `done`, it does NOT require BUILD state or a STEP file: the step is
3
- // whatever you pass, else the in-flight STEP, else the next undone step in intent.
4
- // It gates on a green check, then commits any pending work (or records the existing
5
- // HEAD when the tree is already clean — the human's commit skill may have committed
6
- // first), records the SHA, flips the intent checkbox to `[x]`, clears any STEP/SEAM,
7
- // and returns to DESIGN. The diff's author is irrelevant: `/plumbbob:pb-build`, your hands,
8
- // a vibe session, or another harness all checkpoint the same way.
2
+ // Unlike v1 `done`, it does NOT require a STEP file: the step is whatever you pass,
3
+ // else the in-flight STEP, else the next undone step in intent. It gates on a green
4
+ // check, then commits any pending work (or records the existing HEAD when the tree
5
+ // is already clean — the human's commit skill may have committed first), records the
6
+ // SHA, flips the intent checkbox to `[x]`, and clears any STEP/SEAM (which returns
7
+ // the dashboard to the DESIGN boundary). The diff's author is irrelevant:
8
+ // `/plumbbob:pb-build`, your hands, a vibe session, or another harness all checkpoint
9
+ // the same way.
9
10
  import { appendFileSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
10
11
  import { commit, findRepoRoot, headSha, isDirty, stageAll } from "../lib/git.js";
11
- import { checkpointsPath, hasSession, intentPath, seamPath, stepPath, writeState } from "../lib/sidecar.js";
12
+ import { buildLogPath, checkpointsPath, hasSession, intentPath, seamPath, stepPath } from "../lib/sidecar.js";
12
13
  import { runCheck } from "../lib/check.js";
13
14
  import { markStepDone, parseSteps } from "../lib/orient.js";
15
+ import { appendToSection, checkpointLogLine } from "../lib/buildlog.js";
14
16
  export function checkpoint(cwd, args) {
15
17
  const root = findRepoRoot(cwd);
16
18
  if (root === null || !hasSession(root)) {
@@ -36,10 +38,10 @@ export function checkpoint(cwd, args) {
36
38
  }
37
39
  appendFileSync(checkpointsPath(root), `step ${step} ${sha}\n`);
38
40
  flipIntent(root, step);
41
+ logCheckpoint(root, step, sha);
39
42
  rmSync(seamPath(root), { force: true });
40
43
  rmSync(stepPath(root), { force: true });
41
- writeState(root, 'DESIGN');
42
- process.stdout.write(`plumbbob: step ${step} checkpointed — ${sha.slice(0, 9)}. STATE=DESIGN.\n`);
44
+ process.stdout.write(`plumbbob: step ${step} checkpointed — ${sha.slice(0, 9)}. Back at the boundary.\n`);
43
45
  return 0;
44
46
  }
45
47
  // Step resolution (D3): explicit arg > in-flight STEP file > first undone step in
@@ -68,6 +70,32 @@ function flipIntent(root, step) {
68
70
  // best-effort bookkeeping; the checkpoint SHA is the source of truth.
69
71
  }
70
72
  }
73
+ // Append a dated line to the build-log's `## Log` so the build's history accrues at
74
+ // each checkpoint instead of being reconstructed at wrap. The step's title is lifted
75
+ // from intent.md when still present. Best-effort: a missing/odd build-log never blocks
76
+ // a checkpoint — the `checkpoints` SHA is the source of truth.
77
+ function logCheckpoint(root, step, sha) {
78
+ try {
79
+ const path = buildLogPath(root);
80
+ const date = new Date().toISOString().slice(0, 10);
81
+ const line = checkpointLogLine(date, step, sha, titleForStep(root, step));
82
+ const updated = appendToSection(readFileSync(path, 'utf8'), 'Log', line);
83
+ if (updated !== null) {
84
+ writeFileSync(path, updated);
85
+ }
86
+ }
87
+ catch {
88
+ // best-effort ledger; never fail a checkpoint over the build-log.
89
+ }
90
+ }
91
+ function titleForStep(root, step) {
92
+ try {
93
+ return parseSteps(readFileSync(intentPath(root), 'utf8')).find((s) => s.n === step)?.title ?? null;
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
71
99
  function messageArg(args) {
72
100
  const i = args.indexOf('-m');
73
101
  return i !== -1 && i + 1 < args.length ? (args[i + 1] ?? null) : null;
@@ -86,7 +86,7 @@ export function doctor() {
86
86
  out.push(failed === 0
87
87
  ? 'plumbbob: all checks passed. If a skill still misbehaves, restart Claude Code (or /reload-plugins).'
88
88
  : `plumbbob: ${failed} problem(s) — apply the → fixes, then restart Claude Code.`);
89
- out.push('plumbbob: skills shell a bare `plumbbob`. The marketplace plugin puts it on PATH via bin/; for the skills-dir/global install run `npm i -g plumbbob`. Sessions are per-project via `plumbbob start`.');
89
+ out.push('plumbbob: skills shell a bare `plumbbob`. The marketplace plugin puts it on PATH only inside a Claude Code session (via bin/) — there is no terminal `plumbbob`; for one (or the skills-dir install) run `npm i -g plumbbob`. In-session you can also run this as `/plumbbob:pb-doctor`. Sessions are per-project via `plumbbob start`.');
90
90
  process.stdout.write(`${out.join('\n')}\n`);
91
91
  return failed === 0 ? 0 : 1;
92
92
  }
@@ -4,6 +4,7 @@
4
4
  import { readFileSync, writeFileSync } from 'node:fs';
5
5
  import { findRepoRoot } from "../lib/git.js";
6
6
  import { hasSession, buildLogPath } from "../lib/sidecar.js";
7
+ import { appendToSection } from "../lib/buildlog.js";
7
8
  export function park(cwd, args) {
8
9
  const root = findRepoRoot(cwd);
9
10
  if (root === null || !hasSession(root)) {
@@ -19,7 +20,7 @@ export function park(cwd, args) {
19
20
  return 1;
20
21
  }
21
22
  const path = buildLogPath(root);
22
- const updated = insertParkItem(readFileSync(path, 'utf8'), text);
23
+ const updated = appendToSection(readFileSync(path, 'utf8'), 'Park list', `- [ ] ${text}`);
23
24
  if (updated === null) {
24
25
  process.stderr.write('plumbbob: could not find a "## Park list" section in build-log.md.\n');
25
26
  return 1;
@@ -28,24 +29,3 @@ export function park(cwd, args) {
28
29
  process.stdout.write(`parked: ${text}\n`);
29
30
  return 0;
30
31
  }
31
- // Append after the last non-blank line of the Park list section (i.e. just before
32
- // the next `## ` heading). Returns null if there is no Park list to append to.
33
- function insertParkItem(content, text) {
34
- const lines = content.split('\n');
35
- const headingIdx = lines.findIndex((line) => line.trim() === '## Park list');
36
- if (headingIdx === -1) {
37
- return null;
38
- }
39
- let nextSection = lines.findIndex((line, i) => i > headingIdx && line.startsWith('## '));
40
- if (nextSection === -1) {
41
- nextSection = lines.length;
42
- }
43
- let insertAt = headingIdx + 1;
44
- for (let i = headingIdx + 1; i < nextSection; i++) {
45
- if ((lines[i] ?? '').trim() !== '') {
46
- insertAt = i + 1;
47
- }
48
- }
49
- lines.splice(insertAt, 0, `- [ ] ${text}`);
50
- return lines.join('\n');
51
- }
@@ -13,7 +13,7 @@ import { tmpdir } from 'node:os';
13
13
  import { join } from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { findRepoRoot, resetHard, untrackedPaths } from "../lib/git.js";
16
- import { checkpointsPath, hasSession, seamPath, sidecarDir, stepPath, writeState } from "../lib/sidecar.js";
16
+ import { checkpointsPath, hasSession, seamPath, sidecarDir, stepPath } from "../lib/sidecar.js";
17
17
  import { matchesSeam } from "../lib/intent.js";
18
18
  export function revert(cwd, args) {
19
19
  const root = findRepoRoot(cwd);
@@ -53,8 +53,7 @@ export function revert(cwd, args) {
53
53
  }
54
54
  rmSync(seamPath(root), { force: true });
55
55
  rmSync(stepPath(root), { force: true });
56
- writeState(root, 'DESIGN');
57
- process.stdout.write(`plumbbob: reverted to ${sha.slice(0, 9)} — STATE=DESIGN. Park lines and intent edits were preserved.\n`);
56
+ process.stdout.write(`plumbbob: reverted to ${sha.slice(0, 9)} — back at the boundary. Park lines and intent edits were preserved.\n`);
58
57
  return 0;
59
58
  }
60
59
  // The repo paths that belong to plumbbob, not to the work being reverted: the
@@ -1,10 +1,10 @@
1
1
  // `plumbbob spike` (D18) — the spike lifecycle for a genuine fork the design
2
2
  // phase couldn't settle. `spike "<slug>" [opt…]` creates a sibling git worktree +
3
3
  // `spike/<slug>-<opt>` branch per option OUTSIDE the repo root (default opts a/b)
4
- // and sets STATE=SPIKE; the main tree stays DESIGN-locked while you experiment in
5
- // the worktrees, which are hook-dormant by construction — the untracked sidecar
6
- // (D17) doesn't exist in a fresh checkout, so the hooks find no STATE there.
7
- // `spike done` removes every spike worktree + branch and returns to DESIGN.
4
+ // and drops the SPIKE marker; the main tree stays put while you experiment in the
5
+ // worktrees, which are hook-dormant by construction — the untracked sidecar (D17)
6
+ // doesn't exist in a fresh checkout, so the hooks find no STATE there.
7
+ // `spike done` removes every spike worktree + branch and clears the marker.
8
8
  //
9
9
  // Worktree git calls run directly here rather than via lib/git.ts (which holds the
10
10
  // shared additive read/commit helpers): worktree management is spike-local, and
@@ -13,7 +13,7 @@ import { execFileSync } from 'node:child_process';
13
13
  import { existsSync } from 'node:fs';
14
14
  import { basename, dirname, join } from 'node:path';
15
15
  import { findRepoRoot } from "../lib/git.js";
16
- import { hasSession, readState, writeState } from "../lib/sidecar.js";
16
+ import { hasSession, inSpike, markSpike, clearSpike, stepPath } from "../lib/sidecar.js";
17
17
  const DEFAULT_OPTIONS = ['a', 'b'];
18
18
  export function spike(cwd, args) {
19
19
  const root = findRepoRoot(cwd);
@@ -28,14 +28,13 @@ export function spike(cwd, args) {
28
28
  return spikeStart(root, positionals);
29
29
  }
30
30
  function spikeStart(root, positionals) {
31
- const state = readState(root);
32
- if (state === 'SPIKE') {
31
+ if (inSpike(root)) {
33
32
  process.stderr.write('plumbbob: already in a spike. Run `plumbbob spike done` to close it first.\n');
34
33
  return 1;
35
34
  }
36
- if (state !== 'DESIGN') {
37
- process.stderr.write(`plumbbob: spike starts from DESIGN (current state is ${state ?? 'UNKNOWN'}). ` +
38
- 'A spike is a deliberate fork — close the current step first with `done`.\n');
35
+ if (existsSync(stepPath(root))) {
36
+ process.stderr.write('plumbbob: spike starts from a settled boundary, but a step is in flight. ' +
37
+ 'A spike is a deliberate fork — checkpoint or revert the current step first.\n');
39
38
  return 1;
40
39
  }
41
40
  const slug = sanitize(positionals[0] ?? '');
@@ -55,16 +54,15 @@ function spikeStart(root, positionals) {
55
54
  git(root, ['worktree', 'add', '-b', `spike/${slug}-${opt}`, path, 'HEAD']);
56
55
  created.push(path);
57
56
  }
58
- writeState(root, 'SPIKE');
59
- process.stdout.write(`plumbbob: STATE=SPIKE — the main tree stays DESIGN-locked. Experiment in the throwaway worktrees:\n${created
57
+ markSpike(root);
58
+ process.stdout.write(`plumbbob: spiking — the main tree stays put. Experiment in the throwaway worktrees:\n${created
60
59
  .map((p) => ` ${p}`)
61
60
  .join('\n')}\nWhen you've decided, record the verdict in intent.md and run \`plumbbob spike done\`.\n`);
62
61
  return 0;
63
62
  }
64
63
  function spikeDone(root) {
65
- const state = readState(root);
66
- if (state !== 'SPIKE') {
67
- process.stderr.write(`plumbbob: no active spike (state is ${state ?? 'UNKNOWN'}).\n`);
64
+ if (!inSpike(root)) {
65
+ process.stderr.write('plumbbob: no active spike to close.\n');
68
66
  return 1;
69
67
  }
70
68
  for (const path of spikeWorktrees(root)) {
@@ -74,8 +72,8 @@ function spikeDone(root) {
74
72
  for (const branch of spikeBranches(root)) {
75
73
  git(root, ['branch', '-D', branch]);
76
74
  }
77
- writeState(root, 'DESIGN');
78
- process.stdout.write('plumbbob: spike closed — STATE=DESIGN, worktrees and branches removed. ' +
75
+ clearSpike(root);
76
+ process.stdout.write('plumbbob: spike closed — worktrees and branches removed, back at the boundary. ' +
79
77
  'Record the verdict (which option won, and why) in intent.md before you `build`.\n');
80
78
  return 0;
81
79
  }
@@ -1,10 +1,10 @@
1
- // `plumbbob start "<title>"` — scaffold the sidecar, record the baseline, enter
2
- // DESIGN. Refuses on a dirty tree (D22), an existing session, or a non-git dir.
1
+ // `plumbbob start "<title>"` — scaffold the sidecar, record the baseline, open
2
+ // the session. Refuses on a dirty tree (D22), an existing session, or a non-git dir.
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { findRepoRoot, hasCommit, headSha, isDirty } from "../lib/git.js";
7
- import { sidecarDir, checkpointsPath, configPath, intentPath, buildLogPath, writeState, hasSession, excludeSidecar, } from "../lib/sidecar.js";
7
+ import { sidecarDir, checkpointsPath, configPath, intentPath, buildLogPath, beginSession, hasSession, excludeSidecar, } from "../lib/sidecar.js";
8
8
  const DEFAULT_CHECK = 'pnpm run check';
9
9
  export function start(cwd, args) {
10
10
  const positionals = args.filter((a) => !a.startsWith('--'));
@@ -37,7 +37,7 @@ export function start(cwd, args) {
37
37
  const sha = headSha(root);
38
38
  const check = detectCheck(root);
39
39
  mkdirSync(sidecarDir(root), { recursive: true });
40
- writeState(root, 'DESIGN');
40
+ beginSession(root);
41
41
  writeFileSync(checkpointsPath(root), `baseline ${sha}\n`);
42
42
  writeFileSync(configPath(root), `check=${check.command}\n`);
43
43
  writeFileSync(intentPath(root), stamp(readTemplate('intent.md'), title, check.command));
@@ -46,7 +46,7 @@ export function start(cwd, args) {
46
46
  if (check.warn) {
47
47
  process.stderr.write(`plumbbob: WARNING the heavy check '${check.command}' is not defined in this repo's package.json. Edit .plumbbob/config (check=...) to set the real gate before \`review\`/\`done\`.\n`);
48
48
  }
49
- process.stdout.write(`plumbbob: started "${title}" — STATE=DESIGN, baseline ${sha.slice(0, 9)}. Frame and decide in .plumbbob/intent.md; flip to BUILD only once the decisions are made.\n`);
49
+ process.stdout.write(`plumbbob: started "${title}" — baseline ${sha.slice(0, 9)}. Frame and decide in .plumbbob/intent.md; \`build\` a step once the decisions are made.\n`);
50
50
  return 0;
51
51
  }
52
52
  // D24: default the heavy check to `pnpm run check`; warn (but still record it)
@@ -3,7 +3,7 @@
3
3
  // behavior, so the `NO ACTIVE SESSION` sentinel is kept exact.
4
4
  import { readFileSync } from 'node:fs';
5
5
  import { findRepoRoot } from "../lib/git.js";
6
- import { buildLogPath, checkpointsPath, hasSession, intentPath, readState, stepPath } from "../lib/sidecar.js";
6
+ import { buildLogPath, checkpointsPath, hasSession, inSpike, intentPath, stepPath } from "../lib/sidecar.js";
7
7
  import { formatOrientation, orient } from "../lib/orient.js";
8
8
  function readOr(path) {
9
9
  try {
@@ -21,11 +21,11 @@ export function status(cwd) {
21
21
  }
22
22
  const inFlightRaw = readOr(stepPath(root)).trim();
23
23
  const orientation = orient({
24
- state: readState(root) ?? 'UNKNOWN',
25
24
  intent: readOr(intentPath(root)),
26
25
  buildLog: readOr(buildLogPath(root)),
27
26
  checkpoints: readOr(checkpointsPath(root)),
28
27
  inFlight: /^\d+$/.test(inFlightRaw) ? Number(inFlightRaw) : null,
28
+ spiking: inSpike(root),
29
29
  });
30
30
  process.stdout.write(`${formatOrientation(orientation)}\n`);
31
31
  return 0;
@@ -7,7 +7,7 @@
7
7
  import { appendFileSync, existsSync, readFileSync, rmSync } from 'node:fs';
8
8
  import { join, relative } from 'node:path';
9
9
  import { findRepoRoot } from "../lib/git.js";
10
- import { buildLogPath, checkpointsPath, hasSession, intentPath, seamPath, sidecarDir, stepPath } from "../lib/sidecar.js";
10
+ import { buildLogPath, checkpointsPath, hasSession, intentPath, seamPath, sidecarDir, spikePath, stepPath } from "../lib/sidecar.js";
11
11
  import { archiveSession, reportPath } from "../lib/archive.js";
12
12
  export function wrap(cwd) {
13
13
  const root = findRepoRoot(cwd);
@@ -30,6 +30,7 @@ export function wrap(cwd) {
30
30
  rmSync(reportPath(root), { force: true });
31
31
  rmSync(seamPath(root), { force: true });
32
32
  rmSync(stepPath(root), { force: true });
33
+ rmSync(spikePath(root), { force: true });
33
34
  rmSync(join(sidecarDir(root), 'STATE'), { force: true });
34
35
  process.stdout.write(`plumbbob: wrap — archived to ${relative(root, archived)}. Sidecar cleared. ` +
35
36
  'Run `/plumbbob:pb-plan` (or `plumbbob start "<title>"`) to frame the next goal.\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plumbbob",
3
- "version": "0.4.8",
3
+ "version": "0.4.11",
4
4
  "description": "Attention-first build process: 11 skills + a CLI that keep you the decider — guidance, not enforcement.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -20,8 +20,8 @@
20
20
  "license": "Apache-2.0",
21
21
  "author": "Rob McLarty <hello@robmclarty.com>",
22
22
  "bin": {
23
- "plumbbob": "dist/cli.js",
24
- "pb": "dist/cli.js"
23
+ "plumbbob": "bin/plumbbob",
24
+ "pb": "bin/pb"
25
25
  },
26
26
  "files": [
27
27
  ".claude-plugin",
@@ -40,8 +40,9 @@ at the pause for your approval. **Re-firing `/plumbbob:pb-build` is itself the c
40
40
  does: `plumbbob check` → self-review the diff against the done-when, the
41
41
  Decisions, and the Constraints (a single structured read, D16) → validate → **PAUSE
42
42
  for the human's approval** → only on approval, checkpoint with
43
- `plumbbob checkpoint`. Do **not** bump the version or changelogthat is
44
- the human's `/version` call.
43
+ `plumbbob checkpoint` (which also appends this step to the build-log's `## Log`the
44
+ history writes itself at each checkpoint, so you don't hand-log it). Do **not** bump
45
+ the version or changelog — that is the human's `/version` call.
45
46
 
46
47
  ## `--auto` — let the agent be the clock (opt-in)
47
48
 
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: pb-doctor
3
+ description: Diagnose the plugin install from inside a session — is plumbbob linked, are the skills and hook present, is there a collision. A thin trigger for `plumbbob doctor`. Matters most for a marketplace install, where the CLI is on PATH only inside Claude Code.
4
+ disable-model-invocation: true
5
+ model: haiku
6
+ allowed-tools: Bash(plumbbob doctor:*)
7
+ ---
8
+
9
+ # Plumbbob — doctor (the is-it-installed-right move)
10
+
11
+ Install diagnostic (injected when this skill runs): !`if command -v plumbbob >/dev/null 2>&1; then plumbbob doctor 2>&1; else echo "plumbbob CLI not on PATH in this session. Marketplace install: confirm the plugin is enabled in /plugin, then /reload-plugins. Skills-dir/global install: npm i -g plumbbob && plumbbob init."; fi`
12
+
13
+ This is a **thin driver** for `plumbbob doctor`. The report above is read-only — `doctor`
14
+ inspects, it never writes. This skill carries **no Edit and no Write tool**: never edit a file
15
+ to change what the report says. If a check failed, surface its `→ fix` verbatim and apply
16
+ *that* fix, then have the human **restart Claude Code (or `/reload-plugins`)**. **Never retry.**
17
+
18
+ ## Why a skill (and not just the terminal)
19
+
20
+ The CLI's reach differs by install path, and that is the whole point of running `doctor` here:
21
+
22
+ - **Marketplace plugin** — `plumbbob` is on PATH **only inside a Claude Code session** (its
23
+ `bin/` shims are injected into the Bash tool while the plugin is enabled). There is **no
24
+ terminal `plumbbob`** to run. So this skill — or letting me run `plumbbob doctor` in-session
25
+ — is the *only* way to reach `doctor`. The fact that this skill loaded at all already proves
26
+ the plugin is enabled and the shim is on PATH.
27
+ - **Skills-dir / global install** (`npm i -g plumbbob` + `plumbbob init`) — `plumbbob` is on
28
+ PATH everywhere, so terminal `plumbbob doctor` works too; this skill is just convenience.
29
+
30
+ The one failure `doctor` **cannot** diagnose: a plugin that never loaded at all (no
31
+ `/plumbbob:*` skill appears). That is a `/plugin` / `/reload-plugins` problem, not doctor's —
32
+ and if you are reading this, that is not your situation.
33
+
34
+ ## What it does
35
+
36
+ 1. Surface the injected `doctor` report verbatim — every `✓` and any `✗` with its `→ fix`.
37
+ 2. If all checks passed, say so; if a skill still misbehaves, the fix is a restart, not a re-run.
38
+ 3. If a check failed, apply its named `→ fix`, then restart Claude Code (or `/reload-plugins`).
39
+ 4. If the line above says the CLI is not on PATH, follow the install-path branch it printed.
@@ -13,15 +13,16 @@ Current session state (injected when this skill runs): !`plumbbob status 2>/dev/
13
13
  `/plumbbob:pb-harvest` is the complement of `/plumbbob:pb-park` (D7): you parked ideas as seeds during a
14
14
  build; now, at a boundary, you harvest them — decide what each one is.
15
15
 
16
- ## When to run it — wrong-state refusal
16
+ ## When to run it — boundary only
17
17
 
18
- Harvest at a **step boundary**: after a step is checkpointed and you are back in
19
- DESIGN, not mid-step. Read the state injected above:
18
+ Harvest at a **step boundary**: after a step is checkpointed and you are back at the
19
+ DESIGN boundary, not mid-step. Read the dashboard injected above:
20
20
 
21
21
  - `NO ACTIVE SESSION` — **refuse**; tell the human to `plumbbob start "<title>"` first.
22
- - `BUILD` — a step is in flight; **refuse** and suggest finishing it with `/plumbbob:pb-verify`
23
- before harvesting. Chasing parked items mid-step is the disease parking prevents.
24
- - `DESIGN` (and other settled states) go ahead.
22
+ - A step in flight (the dashboard reads `[BUILD]`, and `next →` points at finishing the
23
+ step) **refuse** and suggest finishing it with `/plumbbob:pb-verify` before harvesting.
24
+ Chasing parked items mid-step is the disease parking prevents.
25
+ - At the boundary (the dashboard reads `[DESIGN]`) — go ahead.
25
26
 
26
27
  ## What this skill does
27
28
 
@@ -16,10 +16,10 @@ it — is the whole point: it protects the step in flight.
16
16
 
17
17
  ## Wrong-state refusal
18
18
 
19
- Parking needs an **active session** to capture into. Read the state injected above: if
19
+ Parking needs an **active session** to capture into. Read the dashboard injected above: if
20
20
  it is `NO ACTIVE SESSION`, **refuse** and tell the human to run `plumbbob start
21
- "<title>"` first. Every active state (`DESIGN`, `BUILD`, `SPIKE`, `FINISH`) is fine
22
- capture is always available, which is the whole point of parking.
21
+ "<title>"` first. Any time the session is live at the boundary, mid-step, or in a spike
22
+ is fine; capture is always available, which is the whole point of parking.
23
23
 
24
24
  ## What this skill does
25
25
 
@@ -38,7 +38,7 @@ an agent can follow with `/plumbbob:pb-build`. The argument only seeds how you g
38
38
  ## What this skill does
39
39
 
40
40
  1. **Scaffold.** If there is no active session, run `plumbbob start "<title>"`
41
- to create `.plumbbob/` (STATE=DESIGN, baseline recorded). If a session already
41
+ to create `.plumbbob/` (baseline recorded, session opened). If a session already
42
42
  exists, say so and edit the existing `intent.md` rather than starting over.
43
43
  2. **Frame** (`.plumbbob/intent.md`), with the human: the **Problem** in plain words,
44
44
  the **smallest thing** that solves it, what **done looks like**, and what you are
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pb-revert
3
- description: Human-triggered driver for `plumbbob revert` — git reset --hard to a checkpoint SHA (discarding the half-done step) and return to DESIGN.
3
+ description: Human-triggered driver for `plumbbob revert` — git reset --hard to a checkpoint SHA (discarding the half-done step) and return to the boundary.
4
4
  disable-model-invocation: true
5
5
  model: haiku
6
6
  allowed-tools: Bash(plumbbob status:*), Bash(plumbbob revert:*)
@@ -31,9 +31,9 @@ this skill verifies it the same way: **it reads the diff, not the author** (D3).
31
31
  the human is the clock. Never checkpoint without it.
32
32
  5. **Checkpoint** *(only after approval)*. Commit the work — the human's
33
33
  commit-with-TIL skill for a rich message, or let `checkpoint` make the WIP commit
34
- — then run `plumbbob checkpoint` to record the SHA, flip the step to done,
35
- and return to DESIGN. Do **not** bump the version or touch the changelog — that is
36
- the human's `/version` call.
34
+ — then run `plumbbob checkpoint` to record the SHA, flip the step to done, append the
35
+ step to the build-log's `## Log`, and return to DESIGN. Do **not** bump the version or
36
+ touch the changelog — that is the human's `/version` call.
37
37
 
38
38
  ## The hard contracts
39
39
 
@@ -16,9 +16,12 @@ and no separate docs phase.
16
16
 
17
17
  ## What this skill does, in order
18
18
 
19
- 1. **Write the report** to `.plumbbob/report.md`, from `intent.md` + `build-log.md`:
20
- - **What shipped** the steps completed and what they delivered.
21
- - **Decisions and why** the settled calls that shaped the build.
19
+ 1. **Write the report** to `.plumbbob/report.md`, from `intent.md` + `build-log.md`.
20
+ The build-log's `## Log` is already the chronological history `plumbbob checkpoint`
21
+ wrote a dated line for every step as it landed. **Read it as the spine; do not
22
+ re-narrate it.** The report adds only what the log does not already carry:
23
+ - **What shipped** — summarize from the `## Log` timeline; don't rebuild it step by step.
24
+ - **Decisions and why** — the settled calls that shaped the build (the *why* behind the log).
22
25
  - **Parked & harvested** — what was captured and how each was classified.
23
26
  - **Final status** — done or partial, and what is left.
24
27
  - **Deferred tangents** — the harvested items that become future work.
@@ -33,6 +36,9 @@ and no separate docs phase.
33
36
 
34
37
  - **Report by default, never a gate.** Always offer the report; never wall the exit. A
35
38
  bug fix's report can be three lines — size it to the work.
39
+ - **The log is the history; the report is the synthesis.** `checkpoint` already recorded
40
+ what shipped, step by step — wrap applies the *unique additions* (the why, the deferred
41
+ tangents, the final status), it does not rewrite the timeline.
36
42
  - **Archive-then-clear, never destroy** (C4). The archive is the record; the active
37
43
  files only clear once they are safely copied.
38
44
  - **No version bump, no docs phase.** Updating real docs is a separate, explicit ask;
@@ -5,12 +5,13 @@ step boundaries. The antidote to "my plan got lost in the noise."
5
5
  Steps : where you are. One step in flight at a time.
6
6
  Park list : where ideas go so you do not chase them. CAPTURE, never act inline.
7
7
  Harvest : the boundary ritual that keeps you on one branch.
8
- Log : the audit trail. Feeds the /plumbbob:pb-wrap report, then gets archived.
8
+ Log : the build's history. `plumbbob checkpoint` appends a line per step as it
9
+ lands; feeds the /plumbbob:pb-wrap report, then gets archived.
9
10
  -->
10
11
 
11
12
  # Build log — {{TITLE}}
12
13
 
13
- **Current step:** none (DESIGN) · **STATE:** DESIGN
14
+ **Current step:** none (at the boundary)
14
15
  **Heavy check:** {{CHECK}}
15
16
 
16
17
  ## Steps
@@ -47,8 +48,9 @@ Harvest results this boundary:
47
48
 
48
49
  ## Log
49
50
 
50
- *(Append-only. One decision or event per line, dated. What you point at to say "I
51
- did that the LLM helped, but those were my calls." `/plumbbob:pb-wrap` reads this for the
52
- report; `plumbbob wrap` archives it under `.plumbbob/archive/`.)*
53
-
54
- - <date> <decision / event / what shipped this step>
51
+ *(The build's history, oldest first. `plumbbob checkpoint` appends a dated line here
52
+ every time a step lands via `/plumbbob:pb-build` or `/plumbbob:pb-verify` so this
53
+ fills in as you go, not at the end. Add your own decision/event lines too: this is what
54
+ you point at to say "I did that — the LLM helped, but those were my calls."
55
+ `/plumbbob:pb-wrap` reads this for the report; `plumbbob wrap` archives it under
56
+ `.plumbbob/archive/`.)*
@@ -9,8 +9,7 @@ open. If the implementor (you-later, or the LLM) has to guess, the doc failed.
9
9
 
10
10
  # {{TITLE}}
11
11
 
12
- **STATE:** DESIGN
13
- **Phase** (bookkeeping while in DESIGN): frame
12
+ **Phase** (your own bookkeeping while framing): frame
14
13
  **Size:** tiny | small | medium
15
14
 
16
15
  ## Frame