plumbbob 0.1.3
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/LICENSE +201 -0
- package/README.md +137 -0
- package/hooks/bash-guard.sh +58 -0
- package/hooks/post-edit.sh +59 -0
- package/hooks/pre-edit.sh +95 -0
- package/package.json +60 -0
- package/skills/park/SKILL.md +26 -0
- package/skills/plumbbob-docs/SKILL.md +25 -0
- package/skills/plumbbob-interrogate/SKILL.md +33 -0
- package/skills/plumbbob-report/SKILL.md +35 -0
- package/skills/plumbbob-triage/SKILL.md +38 -0
- package/src/cli.ts +121 -0
- package/src/lib/archive.ts +76 -0
- package/src/lib/check.ts +29 -0
- package/src/lib/git.ts +73 -0
- package/src/lib/intent.ts +103 -0
- package/src/lib/settings.ts +90 -0
- package/src/lib/sidecar.ts +82 -0
- package/src/verbs/build.ts +38 -0
- package/src/verbs/done.ts +71 -0
- package/src/verbs/finish.ts +72 -0
- package/src/verbs/mode.ts +27 -0
- package/src/verbs/park.ts +56 -0
- package/src/verbs/revert.ts +110 -0
- package/src/verbs/review.ts +30 -0
- package/src/verbs/setup.ts +95 -0
- package/src/verbs/spike.ts +129 -0
- package/src/verbs/start.ts +103 -0
- package/src/verbs/status.ts +15 -0
- package/src/verbs/wrap.ts +34 -0
- package/templates/build-log.md +54 -0
- package/templates/intent.md +67 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// `plumbbob spike` (D18) — the spike lifecycle for a genuine fork the design
|
|
2
|
+
// phase couldn't settle. `spike "<slug>" [opt…]` creates a sibling git worktree +
|
|
3
|
+
// `spike/<slug>-<opt>` branch per option OUTSIDE the repo root (default opts a/b)
|
|
4
|
+
// and sets STATE=SPIKE; the main tree stays DESIGN-locked while you experiment in
|
|
5
|
+
// the worktrees, which are hook-dormant by construction — the untracked sidecar
|
|
6
|
+
// (D17) doesn't exist in a fresh checkout, so the hooks find no STATE there.
|
|
7
|
+
// `spike done` removes every spike worktree + branch and returns to DESIGN.
|
|
8
|
+
//
|
|
9
|
+
// Worktree git calls run directly here rather than via lib/git.ts (which holds the
|
|
10
|
+
// shared additive read/commit helpers): worktree management is spike-local, and
|
|
11
|
+
// this is the only place Plumbbob creates branches.
|
|
12
|
+
|
|
13
|
+
import { execFileSync } from 'node:child_process'
|
|
14
|
+
import { existsSync } from 'node:fs'
|
|
15
|
+
import { basename, dirname, join } from 'node:path'
|
|
16
|
+
import { findRepoRoot } from '../lib/git.ts'
|
|
17
|
+
import { hasSession, readState, writeState } from '../lib/sidecar.ts'
|
|
18
|
+
|
|
19
|
+
const DEFAULT_OPTIONS: ReadonlyArray<string> = ['a', 'b']
|
|
20
|
+
|
|
21
|
+
export function spike(cwd: string, args: ReadonlyArray<string>): number {
|
|
22
|
+
const root = findRepoRoot(cwd)
|
|
23
|
+
if (root === null || !hasSession(root)) {
|
|
24
|
+
process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n')
|
|
25
|
+
return 1
|
|
26
|
+
}
|
|
27
|
+
const positionals = args.filter((a) => !a.startsWith('--'))
|
|
28
|
+
if (positionals[0] === 'done') {
|
|
29
|
+
return spikeDone(root)
|
|
30
|
+
}
|
|
31
|
+
return spikeStart(root, positionals)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function spikeStart(root: string, positionals: ReadonlyArray<string>): number {
|
|
35
|
+
const state = readState(root)
|
|
36
|
+
if (state === 'SPIKE') {
|
|
37
|
+
process.stderr.write('plumbbob: already in a spike. Run `plumbbob spike done` to close it first.\n')
|
|
38
|
+
return 1
|
|
39
|
+
}
|
|
40
|
+
if (state !== 'DESIGN') {
|
|
41
|
+
process.stderr.write(
|
|
42
|
+
`plumbbob: spike starts from DESIGN (current state is ${state ?? 'UNKNOWN'}). ` +
|
|
43
|
+
'A spike is a deliberate fork — close the current step first with `done`.\n',
|
|
44
|
+
)
|
|
45
|
+
return 1
|
|
46
|
+
}
|
|
47
|
+
const slug = sanitize(positionals[0] ?? '')
|
|
48
|
+
if (slug.length === 0) {
|
|
49
|
+
process.stderr.write('plumbbob: spike needs a slug. Try: plumbbob spike "auth-store" a b.\n')
|
|
50
|
+
return 1
|
|
51
|
+
}
|
|
52
|
+
const explicit = positionals.slice(1).map(sanitize).filter((o) => o.length > 0)
|
|
53
|
+
const options = explicit.length > 0 ? explicit : DEFAULT_OPTIONS
|
|
54
|
+
|
|
55
|
+
const created: string[] = []
|
|
56
|
+
for (const opt of options) {
|
|
57
|
+
const path = join(dirname(root), `${basename(root)}-spike-${slug}-${opt}`)
|
|
58
|
+
if (existsSync(path)) {
|
|
59
|
+
process.stderr.write(`plumbbob: ${path} already exists — remove it or run \`plumbbob spike done\` first.\n`)
|
|
60
|
+
return 1
|
|
61
|
+
}
|
|
62
|
+
git(root, ['worktree', 'add', '-b', `spike/${slug}-${opt}`, path, 'HEAD'])
|
|
63
|
+
created.push(path)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
writeState(root, 'SPIKE')
|
|
67
|
+
process.stdout.write(
|
|
68
|
+
`plumbbob: STATE=SPIKE — the main tree stays DESIGN-locked. Experiment in the throwaway worktrees:\n${created
|
|
69
|
+
.map((p) => ` ${p}`)
|
|
70
|
+
.join('\n')}\nWhen you've decided, record the verdict in intent.md and run \`plumbbob spike done\`.\n`,
|
|
71
|
+
)
|
|
72
|
+
return 0
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function spikeDone(root: string): number {
|
|
76
|
+
const state = readState(root)
|
|
77
|
+
if (state !== 'SPIKE') {
|
|
78
|
+
process.stderr.write(`plumbbob: no active spike (state is ${state ?? 'UNKNOWN'}).\n`)
|
|
79
|
+
return 1
|
|
80
|
+
}
|
|
81
|
+
for (const path of spikeWorktrees(root)) {
|
|
82
|
+
git(root, ['worktree', 'remove', '--force', path])
|
|
83
|
+
}
|
|
84
|
+
git(root, ['worktree', 'prune'])
|
|
85
|
+
for (const branch of spikeBranches(root)) {
|
|
86
|
+
git(root, ['branch', '-D', branch])
|
|
87
|
+
}
|
|
88
|
+
writeState(root, 'DESIGN')
|
|
89
|
+
process.stdout.write(
|
|
90
|
+
'plumbbob: spike closed — STATE=DESIGN, worktrees and branches removed. ' +
|
|
91
|
+
'Record the verdict (which option won, and why) in intent.md before you `build`.\n',
|
|
92
|
+
)
|
|
93
|
+
return 0
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Worktree paths whose checked-out branch is under spike/ — parsed from the
|
|
97
|
+
// porcelain output (blank-line-separated `worktree <path>` / `branch <ref>` blocks).
|
|
98
|
+
function spikeWorktrees(root: string): ReadonlyArray<string> {
|
|
99
|
+
const out = git(root, ['worktree', 'list', '--porcelain'])
|
|
100
|
+
const paths: string[] = []
|
|
101
|
+
let current: string | null = null
|
|
102
|
+
for (const line of out.split('\n')) {
|
|
103
|
+
if (line.startsWith('worktree ')) {
|
|
104
|
+
current = line.slice('worktree '.length)
|
|
105
|
+
} else if (current !== null && line.startsWith('branch refs/heads/spike/')) {
|
|
106
|
+
paths.push(current)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return paths
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function spikeBranches(root: string): ReadonlyArray<string> {
|
|
113
|
+
const out = git(root, ['for-each-ref', '--format=%(refname:short)', 'refs/heads/spike/'])
|
|
114
|
+
return out.length === 0 ? [] : out.split('\n').filter((b) => b.length > 0)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sanitize(raw: string): string {
|
|
118
|
+
return raw
|
|
119
|
+
.toLowerCase()
|
|
120
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
121
|
+
.replace(/^-+|-+$/g, '')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function git(root: string, args: ReadonlyArray<string>): string {
|
|
125
|
+
return execFileSync('git', ['-C', root, ...args], {
|
|
126
|
+
encoding: 'utf8',
|
|
127
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
128
|
+
}).trim()
|
|
129
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// `plumbbob start "<title>"` — scaffold the sidecar, record the baseline, enter
|
|
2
|
+
// DESIGN. Refuses on a dirty tree (D22), an existing session, or a non-git dir.
|
|
3
|
+
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { findRepoRoot, hasCommit, headSha, isDirty } from '../lib/git.ts'
|
|
8
|
+
import {
|
|
9
|
+
sidecarDir,
|
|
10
|
+
checkpointsPath,
|
|
11
|
+
configPath,
|
|
12
|
+
intentPath,
|
|
13
|
+
buildLogPath,
|
|
14
|
+
writeState,
|
|
15
|
+
hasSession,
|
|
16
|
+
excludeSidecar,
|
|
17
|
+
} from '../lib/sidecar.ts'
|
|
18
|
+
|
|
19
|
+
const DEFAULT_CHECK = 'pnpm run check'
|
|
20
|
+
|
|
21
|
+
export function start(cwd: string, args: ReadonlyArray<string>): number {
|
|
22
|
+
const positionals = args.filter((a) => !a.startsWith('--'))
|
|
23
|
+
const allowDirty = args.includes('--allow-dirty')
|
|
24
|
+
const title = (positionals[0] ?? '').trim()
|
|
25
|
+
|
|
26
|
+
if (title.length === 0) {
|
|
27
|
+
process.stderr.write('plumbbob: start needs a title. Try: plumbbob start "what you are building".\n')
|
|
28
|
+
return 1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const root = findRepoRoot(cwd)
|
|
32
|
+
if (root === null) {
|
|
33
|
+
process.stderr.write(
|
|
34
|
+
'plumbbob: not a git repository. Plumbbob records a baseline commit — run `git init` and make an initial commit first.\n',
|
|
35
|
+
)
|
|
36
|
+
return 1
|
|
37
|
+
}
|
|
38
|
+
if (!hasCommit(root)) {
|
|
39
|
+
process.stderr.write(
|
|
40
|
+
'plumbbob: this repository has no commits yet. Make an initial commit so `start` can record a baseline.\n',
|
|
41
|
+
)
|
|
42
|
+
return 1
|
|
43
|
+
}
|
|
44
|
+
if (hasSession(root)) {
|
|
45
|
+
process.stderr.write(
|
|
46
|
+
'plumbbob: a session is already active here. Run `plumbbob finish` to close it before starting another.\n',
|
|
47
|
+
)
|
|
48
|
+
return 1
|
|
49
|
+
}
|
|
50
|
+
if (isDirty(root)) {
|
|
51
|
+
if (!allowDirty) {
|
|
52
|
+
process.stderr.write(
|
|
53
|
+
'plumbbob: the working tree is dirty. Commit or stash first, or `plumbbob start --allow-dirty "<title>"` to record the current HEAD as the baseline.\n',
|
|
54
|
+
)
|
|
55
|
+
return 1
|
|
56
|
+
}
|
|
57
|
+
process.stderr.write(
|
|
58
|
+
'plumbbob: WARNING --allow-dirty: recording HEAD as baseline with a dirty tree. A later revert-to-baseline will DISCARD the uncommitted work.\n',
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const sha = headSha(root)
|
|
63
|
+
const check = detectCheck(root)
|
|
64
|
+
|
|
65
|
+
mkdirSync(sidecarDir(root), { recursive: true })
|
|
66
|
+
writeState(root, 'DESIGN')
|
|
67
|
+
writeFileSync(checkpointsPath(root), `baseline ${sha}\n`)
|
|
68
|
+
writeFileSync(configPath(root), `check=${check.command}\n`)
|
|
69
|
+
writeFileSync(intentPath(root), stamp(readTemplate('intent.md'), title, check.command))
|
|
70
|
+
writeFileSync(buildLogPath(root), stamp(readTemplate('build-log.md'), title, check.command))
|
|
71
|
+
excludeSidecar(root)
|
|
72
|
+
|
|
73
|
+
if (check.warn) {
|
|
74
|
+
process.stderr.write(
|
|
75
|
+
`plumbbob: WARNING the heavy check '${check.command}' is not defined in this repo's package.json. Edit .plumbbob/config (check=...) to set the real gate before \`review\`/\`done\`.\n`,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
process.stdout.write(
|
|
79
|
+
`plumbbob: started "${title}" — STATE=DESIGN, baseline ${sha.slice(0, 9)}. Frame and decide in .plumbbob/intent.md; flip to BUILD only once the decisions are made.\n`,
|
|
80
|
+
)
|
|
81
|
+
return 0
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// D24: default the heavy check to `pnpm run check`; warn (but still record it)
|
|
85
|
+
// when the target repo has no such script, so a non-pnpm repo gets a clear nudge.
|
|
86
|
+
function detectCheck(root: string): { readonly command: string; readonly warn: boolean } {
|
|
87
|
+
try {
|
|
88
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as {
|
|
89
|
+
scripts?: Record<string, string>
|
|
90
|
+
}
|
|
91
|
+
return { command: DEFAULT_CHECK, warn: pkg.scripts?.check === undefined }
|
|
92
|
+
} catch {
|
|
93
|
+
return { command: DEFAULT_CHECK, warn: true }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readTemplate(name: string): string {
|
|
98
|
+
return readFileSync(fileURLToPath(new URL(`../../templates/${name}`, import.meta.url)), 'utf8')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function stamp(template: string, title: string, check: string): string {
|
|
102
|
+
return template.split('{{TITLE}}').join(title).split('{{CHECK}}').join(check)
|
|
103
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// `plumbbob status` — print the session state, or NO ACTIVE SESSION. Read-only,
|
|
2
|
+
// always exits 0. Skills pre-inject this output to gate their own behavior.
|
|
3
|
+
|
|
4
|
+
import { findRepoRoot } from '../lib/git.ts'
|
|
5
|
+
import { hasSession, readState } from '../lib/sidecar.ts'
|
|
6
|
+
|
|
7
|
+
export function status(cwd: string): number {
|
|
8
|
+
const root = findRepoRoot(cwd)
|
|
9
|
+
if (root === null || !hasSession(root)) {
|
|
10
|
+
process.stdout.write('NO ACTIVE SESSION\n')
|
|
11
|
+
return 0
|
|
12
|
+
}
|
|
13
|
+
process.stdout.write(`STATE: ${readState(root) ?? 'UNKNOWN'}\n`)
|
|
14
|
+
return 0
|
|
15
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// `plumbbob wrap` (D19/D28) — the FINISH-entry verb. Sets STATE=FINISH so
|
|
2
|
+
// /plumbbob-report can write report.md and /plumbbob-docs can touch docs/.
|
|
3
|
+
// `finish` stays the closing gate; wrap just opens the one state where
|
|
4
|
+
// documentation may be projected. A transition verb, so dispatch refuses it under
|
|
5
|
+
// CLAUDECODE (D21).
|
|
6
|
+
|
|
7
|
+
import { findRepoRoot } from '../lib/git.ts'
|
|
8
|
+
import { hasSession, readState, writeState } from '../lib/sidecar.ts'
|
|
9
|
+
|
|
10
|
+
export function wrap(cwd: string): number {
|
|
11
|
+
const root = findRepoRoot(cwd)
|
|
12
|
+
if (root === null || !hasSession(root)) {
|
|
13
|
+
process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n')
|
|
14
|
+
return 1
|
|
15
|
+
}
|
|
16
|
+
const state = readState(root)
|
|
17
|
+
if (state === 'FINISH') {
|
|
18
|
+
process.stdout.write('plumbbob: already in FINISH. Run `/plumbbob-report`, then `plumbbob finish` to close.\n')
|
|
19
|
+
return 0
|
|
20
|
+
}
|
|
21
|
+
if (state !== 'DESIGN') {
|
|
22
|
+
process.stderr.write(
|
|
23
|
+
`plumbbob: wrap enters FINISH from DESIGN (current state is ${state ?? 'UNKNOWN'}). ` +
|
|
24
|
+
'Close the current step first — `done` from BUILD/REVIEW, or `spike done` from SPIKE.\n',
|
|
25
|
+
)
|
|
26
|
+
return 1
|
|
27
|
+
}
|
|
28
|
+
writeState(root, 'FINISH')
|
|
29
|
+
process.stdout.write(
|
|
30
|
+
'plumbbob: STATE=FINISH. Now `/plumbbob-report` writes the report (and `/plumbbob-docs` may touch docs/); ' +
|
|
31
|
+
'then `plumbbob finish` archives and closes.\n',
|
|
32
|
+
)
|
|
33
|
+
return 0
|
|
34
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
build-log.md — your live ledger for execution. Append constantly; reorganize at
|
|
3
|
+
step boundaries. The antidote to "my plan got lost in the noise."
|
|
4
|
+
|
|
5
|
+
Steps : where you are. One step in flight at a time.
|
|
6
|
+
Park list : where ideas go so you do not chase them. CAPTURE, never act inline.
|
|
7
|
+
Triage : the boundary ritual that keeps you on one branch.
|
|
8
|
+
Log : the audit trail. Feeds /plumbbob-report, then gets archived.
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
# Build log — {{TITLE}}
|
|
12
|
+
|
|
13
|
+
**Current step:** none (DESIGN) · **STATE:** DESIGN
|
|
14
|
+
**Heavy check:** {{CHECK}}
|
|
15
|
+
|
|
16
|
+
## Steps
|
|
17
|
+
|
|
18
|
+
*(Mirror of intent.md's Steps, with live status. Only ONE step is in flight. A step
|
|
19
|
+
is done only after `plumbbob done` — check green + checkpoint taken.)*
|
|
20
|
+
|
|
21
|
+
- ☐ 1. <step>
|
|
22
|
+
|
|
23
|
+
## Park list
|
|
24
|
+
|
|
25
|
+
> Mid-step, every new problem / idea / "ooh what if" lands HERE, untouched, and you
|
|
26
|
+
> go straight back to the step. Acting the instant an idea arrives is the disease.
|
|
27
|
+
> Capture is one line (`/park` composes it, or raw `plumbbob park`). Triage happens
|
|
28
|
+
> only at the boundary.
|
|
29
|
+
|
|
30
|
+
## Triage *(run at each step boundary, after green)*
|
|
31
|
+
|
|
32
|
+
Classify each parked item as exactly ONE. Naming it before acting is what keeps you
|
|
33
|
+
from sprawling across branches.
|
|
34
|
+
|
|
35
|
+
| Class | Meaning | Action |
|
|
36
|
+
|------------------|-------------------------------------------|-------------------------------|
|
|
37
|
+
| **blocker** | Plan was wrong/incomplete; can't proceed | `revert`, fold into intent.md |
|
|
38
|
+
| **tangent** | A different path, not clearly better | Defer or kill. Default here. |
|
|
39
|
+
| **pivot signal** | Evidence the whole approach is wrong | Stop. Replan deliberately. |
|
|
40
|
+
|
|
41
|
+
> Reality check: almost everything that *feels* like a pivot is a tangent. Require a
|
|
42
|
+
> failed assumption, not a shinier idea, before you pivot.
|
|
43
|
+
|
|
44
|
+
Triage results this boundary:
|
|
45
|
+
|
|
46
|
+
- (none yet)
|
|
47
|
+
|
|
48
|
+
## Log
|
|
49
|
+
|
|
50
|
+
*(Append-only. One decision or event per line, dated. What you point at to say "I
|
|
51
|
+
did that — the LLM helped, but those were my calls." `/plumbbob-report` reads this;
|
|
52
|
+
`plumbbob finish` archives it under `.plumbbob/archive/`.)*
|
|
53
|
+
|
|
54
|
+
- <date> — <decision / event / what shipped this step>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
intent.md — your canonical intent, written in DESIGN before any code. This is the
|
|
3
|
+
head; the chat is the hand. When the model floods you, read this, not your memory.
|
|
4
|
+
|
|
5
|
+
Size to the work: a small change fills Frame + a couple of Decisions and deletes
|
|
6
|
+
the rest; a medium feature fills it all. Opinionated where decided, explicit where
|
|
7
|
+
open. If the implementor (you-later, or the LLM) has to guess, the doc failed.
|
|
8
|
+
-->
|
|
9
|
+
|
|
10
|
+
# {{TITLE}}
|
|
11
|
+
|
|
12
|
+
**STATE:** DESIGN
|
|
13
|
+
**Phase** (bookkeeping while in DESIGN): frame
|
|
14
|
+
**Size:** tiny | small | medium
|
|
15
|
+
|
|
16
|
+
## Frame
|
|
17
|
+
|
|
18
|
+
*(You, on paper first. The problem in plain words — before any solution.)*
|
|
19
|
+
|
|
20
|
+
- **Problem:** <what is wrong or missing, and why it matters>
|
|
21
|
+
- **Smallest thing that solves it:** <the minimal change, not the ideal system>
|
|
22
|
+
- **Done looks like:** <the observable, checkable outcome>
|
|
23
|
+
- **Explicitly NOT doing:** <scope you are refusing, so it cannot creep in>
|
|
24
|
+
|
|
25
|
+
## Architecture sketch
|
|
26
|
+
|
|
27
|
+
*(Hand-drawn is best. Photograph it in, or describe the boxes and arrows.)*
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
<ascii, or a link to the paper sketch>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Decisions
|
|
34
|
+
|
|
35
|
+
*(One line each. Settled, not re-litigated in the chat. Grows as you resolve the
|
|
36
|
+
holes the interrogation surfaces, and as blockers fold in during BUILD.)*
|
|
37
|
+
|
|
38
|
+
- D1: <decision> — *because* <the one reason that mattered>
|
|
39
|
+
|
|
40
|
+
## Constraints
|
|
41
|
+
|
|
42
|
+
*(Hard rules the build must honor. Triage and review read against these.)*
|
|
43
|
+
|
|
44
|
+
- C1: <e.g. functional/procedural only; no new dependencies>
|
|
45
|
+
|
|
46
|
+
## Steps
|
|
47
|
+
|
|
48
|
+
*(The build plan. Each step carries its own seam — the paths `plumbbob build <n>`
|
|
49
|
+
writes into `.plumbbob/SEAM` and the seam-guard enforces. Keep each step small
|
|
50
|
+
enough to verify in one review pass. A blocker may rewrite a step's seam: fold the
|
|
51
|
+
new decision, revise the seam here, then `build <n>` again.)*
|
|
52
|
+
|
|
53
|
+
1. [ ] <step> — **done when:** <criterion, ideally a test or check result>
|
|
54
|
+
- seam: `<file>`, `<file>`
|
|
55
|
+
|
|
56
|
+
## Open questions
|
|
57
|
+
|
|
58
|
+
*(Holes you could NOT resolve on paper. Do not guess them into Decisions. A genuine
|
|
59
|
+
fork goes to a SPIKE; record the verdict below and in Decisions.)*
|
|
60
|
+
|
|
61
|
+
- Q1: <unresolved> — *resolve by:* decide | spike | ask
|
|
62
|
+
|
|
63
|
+
## Verdicts
|
|
64
|
+
|
|
65
|
+
*(Filled in as spikes and forks resolve — the audit trail of "these were my calls.")*
|
|
66
|
+
|
|
67
|
+
- <date> — <fork> → chose <option> because <reason>; deleted <the rest>
|