plumbbob 0.8.13 → 0.8.14

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.13",
4
+ "version": "0.8.14",
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",
@@ -149,6 +149,26 @@ export function parseBuildTitle(content) {
149
149
  }
150
150
  return '';
151
151
  }
152
+ const SCOPE_HEADER = /^\*\*Scope:\*\*\s*(.*)$/;
153
+ const SCOPE_PLACEHOLDER = /^<.*>$/;
154
+ // The build's default Conventional-Commit scope (D4): the `**Scope:**` header
155
+ // field authored once at plan time, its value read verbatim off the line with any
156
+ // trailing `<!-- ... -->` guidance note stripped. An unfilled placeholder — an
157
+ // empty value or an angle-bracket placeholder like `<feature>` — parses as absent
158
+ // (D7), so a freshly scaffolded, not-yet-edited build falls through to the next
159
+ // scope rung (the build slug) rather than landing commits scoped `(<feature>)`.
160
+ // null when the header is absent entirely — the pre-D4 shape (C2 back-compat).
161
+ export function parseBuildScope(content) {
162
+ for (const line of content.split('\n')) {
163
+ const m = SCOPE_HEADER.exec(line.trim());
164
+ if (m === null) {
165
+ continue;
166
+ }
167
+ const value = (m[1] ?? '').split('<!--')[0]?.trim() ?? '';
168
+ return value === '' || SCOPE_PLACEHOLDER.test(value) ? null : value;
169
+ }
170
+ return null;
171
+ }
152
172
  export function scrapeBullets(content, heading) {
153
173
  const lines = content.split('\n');
154
174
  const start = lines.findIndex((l) => l.trim() === heading);
@@ -73,10 +73,11 @@ export function markStepDone(intent, n) {
73
73
  })
74
74
  .join('\n');
75
75
  }
76
- // Open questions still open: `- Q\d+:` lines that do not say "resolved".
76
+ // Open questions still open: `- Q\d+:` openers that do not say "resolved". The
77
+ // opener may carry a slug-at-birth gloss — `- Q2 (some-slug): ...` (D3) — which
78
+ // the count reads through; sub-lines (`*plain:*`/`*lean:*`) never match.
77
79
  export function parseOpenQuestions(intent) {
78
- return sectionLines(intent, '## Open questions').filter((l) => /^- Q\d+:/.test(l.trim()) && !/resolved/i.test(l))
79
- .length;
80
+ return sectionLines(intent, '## Open questions').filter((l) => /^- Q\d+(?: \([^)]+\))?:/.test(l.trim()) && !/resolved/i.test(l)).length;
80
81
  }
81
82
  // Open parked items: `- [ ]` lines under `## Park list` (the `park` verb's format).
82
83
  // A harvested item is flipped to `- [x]` by `/pb-harvest` and no longer counts; the
