plumbbob 0.10.0 → 0.10.1

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.10.0",
4
+ "version": "0.10.1",
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
@@ -10,7 +10,7 @@
10
10
  <a href="https://robmclarty.github.io/plumbbob/api.html">API reference</a>
11
11
  </p>
12
12
 
13
- PlumbBob is a Claude Code plugin (thirteen skills and a small CLI) that runs
13
+ PlumbBob is a Claude Code plugin (fourteen skills and a small CLI) that runs
14
14
  LLM-assisted coding as a loop you control. You and the model settle the plan in a file
15
15
  *before* any code; then the model builds one small step at a time and **stops after
16
16
  each one**, waiting for you to read the diff and approve it before anything is
@@ -145,7 +145,7 @@ You drive the whole loop from your IDE with `/plumbbob:*` skills: no step number
145
145
  remember, no raw CLI to type. Every skill is `disable-model-invocation`, so *you* fire
146
146
  every move, and `/plumbbob:status` always names your next one. (Claude Code namespaces
147
147
  them under the plugin, so the command is `/plumbbob:plan`. The bare `/plan` reaches it
148
- too, but only where nothing else owns that name, and four of the thirteen share one with
148
+ too, but only where nothing else owns that name, and four of the fourteen share one with
149
149
  a Claude Code built-in, so these docs write the full form.)
150
150
 
151
151
  **The happy path**: the three moves every session makes; many sessions need
@@ -172,6 +172,7 @@ nothing else:
172
172
  | `/plumbbob:status` | orient: where you are, the next step's done-when and seam, and the next move |
173
173
  | `/plumbbob:verify` | the tick, standalone: check → self-review → validate → **PAUSE** → checkpoint, for a diff `/plumbbob:build` didn't write |
174
174
  | `/plumbbob:revert` | rewind: `git reset --hard` to a recorded checkpoint |
175
+ | `/plumbbob:abandon` | drop the in-flight step, keep its work in the tree; the step stays planned |
175
176
  | `/plumbbob:recover` | reconcile the session's own state when the dashboard looks wrong |
176
177
  | `/plumbbob:doctor` | check the install from inside a session |
177
178
 
@@ -182,7 +183,7 @@ nothing else:
182
183
  | `/plumbbob:park` | capture a mid-build idea without chasing it |
183
184
  | `/plumbbob:harvest` | triage parked ideas between steps (blocker / tangent / pivot) |
184
185
 
185
- All thirteen, with inputs and effects, are in
186
+ All fourteen, with inputs and effects, are in
186
187
  [`docs/skills-reference.md`](docs/skills-reference.md).
187
188
 
188
189
  Under the skills ships a lean `plumbbob` CLI (the mechanical verbs the
@@ -240,7 +241,7 @@ modes, spec kits, native checkpoints), and that convergence is evidence *for* th
240
241
  premise. PlumbBob is deliberately thin against that future: the artifacts are
241
242
  plain markdown and ordinary git commits in your own repo, the CLI is lean
242
243
  (node builtins plus one deliberate dependency), and nothing locks you in. The
243
- investment is small (thirteen
244
+ investment is small (fourteen
244
245
  skills over a plain-markdown sidecar, learnable in an afternoon), and what you're
245
246
  actually learning is the method: decisions before code, one verified step at a
246
247
  time, capture instead of chase. That transfers to whatever tool wins. If
@@ -289,7 +290,7 @@ Each doc answers one question (in rough reading order for a new user):
289
290
  - *Show me the artifacts it leaves behind.* → [`examples/`](examples/): that same session's finished build folder, file by file.
290
291
  - *Should I / can I / what about…?* → [`docs/faq.md`](docs/faq.md): the adoption questions, answered straight.
291
292
  - *What is each method for?* → [`docs/techniques.md`](docs/techniques.md): steps, seams, the pause, park/harvest, spikes.
292
- - *What does each skill do?* → [`docs/skills-reference.md`](docs/skills-reference.md): all thirteen skills: inputs, effects, when to reach for each.
293
+ - *What does each skill do?* → [`docs/skills-reference.md`](docs/skills-reference.md): all fourteen skills: inputs, effects, when to reach for each.
293
294
  - *How do I plug in my own agent?* → [`docs/agents.md`](docs/agents.md): the subprocess envelope, the manifest, `harness.json`, and working examples (including a local-model reviewer via Ollama).
294
295
  - *How do I get a local model reviewing my steps?* → [`docs/local-model-review.md`](docs/local-model-review.md): the ollama-reviewer example walked end to end, install to every-pause review.
295
296
  - *How do I install it, exactly?* → [`docs/install.md`](docs/install.md): the full guide and the agent-neutral roadmap.
package/dist/cli-core.js CHANGED
@@ -19,6 +19,7 @@ import { handoff } from "./verbs/handoff.js";
19
19
  import { check } from "./verbs/check.js";
20
20
  import { checkpoint } from "./verbs/checkpoint.js";
21
21
  import { revert } from "./verbs/revert.js";
22
+ import { abandon } from "./verbs/abandon.js";
22
23
  import { spike } from "./verbs/spike.js";
23
24
  import { use } from "./verbs/use.js";
24
25
  import { finish } from "./verbs/finish.js";
@@ -113,6 +114,13 @@ const VERBS = [
113
114
  ],
114
115
  notes: 'Refuses (exit 1) with no session, an invalid --to, or a step with no recorded checkpoint.',
115
116
  },
