plumbbob 0.2.3 → 0.3.0

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.
@@ -0,0 +1,83 @@
1
+ // `plumbbob checkpoint [<n>] [-m <msg>]` — the executor-agnostic commit tick (D3).
2
+ // Unlike v1 `done`, it does NOT require BUILD state or a STEP file: the step is
3
+ // whatever you pass, else the in-flight STEP, else the next undone step in intent.
4
+ // It gates on a green check, then commits any pending work (or records the existing
5
+ // HEAD when the tree is already clean — the human's commit skill may have committed
6
+ // first), records the SHA, flips the intent checkbox to `[x]`, clears any STEP/SEAM,
7
+ // and returns to DESIGN. The diff's author is irrelevant: `/pb-build`, your hands,
8
+ // a vibe session, or another harness all checkpoint the same way.
9
+ import { appendFileSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
10
+ import { commit, findRepoRoot, headSha, isDirty, stageAll } from "../lib/git.js";
11
+ import { checkpointsPath, hasSession, intentPath, seamPath, stepPath, writeState } from "../lib/sidecar.js";
12
+ import { runCheck } from "../lib/check.js";
13
+ import { markStepDone, parseSteps } from "../lib/orient.js";
14
+ export function checkpoint(cwd, args) {
15
+ const root = findRepoRoot(cwd);
16
+ if (root === null || !hasSession(root)) {
17
+ process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n');
18
+ return 1;
19
+ }
20
+ const step = resolveStep(root, args);
21
+ if (step === null) {
22
+ process.stderr.write('plumbbob: no step to checkpoint — pass a number, or plan a step in intent.md first.\n');
23
+ return 1;
24
+ }
25
+ if (runCheck(root) !== 0) {
26
+ process.stderr.write('plumbbob: check failed (red) — checkpoint refuses on red. Fix it and re-run.\n');
27
+ return 1;
28
+ }
29
+ let sha;
30
+ if (isDirty(root)) {
31
+ stageAll(root);
32
+ sha = commit(root, messageArg(args) ?? `plumbbob: step ${step} done`);
33
+ }
34
+ else {
35
+ sha = headSha(root);
36
+ }
37
+ appendFileSync(checkpointsPath(root), `step ${step} ${sha}\n`);
38
+ flipIntent(root, step);
39
+ rmSync(seamPath(root), { force: true });
40
+ rmSync(stepPath(root), { force: true });
41
+ writeState(root, 'DESIGN');
42
+ process.stdout.write(`plumbbob: step ${step} checkpointed — ${sha.slice(0, 9)}. STATE=DESIGN.\n`);
43
+ return 0;
44
+ }
45
+ // Step resolution (D3): explicit arg > in-flight STEP file > first undone step in
46
+ // intent.md. Returns null when none can be determined.
47
+ function resolveStep(root, args) {
48
+ const explicit = args.find((a) => /^\d+$/.test(a));
49
+ if (explicit !== undefined) {
50
+ return Number(explicit);
51
+ }
52
+ const inFlight = readStep(root);
53
+ if (inFlight !== null) {
54
+ return inFlight;
55
+ }
56
+ try {
57
+ return parseSteps(readFileSync(intentPath(root), 'utf8')).find((s) => !s.done)?.n ?? null;
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ function flipIntent(root, step) {
64
+ try {
65
+ writeFileSync(intentPath(root), markStepDone(readFileSync(intentPath(root), 'utf8'), step));
66
+ }
67
+ catch {
68
+ // best-effort bookkeeping; the checkpoint SHA is the source of truth.
69
+ }
70
+ }
71
+ function messageArg(args) {
72
+ const i = args.indexOf('-m');
73
+ return i !== -1 && i + 1 < args.length ? (args[i + 1] ?? null) : null;
74
+ }
75
+ function readStep(root) {
76
+ try {
77
+ const raw = readFileSync(stepPath(root), 'utf8').trim();
78
+ return /^\d+$/.test(raw) ? Number(raw) : null;
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
@@ -0,0 +1,54 @@
1
+ // `plumbbob reset` (D9) — the v2 close-out, replacing the v1
2
+ // wrap → report → docs → finish ceremony. It archives intent + build-log + report
3
+ // (the `/pb-reset` skill writes the report by default) under .plumbbob/archive/,
4
+ // clears the active files, and deletes the control state (STATE last). Unlike v1
5
+ // `finish` there is NO refuse-without-report gate — guidance offers the artifact, it
6
+ // does not wall the exit. Archive-then-clear, never destroy (C4); git untouched (C5).
7
+ import { appendFileSync, existsSync, readFileSync, rmSync } from 'node:fs';
8
+ import { join, relative } from 'node:path';
9
+ import { findRepoRoot } from "../lib/git.js";
10
+ import { buildLogPath, checkpointsPath, hasSession, intentPath, seamPath, sidecarDir, stepPath } from "../lib/sidecar.js";
11
+ import { archiveSession, reportPath } from "../lib/archive.js";
12
+ export function reset(cwd) {
13
+ const root = findRepoRoot(cwd);
14
+ if (root === null || !hasSession(root)) {
15
+ process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n');
16
+ return 1;
17
+ }
18
+ if (existsSync(reportPath(root))) {
19
+ appendCheckpointShas(root);
20
+ }
21
+ else {
22
+ process.stderr.write('plumbbob: note — no report.md found; archiving intent + build-log without one ' +
23
+ '(/pb-reset normally writes the report first). No gate (D9).\n');
24
+ }
25
+ const archived = archiveSession(root);
26
+ // Clear the active files — now safely archived — then the control state, STATE
27
+ // last so "no session" flips exactly at the end.
28
+ rmSync(intentPath(root), { force: true });
29
+ rmSync(buildLogPath(root), { force: true });
30
+ rmSync(reportPath(root), { force: true });
31
+ rmSync(seamPath(root), { force: true });
32
+ rmSync(stepPath(root), { force: true });
33
+ rmSync(join(sidecarDir(root), 'STATE'), { force: true });
34
+ process.stdout.write(`plumbbob: reset — archived to ${relative(root, archived)}. Sidecar cleared. ` +
35
+ 'Run `/pb-plan` (or `plumbbob start "<title>"`) to frame the next goal.\n');
36
+ return 0;
37
+ }
38
+ // Append the recorded checkpoints (baseline + each `step n <sha>`) to the report so
39
+ // the archived report lists the SHAs.
40
+ function appendCheckpointShas(root) {
41
+ let raw = '';
42
+ try {
43
+ raw = readFileSync(checkpointsPath(root), 'utf8');
44
+ }
45
+ catch {
46
+ raw = '';
47
+ }
48
+ const lines = raw
49
+ .split('\n')
50
+ .map((l) => l.trim())
51
+ .filter((l) => l.length > 0)
52
+ .map((l) => `- ${l}`);
53
+ appendFileSync(reportPath(root), ['', '## Checkpoints', '', ...lines, ''].join('\n'));
54
+ }
@@ -24,7 +24,7 @@ import { join, sep } from 'node:path';
24
24
  import { fileURLToPath } from 'node:url';
25
25
  import { findRepoRoot } from "../lib/git.js";
26
26
  import { mergeRegistration, readSettings, stripRegistration, writeSettings } from "../lib/settings.js";
27
- const HOOK_FILES = ['pre-edit.sh', 'bash-guard.sh', 'post-edit.sh'];
27
+ const HOOK_FILES = ['post-edit.sh'];
28
28
  // The placeholder every skill carries in place of its `plumbbob` invocation;
29
29
  // setup substitutes it with the form the chosen install shape resolves.
30
30
  const BIN_PLACEHOLDER = '__PLUMBBOB_BIN__';
@@ -1,13 +1,32 @@
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.
1
+ // `plumbbob status` — the orientation dashboard (D8/D15), or NO ACTIVE SESSION.
2
+ // Read-only, always exits 0. Skills pre-inject this output to gate their own
3
+ // behavior, so the `NO ACTIVE SESSION` sentinel is kept exact.
4
+ import { readFileSync } from 'node:fs';
3
5
  import { findRepoRoot } from "../lib/git.js";
4
- import { hasSession, readState } from "../lib/sidecar.js";
6
+ import { buildLogPath, checkpointsPath, hasSession, intentPath, readState, stepPath } from "../lib/sidecar.js";
7
+ import { formatOrientation, orient } from "../lib/orient.js";
8
+ function readOr(path) {
9
+ try {
10
+ return readFileSync(path, 'utf8');
11
+ }
12
+ catch {
13
+ return '';
14
+ }
15
+ }
5
16
  export function status(cwd) {
6
17
  const root = findRepoRoot(cwd);
7
18
  if (root === null || !hasSession(root)) {
8
19
  process.stdout.write('NO ACTIVE SESSION\n');
9
20
  return 0;
10
21
  }
11
- process.stdout.write(`STATE: ${readState(root) ?? 'UNKNOWN'}\n`);
22
+ const inFlightRaw = readOr(stepPath(root)).trim();
23
+ const orientation = orient({
24
+ state: readState(root) ?? 'UNKNOWN',
25
+ intent: readOr(intentPath(root)),
26
+ buildLog: readOr(buildLogPath(root)),
27
+ checkpoints: readOr(checkpointsPath(root)),
28
+ inFlight: /^\d+$/.test(inFlightRaw) ? Number(inFlightRaw) : null,
29
+ });
30
+ process.stdout.write(`${formatOrientation(orientation)}\n`);
12
31
  return 0;
13
32
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "plumbbob",
3
- "version": "0.2.3",
4
- "description": "Attention-first build process: a CLI + hooks that enforce the deciding/executing boundary.",
3
+ "version": "0.3.0",
4
+ "description": "Attention-first build process: eight skills + a CLI that keep you the decider — guidance, not enforcement.",
5
5
  "keywords": [
6
6
  "claude-code",
7
7
  "claude",
@@ -1,19 +1,49 @@
1
1
  ---
2
2
  name: pb-build
3
- description: Human-triggered driver for `plumbbob build <n>` write the SEAM for a step and enter BUILD (edits unlocked to those paths only), from the chat.
3
+ description: The optional engineread the current planned step from intent, implement it (its done-when, seam, Decisions, Constraints), then verify it through to the approval pause. Skip it entirely to implement by hand, vibed, or with another harness.
4
4
  disable-model-invocation: true
5
- model: haiku
6
- allowed-tools: Bash(__PLUMBBOB_BIN__ status:*), Bash(__PLUMBBOB_BIN__ build:*)
5
+ model: opus
6
+ allowed-tools: Read, Edit, Write, Bash(__PLUMBBOB_BIN__ status:*), Bash(__PLUMBBOB_BIN__ build:*), Bash(__PLUMBBOB_BIN__ check:*), Bash(__PLUMBBOB_BIN__ checkpoint:*), Bash(git diff:*)
7
7
  ---
8
8
 
9
- # Plumbbob — build a step (driver)
9
+ # Plumbbob — build a step (the optional engine)
10
10
 
11
11
  Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status`
12
12
 
13
- This is a **driver skill** — a chat-side trigger for the mechanical `plumbbob build` verb, so the whole workflow runs from the agent window instead of a terminal. It is `disable-model-invocation: true`: only the human fires it. It carries **no Edit and no Write tool** — its only action is to shell the verb and report the verb's output verbatim, including any refusal. The CLI is the source of truth: never retry a refused transition, and never edit a file to work around one.
13
+ This is the **bundled executor** — one way to turn a planned step into code. It is
14
+ **optional** (D3): you can implement any step by hand, in a vibe session, or with
15
+ another harness and go straight to `/pb-verify` instead — plumbbob does not care how
16
+ the diff appeared. When you do run it, it reads the plan, writes the step, and
17
+ carries straight through to the verify pause.
14
18
 
15
- ## What it does
19
+ ## What this skill does, in order
16
20
 
17
- 1. Read the step number from the way you were invoked (e.g. `/pb-build 3` → step `3`). If no number is present, ask for one and run nothing.
18
- 2. Run `__PLUMBBOB_BIN__ build <n>` via Bash.
19
- 3. Report the verb's output verbatim — the SEAM it wrote and the new state, or any refusal.
21
+ 1. **Pick the step.** Use the number you were invoked with (e.g. `/pb-build 4`), else
22
+ the next undone, planned step in `.plumbbob/intent.md`. If there is no planned step
23
+ to build, stop and tell the human to `/pb-step` first.
24
+ 2. **Enter the step.** Run `__PLUMBBOB_BIN__ build <n>` (records the in-flight STEP +
25
+ SEAM so `/pb-status` shows the step in flight; in v2 the seam is awareness, not a
26
+ lock).
27
+ 3. **Read the plan.** Read the step's **done-when**, its **seam**, and the
28
+ **Decisions** and **Constraints** in `intent.md`. Build to *that* — the deciding
29
+ already happened, off the chat.
30
+ 4. **Implement** the step, and only that step, staying within the declared seam. A
31
+ new problem or "ooh what if" that surfaces mid-build is a `/pb-park`, **not** an
32
+ edit — capture it and stay on the step. If you genuinely cannot finish without
33
+ touching more than the seam, that is scope drift: surface it to the human rather
34
+ than sprawling.
35
+ 5. **Verify, through to the pause.** Run the verify tick exactly as `/pb-verify`
36
+ does: `__PLUMBBOB_BIN__ check` → self-review the diff against the done-when, the
37
+ Decisions, and the Constraints (a single structured read, D16) → validate → **PAUSE
38
+ for the human's approval** → only on approval, checkpoint with
39
+ `__PLUMBBOB_BIN__ checkpoint`. Do **not** bump the version or changelog — that is
40
+ the human's `/version` call.
41
+
42
+ ## The hard contracts
43
+
44
+ - **Optional, never required.** The loop works without this skill; `/pb-verify`
45
+ checkpoints a hand-built or vibed diff just the same (D3).
46
+ - **Build the decided step, not a new one.** Implement what `intent.md` settled. A
47
+ new idea mid-build is a `/pb-park`, not an edit.
48
+ - **Always end at the pause.** Implement → verify → wait for approval. Never
49
+ checkpoint without it; the human is the clock.
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: pb-harvest
3
+ description: Triage the park list at a step boundary — propose one class (blocker/tangent/pivot) per parked item, write only after the human confirms each, record under ## Harvest, and fold a confirmed blocker into intent.
4
+ disable-model-invocation: true
5
+ model: opus
6
+ allowed-tools: Read, Edit, Bash(__PLUMBBOB_BIN__ status:*)
7
+ ---
8
+
9
+ # Plumbbob — harvest the park list
10
+
11
+ Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status`
12
+
13
+ `/pb-harvest` is the complement of `/pb-park` (D7): you parked ideas as seeds during a
14
+ build; now, at a boundary, you harvest them — decide what each one is.
15
+
16
+ ## When to run it — wrong-state refusal
17
+
18
+ Harvest at a **step boundary**: after a step is checkpointed and you are back in
19
+ DESIGN, not mid-step. Read the state injected above:
20
+
21
+ - `NO ACTIVE SESSION` — **refuse**; tell the human to `plumbbob start "<title>"` first.
22
+ - `BUILD` — a step is in flight; **refuse** and suggest finishing it with `/pb-verify`
23
+ before harvesting. Chasing parked items mid-step is the disease parking prevents.
24
+ - `DESIGN` (and other settled states) — go ahead.
25
+
26
+ ## What this skill does
27
+
28
+ Walk the **Park list** in `build-log.md` item by item and, for each, **propose exactly
29
+ one class**:
30
+
31
+ - **blocker** — the plan was wrong or incomplete; can't proceed. Folds into `intent.md`
32
+ and is handled now.
33
+ - **tangent** — a different path, not clearly better. **The default** — defer or kill.
34
+ - **pivot signal** — real evidence the whole approach is wrong. Stop and replan.
35
+
36
+ ## The one hard contract
37
+
38
+ You **propose**; the **human calls it**. For each item, state your proposed class and
39
+ one line of reasoning, then **wait for the human to confirm or override**. Write
40
+ **only after** per-item confirmation:
41
+
42
+ - Record each confirmed class in `build-log.md`'s `## Harvest` section.
43
+ - **Flip the harvested item** from `- [ ]` to `- [x]` in the Park list, so `/pb-status`
44
+ stops counting it as open.
45
+ - A confirmed **blocker** also folds its decision into `intent.md`.
46
+ - Never reclassify or resolve an item the human hasn't confirmed, and default every
47
+ uncertain item to **tangent**, never to blocker.
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: pb-park
3
+ description: Compose one tidy tagged park line, get the human's OK in-turn, then capture it by shelling `plumbbob park` — never by editing a file. The capture half of the park/harvest loop.
4
+ disable-model-invocation: true
5
+ model: haiku
6
+ allowed-tools: Bash(__PLUMBBOB_BIN__ status:*), Bash(__PLUMBBOB_BIN__ park:*)
7
+ ---
8
+
9
+ # Plumbbob — park an idea (capture, don't chase)
10
+
11
+ Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status`
12
+
13
+ `/pb-park` is the **capture** half of the loop; `/pb-harvest` is where parked items
14
+ get triaged later (D7). Capturing the instant an idea arrives — instead of acting on
15
+ it — is the whole point: it protects the step in flight.
16
+
17
+ ## Wrong-state refusal
18
+
19
+ Parking needs an **active session** to capture into. Read the state injected above: if
20
+ it is `NO ACTIVE SESSION`, **refuse** and tell the human to run `plumbbob start
21
+ "<title>"` first. Every active state (`DESIGN`, `BUILD`, `SPIKE`, `FINISH`) is fine —
22
+ capture is always available, which is the whole point of parking.
23
+
24
+ ## What this skill does
25
+
26
+ Take the idea, problem, or "ooh what if" the human just had and **compose it into one
27
+ tidy, tagged line** — short, legible, self-contained, so it still reads cleanly weeks
28
+ later. Then:
29
+
30
+ 1. **Show the composed line to the human** and wait for **in-turn approval** — they
31
+ confirm it as-is or edit the wording.
32
+ 2. **Only after** that approval, capture it by running `__PLUMBBOB_BIN__ park "<the
33
+ approved line>"` via Bash.
34
+
35
+ ## The one hard contract
36
+
37
+ The capture itself is the **dumb CLI**, never an edit. This skill carries **no Edit and
38
+ no Write tool** on purpose: you may not append to `build-log.md` yourself. Compose, get
39
+ approval, then shell `__PLUMBBOB_BIN__ park` — that is the only write path. If approval
40
+ never comes, capture nothing.
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: pb-plan
3
+ description: Frame a fresh goal — scaffold the session and author the intent's Frame, Decisions, and Constraints before any code. Steps stay empty; they come one at a time from /pb-step.
4
+ disable-model-invocation: true
5
+ model: opus
6
+ allowed-tools: Read, Edit, Write, Bash(__PLUMBBOB_BIN__ status:*), Bash(__PLUMBBOB_BIN__ start:*)
7
+ ---
8
+
9
+ # Plumbbob — frame a goal (the whole-goal move)
10
+
11
+ Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status`
12
+
13
+ `/pb-plan` is the **whole-goal** move — it opens a session and gets the deciding out
14
+ of your head and onto `intent.md` *before* any code. (Planning a single increment is
15
+ the separate `/pb-step` move; do not confuse the two.)
16
+
17
+ ## What this skill does
18
+
19
+ 1. **Scaffold.** If there is no active session, run `__PLUMBBOB_BIN__ start "<title>"`
20
+ to create `.plumbbob/` (STATE=DESIGN, baseline recorded). If a session already
21
+ exists, say so and edit the existing `intent.md` rather than starting over.
22
+ 2. **Frame** (`.plumbbob/intent.md`), with the human: the **Problem** in plain words,
23
+ the **smallest thing** that solves it, what **done looks like**, and what you are
24
+ **explicitly NOT doing**. This is the human's convergence — propose wording, but
25
+ the human decides every line.
26
+ 3. **Decisions & Constraints.** Record the settled calls (one line each, with the
27
+ *because*) and the hard rules the build must honor. An unresolved hole goes to
28
+ **Open questions**, never guessed into a Decision.
29
+ 4. **Leave `## Steps` empty.** Steps are planned just-in-time (D6) — one at a time
30
+ with `/pb-step` — so do not write the build plan here.
31
+
32
+ ## The hard contracts
33
+
34
+ - **Deciding before code.** `/pb-plan` writes `intent.md` only — never source.
35
+ - **The human converges.** You surface options and draft wording; the human picks.
36
+ An unresolved hole is an Open question, not a guessed Decision.
37
+ - **Size to the work.** A small change fills Frame + a couple of Decisions and stops;
38
+ ceremony on a one-liner is the failure mode, not thoroughness.
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: pb-reset
3
+ description: Close out the build — write the report (what shipped, decisions, parked/harvested items, deferred tangents), then archive intent + build-log + report and clear for a fresh goal. Report by default, no gate.
4
+ disable-model-invocation: true
5
+ model: opus
6
+ allowed-tools: Read, Write, Bash(__PLUMBBOB_BIN__ status:*), Bash(__PLUMBBOB_BIN__ reset:*)
7
+ ---
8
+
9
+ # Plumbbob — reset for a new goal (the close-out)
10
+
11
+ Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status`
12
+
13
+ `/pb-reset` ends the build: it captures what happened, archives it, and clears the
14
+ sidecar for the next goal. **Report by default** (D9) — no refuse-without-report gate,
15
+ and no separate docs phase.
16
+
17
+ ## What this skill does, in order
18
+
19
+ 1. **Write the report** to `.plumbbob/report.md`, from `intent.md` + `build-log.md`:
20
+ - **What shipped** — the steps completed and what they delivered.
21
+ - **Decisions and why** — the settled calls that shaped the build.
22
+ - **Parked & harvested** — what was captured and how each was classified.
23
+ - **Final status** — done or partial, and what is left.
24
+ - **Deferred tangents** — the harvested items that become future work.
25
+ This is the "yeah, I did that" artifact. Write it by default; the human may edit it.
26
+ 2. **Archive & clear.** Run `__PLUMBBOB_BIN__ reset`, which appends the checkpoint SHAs
27
+ to the report, archives intent + build-log + report to
28
+ `.plumbbob/archive/<date>-<slug>/`, and clears the sidecar (STATE last). Git is not
29
+ touched.
30
+ 3. **Point at the next goal** — `/pb-plan` to frame the next one.
31
+
32
+ ## The hard contracts
33
+
34
+ - **Report by default, never a gate.** Always offer the report; never wall the exit. A
35
+ bug fix's report can be three lines — size it to the work.
36
+ - **Archive-then-clear, never destroy** (C4). The archive is the record; the active
37
+ files only clear once they are safely copied.
38
+ - **No version bump, no docs phase.** Updating real docs is a separate, explicit ask;
39
+ cutting a release is the human's `/version`.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: pb-status
3
+ description: Show the orientation dashboard — where you are, what's done, what's parked, and the next move. A thin trigger for `plumbbob status`.
4
+ disable-model-invocation: true
5
+ model: haiku
6
+ allowed-tools: Bash(__PLUMBBOB_BIN__ status:*)
7
+ ---
8
+
9
+ # Plumbbob — orient (the where-am-I move)
10
+
11
+ Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status`
12
+
13
+ This is a **thin driver** for `__PLUMBBOB_BIN__ status`. The dashboard above is your
14
+ orientation — the intent, the step list, the parked/open-question counts, and the
15
+ inferred next move. Report it verbatim and point the human at that next move. This
16
+ skill carries **no Edit and no Write tool**: the CLI is the source of truth, so
17
+ **never retry**, and never edit a file to change what the orientation says.
18
+
19
+ ## What it does
20
+
21
+ 1. Surface the injected `status` output — the dashboard and its suggested next move.
22
+ 2. If it reads `NO ACTIVE SESSION`, tell the human to `/pb-plan` to frame a goal.
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: pb-step
3
+ description: Plan the next increment — propose one small step with a done-when criterion and a seam, get the human's OK, and append it to intent's ## Steps. One by default; several only if the human already knows them.
4
+ disable-model-invocation: true
5
+ model: opus
6
+ allowed-tools: Read, Edit, Bash(__PLUMBBOB_BIN__ status:*)
7
+ ---
8
+
9
+ # Plumbbob — plan the next step (the single-increment move)
10
+
11
+ Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status`
12
+
13
+ `/pb-step` plans the **next increment** just-in-time (D6) — the smallest verifiable
14
+ piece of the framed goal, planned only when you reach it. (Framing the whole goal is
15
+ the separate `/pb-plan` move.)
16
+
17
+ ## What this skill does
18
+
19
+ 1. **Read the plan.** Read `intent.md`'s Frame, Decisions, Constraints, and the steps
20
+ already done, to see what the *next* increment should be.
21
+ 2. **Propose one step.** Draft a single step:
22
+ - a one-line **title**,
23
+ - a **done-when** criterion — ideally a test or check result, something
24
+ `/pb-verify` can actually validate,
25
+ - a **seam**: the specific files it should touch (exact paths, or a `dir/` grant).
26
+ Keep it small enough to verify in one review pass.
27
+ 3. **Get the human's OK**, then **append** it to `## Steps` in the standard format —
28
+ `N. [ ] <title> — **done when:** <criterion>` with a `- seam:` sub-line. Default
29
+ to **one** step; plan several only when the human already knows them.
30
+
31
+ ## The hard contracts
32
+
33
+ - **One verifiable increment.** Each step carries a done-when `/pb-verify` can check
34
+ and a seam small enough to review in one pass.
35
+ - **Append to `## Steps` only**, in the standard format `status` and `build` parse —
36
+ never the Roadmap, never loose prose.
37
+ - **The human approves the step** before it lands. You propose; they decide.
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: pb-verify
3
+ description: The verify tick — run the check, self-review the diff against intent, validate the step's done-when, pause for your approval, then checkpoint. Executor-agnostic: it reads the diff, not who wrote it.
4
+ disable-model-invocation: true
5
+ model: opus
6
+ allowed-tools: Read, Bash(__PLUMBBOB_BIN__ status:*), Bash(__PLUMBBOB_BIN__ check:*), Bash(__PLUMBBOB_BIN__ checkpoint:*), Bash(git diff:*), Bash(git status:*)
7
+ ---
8
+
9
+ # Plumbbob — verify a step (the tick)
10
+
11
+ Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status`
12
+
13
+ This is the **tick** — the one beat where the human is the clock. Whatever produced
14
+ the current diff — `/pb-build`, your own hands, a vibe session, another harness —
15
+ this skill verifies it the same way: **it reads the diff, not the author** (D3).
16
+
17
+ ## What this skill does, in order
18
+
19
+ 1. **Check.** Run `__PLUMBBOB_BIN__ check` (the heavy gate). If it comes back
20
+ **red**, stop here: report what failed and do **not** pause for approval — there
21
+ is nothing to approve yet. The human fixes it and re-invokes.
22
+ 2. **Self-review** *(a single structured read, D16)*. Read `git diff` and
23
+ `.plumbbob/intent.md`, then in one pass check the diff against:
24
+ - the current step's **done-when** criterion — is it actually met?
25
+ - the **Decisions** — does anything contradict a settled call?
26
+ - the **Constraints** — are any violated?
27
+ Surface every mismatch plainly. You are reviewing, not building — do not fix anything.
28
+ 3. **Validate.** State, yes or no, whether the step's done-when is met, with the evidence.
29
+ 4. **PAUSE.** Present the check result, the self-review, and the validation, then
30
+ **stop and wait for the human's explicit approval.** This is the convergence beat;
31
+ the human is the clock. Never checkpoint without it.
32
+ 5. **Checkpoint** *(only after approval)*. Commit the work — the human's
33
+ commit-with-TIL skill for a rich message, or let `checkpoint` make the WIP commit
34
+ — then run `__PLUMBBOB_BIN__ checkpoint` to record the SHA, flip the step to done,
35
+ and return to DESIGN. Do **not** bump the version or touch the changelog — that is
36
+ the human's `/version` call.
37
+
38
+ ## The hard contracts
39
+
40
+ - **Never skip the pause.** Check → self-review → validate, then wait. Approval is
41
+ the only thing that triggers the checkpoint.
42
+ - **Read the diff, not the author** (D3). Verify identically whether the code was
43
+ built by `/pb-build`, by hand, vibed, or by another harness.
44
+ - **Red means stop, not pause.** A failing check is not an approval decision; report
45
+ it and end your turn.
46
+ - **You review; you do not build.** If the self-review finds a problem, surface it
47
+ and stop — fixing is a new build beat, not part of verify.
@@ -1,63 +0,0 @@
1
- // `plumbbob done` — the step gate. Refuses on a red check, then stages the whole
2
- // step (D8: `git add -A`; the sidecar is git-excluded so it never lands), warns
3
- // about anything committed outside the SEAM, takes the checkpoint commit, records
4
- // its SHA, and returns to DESIGN.
5
- import { appendFileSync, readFileSync, rmSync } from 'node:fs';
6
- import { commit, findRepoRoot, stageAll, stagedPaths } from "../lib/git.js";
7
- import { checkpointsPath, hasSession, readState, seamPath, stepPath, writeState } from "../lib/sidecar.js";
8
- import { runCheck } from "../lib/check.js";
9
- import { matchesSeam } from "../lib/intent.js";
10
- export function done(cwd) {
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 !== 'BUILD' && state !== 'REVIEW') {
18
- process.stderr.write(`plumbbob: done runs from BUILD or REVIEW (current state is ${state ?? 'UNKNOWN'}). Run \`plumbbob build <n>\` first.\n`);
19
- return 1;
20
- }
21
- const step = readStepNumber(root);
22
- if (step === null) {
23
- process.stderr.write('plumbbob: no in-flight step — run `plumbbob build <n>` first.\n');
24
- return 1;
25
- }
26
- if (runCheck(root) !== 0) {
27
- process.stderr.write('plumbbob: check failed (red) — done refuses on red. Fix it and re-run `done`.\n');
28
- return 1;
29
- }
30
- stageAll(root);
31
- const seam = readSeamTokens(root);
32
- const outside = stagedPaths(root).filter((p) => !matchesSeam(p, seam));
33
- if (outside.length > 0) {
34
- process.stderr.write(`plumbbob: WARNING committed paths outside the SEAM: ${outside.join(', ')}. The checkpoint captures them, but scope drift may mean the plan needs revising.\n`);
35
- }
36
- const sha = commit(root, `plumbbob: step ${step} done`);
37
- appendFileSync(checkpointsPath(root), `step ${step} ${sha}\n`);
38
- rmSync(seamPath(root), { force: true });
39
- rmSync(stepPath(root), { force: true });
40
- writeState(root, 'DESIGN');
41
- process.stdout.write(`plumbbob: step ${step} done — checkpoint ${sha.slice(0, 9)}. STATE=DESIGN.\n`);
42
- return 0;
43
- }
44
- function readStepNumber(root) {
45
- try {
46
- const raw = readFileSync(stepPath(root), 'utf8').trim();
47
- return /^\d+$/.test(raw) ? Number(raw) : null;
48
- }
49
- catch {
50
- return null;
51
- }
52
- }
53
- function readSeamTokens(root) {
54
- try {
55
- return readFileSync(seamPath(root), 'utf8')
56
- .split('\n')
57
- .map((l) => l.trim())
58
- .filter((l) => l.length > 0);
59
- }
60
- catch {
61
- return [];
62
- }
63
- }
@@ -1,54 +0,0 @@
1
- // `plumbbob finish` (D19/D20) — the closing gate, symmetric with the step gate.
2
- // Refuses unless a report exists, appends the checkpoint SHA list to it, archives
3
- // intent + build-log + report under .plumbbob/archive/, clears the active files,
4
- // and deletes SEAM, STEP, then STATE LAST (deleting STATE is what switches the
5
- // muzzle off, so it happens exactly at session end). Never touches git (C5).
6
- import { appendFileSync, existsSync, readFileSync, rmSync } from 'node:fs';
7
- import { join, relative } from 'node:path';
8
- import { findRepoRoot } from "../lib/git.js";
9
- import { buildLogPath, checkpointsPath, hasSession, intentPath, seamPath, sidecarDir, stepPath, } from "../lib/sidecar.js";
10
- import { archiveSession, reportPath } from "../lib/archive.js";
11
- export function finish(cwd) {
12
- const root = findRepoRoot(cwd);
13
- if (root === null || !hasSession(root)) {
14
- process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n');
15
- return 1;
16
- }
17
- if (!existsSync(reportPath(root))) {
18
- process.stderr.write('plumbbob: finish refuses without a report — run `/plumbbob-report` first (it writes .plumbbob/report.md). ' +
19
- 'The closing gate is symmetric with the step gate: you do not walk away without capturing what happened.\n');
20
- return 1;
21
- }
22
- appendCheckpointShas(root);
23
- const archived = archiveSession(root);
24
- // Clear the active files — now safely archived.
25
- rmSync(intentPath(root), { force: true });
26
- rmSync(buildLogPath(root), { force: true });
27
- rmSync(reportPath(root), { force: true });
28
- // Delete the control files LAST, STATE the very last: while STATE exists the
29
- // muzzle is live, so it comes off only once everything else is torn down.
30
- rmSync(seamPath(root), { force: true });
31
- rmSync(stepPath(root), { force: true });
32
- rmSync(join(sidecarDir(root), 'STATE'), { force: true });
33
- process.stdout.write(`plumbbob: finished — archived to ${relative(root, archived)}. STATE cleared (muzzle off). ` +
34
- 'Run `plumbbob start "<title>"` to begin the next task.\n');
35
- return 0;
36
- }
37
- // Append the recorded checkpoints (baseline + each `step n <sha>`) to the report,
38
- // so the archived report lists the SHAs (spec: "finish lists the SHAs in the
39
- // report").
40
- function appendCheckpointShas(root) {
41
- let raw = '';
42
- try {
43
- raw = readFileSync(checkpointsPath(root), 'utf8');
44
- }
45
- catch {
46
- raw = '';
47
- }
48
- const lines = raw
49
- .split('\n')
50
- .map((l) => l.trim())
51
- .filter((l) => l.length > 0)
52
- .map((l) => `- ${l}`);
53
- appendFileSync(reportPath(root), ['', '## Checkpoints', '', ...lines, ''].join('\n'));
54
- }