plumbbob 0.8.12 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli-core.js +1 -1
- package/dist/lib/buildlog.js +49 -2
- package/dist/lib/buildlogsync.js +34 -0
- package/dist/lib/intent.js +20 -0
- package/dist/lib/orient.js +4 -3
- package/dist/lib/sidecar.js +25 -0
- package/dist/lib/templates.js +19 -0
- package/dist/verbs/build.js +5 -0
- package/dist/verbs/checkpoint.js +35 -11
- package/dist/verbs/finish.js +24 -8
- package/dist/verbs/revert.js +5 -0
- package/dist/verbs/spike.js +80 -6
- package/dist/verbs/start.js +10 -11
- package/package.json +1 -1
- package/skills/pb-build/SKILL.md +15 -1
- package/skills/pb-park/SKILL.md +3 -2
- package/skills/pb-plan/SKILL.md +30 -9
- package/skills/pb-refine/SKILL.md +21 -6
- package/skills/pb-spike/SKILL.md +14 -5
- package/skills/pb-step/SKILL.md +10 -5
- package/skills/pb-verify/SKILL.md +12 -1
- package/templates/build-log.md +7 -4
- package/templates/intent.md +55 -11
- package/templates/spike-report.md +38 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plumbbob",
|
|
3
3
|
"displayName": "PlumbBob",
|
|
4
|
-
"version": "0.8.
|
|
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",
|
package/dist/cli-core.js
CHANGED
|
@@ -29,7 +29,7 @@ const VERBS = [
|
|
|
29
29
|
{ name: 'checkpoint', summary: 'checkpoint [<n>]: gate on green, commit/record SHA, mark step done (executor-agnostic)' },
|
|
30
30
|
{ name: 'revert', summary: 'revert [--to n]: git reset --hard to a checkpoint SHA' },
|
|
31
31
|
{ name: 'park', summary: 'park "<text>": append a raw line to the park list' },
|
|
32
|
-
{ name: 'spike', summary: 'spike "<slug>" | spike done:
|
|
32
|
+
{ name: 'spike', summary: 'spike "<slug>" | spike report "<slug>" | spike done: worktree experiment + report' },
|
|
33
33
|
{ name: 'use', summary: 'use <slug>: re-point the active-build cursor and resume that build' },
|
|
34
34
|
{ name: 'finish', summary: 'close-out: report + final commit (no gate), clear the control state, close the session' },
|
|
35
35
|
{ name: 'init', summary: 'link plumbbob into Claude Code as an in-place plugin (~/.claude/skills/plumbbob); --uninstall to undo' },
|
package/dist/lib/buildlog.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Mutations to the active build-log.md — the human-facing ledger. `park` appends to
|
|
2
2
|
// its Park list; `checkpoint` appends to its Log so the build's history accrues
|
|
3
3
|
// line-by-line *as it happens* rather than being reconstructed at finish (D-history).
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// `build`/`checkpoint`/`revert` keep its `## Steps` mirror and `**Current step:**`
|
|
5
|
+
// line in sync with intent.md (D69), so the ledger's top half tracks reality without
|
|
6
|
+
// a model turn. Pure string transforms (no fs, no classes — C1/C2); callers own the
|
|
7
|
+
// IO so these stay trivially testable.
|
|
6
8
|
// Append `line` after the last non-blank line of the `## <heading>` section — i.e.
|
|
7
9
|
// just before the next `## ` heading or EOF. Returns null when the section is absent
|
|
8
10
|
// so the caller can report rather than silently corrupt the doc.
|
|
@@ -25,6 +27,51 @@ export function appendToSection(content, heading, line) {
|
|
|
25
27
|
lines.splice(insertAt, 0, line);
|
|
26
28
|
return lines.join('\n');
|
|
27
29
|
}
|
|
30
|
+
// Replace the `**Current step:**` line with `**Current step:** <label>` — `build`
|
|
31
|
+
// sets it to `<n> — <title>` when a step opens; `checkpoint`/`revert` reset it to
|
|
32
|
+
// `none (at the boundary)`. Returns null when the line is absent so the caller skips
|
|
33
|
+
// rather than corrupting the doc (C3).
|
|
34
|
+
export function setCurrentStep(content, label) {
|
|
35
|
+
const lines = content.split('\n');
|
|
36
|
+
const idx = lines.findIndex((l) => /^\*\*Current step:\*\*/.test(l.trim()));
|
|
37
|
+
if (idx === -1) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
lines[idx] = `**Current step:** ${label}`;
|
|
41
|
+
return lines.join('\n');
|
|
42
|
+
}
|
|
43
|
+
// Regenerate the `## Steps` mirror from intent.md's parsed steps — one
|
|
44
|
+
// `- ☑/☐ N. <title>` line each, in order. Only the mirror's own `- ☑/☐` list lines
|
|
45
|
+
// are owned; the italic instructions paragraph (and any other prose) is preserved,
|
|
46
|
+
// and exactly one blank line separates it from the list. Returns null when the
|
|
47
|
+
// section is absent (C3). With no steps the mirror renders empty — the plan is not
|
|
48
|
+
// authored yet — rather than inventing a placeholder.
|
|
49
|
+
export function syncStepsSection(content, steps) {
|
|
50
|
+
const lines = content.split('\n');
|
|
51
|
+
const headingIdx = lines.findIndex((l) => l.trim() === '## Steps');
|
|
52
|
+
if (headingIdx === -1) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
let end = lines.findIndex((l, i) => i > headingIdx && l.startsWith('## '));
|
|
56
|
+
if (end === -1) {
|
|
57
|
+
end = lines.length;
|
|
58
|
+
}
|
|
59
|
+
// The section body minus the mirror list lines, with trailing blanks trimmed, is
|
|
60
|
+
// the prose to preserve (the instructions paragraph). Rebuild: prose, one blank,
|
|
61
|
+
// the fresh list, one blank before the next heading.
|
|
62
|
+
const prose = lines.slice(headingIdx + 1, end).filter((l) => !isMirrorLine(l));
|
|
63
|
+
while (prose.length > 0 && (prose.at(-1) ?? '').trim() === '') {
|
|
64
|
+
prose.pop();
|
|
65
|
+
}
|
|
66
|
+
const list = steps.map((s) => `- ${s.done ? '☑' : '☐'} ${s.n}. ${s.title}`);
|
|
67
|
+
const body = [...prose, ...(prose.length > 0 ? [''] : []), ...list, ''];
|
|
68
|
+
return [...lines.slice(0, headingIdx + 1), ...body, ...lines.slice(end)].join('\n');
|
|
69
|
+
}
|
|
70
|
+
// A mirror list line the CLI owns: `- ☑ …` / `- ☐ …`. The template's initial
|
|
71
|
+
// `- ☐ 1. <step>` placeholder matches, so the first sync replaces it in place.
|
|
72
|
+
function isMirrorLine(line) {
|
|
73
|
+
return /^-\s+[☑☐]/.test(line);
|
|
74
|
+
}
|
|
28
75
|
// The Log line `checkpoint` writes for a landed step: dated, names the step (its
|
|
29
76
|
// title when intent.md still carries it), and carries the short SHA. One line of the
|
|
30
77
|
// build's history — `/pb-finish` reads these instead of re-narrating the build.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Keep the build-log's top half — the `**Current step:**` line and the `## Steps`
|
|
2
|
+
// mirror — in sync with intent.md (D69). `build` sets the current-step label when a
|
|
3
|
+
// step opens; `checkpoint` and `revert` reset it to the boundary; all three re-render
|
|
4
|
+
// the mirror from intent.md's parsed steps so it tracks reality without a model turn.
|
|
5
|
+
// The write is best-effort (a missing or hand-edited build-log never fails a verb —
|
|
6
|
+
// the checkpoints ledger and intent.md stay the source of truth), matching how
|
|
7
|
+
// `checkpoint`'s Log append already behaves.
|
|
8
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { buildLogPath, intentPath } from "./sidecar.js";
|
|
10
|
+
import { parseSteps } from "./orient.js";
|
|
11
|
+
import { setCurrentStep, syncStepsSection } from "./buildlog.js";
|
|
12
|
+
export const AT_BOUNDARY = 'none (at the boundary)';
|
|
13
|
+
// The current-step label for an open step: `<n> — <title>`, or a bare `<n>` when the
|
|
14
|
+
// step carries no title. `build` passes this; the boundary verbs pass AT_BOUNDARY.
|
|
15
|
+
export function stepLabel(n, title) {
|
|
16
|
+
return title !== null && title.trim().length > 0 ? `${n} — ${title.trim()}` : String(n);
|
|
17
|
+
}
|
|
18
|
+
// Rewrite the active build's build-log top half: set the Current step line to `label`
|
|
19
|
+
// and re-render the `## Steps` mirror from intent.md. Each transform is skipped when
|
|
20
|
+
// its target is absent (null), and the whole thing swallows any IO error — the ledger
|
|
21
|
+
// must never block the verb.
|
|
22
|
+
export function syncBuildLogState(root, slug, label) {
|
|
23
|
+
try {
|
|
24
|
+
const path = buildLogPath(root, slug);
|
|
25
|
+
const steps = parseSteps(readFileSync(intentPath(root, slug), 'utf8'));
|
|
26
|
+
let content = readFileSync(path, 'utf8');
|
|
27
|
+
content = setCurrentStep(content, label) ?? content;
|
|
28
|
+
content = syncStepsSection(content, steps) ?? content;
|
|
29
|
+
writeFileSync(path, content);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// best-effort ledger; never fail a verb over the build-log.
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/lib/intent.js
CHANGED
|
@@ -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);
|
package/dist/lib/orient.js
CHANGED
|
@@ -73,10 +73,11 @@ export function markStepDone(intent, n) {
|
|
|
73
73
|
})
|
|
74
74
|
.join('\n');
|
|
75
75
|
}
|
|
76
|
-
// Open questions still open: `- Q\d+:`
|
|
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
|
|
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
|
package/dist/lib/sidecar.js
CHANGED
|
@@ -158,6 +158,31 @@ export function buildLogPath(root, slug) {
|
|
|
158
158
|
export function reportPath(root, slug) {
|
|
159
159
|
return join(artifactDir(root, slug), 'report.md');
|
|
160
160
|
}
|
|
161
|
+
// Spike reports (D70): `spike-NN-<slug>.md` in the build folder, beside report.md.
|
|
162
|
+
// One per spike — an explicit `/pb-spike` or a planned `spike:` step — so a build with
|
|
163
|
+
// several forks tells its decision history through the folder listing. NN is
|
|
164
|
+
// CLI-allocated (the human never numbers them), zero-padded for a stable sort.
|
|
165
|
+
// The existing spike-report filenames in the build folder, sorted. Empty when the
|
|
166
|
+
// folder is absent or holds none.
|
|
167
|
+
export function listSpikeReports(root, slug) {
|
|
168
|
+
try {
|
|
169
|
+
return readdirSync(artifactDir(root, slug))
|
|
170
|
+
.filter((name) => /^spike-\d\d-.+\.md$/.test(name))
|
|
171
|
+
.sort();
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// The path for a NEW spike report: the next free zero-padded index (one past the
|
|
178
|
+
// highest existing NN, so a gap never reallocates a number) and the sanitized spike
|
|
179
|
+
// slug. Pure allocation — the caller writes the stamped template there.
|
|
180
|
+
export function nextSpikeReportPath(root, slug, spikeSlug) {
|
|
181
|
+
const used = listSpikeReports(root, slug).map((name) => Number(name.slice('spike-'.length, 'spike-'.length + 2)));
|
|
182
|
+
const next = (used.length > 0 ? Math.max(...used) : 0) + 1;
|
|
183
|
+
const nn = String(next).padStart(2, '0');
|
|
184
|
+
return join(artifactDir(root, slug), `spike-${nn}-${spikeSlug}.md`);
|
|
185
|
+
}
|
|
161
186
|
// handoff.json is the agent-run ledger (D47): each `agent run` appends its
|
|
162
187
|
// validated envelope here so a later invocation can thread earlier results into
|
|
163
188
|
// the next call's `context[]` — the CLI itself is memoryless between runs, and
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Scaffold templates live at `<pkg-root>/templates/*.md` and carry `{{PLACEHOLDER}}`
|
|
2
|
+
// tokens the verbs fill in. `start` stamps intent.md / build-log.md; `spike` stamps
|
|
3
|
+
// spike-report.md. Shared here so both resolve the template dir the same way (relative
|
|
4
|
+
// to the compiled module URL) and substitute with one mechanism. Pure/procedural,
|
|
5
|
+
// node-builtins-only (C1/C2).
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
export function readTemplate(name) {
|
|
9
|
+
return readFileSync(fileURLToPath(new URL(`../../templates/${name}`, import.meta.url)), 'utf8');
|
|
10
|
+
}
|
|
11
|
+
// Replace every `{{KEY}}` with its value. Generic over the placeholder set so a new
|
|
12
|
+
// template can add tokens (spike-report's `{{VIA}}`/`{{DATE}}`) without touching this.
|
|
13
|
+
export function stampTemplate(template, subs) {
|
|
14
|
+
let out = template;
|
|
15
|
+
for (const [key, value] of Object.entries(subs)) {
|
|
16
|
+
out = out.split(`{{${key}}}`).join(value);
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
package/dist/verbs/build.js
CHANGED
|
@@ -6,6 +6,7 @@ import { findRepoRoot } from "../lib/git.js";
|
|
|
6
6
|
import { hasSession, intentPath, resolveBuild, seamPath, stampStepStat, stampTick, stepPath } from "../lib/sidecar.js";
|
|
7
7
|
import { parseStepSeam } from "../lib/intent.js";
|
|
8
8
|
import { parseSteps } from "../lib/orient.js";
|
|
9
|
+
import { stepLabel, syncBuildLogState } from "../lib/buildlogsync.js";
|
|
9
10
|
export function build(cwd, args) {
|
|
10
11
|
const root = findRepoRoot(cwd);
|
|
11
12
|
if (root === null || !hasSession(root)) {
|
|
@@ -55,6 +56,10 @@ export function build(cwd, args) {
|
|
|
55
56
|
// The wall-clock receipt starts here (research/07 Build 2b): checkpoint stamps
|
|
56
57
|
// landedAt, and the pair becomes the step's duration in the finish report.
|
|
57
58
|
stampStepStat(root, slug, step, 'startedAt', new Date().toISOString());
|
|
59
|
+
// The build-log's top half now shows this step in flight (D69): Current step +
|
|
60
|
+
// the ☐/☑ mirror, re-rendered from intent.md. Best-effort — never blocks the build.
|
|
61
|
+
const title = parseSteps(intent).find((s) => s.n === step)?.title ?? null;
|
|
62
|
+
syncBuildLogState(root, slug, stepLabel(step, title));
|
|
58
63
|
const picked = raw === undefined ? ' (next undone)' : '';
|
|
59
64
|
process.stdout.write(`plumbbob: building step ${step}${picked}. Seam (for orientation; not a lock):\n${parsed.seam.map((p) => ` ${p}`).join('\n')}\n`);
|
|
60
65
|
return 0;
|
package/dist/verbs/checkpoint.js
CHANGED
|
@@ -13,9 +13,10 @@ 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
|
+
import { AT_BOUNDARY, syncBuildLogState } from "../lib/buildlogsync.js";
|
|
19
20
|
export async function checkpoint(cwd, args) {
|
|
20
21
|
const root = findRepoRoot(cwd);
|
|
21
22
|
if (root === null || !hasSession(root)) {
|
|
@@ -64,6 +65,10 @@ export async function checkpoint(cwd, args) {
|
|
|
64
65
|
flipIntent(root, step);
|
|
65
66
|
stampStepStat(root, undefined, step, 'landedAt', new Date().toISOString());
|
|
66
67
|
logCheckpoint(root, step, sha);
|
|
68
|
+
// The step landed: the build-log's mirror flips to ☑ and Current step returns to
|
|
69
|
+
// the boundary (D69), re-read from the intent.md `flipIntent` just wrote. The
|
|
70
|
+
// artifact-plane whitelist keeps this build-log write from ever reading as drift.
|
|
71
|
+
syncBuildLogState(root, activeBuild(root), AT_BOUNDARY);
|
|
67
72
|
rmSync(seamPath(root), { force: true });
|
|
68
73
|
rmSync(stepPath(root), { force: true });
|
|
69
74
|
clearHandoff(root); // the agent-run ledger is step-scoped (D47) — clear it with the markers.
|
|
@@ -96,11 +101,12 @@ function checkpointPlan(root, args) {
|
|
|
96
101
|
process.stdout.write(`plumbbob: plan committed — ${sha.slice(0, 9)}. Baseline → plan → steps.\n`);
|
|
97
102
|
return 0;
|
|
98
103
|
}
|
|
99
|
-
// The plan commit's CLI-owned subject (D68): `chore(<scope>): plan`, the scope
|
|
100
|
-
//
|
|
101
|
-
// `plumbbob plan` identifier rides the body marker, not the
|
|
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.
|
|
102
108
|
function planSubject(root) {
|
|
103
|
-
return conventionalSubject({ type: 'chore', scope:
|
|
109
|
+
return conventionalSubject({ type: 'chore', scope: buildDefaultScope(root), description: 'plan' });
|
|
104
110
|
}
|
|
105
111
|
// Step resolution (D3): explicit arg > in-flight STEP file > first undone step in
|
|
106
112
|
// intent.md. Returns null when none can be determined. A `-m` value is a message,
|
|
@@ -205,18 +211,36 @@ function statsSuffix(root, step) {
|
|
|
205
211
|
return parts.length > 0 ? parts.join(', ') : null;
|
|
206
212
|
}
|
|
207
213
|
// The CLI-owned, deterministic commit subject (D68): a Conventional `type(scope):
|
|
208
|
-
// description`. The
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
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.
|
|
213
221
|
function subjectForStep(root, step) {
|
|
214
|
-
const scope =
|
|
222
|
+
const scope = buildDefaultScope(root);
|
|
215
223
|
const title = titleForStep(root, step);
|
|
216
224
|
return title !== null && title.trim().length > 0
|
|
217
225
|
? subjectFromTitle(title, 'feat', scope)
|
|
218
226
|
: conventionalSubject({ type: 'chore', scope, description: 'checkpoint' });
|
|
219
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
|
+
}
|
|
220
244
|
function titleForStep(root, step) {
|
|
221
245
|
try {
|
|
222
246
|
return parseSteps(readFileSync(intentPath(root), 'utf8')).find((s) => s.n === step)?.title ?? null;
|
package/dist/verbs/finish.js
CHANGED
|
@@ -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
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// The `plumbbob finish` identifier rides the body marker, not the
|
|
61
|
-
|
|
62
|
-
|
|
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/dist/verbs/revert.js
CHANGED
|
@@ -20,6 +20,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
20
20
|
import { findRepoRoot, resetHard, untrackedPaths } from "../lib/git.js";
|
|
21
21
|
import { bumpStepStat, checkpointsPath, hasSession, resolveBuild, seamPath, sidecarDir, stepPath } from "../lib/sidecar.js";
|
|
22
22
|
import { isArtifactPath, matchesSeam } from "../lib/intent.js";
|
|
23
|
+
import { AT_BOUNDARY, syncBuildLogState } from "../lib/buildlogsync.js";
|
|
23
24
|
export function revert(cwd, args) {
|
|
24
25
|
const root = findRepoRoot(cwd);
|
|
25
26
|
if (root === null || !hasSession(root)) {
|
|
@@ -66,6 +67,10 @@ export function revert(cwd, args) {
|
|
|
66
67
|
if (inFlight !== null) {
|
|
67
68
|
bumpStepStat(root, slug, inFlight, 'reverts');
|
|
68
69
|
}
|
|
70
|
+
// The step is abandoned: the build-log returns to the boundary and its mirror
|
|
71
|
+
// re-renders from the preserved intent.md (D69) — revert keeps intent edits (D26),
|
|
72
|
+
// so intent's checkboxes stay the truth to reflect. Best-effort, like the rest.
|
|
73
|
+
syncBuildLogState(root, slug, AT_BOUNDARY);
|
|
69
74
|
process.stdout.write(`plumbbob: reverted to ${sha.slice(0, 9)} — back at the boundary. Park lines and intent edits were preserved.\n`);
|
|
70
75
|
return 0;
|
|
71
76
|
}
|
package/dist/verbs/spike.js
CHANGED
|
@@ -10,11 +10,15 @@
|
|
|
10
10
|
// shared additive read/commit helpers): worktree management is spike-local, and
|
|
11
11
|
// this is the only place Plumbbob creates branches.
|
|
12
12
|
import { execFileSync } from 'node:child_process';
|
|
13
|
-
import { existsSync } from 'node:fs';
|
|
14
|
-
import { basename, dirname, join } from 'node:path';
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { basename, dirname, join, relative } from 'node:path';
|
|
15
15
|
import { findRepoRoot } from "../lib/git.js";
|
|
16
|
-
import { hasSession, inSpike, markSpike,
|
|
16
|
+
import { buildFolder, clearSpike, hasSession, inSpike, listSpikeReports, markSpike, nextSpikeReportPath, resolveBuild, stepPath, } from "../lib/sidecar.js";
|
|
17
|
+
import { readTemplate, stampTemplate } from "../lib/templates.js";
|
|
17
18
|
const DEFAULT_OPTIONS = ['a', 'b'];
|
|
19
|
+
// The verbatim Verdict placeholder from templates/spike-report.md — its presence means
|
|
20
|
+
// the spike's call was never recorded, which `spike done` nudges on (D70).
|
|
21
|
+
const VERDICT_PLACEHOLDER = '*(viable | not viable | partial';
|
|
18
22
|
export function spike(cwd, args) {
|
|
19
23
|
const root = findRepoRoot(cwd);
|
|
20
24
|
if (root === null || !hasSession(root)) {
|
|
@@ -26,8 +30,49 @@ export function spike(cwd, args) {
|
|
|
26
30
|
if (positionals[0] === 'done') {
|
|
27
31
|
return spikeDone(root, buildSlug);
|
|
28
32
|
}
|
|
33
|
+
if (positionals[0] === 'report') {
|
|
34
|
+
return spikeReport(root, buildSlug, positionals.slice(1));
|
|
35
|
+
}
|
|
29
36
|
return spikeStart(root, buildSlug, positionals);
|
|
30
37
|
}
|
|
38
|
+
// `spike report "<slug>"` (D70) — scaffold a spike report WITHOUT worktrees, for the
|
|
39
|
+
// spike-as-step case (a planned step titled `spike: …`, where the increment itself is
|
|
40
|
+
// the experiment). No boundary requirement and no SPIKE marker: a step in flight is
|
|
41
|
+
// exactly when this runs. Provenance is `step <n>` when a STEP is in flight, else
|
|
42
|
+
// `/pb-spike`.
|
|
43
|
+
function spikeReport(root, buildSlug, positionals) {
|
|
44
|
+
const slug = sanitize(positionals[0] ?? '');
|
|
45
|
+
if (slug.length === 0) {
|
|
46
|
+
process.stderr.write('plumbbob: spike report needs a slug. Try: plumbbob spike report "auth-store".\n');
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
const inFlight = readInFlightStep(root, buildSlug);
|
|
50
|
+
const via = inFlight !== null ? `step ${inFlight}` : '/pb-spike';
|
|
51
|
+
const path = scaffoldSpikeReport(root, buildSlug, slug, via);
|
|
52
|
+
process.stdout.write(`plumbbob: spike report scaffolded — ${relative(root, path)}. Fill Findings and the Verdict as you go; ` +
|
|
53
|
+
`a recorded Verdict is what closes a spike step.\n`);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
// Write a fresh spike report from the template at the next free `spike-NN-<slug>.md`
|
|
57
|
+
// in the build folder, and return its path. Shared by the explicit `/pb-spike` open
|
|
58
|
+
// and the spike-as-step `spike report` — one artifact, two entry points (D70).
|
|
59
|
+
function scaffoldSpikeReport(root, buildSlug, spikeSlug, via) {
|
|
60
|
+
const path = nextSpikeReportPath(root, buildSlug, spikeSlug);
|
|
61
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
62
|
+
writeFileSync(path, stampTemplate(readTemplate('spike-report.md'), { TITLE: spikeSlug, VIA: via, DATE: date }));
|
|
63
|
+
return path;
|
|
64
|
+
}
|
|
65
|
+
// The in-flight STEP number for the build, or null — reads the STEP marker `build`
|
|
66
|
+
// writes. Used to stamp a spike-as-step report's provenance.
|
|
67
|
+
function readInFlightStep(root, buildSlug) {
|
|
68
|
+
try {
|
|
69
|
+
const raw = readFileSync(stepPath(root, buildSlug), 'utf8').trim();
|
|
70
|
+
return /^\d+$/.test(raw) ? Number(raw) : null;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
31
76
|
function spikeStart(root, buildSlug, positionals) {
|
|
32
77
|
if (inSpike(root, buildSlug)) {
|
|
33
78
|
process.stderr.write('plumbbob: already in a spike. Run `plumbbob spike done` to close it first.\n');
|
|
@@ -56,9 +101,12 @@ function spikeStart(root, buildSlug, positionals) {
|
|
|
56
101
|
created.push(path);
|
|
57
102
|
}
|
|
58
103
|
markSpike(root, buildSlug);
|
|
104
|
+
// Scaffold the report NOW, while the worktrees live (D70): findings accrue during the
|
|
105
|
+
// experiment, not from memory after the teardown. Provenance names the worktrees.
|
|
106
|
+
const report = scaffoldSpikeReport(root, buildSlug, slug, `/pb-spike — worktrees (${options.join(', ')})`);
|
|
59
107
|
process.stdout.write(`plumbbob: spiking — the main tree stays put. Experiment in the throwaway worktrees:\n${created
|
|
60
108
|
.map((p) => ` ${p}`)
|
|
61
|
-
.join('\n')}\
|
|
109
|
+
.join('\n')}\nRecord findings and the Verdict in ${relative(root, report)} as you go, then run \`plumbbob spike done\`.\n`);
|
|
62
110
|
return 0;
|
|
63
111
|
}
|
|
64
112
|
function spikeDone(root, buildSlug) {
|
|
@@ -66,6 +114,10 @@ function spikeDone(root, buildSlug) {
|
|
|
66
114
|
process.stderr.write('plumbbob: no active spike to close.\n');
|
|
67
115
|
return 1;
|
|
68
116
|
}
|
|
117
|
+
// Check for an unrecorded verdict BEFORE teardown (D70). The reports live in the
|
|
118
|
+
// build folder, not the spike worktrees, so they survive the removal — but the
|
|
119
|
+
// learning that fills them does not, so this is the moment to nudge.
|
|
120
|
+
const unfilled = spikeReportsMissingVerdict(root, buildSlug);
|
|
69
121
|
for (const path of spikeWorktrees(root)) {
|
|
70
122
|
git(root, ['worktree', 'remove', '--force', path]);
|
|
71
123
|
}
|
|
@@ -74,10 +126,32 @@ function spikeDone(root, buildSlug) {
|
|
|
74
126
|
git(root, ['branch', '-D', branch]);
|
|
75
127
|
}
|
|
76
128
|
clearSpike(root, buildSlug);
|
|
77
|
-
|
|
78
|
-
|
|
129
|
+
// Guidance, not a gate (the enforce→guide pivot): a missing verdict is a nudge, and
|
|
130
|
+
// the spike still closes. Name the reports so the human knows where to write it.
|
|
131
|
+
if (unfilled.length > 0) {
|
|
132
|
+
process.stderr.write(`plumbbob: heads-up — no verdict recorded in ${unfilled.join(', ')}. ` +
|
|
133
|
+
`The worktrees are gone; capture the call there before it fades.\n`);
|
|
134
|
+
}
|
|
135
|
+
const reports = listSpikeReports(root, buildSlug);
|
|
136
|
+
const where = reports.length > 0 ? `the spike report${reports.length === 1 ? '' : 's'} (${reports.join(', ')})` : 'intent.md';
|
|
137
|
+
process.stdout.write(`plumbbob: spike closed — worktrees and branches removed, back at the boundary. ` +
|
|
138
|
+
`Record the verdict (which option won, and why) in ${where} before you \`build\`.\n`);
|
|
79
139
|
return 0;
|
|
80
140
|
}
|
|
141
|
+
// The spike-report filenames in the build folder whose Verdict is still the template
|
|
142
|
+
// placeholder — the ones `spike done` nudges on. Best-effort per file: an unreadable
|
|
143
|
+
// report is skipped rather than blocking the close.
|
|
144
|
+
function spikeReportsMissingVerdict(root, buildSlug) {
|
|
145
|
+
const dir = buildFolder(root, buildSlug);
|
|
146
|
+
return listSpikeReports(root, buildSlug).filter((name) => {
|
|
147
|
+
try {
|
|
148
|
+
return readFileSync(join(dir, name), 'utf8').includes(VERDICT_PLACEHOLDER);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
81
155
|
// Worktree paths whose checked-out branch is under spike/ — parsed from the
|
|
82
156
|
// porcelain output (blank-line-separated `worktree <path>` / `branch <ref>` blocks).
|
|
83
157
|
function spikeWorktrees(root) {
|
package/dist/verbs/start.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// `plumbbob start "<title>"` — scaffold the sidecar, record the baseline, open
|
|
2
2
|
// the session. Refuses on a dirty tree (D22), an existing session, or a non-git dir.
|
|
3
|
-
import {
|
|
4
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
5
4
|
import { join } from 'node:path';
|
|
6
5
|
import { detectGate } from "../lib/check.js";
|
|
6
|
+
import { readTemplate, stampTemplate } from "../lib/templates.js";
|
|
7
7
|
import { findRepoRoot, hasCommit, headSha, isDirty } from "../lib/git.js";
|
|
8
8
|
import { sidecarDir, buildDir, listBuilds, slugify, checkpointsPath, intentPath, buildLogPath, beginSession, hasSession, excludeControl, excludeSidecar, setGrant, stampTick, } from "../lib/sidecar.js";
|
|
9
9
|
import { settingsPath } from "../lib/settings.js";
|
|
@@ -73,8 +73,8 @@ export async function start(cwd, args) {
|
|
|
73
73
|
let intentLocation;
|
|
74
74
|
if (local) {
|
|
75
75
|
writeFileSync(checkpointsPath(root), `baseline ${sha}\n`);
|
|
76
|
-
writeFileSync(intentPath(root),
|
|
77
|
-
writeFileSync(buildLogPath(root),
|
|
76
|
+
writeFileSync(intentPath(root), stampBuild(readTemplate('intent.md'), title));
|
|
77
|
+
writeFileSync(buildLogPath(root), stampBuild(readTemplate('build-log.md'), title));
|
|
78
78
|
excludeSidecar(root);
|
|
79
79
|
intentLocation = '.plumbbob/intent.md';
|
|
80
80
|
}
|
|
@@ -82,8 +82,8 @@ export async function start(cwd, args) {
|
|
|
82
82
|
const dir = buildDir(root, slug);
|
|
83
83
|
mkdirSync(dir, { recursive: true });
|
|
84
84
|
writeFileSync(join(dir, 'checkpoints'), `baseline ${sha}\n`);
|
|
85
|
-
writeFileSync(join(dir, 'intent.md'),
|
|
86
|
-
writeFileSync(join(dir, 'build-log.md'),
|
|
85
|
+
writeFileSync(join(dir, 'intent.md'), stampBuild(readTemplate('intent.md'), title));
|
|
86
|
+
writeFileSync(join(dir, 'build-log.md'), stampBuild(readTemplate('build-log.md'), title));
|
|
87
87
|
excludeControl(root);
|
|
88
88
|
intentLocation = `.plumbbob/builds/${slug}/intent.md`;
|
|
89
89
|
}
|
|
@@ -128,9 +128,8 @@ function flagValue(args, flag) {
|
|
|
128
128
|
const i = args.indexOf(flag);
|
|
129
129
|
return i >= 0 ? args[i + 1] : undefined;
|
|
130
130
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return template.split('{{TITLE}}').join(title).split('{{CHECK}}').join(check);
|
|
131
|
+
// Stamp a build template's `{{TITLE}}`/`{{CHECK}}` — the check is always the echo
|
|
132
|
+
// constant (absence of a `check` setting IS the default, D24/D32).
|
|
133
|
+
function stampBuild(template, title) {
|
|
134
|
+
return stampTemplate(template, { TITLE: title, CHECK: CHECK_ECHO });
|
|
136
135
|
}
|
package/package.json
CHANGED
package/skills/pb-build/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: pb-build
|
|
|
3
3
|
description: The default engine — read the next planned step from intent, implement it (its done-when, seam, Decisions, Constraints), then verify it through to the approval pause. Swappable — build by hand/vibed/another harness and run /pb-verify instead. `--auto` self-approves and chains to done; a step range like `1-3` self-approves through step 3, then pauses.
|
|
4
4
|
argument-hint: "[step-number | step-range] [--auto]"
|
|
5
5
|
disable-model-invocation: true
|
|
6
|
-
allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob build:*), Bash(plumbbob handoff:*), Bash(plumbbob check:*), Bash(plumbbob checkpoint:*), Bash(plumbbob park:*), Bash(plumbbob agent:*), Bash(git diff:*)
|
|
6
|
+
allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob build:*), Bash(plumbbob handoff:*), Bash(plumbbob check:*), Bash(plumbbob checkpoint:*), Bash(plumbbob park:*), Bash(plumbbob agent:*), Bash(plumbbob spike:*), Bash(git diff:*)
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# PlumbBob — build a step (the default engine)
|
|
@@ -36,6 +36,13 @@ differs from the model you're running as, say so before implementing — the hum
|
|
|
36
36
|
2. **Enter the step.** Run `plumbbob build <n>` — or bare `plumbbob build` to enter the
|
|
37
37
|
next undone step (records the in-flight STEP + SEAM so `/pb-status` shows the step in
|
|
38
38
|
flight; the seam is awareness, not a lock).
|
|
39
|
+
- **Spike-as-step.** If the step's title opens with `spike:` / `Spike:`, the increment
|
|
40
|
+
*is* the experiment — resolving a fork the plan couldn't settle on paper. After
|
|
41
|
+
entering it, run `plumbbob spike report "<slug>"` (the CLI scaffolds a
|
|
42
|
+
`spike-NN-<slug>.md` in the build folder, stamped `via: step <n>`), then work the
|
|
43
|
+
experiment and record your **Findings** and **Verdict** there. A recorded verdict is
|
|
44
|
+
what the step's done-when should check — the report, not just the code, is the
|
|
45
|
+
deliverable. This is the same artifact `/pb-spike` produces for a mid-build fork (D70).
|
|
39
46
|
3. **Read the plan.** Read the step's **done-when**, its **seam**, and the
|
|
40
47
|
**Decisions** and **Constraints** in `intent.md`. Build to *that* — the deciding
|
|
41
48
|
already happened, off the chat.
|
|
@@ -67,6 +74,13 @@ differs from the model you're running as, say so before implementing — the hum
|
|
|
67
74
|
build-log's `## Log`, so the history writes itself — you only supply the body (or omit
|
|
68
75
|
`--body` for the deterministic done-when + seam + diffstat fallback). Do **not** bump
|
|
69
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).
|
|
70
84
|
- **If `plumbbob status` shows a `harness bindings:` block**, fold any bound
|
|
71
85
|
`after`-agents into that self-review as advisory input — see **§ Running bound
|
|
72
86
|
agents**.
|
package/skills/pb-park/SKILL.md
CHANGED
|
@@ -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,
|
|
29
|
-
later
|
|
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.
|
package/skills/pb-plan/SKILL.md
CHANGED
|
@@ -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.
|
|
62
|
-
|
|
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
|
-
**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
how each step lands a *real* `feat`/`fix`/`chore` in
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
package/skills/pb-spike/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pb-spike
|
|
3
|
-
description: Human-triggered driver for `plumbbob spike` — open a throwaway worktree experiment for a genuine fork, or tear it down with `spike done`.
|
|
4
|
-
argument-hint: "<slug> | done"
|
|
3
|
+
description: Human-triggered driver for `plumbbob spike` — open a throwaway worktree experiment for a genuine fork, scaffold a spike report for a spike-as-step, or tear it down with `spike done`.
|
|
4
|
+
argument-hint: "<slug> | report <slug> | done"
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
model: haiku
|
|
7
7
|
allowed-tools: Bash(plumbbob status:*), Bash(plumbbob spike:*)
|
|
@@ -15,6 +15,15 @@ This is a **driver skill** — a chat-side trigger for the mechanical `plumbbob
|
|
|
15
15
|
|
|
16
16
|
## What it does
|
|
17
17
|
|
|
18
|
-
1. Read the spike target from the way you were invoked: a slug to open one (e.g. `/pb-spike redis-cache`), or the literal `done` to tear the current spike down (`/pb-spike done`). If
|
|
19
|
-
2. Run `plumbbob spike "<slug>"` or `plumbbob spike done` via Bash.
|
|
20
|
-
3. Report the verb's output verbatim — the worktree it created or removed, or any refusal.
|
|
18
|
+
1. Read the spike target from the way you were invoked: a slug to open one (e.g. `/pb-spike redis-cache`), `report <slug>` to scaffold a report without worktrees (`/pb-spike report auth-store`), or the literal `done` to tear the current spike down (`/pb-spike done`). If none is present, ask which and run nothing.
|
|
19
|
+
2. Run `plumbbob spike "<slug>"`, `plumbbob spike report "<slug>"`, or `plumbbob spike done` via Bash.
|
|
20
|
+
3. Report the verb's output verbatim — the worktree it created or removed, the spike report it scaffolded, or any refusal. When the verb names a `spike-NN-<slug>.md` report, point the human at it; when `spike done` nudges that a verdict is unrecorded, relay that nudge verbatim.
|
|
21
|
+
|
|
22
|
+
## The spike report (D70)
|
|
23
|
+
|
|
24
|
+
Every spike leaves a durable `spike-NN-<slug>.md` in the build folder, beside `intent.md`/`report.md`, so the fork's verdict rides the branch into the PR instead of evaporating. The CLI scaffolds it from a template and numbers it — you never create or number the file:
|
|
25
|
+
|
|
26
|
+
- **`/pb-spike <slug>`** scaffolds the report *at open*, so findings accrue while the throwaway worktrees are still live. Fill its **Findings** and **Verdict** before `/pb-spike done`.
|
|
27
|
+
- **`/pb-spike report <slug>`** is for a *spike-as-step* — a planned step titled `spike: …`, where the increment itself is the experiment and there are no worktrees. It stamps the report's provenance as `step <n>` when a step is in flight.
|
|
28
|
+
|
|
29
|
+
This skill stays Write-less: the CLI owns the file. Your job is to run the verb and, between runs, help the human fill the report's prose — but the edits land through the normal build flow, not this driver.
|
package/skills/pb-step/SKILL.md
CHANGED
|
@@ -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.
|
|
43
|
-
`<title>`
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
package/templates/build-log.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
build-log.md — your live ledger for execution. Append constantly; reorganize at
|
|
3
3
|
step boundaries. The antidote to "my plan got lost in the noise."
|
|
4
4
|
|
|
5
|
-
Steps : where you are. One step in flight at a time.
|
|
5
|
+
Steps : where you are. One step in flight at a time. CLI-maintained: `build`,
|
|
6
|
+
`checkpoint`, and `revert` keep this mirror and the Current step line
|
|
7
|
+
in sync with intent.md — you never hand-edit them.
|
|
6
8
|
Park list : where ideas go so you do not chase them. CAPTURE, never act inline.
|
|
7
9
|
Harvest : the boundary ritual that keeps you on one branch.
|
|
8
10
|
Log : the build's history. `plumbbob checkpoint` appends a line per step as it
|
|
@@ -16,9 +18,10 @@ step boundaries. The antidote to "my plan got lost in the noise."
|
|
|
16
18
|
|
|
17
19
|
## Steps
|
|
18
20
|
|
|
19
|
-
*(Mirror of intent.md's Steps, with live status
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
*(Mirror of intent.md's Steps, with live status — CLI-maintained, not hand-edited.
|
|
22
|
+
`build`/`checkpoint`/`revert` re-render this from intent.md, and set the Current step
|
|
23
|
+
line above. Only ONE step is in flight; a step is done only after a checkpoint —
|
|
24
|
+
check green + checkpoint taken, via `/pb-verify` or `/pb-build`.)*
|
|
22
25
|
|
|
23
26
|
- ☐ 1. <step>
|
|
24
27
|
|
package/templates/intent.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
68
|
-
|
|
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
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
spike-report.md — the durable receipt for a spike (a fork the plan couldn't settle
|
|
3
|
+
on paper). One per spike, numbered and named `spike-NN-<slug>.md`, riding the build
|
|
4
|
+
folder into the PR beside intent.md / report.md. Fill it while the experiment is live
|
|
5
|
+
— the worktrees and their learnings are torn down at `spike done`, but this stays.
|
|
6
|
+
|
|
7
|
+
Two ways a spike is born, both landing here:
|
|
8
|
+
- `/pb-spike <slug>` — an explicit mid-build fork with throwaway worktrees.
|
|
9
|
+
- a planned step titled `spike: …` — the increment IS the experiment.
|
|
10
|
+
Record the Verdict before you close the spike (or check off the step).
|
|
11
|
+
-->
|
|
12
|
+
|
|
13
|
+
# Spike — {{TITLE}}
|
|
14
|
+
|
|
15
|
+
**Via:** {{VIA}}
|
|
16
|
+
**Opened:** {{DATE}}
|
|
17
|
+
|
|
18
|
+
## Question
|
|
19
|
+
|
|
20
|
+
*(The fork this resolves, in one or two lines. What are we uncertain about, and what
|
|
21
|
+
would each answer commit us to?)*
|
|
22
|
+
|
|
23
|
+
## Options tried
|
|
24
|
+
|
|
25
|
+
*(Each path you actually ran, and what it took — one bullet each.)*
|
|
26
|
+
|
|
27
|
+
## Findings
|
|
28
|
+
|
|
29
|
+
*(What the experiment showed — evidence, not opinion. Numbers, errors, surprises.)*
|
|
30
|
+
|
|
31
|
+
## Verdict
|
|
32
|
+
|
|
33
|
+
**Verdict:** *(viable | not viable | partial — replace this line with the call and why)*
|
|
34
|
+
|
|
35
|
+
## What this decides
|
|
36
|
+
|
|
37
|
+
*(The intent.md Decision this feeds — which option won, what dies, and the one reason
|
|
38
|
+
that settled it. Copy the call into intent.md's Decisions / Verdicts too.)*
|