@@ -13,7 +13,7 @@ import { activeBuild, buildFolder, buildLogPath, buildScope, bumpStepStat, check
13
13
  import { runCheck } from "../lib/check.js";
14
14
  import { checkLatch } from "../lib/latch.js";
15
15
  import { markStepDone, parseSteps } from "../lib/orient.js";
16
- import { parseStepSeam, scopeDrift } from "../lib/intent.js";
16
+ import { parseBuildScope, parseStepSeam, scopeDrift } from "../lib/intent.js";
17
17
  import { conventionalSubject, subjectFromTitle, withMarker } from "../lib/commitmsg.js";
18
18
  import { appendToSection, checkpointLogLine } from "../lib/buildlog.js";
19
19
  import { AT_BOUNDARY, syncBuildLogState } from "../lib/buildlogsync.js";
@@ -101,11 +101,12 @@ function checkpointPlan(root, args) {
101
101
  process.stdout.write(`plumbbob: plan committed — ${sha.slice(0, 9)}. Baseline → plan → steps.\n`);
102
102
  return 0;
103
103
  }
104
- // The plan commit's CLI-owned subject (D68): `chore(<scope>): plan`, the scope drawn
105
- // from the active build's slug (a bare `chore: plan` when none resolves). The
106
- // `plumbbob plan` identifier rides the body marker, not the subject.
104
+ // The plan commit's CLI-owned subject (D68): `chore(<scope>): plan`, the scope
105
+ // resolved through the build-default fallback chain (a bare `chore: plan` when
106
+ // none resolves). The `plumbbob plan` identifier rides the body marker, not the
107
+ // subject.
107
108
  function planSubject(root) {
108
- return conventionalSubject({ type: 'chore', scope: buildScope(activeBuild(root)), description: 'plan' });
109
+ return conventionalSubject({ type: 'chore', scope: buildDefaultScope(root), description: 'plan' });
109
110
  }
110
111
  // Step resolution (D3): explicit arg > in-flight STEP file > first undone step in
111
112
  // intent.md. Returns null when none can be determined. A `-m` value is a message,
@@ -210,18 +211,36 @@ function statsSuffix(root, step) {
210
211
  return parts.length > 0 ? parts.join(', ') : null;
211
212
  }
212
213
  // The CLI-owned, deterministic commit subject (D68): a Conventional `type(scope):
213
- // description`. The scope comes from the active build's slug; the type and wording
214
- // come from the step's title — an author-written prefix (`fix(parser): …`) is honoured
215
- // verbatim, a bare title defaults to `feat`. A titleless step falls back to
216
- // `chore(<scope>): checkpoint`. The `plumbbob`/`step N` identifiers ride the body
217
- // marker, not the subject; a `-m` override or `--body` prose is a separate concern.
214
+ // description`. The type and wording come from the step's title — an author-written
215
+ // prefix (`fix(parser): …`) is honoured verbatim, a bare title defaults to `feat`.
216
+ // The scope falls back through the chain (D3): the title's own `(scope)` wins
217
+ // (handled inside `subjectFromTitle`), else `buildDefaultScope`. A titleless step
218
+ // falls back to `chore(<scope>): checkpoint`. The `plumbbob`/`step N` identifiers
219
+ // ride the body marker, not the subject; a `-m` override or `--body` prose is a
220
+ // separate concern.
218
221
  function subjectForStep(root, step) {
219
- const scope = buildScope(activeBuild(root));
222
+ const scope = buildDefaultScope(root);
220
223
  const title = titleForStep(root, step);
221
224
  return title !== null && title.trim().length > 0
222
225
  ? subjectFromTitle(title, 'feat', scope)
223
226
  : conventionalSubject({ type: 'chore', scope, description: 'checkpoint' });
224
227
  }
228
+ // The build-default Conventional scope (D3/D4): the `**Scope:**` header field in
229
+ // intent.md when authored and filled, else the build slug (D68's original rung,
230
+ // now the penultimate one), else null — a bare subject (C2 back-compat: a build
231
+ // with neither field behaves exactly as before D4).
232
+ function buildDefaultScope(root) {
233
+ try {
234
+ const fromHeader = parseBuildScope(readFileSync(intentPath(root), 'utf8'));
235
+ if (fromHeader !== null) {
236
+ return fromHeader;
237
+ }
238
+ }
239
+ catch {
240
+ // no intent.md yet — fall through to the slug rung.
241
+ }
242
+ return buildScope(activeBuild(root));
243
+ }
225
244
  function titleForStep(root, step) {
226
245
  try {
227
246
  return parseSteps(readFileSync(intentPath(root), 'utf8')).find((s) => s.n === step)?.title ?? null;
@@ -9,8 +9,9 @@
9
9
  import { appendFileSync, existsSync, readFileSync, rmSync } from 'node:fs';
10
10
  import { join } from 'node:path';
11
11
  import { commit, findRepoRoot, isDirty, stageAll } from "../lib/git.js";
12
- import { buildScope, checkpointsPath, clearTick, hasSession, readStats, reportPath, resolveBuild, seamPath, setGrant, sidecarDir, spikePath, stepPath, } from "../lib/sidecar.js";
12
+ import { buildScope, checkpointsPath, clearTick, hasSession, intentPath, readStats, reportPath, resolveBuild, seamPath, setGrant, sidecarDir, spikePath, stepPath, } from "../lib/sidecar.js";
13
13
  import { conventionalSubject, withMarker } from "../lib/commitmsg.js";
14
+ import { parseBuildScope } from "../lib/intent.js";
14
15
  export function finish(cwd, args = []) {
15
16
  const root = findRepoRoot(cwd);
16
17
  if (root === null || !hasSession(root)) {
@@ -34,7 +35,7 @@ export function finish(cwd, args = []) {
34
35
  if (isDirty(root)) {
35
36
  stageAll(root);
36
37
  }
37
- const sha = commit(root, subject(slug), withMarker('plumbbob finish', bodyArg(args) ?? undefined));
38
+ const sha = commit(root, subject(root, slug), withMarker('plumbbob finish', bodyArg(args) ?? undefined));
38
39
  // Clear the control state: the in-flight markers first, then the session sentinel
39
40
  // last (so "no session" flips exactly at the end). Deleting STATE also drops the
40
41
  // cursor (D28) — cursor and session share the one file now, so a single delete does
@@ -54,12 +55,27 @@ export function finish(cwd, args = []) {
54
55
  'Run `/pb-plan` (or `plumbbob start "<title>"`) to frame the next goal.\n');
55
56
  return 0;
56
57
  }
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' });
58
+ // The CLI-owned final-commit subject (D68): `chore(<scope>): finish`, the scope
59
+ // resolved through the same build-default fallback chain as plan/step subjects
60
+ // (D3/D4): the `**Scope:**` header field, else the build slug, else bare (e.g.
61
+ // `--local`). The `plumbbob finish` identifier rides the body marker, not the
62
+ // subject.
63
+ function subject(root, slug) {
64
+ return conventionalSubject({ type: 'chore', scope: buildDefaultScope(root, slug), description: 'finish' });
65
+ }
66
+ // See checkpoint.ts's twin of this helper (same fallback, this verb's own `slug`
67
+ // resolution instead of the active-build cursor).
68
+ function buildDefaultScope(root, slug) {
69
+ try {
70
+ const fromHeader = parseBuildScope(readFileSync(intentPath(root, slug), 'utf8'));
71
+ if (fromHeader !== null) {
72
+ return fromHeader;
73
+ }
74
+ }
75
+ catch {
76
+ // no intent.md — fall through to the slug rung.
77
+ }
78
+ return buildScope(slug);
63
79
  }
64
80
  // `--body` reads the final-commit body from stdin (the single-quoted heredoc of
65
81
  // 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.13",
3
+ "version": "0.8.14",
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",
@@ -74,6 +74,13 @@ differs from the model you're running as, say so before implementing — the hum
74
74
  build-log's `## Log`, so the history writes itself — you only supply the body (or omit
75
75
  `--body` for the deterministic done-when + seam + diffstat fallback). Do **not** bump
76
76
  the version or changelog — that is the human's `/version` call.
77
+ - **A drifted subject is reconciled at the pause, never silently (D5/D6).** The planned
78
+ title *is* the checkpoint subject (D1). If the diff drifted from it, **present** a
79
+ corrected subject at the pause — `planned title → proposed subject`, one line — for
80
+ explicit approval, and only on approval land it by adding `-m "<subject>"` to the
81
+ checkpoint. Present nothing and the deterministic title-derived subject lands
82
+ untouched: the reconcile is the human-approved exception, not a quiet `-m` swap (which
83
+ is the agent-authored subject D68 refuses).
77
84
  - **If `plumbbob status` shows a `harness bindings:` block**, fold any bound
78
85
  `after`-agents into that self-review as advisory input — see **§ Running bound
79
86
  agents**.
@@ -25,8 +25,9 @@ is fine; capture is always available, which is the whole point of parking.
25
25
  ## What this skill does
26
26
 
27
27
  Take the idea, problem, or "ooh what if" the human just had and **compose it into one
28
- tidy, tagged line** — short, legible, self-contained, so it still reads cleanly weeks
29
- later. Then:
28
+ tidy, tagged line** — short, legible, self-contained, and carrying a **one-clause why**
29
+ (what it is *and* why it's worth revisiting), so it still reads cleanly weeks later when
30
+ harvest triages it cold. Then:
30
31
 
31
32
  1. **Show the composed line to the human** and wait for **in-turn approval** — they
32
33
  confirm it as-is or edit the wording.
@@ -58,13 +58,23 @@ an agent can follow with `/pb-build`. The argument only seeds how you get there.
58
58
  **explicitly NOT doing**. This is the human's convergence — propose wording, but
59
59
  the human decides every line.
60
60
  3. **Decisions & Constraints.** Record the settled calls (one line each, with the
61
- *because*) and the hard rules the build must honor. An unresolved hole goes to
62
- **Open questions**, never guessed into a Decision.
61
+ *because*) and the hard rules the build must honor. **Slug each at birth** — mint a
62
+ two- or three-word slug where the item is born (`D1 (default-waves): …`,
63
+ `C1 (no-new-deps): …`) and copy that slug at every reference site, so a later
64
+ mention reads `D4 (default-waves)` and stays legible: **never a bare `D4`/`C6`/`Q2`
65
+ reference — always carry its gloss**, so a cold reader never has to hunt the number
66
+ down. An unresolved hole goes to **Open questions**, never guessed into a Decision —
67
+ and it goes in the expanded form the template shows: a slugged opener (the hole as a
68
+ question), a `*plain:*` sub-line (what's at stake, in plain words, enough to judge it
69
+ cold), then a `*lean:*` sub-line (your proposed resolution — one answer to react to,
70
+ not a menu), so the human settles it in one pass without a round-trip. (A tiny,
71
+ obvious question may stay one bare line; the sub-lines earn their keep only when a
72
+ human decision genuinely waits on it.)
63
73
  4. **Author the Steps.** Write the **full build plan** under `## Steps` — each step a
64
74
  small, verifiable increment in the exact format the parser reads:
65
75
 
66
76
  ```markdown
67
- 1. [ ] <title> — **done when:** <criterion, ideally a test or check result>
77
+ 1. [ ] feat(scope): <title> — **done when:** <criterion, ideally a test or check result>
68
78
  - seam: `<file>`, `<file>`
69
79
  - model: <optional — smallest that can carry it, with the one-phrase why>
70
80
  ```
@@ -74,12 +84,23 @@ an agent can follow with `/pb-build`. The argument only seeds how you get there.
74
84
  sharpened just-in-time when you reach them with `/pb-step`. Keep each small enough to
75
85
  verify in one review pass.
76
86
 
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)).
87
+ **The title *is* the commit subject — author it as one** *(D1)*: write each
88
+ `<title>` as a plain, single-line Conventional-Commit subject,
89
+ `type(scope): description`, and it lands in `git log` verbatim (its breaking `!`
90
+ honored) — this is how each step lands a *real* `feat`/`fix`/`chore` in the history.
91
+ Keep load-bearing detail — file paths, module names — in `seam` and `done-when`; it is
92
+ **never jammed into the title** (D2), which has to read as plain English and as a clean
93
+ subject at once. Aim for **≤72 characters** (GitHub's subject convention) — soft
94
+ guidance you eyeball at plan time, no lint, no gate (D9).
95
+
96
+ **Set the build's default scope, then let steps override it** *(D8)*: fill the
97
+ `**Scope:**` header once with a short feature-level name — the catch-all every step
98
+ inherits — and give a step its own `(scope)` when it touches a distinct code area
99
+ (`pb-plan`, `commitmsg`, `docs`). A step's `(scope)` names the *code area*; the header
100
+ names the *feature*, so scopes stay consistent and greppable across builds. Scope
101
+ resolves title-scope → `**Scope:**` default → build slug → bare, and type resolves
102
+ title-type → `feat`, so a scopeless plain title still yields a clean
103
+ `feat(<slug>): <title>` unchanged ([D68](../../docs/decisions.md#d68), D3).
83
104
 
84
105
  **Recommend a model per step where the signal is clear** *(optional)*: the
85
106
  `- model:` sub-line names the **smallest model that can carry the step**, with the
@@ -27,13 +27,28 @@ is fine — refining is always available.
27
27
  - **Attack (diverge in the problem space).** Hand the **Frame**, **Decisions**, and
28
28
  **Architecture sketch** a cold, adversarial read and surface holes — ambiguities,
29
29
  unhandled edge cases, hidden assumptions, collisions with the existing code. Append
30
- each as a one-line question under `## Open questions`, **never** as a Decision:
31
- resolving a hole is the human's convergence, not yours. This mode surfaces; it does
32
- not decide.
30
+ each under `## Open questions` in the expanded form the template shows — an opener
31
+ line (the hole as a question, slugged at birth: `- Q2 (some-slug): …`), a `*plain:*`
32
+ sub-line (what's at stake in plain words, enough to judge it cold), and a `*lean:*`
33
+ sub-line (your proposed resolution — one answer to react to, not a menu). Never
34
+ append a hole as a Decision: resolving it is the human's convergence, not yours —
35
+ the lean proposes; this mode surfaces, it does not decide. (A tiny, obvious question
36
+ may stay one bare line; the sub-lines earn their keep only when a human decision
37
+ genuinely waits on it.)
38
+
39
+ Then walk the human through it in the chat: list every hole you appended, each as
40
+ its explanation first and its lean second, and invite per-question answers — the
41
+ human can settle any of them right there in one message, no doc-flipping, no extra
42
+ round-trip per hole.
33
43
  - **Repair (re-sync to reality).** When the plan has drifted — a Decision was overtaken
34
- by what you built, a Constraint changed, a Step no longer matches the seam — propose
35
- the edits that bring `intent.md` back in line with the truth. Show the before/after for
36
- each, and **write only what the human approves**.
44
+ by what you built, a Constraint changed, a Step no longer matches the seam, or a
45
+ **step's title no longer describes what the diff actually does** — propose the edits
46
+ that bring `intent.md` back in line with the truth. A step title *is* its checkpoint
47
+ commit subject (D1), so bringing a drifted one back in line means keeping it a plain,
48
+ single-line `type(scope): description` subject, with load-bearing detail (file paths,
49
+ module names) in `seam` and `done-when`, **never jammed into the title** (D2) — the
50
+ checkpoint body pass then reconciles and lands that subject at the verify pause (D5).
51
+ Show the before/after for each, and **write only what the human approves**.
37
52
 
38
53
  ## The hard contracts
39
54
 
@@ -39,11 +39,16 @@ 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. 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.
42
+ `N. [ ] <title> — **done when:** <criterion>` with a `- seam:` sub-line. Keep the
43
+ sharpened `<title>` a plain, single-line Conventional-Commit subject,
44
+ `type(scope): description` — it lands in `git log` verbatim, so as the step tightens
45
+ keep load-bearing detail (file paths, module names) in `seam` and `done-when`,
46
+ **never jammed into the title** (D1/D2). A sharpened step carries its own `(scope)`
47
+ when it touches a distinct code area — one that **overrides the build-default**
48
+ `**Scope:**` header; drop the scope and it falls back to that default → build slug →
49
+ bare, and the type to `feat` ([D68](../../docs/decisions.md#d68), D8). Aim for a soft
50
+ ≤72 chars, no gate (D9). Revise the existing step in place; only append when you are
51
+ genuinely adding an increment.
47
52
  4. **Revise the step's harness bindings if they drifted too** *(optional)*. If the
48
53
  build carries a `harness.json` (beside `intent.md`) and the reality that moved the
49
54
  step also changed which agents it wants, sharpen that step's slot bindings
@@ -45,6 +45,14 @@ this skill verifies it the same way: **it reads the diff, not the author**.
45
45
  findings), and the validation, then
46
46
  **stop and wait for the human's explicit approval.** This is the convergence beat;
47
47
  the human is the clock. Never checkpoint without it.
48
+ - **Reconcile a drifted subject here, in the open (D5).** The planned title *is* the
49
+ checkpoint subject (D1). If the diff drifted from it — the step landed something the
50
+ title no longer describes — the body pass may propose a corrected subject, but it
51
+ **presents** it at this pause for explicit approval: show `planned title → proposed
52
+ subject`, one line, as part of what the human OKs. This is the exception, not the
53
+ default (D6): with **nothing presented**, the deterministic title-derived subject
54
+ lands untouched. A silent `-m` swap is exactly the agent-authored subject D68 refuses
55
+ — so a reconcile is *only ever* the visible, approved kind.
48
56
  6. **Checkpoint** *(only after approval)*. Run `plumbbob checkpoint`: it makes the WIP
49
57
  commit, records the SHA, flips the step to done, appends the step to the build-log's
50
58
  `## Log`, and returns to DESIGN. The CLI owns the commit **subject** — a Conventional
@@ -62,7 +70,10 @@ this skill verifies it the same way: **it reads the diff, not the author**.
62
70
  Do **not** run a TIL scan or reach for a separate commit skill — the body is yours to
63
71
  write in one breath. Omit `--body` entirely and the CLI writes a deterministic body
64
72
  (done-when + seam + diffstat) on its own. Either way, do **not** bump the version or
65
- touch the changelog — that is the human's `/version` call.
73
+ touch the changelog — that is the human's `/version` call. And if step 5 surfaced a
74
+ drifted subject the human approved, land *that* subject by adding `-m "<subject>"` —
75
+ overriding the title-derived default for this one commit; unpresented, the CLI's
76
+ deterministic subject stands.
66
77
  7. **Hand off with the next model** *(once the checkpoint lands)*. `plumbbob checkpoint`
67
78
  returns to DESIGN; then run `plumbbob handoff` and relay its boundary block. With the
68
79
  step gone from in-flight it renders `step N checkpointed — back at the boundary` and
@@ -5,12 +5,18 @@ head; the chat is the hand. When the model floods you, read this, not your memor
5
5
  Size to the work: a small change fills Frame + a couple of Decisions and deletes
6
6
  the rest; a medium feature fills it all. Opinionated where decided, explicit where
7
7
  open. If the implementor (you-later, or the LLM) has to guess, the doc failed.
8
+
9
+ One rule runs through the whole doc: compress what's settled; expand what's pending.
10
+ Decisions and Constraints stay one line each — they re-inject into every build step,
11
+ so their tokens recur. Open questions expand into plain-then-lean prose — a human
12
+ reads them once, to decide, and that legibility buys back a chat round-trip.
8
13
  -->
9
14
 
10
15
  # {{TITLE}}
11
16
 
12
17
  **Phase** (your own bookkeeping while framing): frame
13
18
  **Size:** tiny | small | medium
19
+ **Scope:** <short-scope> <!-- the build's default commit scope (D4): a short feature-level name each step's own `(scope)` overrides. Leave the `<…>` placeholder (or blank) and it parses as absent — commits fall through to the build slug (D7). -->
14
20
 
15
21
  ## Frame
16
22
 
@@ -31,16 +37,20 @@ open. If the implementor (you-later, or the LLM) has to guess, the doc failed.
31
37
 
32
38
  ## Decisions
33
39
 
34
- *(One line each. Settled, not re-litigated in the chat. Grows as you resolve the
35
- holes `/pb-refine` surfaces, and as blockers fold in during BUILD.)*
40
+ *(One line each — settled, not re-litigated in the chat. Grows as you resolve the
41
+ holes `/pb-refine` surfaces, and as blockers fold in during BUILD. Mint a two- or
42
+ three-word slug where each item is born — `D1 (slug-here)` — and copy that slug at
43
+ every reference site, so a later `D4 (default-waves)` never decays into a bare `D4`
44
+ a cold reader has to hunt down.)*
36
45
 
37
- - D1: <decision> — *because* <the one reason that mattered>
46
+ - D1 (slug-here): <decision> — *because* <the one reason that mattered>
38
47
 
39
48
  ## Constraints
40
49
 
41
- *(Hard rules the build must honor. `/pb-verify` and `/pb-refine` read against these.)*
50
+ *(Hard rules the build must honor. `/pb-verify` and `/pb-refine` read against these.
51
+ Slug and gloss them at birth too — `C1 (no-new-deps)` — same one line, same rule.)*
42
52
 
43
- - C1: <e.g. functional/procedural only; no new dependencies>
53
+ - C1 (no-new-deps): <e.g. functional/procedural only; no new dependencies>
44
54
 
45
55
  ## Steps
46
56
 
@@ -50,9 +60,13 @@ which `/pb-build` records in the build folder's `SEAM` for orientation — aware
50
60
  An optional **model** line recommends the smallest model that can carry the step —
51
61
  mechanical work runs fine on a small model; subtle or creative work earns a frontier
52
62
  one. Advisory for the human, never a gate; write it plain, no backticks (D62).
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
63
+ The step title *is* the checkpoint's commit subject: write it as one plain line,
64
+ `type(scope): description` — load-bearing paths live in `seam`, never jammed into the
65
+ title (D2) — aiming for ≤72 characters (GitHub's subject convention: soft, no gate, D9).
66
+ The `(scope)` names the primary code area the step touches (`pb-plan`, `commitmsg`); the
67
+ build's `**Scope:**` header is the feature-level catch-all a step overrides (D8). Scope
68
+ resolves title-scope → `**Scope:**` default → build slug → bare, and type resolves
69
+ title-type → `feat` (D3/D68). Then drive `/pb-build` until done. Later steps may be
56
70
  fuzzier than the first; sharpen the next one just-in-time with `/pb-step` (empty input
57
71
  auto-syncs it), and use `/pb-refine` to repair the whole plan when a blocker rewrites it.)*
58
72
 
@@ -64,10 +78,40 @@ auto-syncs it), and use `/pb-refine` to repair the whole plan when a blocker rew
64
78
 
65
79
  ## Open questions
66
80
 
67
- *(Holes you could NOT resolve on paper. Do not guess them into Decisions. A genuine
68
- fork goes to a SPIKE; record the verdict below and in Decisions.)*
81
+ *(Holes you could NOT resolve on paper — the one section that expands rather than
82
+ compresses. Do not guess them into Decisions; a genuine fork goes to a SPIKE, with
83
+ the verdict recorded below and in Decisions.)*
84
+
85
+ <!--
86
+ Write each open question so a cold reader could answer it without flipping back to
87
+ the code or the chat — that cold-reader test is the whole point of expanding it.
88
+ Three lines:
89
+
90
+ · the opener — the hole as a question, slugged at birth, tagged
91
+ *resolve by:* decide | spike | ask
92
+ · *plain:* — what's at stake, in plain words: the context and the cost of getting
93
+ it wrong, enough to judge it cold
94
+ · *lean:* — the model's proposed resolution: one answer to react to, not a menu
95
+
96
+ The human reads *plain* to understand, then *lean* to approve or redirect — one
97
+ pass, no round-trip.
98
+
99
+ When a question resolves, swap *resolve by:* for *resolved:* <date, the call> ON THE
100
+ OPENER LINE, e.g.
101
+
102
+ - Q3 (default-waves): *resolved:* 2026-07-18, default off
103
+
104
+ The status counter reads opener lines only, so a *resolved:* left on a *plain:* or
105
+ *lean:* sub-line leaves the question still counted as open.
106
+
107
+ Size to the work: a tiny build's single, obvious question may stay one bare line —
108
+ the *plain:*/*lean:* pair earns its keep only when a human decision genuinely waits
109
+ on it.
110
+ -->
69
111
 
70
- - Q1: <unresolved> — *resolve by:* decide | spike | ask
112
+ - Q1 (slug-here): <the unresolved hole, framed as a question> — *resolve by:* decide | spike | ask
113
+ - *plain:* <what's at stake, in plain words — enough to judge it cold>
114
+ - *lean:* <the model's proposed resolution — the answer to react to>
71
115
 
72
116
  ## Verdicts
73
117