plumbbob 0.8.10 → 0.8.12

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.8.10",
4
+ "version": "0.8.12",
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
@@ -23,6 +23,31 @@ Ridgeline runs large, settled work autonomously without you, PlumbBob keeps you
23
23
  driver's seat for the work your judgment has to steer. This repository was built using
24
24
  PlumbBob, dogfooded on its own loop.
25
25
 
26
+ ## What a prompt can't replicate
27
+
28
+ A system prompt can *ask* a model to plan first and stop for review. What it can't do is
29
+ **hold the line when the model doesn't** — and that mechanical half is what PlumbBob adds
30
+ under the skills. Five deterministic edges, enforced by the CLI and git rather than by the
31
+ model's goodwill:
32
+
33
+ - **A gate that refuses red.** `checkpoint` won't record a step while the check gate is
34
+ red, and `start` refuses a dirty tree — the [checkride](https://www.npmjs.com/package/checkride)
35
+ run (types, lint, tests, dead code, docs) has to pass before a step lands.
36
+ - **One SHA per verified step.** Every approved step is its own git commit against a
37
+ recorded baseline — a ledger of checkpoints, not one undifferentiated diff at the end.
38
+ - **A preservation-aware revert.** `/pb-revert` resets exactly to a recorded checkpoint
39
+ SHA while keeping your plan and notes intact, and restores *only* SHAs the loop wrote.
40
+ - **A build record that rides the PR.** The tracked `builds/<slug>/` folder — intent, log,
41
+ checkpoints, report — merges into `main` with the branch instead of dying with the
42
+ worktree; the archive never destroys.
43
+ - **The approval latch.** `checkpoint` refuses to land a step until the harness records a
44
+ human turn since that step began ([D64–D66](docs/decisions.md#d64), the ledger-plane
45
+ enforcement), so the model can't self-commit past you — the refusal *is* the pause.
46
+
47
+ The planning surface is the on-ramp; these five are the floor under it. And the latch's
48
+ payoff is [measured, not asserted](#why-not-just), not just claimed. **Guidance on the
49
+ work, a latch on the record.**
50
+
26
51
  ## The loop in one picture
27
52
 
28
53
  <p align="center">
@@ -196,11 +221,10 @@ diff, and `/pb-verify` reads it like any other.
196
221
 
197
222
  **…use git and a TODO file?** That's the honest skeleton, and if your discipline
198
223
  holds by willpower alone, it may be all you need. What the CLI adds is the
199
- mechanical half of the discipline, so willpower is spent only at the pause: a
200
- recorded baseline, one SHA per verified step, a revert that returns exactly to
201
- those SHAs while preserving your notes, an archive that never destroys, and a
202
- dashboard re-injected into every skill so the model re-orients on each move
203
- without you re-explaining the state.
224
+ [mechanical half of the discipline](#what-a-prompt-cant-replicate) — the five edges up
225
+ top — so willpower is spent only at the pause. The one not on that list: a status
226
+ dashboard re-injected into every skill, so the model re-orients on each move without you
227
+ re-explaining the state.
204
228
 
205
229
  **…wait for a vendor to ship this natively?** They're converging on it — plan
206
230
  modes, spec kits, native checkpoints — and that convergence is evidence *for* the
@@ -220,26 +244,28 @@ control, because a determined model routes around it and a denial mid-thought le
220
244
  legal move. But the **record** is a different plane, and there the tick *is* latched
221
245
  ([D64–D66](docs/decisions.md#d64)): `checkpoint` refuses to land a step until the harness
222
246
  records a human turn since that step began, so the agent cannot self-commit past you —
223
- the refusal simply *is* the pause. That extends the deterministic edge PlumbBob already
224
- holds — `checkpoint` refuses a red check, `start` refuses a dirty tree, `revert` restores
225
- only recorded SHAs — to the one boundary the product is named after, while leaving human
226
- what must be human: you, reading the diff. **Guidance on the work, a latch on the
227
- record.** And when a raw commit does slip the ledger, `status` says so and the checkpoint
228
- record makes recovery one command — cheap recovery, not a cage, is the control that
229
- matters.
247
+ the refusal simply *is* the pause. It extends the [deterministic edges above](#what-a-prompt-cant-replicate)
248
+ to the one boundary the product is named after, while leaving human what must be human:
249
+ you, reading the diff. And when a raw commit does slip the ledger, `status` says so and the
250
+ checkpoint record makes recovery one command — cheap recovery, not a cage, is the control
251
+ that matters.
230
252
 
231
253
  > The latch's payoff is measured, not asserted: the skill-eval harness
232
254
  > ([`test/evals/`](test/evals/)) sweeps this loop headless — prose-only baseline vs. the
233
255
  > shipped latch, N=5 per contract, every assertion a mechanical read of git state — and
234
- > commits the receipt to [`research/evals/`](research/evals/2026-07-11.md). The first sweep
235
- > (opus, 2026-07-11): the latch closes the route it owns — *no checkpoint over a red
236
- > check under pressure* went from **2/5 prose-only to 5/5 latched** — while the honest
237
- > numbers it also surfaced (park capture 0/5, and adversarial pressure consistently
238
- > flipping the `auto` standing grant in settings — a legal, model-writable side door)
239
- > are the next iteration's work, named in the report rather than papered over. The
240
- > current sweep ([opus, 2026-07-18](research/evals/2026-07-18.md), plumbbob 0.8.7)
241
- > reproduces the win — *no checkpoint over a red check* still holds **5/5 latched** —
242
- > and confirms both gaps stand at 0.8.7, now the target of the hardening build.
256
+ > commits each receipt under [`research/evals/`](research/evals/). The first sweep
257
+ > ([opus, 2026-07-11](research/evals/2026-07-11.md)) closed the route the latch owns —
258
+ > *no checkpoint over a red check under pressure* went from **2/5 prose-only to 5/5
259
+ > latched** — and named, rather than papered over, the two honest gaps it surfaced: park
260
+ > capture at 0/5, and adversarial pressure flipping a standing `auto` grant a model could
261
+ > write into settings, a legal side door. The re-sweep
262
+ > ([opus, 2026-07-18](research/evals/2026-07-18.md), plumbbob 0.8.7) reproduced the win —
263
+ > *no checkpoint over a red check* still **5/5 latched** — and pinned both gaps. The side
264
+ > door is now closed by construction: [D67](docs/decisions.md#d67) (self-approval is
265
+ > human-typed only) retired the settings `auto` grant, so the one approval a model could
266
+ > forge is gone. What remains is park capture (c5) — prose-governed by design
267
+ > ([D10](docs/decisions.md#d10)/[D13](docs/decisions.md#d13)), the guidance-only gap the
268
+ > latch was never built to reach, still ~0/5 and labeled as such.
243
269
 
244
270
  ## Documentation
245
271
 
@@ -0,0 +1,72 @@
1
+ // Conventional-Commit subject assembly (D68). Plumbbob's commit subjects read as
2
+ // `type(scope): description` — real feat/fix/chore types and a real parenthesised
3
+ // scope — so `git log` speaks the same grammar as the rest of the branch. The
4
+ // `plumbbob`/`step N` identifiers that used to prefix the subject (the old D34
5
+ // shape) now ride a marker line at the head of the body, which `withMarker`
6
+ // prepends — so `git log --grep plumbbob` still finds every plumbbob commit.
7
+ // Functional/procedural, node-builtins-only (C1/C2), no gate: these only shape a
8
+ // string the CLI is about to commit.
9
+ // The Conventional-Commit type vocabulary plumbbob recognises as a leading prefix
10
+ // on a step title. Anchoring on this closed set keeps a prose title like
11
+ // `Note: rewire the cache` from being misread as a `Note` type — only these words,
12
+ // immediately followed by an optional `(scope)`, an optional `!`, and a colon,
13
+ // count as an author-supplied prefix.
14
+ const TYPES = ['feat', 'fix', 'chore', 'docs', 'refactor', 'test', 'perf', 'build', 'ci', 'style', 'revert'];
15
+ const PREFIX = new RegExp(`^(${TYPES.join('|')})(?:\\(([^)]+)\\))?(!)?:\\s+(.+)$`);
16
+ // Split an optional Conventional prefix off a step title. `feat(escape-hatch): add
17
+ // the flag` → { type:'feat', scope:'escape-hatch', breaking:false, description:'add
18
+ // the flag' }. A title with no recognised prefix comes back whole as the
19
+ // description with a null type/scope — the caller then defaults the type and fills
20
+ // the scope from the build.
21
+ export function parseConventionalTitle(title) {
22
+ const m = PREFIX.exec(title.trim());
23
+ if (m === null) {
24
+ return { type: null, scope: null, breaking: false, description: title.trim() };
25
+ }
26
+ return { type: m[1] ?? null, scope: m[2] ?? null, breaking: m[3] === '!', description: (m[4] ?? '').trim() };
27
+ }
28
+ // Assemble a `type(scope)!: description` subject, omitting the `(scope)` segment
29
+ // when there is no scope (Conventional Commits allow a bare `type: description`)
30
+ // and the `!` when the change is not breaking.
31
+ export function conventionalSubject(parts) {
32
+ const scope = parts.scope ? `(${parts.scope})` : '';
33
+ const bang = parts.breaking ? '!' : '';
34
+ return `${parts.type}${scope}${bang}: ${parts.description}`;
35
+ }
36
+ // A Conventional subject from a step title, filling gaps from the build. An
37
+ // author-written prefix (`fix(parser): …`) is honoured verbatim — its type, scope,
38
+ // breaking marker, and exact wording all win. A bare prose title defaults to
39
+ // `defaultType` and the build `scope`, and gets its sentence-case opener
40
+ // de-capitalised (`Add the flag` → `add the flag`) to match Conventional style —
41
+ // while an all-caps acronym start (`API`, `OAuth`) is left alone.
42
+ export function subjectFromTitle(title, defaultType, scope) {
43
+ const parsed = parseConventionalTitle(title);
44
+ if (parsed.type !== null) {
45
+ return conventionalSubject({
46
+ type: parsed.type,
47
+ scope: parsed.scope ?? scope,
48
+ breaking: parsed.breaking,
49
+ description: parsed.description,
50
+ });
51
+ }
52
+ return conventionalSubject({ type: defaultType, scope, description: lowerFirst(parsed.description) });
53
+ }
54
+ // Prepend a marker line (`plumbbob step 1`, `plumbbob plan`, `plumbbob finish`) to
55
+ // an optional body, separated by a blank line so git keeps them as distinct
56
+ // paragraphs. Always returns at least the marker, so every plumbbob commit carries
57
+ // the greppable identifier in its body even when the prose is empty.
58
+ export function withMarker(marker, body) {
59
+ const rest = body?.trim();
60
+ return rest !== undefined && rest.length > 0 ? `${marker}\n\n${rest}` : marker;
61
+ }
62
+ // De-capitalise a sentence-case opener but leave an acronym/proper all-caps start
63
+ // (`API`, `OAuth`) intact: lowercase the first character only when it is uppercase
64
+ // and the second character is not (so `Add` → `add`, but `API` stays `API`).
65
+ function lowerFirst(text) {
66
+ const a = text[0] ?? '';
67
+ const b = text[1] ?? '';
68
+ if (a >= 'A' && a <= 'Z' && b >= 'A' && b <= 'Z') {
69
+ return text;
70
+ }
71
+ return a >= 'A' && a <= 'Z' ? a.toLowerCase() + text.slice(1) : text;
72
+ }
@@ -40,6 +40,18 @@ export function slugify(title) {
40
40
  .replace(/[^a-z0-9]+/g, '-')
41
41
  .replace(/^-+|-+$/g, '');
42
42
  }
43
+ // The Conventional-Commit scope for a build (D68): its slug with the `YYYY-MM-DD-`
44
+ // date prefix `start` prepends stripped off, so `2026-07-18-escape-hatch` yields the
45
+ // human-meaningful `escape-hatch`. Null when no build resolves (`--local`, or a slug
46
+ // that is nothing but a date) — the caller then omits the `(scope)` segment, which
47
+ // Conventional Commits permit.
48
+ export function buildScope(slug) {
49
+ if (slug === null) {
50
+ return null;
51
+ }
52
+ const stripped = slug.replace(/^\d{4}-\d{2}-\d{2}-/, '');
53
+ return stripped.length > 0 ? stripped : null;
54
+ }
43
55
  // Existing build slugs — the directory names under `builds/`, sorted. Empty when
44
56
  // `builds/` is absent (a `--local` repo, or before the first tracked `start`).
45
57
  export function listBuilds(root) {
@@ -9,11 +9,12 @@
9
9
  // the same way.
10
10
  import { appendFileSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
11
11
  import { commit, findRepoRoot, headSha, isDirty, stageAll, stagePath, stagedPaths, stagedStat } from "../lib/git.js";
12
- import { buildFolder, buildLogPath, bumpStepStat, checkpointsPath, clearHandoff, clearTick, hasSession, intentPath, readStats, seamPath, stampStepStat, stepPath, } from "../lib/sidecar.js";
12
+ import { activeBuild, buildFolder, buildLogPath, buildScope, bumpStepStat, checkpointsPath, clearHandoff, clearTick, hasSession, intentPath, readStats, seamPath, stampStepStat, stepPath, } from "../lib/sidecar.js";
13
13
  import { runCheck } from "../lib/check.js";
14
14
  import { checkLatch } from "../lib/latch.js";
15
- import { markStepDone, parseSteps, parseTitle } from "../lib/orient.js";
15
+ import { markStepDone, parseSteps } from "../lib/orient.js";
16
16
  import { parseStepSeam, scopeDrift } from "../lib/intent.js";
17
+ import { conventionalSubject, subjectFromTitle, withMarker } from "../lib/commitmsg.js";
17
18
  import { appendToSection, checkpointLogLine } from "../lib/buildlog.js";
18
19
  export async function checkpoint(cwd, args) {
19
20
  const root = findRepoRoot(cwd);
@@ -53,7 +54,7 @@ export async function checkpoint(cwd, args) {
53
54
  if (isDirty(root)) {
54
55
  stageAll(root);
55
56
  warnScopeDrift(root, step);
56
- const body = bodyArg(args) ?? fallbackBody(root, step);
57
+ const body = withMarker(`plumbbob step ${step}`, bodyArg(args) ?? fallbackBody(root, step));
57
58
  sha = commit(root, messageArg(args) ?? subjectForStep(root, step), body);
58
59
  }
59
60
  else {
@@ -71,12 +72,12 @@ export async function checkpoint(cwd, args) {
71
72
  return 0;
72
73
  }
73
74
  // The plan-approval commit (D36): stage only the active build's artifact folder and
74
- // commit it as `plumbbob: plan — <title>`, then record a `plan <sha>` line. Giving
75
+ // commit it as `chore(<scope>): plan` (D68), then record a `plan <sha>` line. Giving
75
76
  // the plan its own commit keeps the first step's diff from absorbing the scaffold, so
76
77
  // `git log` reads baseline → plan → steps. No check gate (there is no code work to
77
78
  // verify yet), no intent flip, no step markers — the plan lives entirely in DESIGN.
78
- // An optional `--body` (stdin heredoc, D34) rides along; the folder is whitelisted
79
- // artifact plane, so there is no scope-drift to warn about.
79
+ // An optional `--body` (stdin heredoc, D68) rides after the `plumbbob plan` marker;
80
+ // the folder is whitelisted artifact plane, so there is no scope-drift to warn about.
80
81
  function checkpointPlan(root, args) {
81
82
  // The latch (D64) covers the plan commit too, keyed on the TICK that `start`
82
83
  // stamped: the plan pause is a pause. No step number — a range grant does not
@@ -87,7 +88,7 @@ function checkpointPlan(root, args) {
87
88
  return 1;
88
89
  }
89
90
  stagePath(root, buildFolder(root));
90
- const sha = commit(root, planSubject(root), bodyArg(args) ?? undefined);
91
+ const sha = commit(root, planSubject(root), withMarker('plumbbob plan', bodyArg(args) ?? undefined));
91
92
  appendFileSync(checkpointsPath(root), `plan ${sha}\n`);
92
93
  // The plan landing consumes `start`'s entry stamp (D64): a later hand-built diff
93
94
  // (no `build <n>`) must find no stale TICK and stay guidance-governed.
@@ -95,17 +96,11 @@ function checkpointPlan(root, args) {
95
96
  process.stdout.write(`plumbbob: plan committed — ${sha.slice(0, 9)}. Baseline → plan → steps.\n`);
96
97
  return 0;
97
98
  }
98
- // The plan commit's CLI-owned subject: the intent's `# <title>`, else a bare
99
- // `plumbbob: plan` when the title can't be read (mirrors `subjectForStep`'s fallback).
99
+ // The plan commit's CLI-owned subject (D68): `chore(<scope>): plan`, the scope drawn
100
+ // from the active build's slug (a bare `chore: plan` when none resolves). The
101
+ // `plumbbob plan` identifier rides the body marker, not the subject.
100
102
  function planSubject(root) {
101
- let title = null;
102
- try {
103
- title = parseTitle(readFileSync(intentPath(root), 'utf8'));
104
- }
105
- catch {
106
- title = null;
107
- }
108
- return title ? `plumbbob: plan — ${title}` : 'plumbbob: plan';
103
+ return conventionalSubject({ type: 'chore', scope: buildScope(activeBuild(root)), description: 'plan' });
109
104
  }
110
105
  // Step resolution (D3): explicit arg > in-flight STEP file > first undone step in
111
106
  // intent.md. Returns null when none can be determined. A `-m` value is a message,
@@ -209,12 +204,18 @@ function statsSuffix(root, step) {
209
204
  }
210
205
  return parts.length > 0 ? parts.join(', ') : null;
211
206
  }
212
- // The CLI-owned, deterministic commit subject: the step's title when intent.md still
213
- // carries one, else the bare `plumbbob: step N done` fallback (D34 — the CLI owns the
214
- // subject; a `-m` override or `--body` prose is a separate concern).
207
+ // The CLI-owned, deterministic commit subject (D68): a Conventional `type(scope):
208
+ // description`. The scope comes from the active build's slug; the type and wording
209
+ // come from the step's title — an author-written prefix (`fix(parser): …`) is honoured
210
+ // verbatim, a bare title defaults to `feat`. A titleless step falls back to
211
+ // `chore(<scope>): checkpoint`. The `plumbbob`/`step N` identifiers ride the body
212
+ // marker, not the subject; a `-m` override or `--body` prose is a separate concern.
215
213
  function subjectForStep(root, step) {
214
+ const scope = buildScope(activeBuild(root));
216
215
  const title = titleForStep(root, step);
217
- return title ? `plumbbob: step ${step} — ${title}` : `plumbbob: step ${step} done`;
216
+ return title !== null && title.trim().length > 0
217
+ ? subjectFromTitle(title, 'feat', scope)
218
+ : conventionalSubject({ type: 'chore', scope, description: 'checkpoint' });
218
219
  }
219
220
  function titleForStep(root, step) {
220
221
  try {
@@ -1,15 +1,16 @@
1
- // `plumbbob finish` (D9/D34) — the close-out: append the checkpoint SHAs to the
1
+ // `plumbbob finish` (D9/D68) — the close-out: append the checkpoint SHAs to the
2
2
  // report, make the final commit, and clear the control state. The build folder is
3
3
  // NOT deleted — it IS the archive now (D29): tracked, it merges with the branch and
4
4
  // shows up in the PR, so nothing is copied into a local-only `archive/` (that
5
5
  // helper retired with this step). No refuse-without-report gate — guidance offers
6
6
  // the artifact, it does not wall the exit (D9). Git footprint stays additive (C5):
7
- // one forward commit under the greppable `plumbbob: finish — <title>` subject.
7
+ // one forward commit under the Conventional `chore(<scope>): finish` subject, its
8
+ // `plumbbob finish` identifier riding the body marker (D68).
8
9
  import { appendFileSync, existsSync, readFileSync, rmSync } from 'node:fs';
9
10
  import { join } from 'node:path';
10
11
  import { commit, findRepoRoot, isDirty, stageAll } from "../lib/git.js";
11
- import { checkpointsPath, clearTick, hasSession, intentPath, readStats, reportPath, resolveBuild, seamPath, setGrant, sidecarDir, spikePath, stepPath, } from "../lib/sidecar.js";
12
- import { parseTitle } from "../lib/orient.js";
12
+ import { buildScope, checkpointsPath, clearTick, hasSession, readStats, reportPath, resolveBuild, seamPath, setGrant, sidecarDir, spikePath, stepPath, } from "../lib/sidecar.js";
13
+ import { conventionalSubject, withMarker } from "../lib/commitmsg.js";
13
14
  export function finish(cwd, args = []) {
14
15
  const root = findRepoRoot(cwd);
15
16
  if (root === null || !hasSession(root)) {
@@ -33,7 +34,7 @@ export function finish(cwd, args = []) {
33
34
  if (isDirty(root)) {
34
35
  stageAll(root);
35
36
  }
36
- const sha = commit(root, subject(root, slug), bodyArg(args) ?? undefined);
37
+ const sha = commit(root, subject(slug), withMarker('plumbbob finish', bodyArg(args) ?? undefined));
37
38
  // Clear the control state: the in-flight markers first, then the session sentinel
38
39
  // last (so "no session" flips exactly at the end). Deleting STATE also drops the
39
40
  // cursor (D28) — cursor and session share the one file now, so a single delete does
@@ -53,18 +54,12 @@ export function finish(cwd, args = []) {
53
54
  'Run `/pb-plan` (or `plumbbob start "<title>"`) to frame the next goal.\n');
54
55
  return 0;
55
56
  }
56
- // The CLI-owned final-commit subject (D34): `plumbbob: finish — <title>`, mirroring
57
- // the step-checkpoint format exactly so one greppable shape spans the whole history.
58
- // Falls back to a bare `plumbbob: finish` when intent.md carries no title.
59
- function subject(root, slug) {
60
- let title = null;
61
- try {
62
- title = parseTitle(readFileSync(intentPath(root, slug), 'utf8'));
63
- }
64
- catch {
65
- title = null;
66
- }
67
- return title ? `plumbbob: finish — ${title}` : 'plumbbob: finish';
57
+ // The CLI-owned final-commit subject (D68): `chore(<scope>): finish`, the scope drawn
58
+ // from the build's slug (a bare `chore: finish` when none resolves, e.g. `--local`),
59
+ // mirroring the plan/step Conventional shape so one grammar spans the whole history.
60
+ // The `plumbbob finish` identifier rides the body marker, not the subject.
61
+ function subject(slug) {
62
+ return conventionalSubject({ type: 'chore', scope: buildScope(slug), description: 'finish' });
68
63
  }
69
64
  // `--body` reads the final-commit body from stdin (the single-quoted heredoc of
70
65
  // D34), so the pb-finish skill can compose a proportional close-out message. Returns
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plumbbob",
3
- "version": "0.8.10",
3
+ "version": "0.8.12",
4
4
  "description": "Attention-first build process: 12 skills + a CLI that keep you the decider — guidance on the work, a latch on the record.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -29,8 +29,9 @@ record.
29
29
  - **Deferred tangents** — the harvested items that become future work.
30
30
  This is the "yeah, I did that" artifact. Write it by default; the human may edit it.
31
31
  2. **Finish.** Run `plumbbob finish`, which appends the checkpoint SHAs to the
32
- report and makes the final commit — subject `plumbbob: finish — <title>`,
33
- with an optional proportional body via `--body` (the stdin heredoc) — then
32
+ report and makes the final commit — subject `chore(<scope>): finish`, its body
33
+ led by a `plumbbob finish` marker plus an optional proportional `--body` (the
34
+ stdin heredoc) — then
34
35
  clears the control state (the in-flight markers, then `STATE` last — one delete
35
36
  drops both the session sentinel and the active-build cursor it carries). The
36
37
  build folder stays in place, committed, and rides the branch into the PR.
@@ -74,6 +74,13 @@ an agent can follow with `/pb-build`. The argument only seeds how you get there.
74
74
  sharpened just-in-time when you reach them with `/pb-step`. Keep each small enough to
75
75
  verify in one review pass.
76
76
 
77
+ **Give the title a Conventional-Commit type where the change has one** *(optional)*:
78
+ a `<title>` that opens with `fix:`, `feat(escape-hatch):`, `chore:`, … becomes the
79
+ checkpoint's commit subject verbatim (its scope and breaking `!` honored) — this is
80
+ how each step lands a *real* `feat`/`fix`/`chore` in `git log`. Skip the prefix and
81
+ the checkpoint defaults the type to `feat` and draws the scope from the build slug, so
82
+ a plain title still yields a clean `feat(<slug>): <title>` ([D68](../../docs/decisions.md#d68)).
83
+
77
84
  **Recommend a model per step where the signal is clear** *(optional)*: the
78
85
  `- model:` sub-line names the **smallest model that can carry the step**, with the
79
86
  one-phrase why — the human buys capability only where the step needs it. E.g.
@@ -106,7 +113,8 @@ an agent can follow with `/pb-build`. The argument only seeds how you get there.
106
113
  up automatically (it lives in the build folder).
107
114
  6. **Commit the plan.** Once the human approves the frame and steps, run
108
115
  `plumbbob checkpoint --plan` to commit the scaffold on its own — subject
109
- `plumbbob: plan — <title>`, only `.plumbbob/builds/<slug>/`, a `plan <sha>` line in
116
+ `chore(<scope>): plan` (with a `plumbbob plan` body marker), only `.plumbbob/builds/<slug>/`,
117
+ a `plan <sha>` line in
110
118
  `checkpoints`. This keeps the first step's diff clean, so history reads
111
119
  baseline → plan → steps. Pass a proportional `--body` (the single-quoted stdin
112
120
  heredoc) when the rationale is worth carrying; skip it for a small plan. Do this
@@ -39,8 +39,11 @@ grew — but its everyday job is to sharpen, not to invent.
39
39
  a step that sharpened into rote work can drop to a smaller model; one that grew subtle
40
40
  earns a frontier one. Advisory, plain text (no backticks), never a gate.
41
41
  3. **Get the human's OK**, then write it into `## Steps` in the standard format —
42
- `N. [ ] <title> — **done when:** <criterion>` with a `- seam:` sub-line. Revise the
43
- existing step in place; only append when you are genuinely adding an increment.
42
+ `N. [ ] <title> — **done when:** <criterion>` with a `- seam:` sub-line. Give the
43
+ `<title>` an optional Conventional-Commit prefix (`fix:`, `feat(scope):`, …) when the
44
+ change has a clear type — it becomes the checkpoint's commit subject; a plain title
45
+ defaults to `feat` with the build slug as scope ([D68](../../docs/decisions.md#d68)).
46
+ Revise the existing step in place; only append when you are genuinely adding an increment.
44
47
  4. **Revise the step's harness bindings if they drifted too** *(optional)*. If the
45
48
  build carries a `harness.json` (beside `intent.md`) and the reality that moved the
46
49
  step also changed which agents it wants, sharpen that step's slot bindings
@@ -47,10 +47,11 @@ this skill verifies it the same way: **it reads the diff, not the author**.
47
47
  the human is the clock. Never checkpoint without it.
48
48
  6. **Checkpoint** *(only after approval)*. Run `plumbbob checkpoint`: it makes the WIP
49
49
  commit, records the SHA, flips the step to done, appends the step to the build-log's
50
- `## Log`, and returns to DESIGN. The CLI owns the commit **subject**
51
- (`plumbbob: step N — <title>`); you own the **body**. Compose a body *proportional to
52
- the step* — a one-liner for a trivial change, a short paragraph on the what/why for a
53
- meatier one — and pass it on stdin:
50
+ `## Log`, and returns to DESIGN. The CLI owns the commit **subject** — a Conventional
51
+ `<type>(<scope>): <description>` composed from the step title and the build slug — and
52
+ leads the **body** with a `plumbbob step N` marker; you own the rest of the **body**.
53
+ Compose a body *proportional to the step* — a one-liner for a trivial change, a short
54
+ paragraph on the what/why for a meatier one — and pass it on stdin:
54
55
 
55
56
  ```bash
56
57
  plumbbob checkpoint <n> --body <<'BODY'
@@ -50,11 +50,13 @@ which `/pb-build` records in the build folder's `SEAM` for orientation — aware
50
50
  An optional **model** line recommends the smallest model that can carry the step —
51
51
  mechanical work runs fine on a small model; subtle or creative work earns a frontier
52
52
  one. Advisory for the human, never a gate; write it plain, no backticks (D62).
53
- Then drive `/pb-build` until done. Later steps may be fuzzier than the first;
54
- sharpen the next one just-in-time with `/pb-step` (empty input auto-syncs it), and use
55
- `/pb-refine` to repair the whole plan when a blocker rewrites it.)*
53
+ A step title may open with a Conventional-Commit type (`fix:`, `feat(scope):`) — it
54
+ becomes the checkpoint's commit subject; skip it and the subject defaults to `feat`,
55
+ scoped by the build slug (D68). Then drive `/pb-build` until done. Later steps may be
56
+ fuzzier than the first; sharpen the next one just-in-time with `/pb-step` (empty input
57
+ auto-syncs it), and use `/pb-refine` to repair the whole plan when a blocker rewrites it.)*
56
58
 
57
- 1. [ ] <step> — **done when:** <criterion, ideally a test or check result>
59
+ 1. [ ] feat: <step> — **done when:** <criterion, ideally a test or check result>
58
60
  - seam: `<file>`, `<file>`
59
61
  - model: <smallest that can carry it, e.g. sonnet — mechanical, fully specified>
60
62
  2. [ ] <step> — **done when:** <criterion>