117
+ {
118
+ name: 'abandon',
119
+ description: 'drop the in-flight step but keep its work in the tree',
120
+ synopsis: ['abandon [--build <slug>]'],
121
+ flags: [BUILD_FLAG],
122
+ notes: 'Refuses (exit 1) with no session, no step in flight, or a latched boundary (no human turn since the step began). Clears STEP/SEAM/TICK/handoff and logs the abandon; never touches the working tree, git, or the intent checkbox.',
123
+ },
116
124
  {
117
125
  name: 'park',
118
126
  description: 'append a raw line to the park list',
@@ -331,6 +339,8 @@ async function dispatch(verb, cwd, rest) {
331
339
  return checkpoint(cwd, rest);
332
340
  case 'revert':
333
341
  return revert(cwd, rest);
342
+ case 'abandon':
343
+ return abandon(cwd, rest);
334
344
  case 'spike':
335
345
  return spike(cwd, rest);
336
346
  case 'use':
@@ -102,3 +102,13 @@ export function checkpointLogLine(date, step, sha, title, stats = null) {
102
102
  const titled = title !== null && title.length > 0 ? `${head} — ${title}` : head;
103
103
  return stats !== null && stats.length > 0 ? `${titled} (${stats})` : titled;
104
104
  }
105
+ /**
106
+ * The Log line `abandon` writes for a dropped-but-kept step: the first
107
+ * non-checkpoint event the log records. Dated, names the step (its title when
108
+ * intent.md still carries it), and states plainly that the work stayed in the
109
+ * tree, so the narrative never has a step go quiet mid-flight.
110
+ */
111
+ export function abandonLogLine(date, step, title) {
112
+ const head = `- ${date} — step ${step} abandoned · work kept in tree`;
113
+ return title !== null && title.length > 0 ? `${head} — ${title}` : head;
114
+ }
@@ -191,12 +191,28 @@ export function spikePath(root, slug) {
191
191
  export function seamPath(root, slug) {
192
192
  return join(artifactDir(root, slug), 'SEAM');
193
193
  }
194
+ /**
195
+ * Remove the SEAM marker: `abandon` drops the in-flight step's edit grant, the
196
+ * same clear `checkpoint` and `revert` do inline. Homed here so a verb not
197
+ * sanctioned to delete on its own can still return to the boundary; absent is a
198
+ * no-op.
199
+ */
200
+ export function clearSeam(root, slug) {
201
+ rmSync(seamPath(root, slug), { force: true });
202
+ }
194
203
  /**
195
204
  * STEP: the number of the step in flight, a bare number in a flat file.
196
205
  */
197
206
  export function stepPath(root, slug) {
198
207
  return join(artifactDir(root, slug), 'STEP');
199
208
  }
209
+ /**
210
+ * Remove the STEP marker alongside SEAM, dropping the build back to the DESIGN
211
+ * boundary; absent is a no-op.
212
+ */
213
+ export function clearStep(root, slug) {
214
+ rmSync(stepPath(root, slug), { force: true });
215
+ }
200
216
  /**
201
217
  * The build's `checkpoints` ledger: the recorded SHAs of landed plan and step
202
218
  * commits, one line each.
@@ -0,0 +1,122 @@
1
+ // `plumbbob abandon`: the third exit from an in-flight step. Checkpoint lands
2
+ // the step and revert destroys the work; abandon drops the attempt but keeps the
3
+ // working-tree diff exactly where it is. Its only writes are the in-flight
4
+ // control markers it clears (STEP, SEAM, TICK, and the step-scoped handoff
5
+ // ledger), a build-log line, and the stats receipt: it touches neither the
6
+ // working tree nor git nor the intent checkbox, so the step stays planned and
7
+ // re-buildable with its diff still in the tree for the human to keep, rework, or
8
+ // commit by hand.
9
+ //
10
+ // A step exit is a boundary crossing, so abandon honors the same approval latch
11
+ // as checkpoint. That is not decoration: abandon clears the entry stamp the
12
+ // latch reads, so an unlatched abandon followed by a same-turn checkpoint would
13
+ // hand that checkpoint an unlatched land, the same side door the settings-auto
14
+ // case closed. Every deletion routes through the sidecar helpers that own
15
+ // control-state clears; abandon itself never calls the raw fs deleters.
16
+ import { readFileSync, writeFileSync } from 'node:fs';
17
+ import { findRepoRoot } from "../lib/git.js";
18
+ import { bumpStepStat, buildLogPath, clearHandoff, clearSeam, clearStep, clearTick, hasSession, intentPath, resolveBuild, stepPath, } from "../lib/sidecar.js";
19
+ import { checkLatch } from "../lib/latch.js";
20
+ import { abandonLogLine, appendToSection } from "../lib/buildlog.js";
21
+ import { parseSteps } from "../lib/orient.js";
22
+ /**
23
+ * Drop the in-flight step while keeping its work: latch, clear the markers,
24
+ * record the event, and return to the boundary.
25
+ *
26
+ * Exit 1 on any refusal: no session, no step in flight, or a latched boundary
27
+ * (no human turn since the step began). Each refusal says what unblocks it.
28
+ */
29
+ export function abandon(cwd, args) {
30
+ const root = findRepoRoot(cwd);
31
+ if (root === null || !hasSession(root)) {
32
+ process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n');
33
+ return 1;
34
+ }
35
+ const { build: slug } = resolveBuild(root, args);
36
+ const step = readInFlightStep(root, slug);
37
+ if (step === null) {
38
+ process.stderr.write('plumbbob: no step in flight to abandon. abandon drops an in-flight step; from the boundary there is nothing to drop.\n');
39
+ return 1;
40
+ }
41
+ // The same latch checkpoint runs, keyed on this step: a step exit is a boundary
42
+ // crossing, and abandon clears the entry stamp, so an unlatched abandon would
43
+ // hand a same-turn checkpoint an unlatched land. Refusal is the pause, not an
44
+ // error: exit 1 and hand the turn back.
45
+ const latch = checkLatch(root, step);
46
+ if (!latch.allow) {
47
+ process.stderr.write(abandonPause(latch.reason, step));
48
+ return 1;
49
+ }
50
+ // Keep the work: clear only the in-flight control markers, never the tree,
51
+ // git, or the intent checkbox. The step stays [ ] and re-buildable.
52
+ clearStep(root, slug);
53
+ clearSeam(root, slug);
54
+ clearTick(root, slug);
55
+ clearHandoff(root, slug);
56
+ bumpStepStat(root, slug, step, 'abandons');
57
+ logAbandon(root, slug, step);
58
+ process.stdout.write(`plumbbob: step ${step} abandoned — work kept in the tree, back at the boundary. The step stays planned.\n`);
59
+ return 0;
60
+ }
61
+ /**
62
+ * The abandon-worded pause affordance.
63
+ *
64
+ * abandon runs checkpoint's latch, but the pause it hits is its own: the wording
65
+ * names the verb the human ran and the one self-approval that lands it, rather
66
+ * than sending them back to `checkpoint`. A `ceiling` reason (a range grant the
67
+ * step overran) keeps the range wording.
68
+ */
69
+ function abandonPause(reason, step) {
70
+ if (reason === 'ceiling') {
71
+ return `plumbbob: abandon refused — step ${step} is past the range you granted; pause here, then run it again to continue.\n`;
72
+ }
73
+ return `plumbbob: abandon refused — no human turn since this step began. A step exit crosses the same
74
+ boundary as a checkpoint, so abandon honors the same pause: end the turn, and the human's approval on
75
+ their next turn is what lets it land. (An explicit \`/plumbbob:build --auto\` or a step range in the
76
+ human's own prompt is the only self-approval.)
77
+ `;
78
+ }
79
+ /**
80
+ * The in-flight STEP number from the marker, or null when none is in flight.
81
+ */
82
+ function readInFlightStep(root, slug) {
83
+ try {
84
+ const raw = readFileSync(stepPath(root, slug), 'utf8').trim();
85
+ return /^\d+$/.test(raw) ? Number(raw) : null;
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ }
91
+ /**
92
+ * Append the abandon line to the build-log's `## Log`, the first non-checkpoint
93
+ * event the log records.
94
+ *
95
+ * Best-effort like every build-log write: a missing or odd log never fails an
96
+ * abandon, whose real record is the cleared markers and the stats bump.
97
+ */
98
+ function logAbandon(root, slug, step) {
99
+ try {
100
+ const path = buildLogPath(root, slug);
101
+ const date = new Date().toISOString().slice(0, 10);
102
+ const line = abandonLogLine(date, step, titleForStep(root, slug, step));
103
+ const updated = appendToSection(readFileSync(path, 'utf8'), 'Log', line);
104
+ if (updated !== null) {
105
+ writeFileSync(path, updated);
106
+ }
107
+ }
108
+ catch {
109
+ // best-effort ledger; never fail an abandon over the build-log.
110
+ }
111
+ }
112
+ /**
113
+ * The step's title from intent.md, or null when absent or unreadable.
114
+ */
115
+ function titleForStep(root, slug, step) {
116
+ try {
117
+ return parseSteps(readFileSync(intentPath(root, slug), 'utf8')).find((s) => s.n === step)?.title ?? null;
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
@@ -252,8 +252,11 @@ function narrowExcludes(root) {
252
252
  /**
253
253
  * Perform the migration and return a human-readable list of what moved.
254
254
  *
255
- * STAGES the result but never commits: the human owns that commit. Call only when
256
- * `inspectLegacy` reported a legacy layout; doctor prints the returned actions.
255
+ * STAGES the result but never commits: the human owns that commit. In a repo
256
+ * that gitignores the sidecar the stage is skipped (stagePath's record-only
257
+ * guard) and the report says so: the files move but stay untracked. Call only
258
+ * when `inspectLegacy` reported a legacy layout; doctor prints the returned
259
+ * actions.
257
260
  */
258
261
  export function migrateSidecar(root) {
259
262
  const dir = sidecarDir(root);
@@ -297,8 +300,10 @@ export function migrateSidecar(root) {
297
300
  }
298
301
  rmSync(archiveDir, { recursive: true, force: true });
299
302
  narrowExcludes(root);
300
- stagePath(root, '.plumbbob');
301
- actions.push('staged the move (builds/ + settings.json) — commit it yourself');
303
+ const staged = stagePath(root, '.plumbbob');
304
+ actions.push(staged
305
+ ? 'staged the move (builds/ + settings.json) — commit it yourself'
306
+ : 'moved but not staged (the sidecar is gitignored) — the files stay untracked');
302
307
  return actions;
303
308
  }
304
309
  /**
@@ -159,7 +159,7 @@ function phaseFindings(root, slug, stepInFlight) {
159
159
  name: 'step marker',
160
160
  ok: false,
161
161
  detail: `step ${marker} is in flight but the plan has no step ${marker} (planned: ${planned.join(', ')})`,
162
- hint: 'the plan was rewritten under the step — sharpen it with /plumbbob:step, or plumbbob revert to drop the step',
162
+ hint: 'the plan was rewritten under the step — sharpen it with /plumbbob:step, plumbbob revert to drop the step and its work, or plumbbob abandon to drop the step but keep the work',
163
163
  });
164
164
  }
165
165
  }
@@ -104,9 +104,9 @@ function readInFlightStep(root, buildSlug) {
104
104
  * Open a spike: one throwaway worktree + `spike/<slug>-<opt>` branch per option.
105
105
  *
106
106
  * Refuses when a spike is already open or a step is in flight: a spike is a
107
- * deliberate fork from a settled boundary, so the current step must checkpoint
108
- * or revert first. Marks the SPIKE control file and scaffolds the report before
109
- * returning.
107
+ * deliberate fork from a settled boundary, so the current step must exit first
108
+ * (checkpoint, abandon, or revert). Marks the SPIKE control file and scaffolds
109
+ * the report before returning.
110
110
  */
111
111
  function spikeStart(root, buildSlug, positionals) {
112
112
  if (inSpike(root, buildSlug)) {
@@ -115,7 +115,7 @@ function spikeStart(root, buildSlug, positionals) {
115
115
  }
116
116
  if (existsSync(stepPath(root, buildSlug))) {
117
117
  process.stderr.write('plumbbob: spike starts from a settled boundary, but a step is in flight. ' +
118
- 'A spike is a deliberate fork — checkpoint or revert the current step first.\n');
118
+ 'A spike is a deliberate fork — checkpoint, abandon, or revert the current step first.\n');
119
119
  return 1;
120
120
  }
121
121
  const slug = sanitize(positionals[0] ?? '');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "plumbbob",
3
- "version": "0.10.0",
4
- "description": "Attention-first build process: 12 skills + a CLI that keep you the decider — guidance on the work, a latch on the record.",
3
+ "version": "0.10.1",
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",
7
7
  "claude",
@@ -42,7 +42,8 @@
42
42
  "test": "vitest run",
43
43
  "eval:baseline": "pnpm build && node test/evals/run.ts --sweep baseline",
44
44
  "eval:latched": "pnpm build && node test/evals/run.ts --sweep latched",
45
- "eval:report": "node test/evals/run.ts --report"
45
+ "eval:report": "node test/evals/run.ts --report",
46
+ "size:prose": "node scripts/count-prose.ts"
46
47
  },
47
48
  "devDependencies": {
48
49
  "@ast-grep/cli": "0.45.1",
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: abandon
3
+ description: "Human-triggered driver for `plumbbob abandon`: drop the in-flight step while keeping its work in the tree, and return to the boundary. The step stays planned."
4
+ disable-model-invocation: true
5
+ model: haiku
6
+ allowed-tools: Bash(plumbbob status:*), Bash(plumbbob abandon:*)
7
+ ---
8
+
9
+ # PlumbBob: abandon the in-flight step (driver)
10
+
11
+ Current session state (injected when this skill runs): !`plumbbob status 2>/dev/null || 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."`
12
+
13
+ This is a **driver skill**: a chat-side trigger for the mechanical `plumbbob abandon` verb, so the whole workflow runs from the agent window instead of a terminal. It is `disable-model-invocation: true`: only the human fires it. It carries **no Edit and no Write tool**; its only action is to shell the verb and report the verb's output verbatim, including any refusal. The CLI is the source of truth: never retry a refused transition, and never edit a file to work around one.
14
+
15
+ Abandon is the third exit from an in-flight step. `/plumbbob:verify` lands the step and `/plumbbob:revert` destroys its work; abandon drops the attempt and keeps the working-tree diff exactly where it is. The step keeps its `[ ]` in the plan, re-buildable later, and the diff stays yours to keep, rework, or commit by hand.
16
+
17
+ ## What it does
18
+
19
+ 1. Run `plumbbob abandon` via Bash. It clears the in-flight markers (`STEP`, `SEAM`, `TICK`, and the step's agent handoff ledger), appends an abandon line to the build log, and touches neither the working tree nor git.
20
+ 2. Report the verb's output verbatim: the step it dropped, or any refusal. A step exit crosses the same boundary as a checkpoint, so abandon honors the same approval latch; a "no human turn since this step began" refusal is the pause working, not an error. End the turn, and the human's next turn is what lets a re-fired abandon land.
@@ -25,4 +25,4 @@ Reach for it when the dashboard and reality disagree: `status` shows a build you
25
25
 
26
26
  - **Spike leftovers are reported, never removed.** Those worktrees sit outside the repo and may hold the only copy of what a spike learned, so the verb prints the exact `git worktree remove` / `git branch -D` commands and stops. Relay them; do not run them yourself.
27
27
  - **A contradictory phase is yours to settle.** A spike and a step both marked in flight, or a step number the plan no longer contains, need a decision about which one is real; `--fix` deliberately leaves them alone.
28
- - **It is not a rewind.** Recovering reconciles bookkeeping; it never restores lost work. Discarding a half-done step is `/plumbbob:revert`, and that is a different, destructive move the human asks for by name.
28
+ - **It is not a rewind, and not an exit.** Recovering reconciles bookkeeping; it never restores lost work and never drops a step. Discarding a half-done step is `/plumbbob:revert` (destructive), and dropping one while keeping its work in the tree is `/plumbbob:abandon`; both are different moves the human asks for by name.
@@ -16,5 +16,5 @@ This is a **driver skill**: a chat-side trigger for the mechanical `plumbbob rev
16
16
  ## What it does
17
17
 
18
18
  1. Read an optional target step from the way you were invoked (for example `/plumbbob:revert --to 2` → step `2`). With no target, revert goes to the last done-checkpoint.
19
- 2. Run `plumbbob revert` (or `plumbbob revert --to <n>`) via Bash. This is a `git reset --hard`; it discards the current in-progress step. Run it exactly as the human asked; do not add or drop the `--to` on your own.
19
+ 2. Run `plumbbob revert` (or `plumbbob revert --to <n>`) via Bash. This is a `git reset --hard`; it discards the current in-progress step *and its work*. If the human wants to drop the step but keep the working-tree diff, that is `/plumbbob:abandon`, not revert. Run it exactly as the human asked; do not add or drop the `--to` on your own.
20
20
  3. Report the verb's output verbatim: which checkpoint it reset to, or any refusal.