plumbbob 0.3.1 → 0.4.2
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 +11 -0
- package/README.md +199 -53
- package/dist/cli-core.js +83 -0
- package/dist/cli.js +6 -82
- package/dist/lib/archive.js +1 -1
- package/dist/lib/orient.js +35 -15
- package/dist/verbs/check.js +1 -1
- package/dist/verbs/checkpoint.js +1 -1
- package/dist/verbs/doctor.js +44 -142
- package/dist/verbs/init.js +81 -0
- package/dist/verbs/{reset.js → wrap.js} +7 -7
- package/hooks/hooks.json +12 -0
- package/package.json +4 -3
- package/skills/build/SKILL.md +70 -0
- package/skills/{pb-harvest → harvest}/SKILL.md +6 -6
- package/skills/{pb-park → park}/SKILL.md +6 -6
- package/skills/plan/SKILL.md +85 -0
- package/skills/refine/SKILL.md +45 -0
- package/skills/{pb-revert → revert}/SKILL.md +5 -5
- package/skills/{pb-spike → spike}/SKILL.md +5 -5
- package/skills/{pb-status → status}/SKILL.md +5 -5
- package/skills/step/SKILL.md +49 -0
- package/skills/{pb-verify → verify}/SKILL.md +8 -8
- package/skills/{pb-reset → wrap}/SKILL.md +8 -8
- package/templates/build-log.md +14 -14
- package/templates/intent.md +10 -6
- package/dist/lib/settings.js +0 -83
- package/dist/verbs/setup.js +0 -163
- package/skills/pb-build/SKILL.md +0 -49
- package/skills/pb-plan/SKILL.md +0 -38
- package/skills/pb-step/SKILL.md +0 -37
- package/skills/plumbbob-interrogate/SKILL.md +0 -33
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: refine
|
|
3
|
+
description: Keep intent.md true — attack the plan for holes (append as Open questions) and refine or repair the Frame, Decisions, Constraints, and Steps to match reality. Usable at any point; you propose, the human approves.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
model: opus
|
|
6
|
+
allowed-tools: Read, Edit, Bash(plumbbob status:*)
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Plumbbob — refine the plan
|
|
10
|
+
|
|
11
|
+
Current session state (injected when this skill runs): !`plumbbob status 2>/dev/null || echo "plumbbob CLI not found - install the dep and re-run: npm i -g plumbbob && plumbbob init"`
|
|
12
|
+
|
|
13
|
+
`/plumbbob:refine` keeps `intent.md` honest. Use it at **any point** — right after `/plumbbob:plan`
|
|
14
|
+
to stress-test a fresh frame, or mid-build to repair a plan that drifted from what the
|
|
15
|
+
code is actually doing. It is the document-level complement to `/plumbbob:step` (which only
|
|
16
|
+
sharpens the *next* step): `/plumbbob:refine` works the *whole* plan.
|
|
17
|
+
|
|
18
|
+
## No-session refusal
|
|
19
|
+
|
|
20
|
+
This skill refines an existing plan, so it needs one. Read the state injected above: if
|
|
21
|
+
it is `NO ACTIVE SESSION`, **refuse** in one line and tell the human to run
|
|
22
|
+
`plumbbob start "<title>"` (or `/plumbbob:plan`) first, and edit nothing. Every active state
|
|
23
|
+
is fine — refining is always available.
|
|
24
|
+
|
|
25
|
+
## Two modes
|
|
26
|
+
|
|
27
|
+
- **Attack (diverge in the problem space).** Hand the **Frame**, **Decisions**, and
|
|
28
|
+
**Architecture sketch** a cold, adversarial read and surface holes — ambiguities,
|
|
29
|
+
unhandled edge cases, hidden assumptions, collisions with the existing code. Append
|
|
30
|
+
each as a one-line question under `## Open questions`, **never** as a Decision:
|
|
31
|
+
resolving a hole is the human's convergence, not yours. This mode surfaces; it does
|
|
32
|
+
not decide.
|
|
33
|
+
- **Repair (re-sync to reality).** When the plan has drifted — a Decision was overtaken
|
|
34
|
+
by what you built, a Constraint changed, a Step no longer matches the seam — propose
|
|
35
|
+
the edits that bring `intent.md` back in line with the truth. Show the before/after for
|
|
36
|
+
each, and **write only what the human approves**.
|
|
37
|
+
|
|
38
|
+
## The hard contracts
|
|
39
|
+
|
|
40
|
+
- **You propose; the human converges.** Surface holes and draft repairs, but the human
|
|
41
|
+
approves every change to `intent.md`. Never guess a hole into a Decision.
|
|
42
|
+
- **Open questions for holes, edits for drift.** New uncertainty goes to
|
|
43
|
+
`## Open questions`; settled drift gets repaired in place once the human OKs it.
|
|
44
|
+
- **Refine the plan, not the code.** `/plumbbob:refine` touches `intent.md` only — turning a
|
|
45
|
+
decision into a diff is `/plumbbob:build` or your own hands, never this skill.
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: revert
|
|
3
3
|
description: Human-triggered driver for `plumbbob revert` — git reset --hard to a checkpoint SHA (discarding the half-done step) and return to DESIGN.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
model: haiku
|
|
6
|
-
allowed-tools: Bash(
|
|
6
|
+
allowed-tools: Bash(plumbbob status:*), Bash(plumbbob revert:*)
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Plumbbob — revert to a checkpoint (driver)
|
|
10
10
|
|
|
11
|
-
Current session state (injected when this skill runs): !`
|
|
11
|
+
Current session state (injected when this skill runs): !`plumbbob status 2>/dev/null || echo "plumbbob CLI not found - install the dep and re-run: npm i -g plumbbob && plumbbob init"`
|
|
12
12
|
|
|
13
13
|
This is a **driver skill** — a chat-side trigger for the mechanical `plumbbob revert` 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.
|
|
14
14
|
|
|
15
15
|
## What it does
|
|
16
16
|
|
|
17
|
-
1. Read an optional target step from the way you were invoked (e.g. `/
|
|
18
|
-
2. Run `
|
|
17
|
+
1. Read an optional target step from the way you were invoked (e.g. `/plumbbob:revert --to 2` → step `2`). With no target, revert goes to the last done-checkpoint.
|
|
18
|
+
2. Run `plumbbob revert` (or `plumbbob revert --to <n>`) via Bash. This is a `git reset --hard` — it discards the current in-progress step. Run it exactly as the human asked; do not add or drop the `--to` on your own.
|
|
19
19
|
3. Report the verb's output verbatim — which checkpoint it reset to, or any refusal.
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: spike
|
|
3
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
4
|
disable-model-invocation: true
|
|
5
5
|
model: haiku
|
|
6
|
-
allowed-tools: Bash(
|
|
6
|
+
allowed-tools: Bash(plumbbob status:*), Bash(plumbbob spike:*)
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Plumbbob — spike an experiment (driver)
|
|
10
10
|
|
|
11
|
-
Current session state (injected when this skill runs): !`
|
|
11
|
+
Current session state (injected when this skill runs): !`plumbbob status 2>/dev/null || echo "plumbbob CLI not found - install the dep and re-run: npm i -g plumbbob && plumbbob init"`
|
|
12
12
|
|
|
13
13
|
This is a **driver skill** — a chat-side trigger for the mechanical `plumbbob spike` 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.
|
|
14
14
|
|
|
15
15
|
## What it does
|
|
16
16
|
|
|
17
|
-
1. Read the spike target from the way you were invoked: a slug to open one (e.g. `/
|
|
18
|
-
2. Run `
|
|
17
|
+
1. Read the spike target from the way you were invoked: a slug to open one (e.g. `/plumbbob:spike redis-cache`), or the literal `done` to tear the current spike down (`/plumbbob:spike done`). If neither is present, ask which and run nothing.
|
|
18
|
+
2. Run `plumbbob spike "<slug>"` or `plumbbob spike done` via Bash.
|
|
19
19
|
3. Report the verb's output verbatim — the worktree it created or removed, or any refusal.
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: status
|
|
3
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
4
|
disable-model-invocation: true
|
|
5
5
|
model: haiku
|
|
6
|
-
allowed-tools: Bash(
|
|
6
|
+
allowed-tools: Bash(plumbbob status:*)
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Plumbbob — orient (the where-am-I move)
|
|
10
10
|
|
|
11
|
-
Current session state (injected when this skill runs): !`
|
|
11
|
+
Current session state (injected when this skill runs): !`plumbbob status 2>/dev/null || echo "plumbbob CLI not found - install the dep and re-run: npm i -g plumbbob && plumbbob init"`
|
|
12
12
|
|
|
13
|
-
This is a **thin driver** for `
|
|
13
|
+
This is a **thin driver** for `plumbbob status`. The dashboard above is your
|
|
14
14
|
orientation — the intent, the step list, the parked/open-question counts, and the
|
|
15
15
|
inferred next move. Report it verbatim and point the human at that next move. This
|
|
16
16
|
skill carries **no Edit and no Write tool**: the CLI is the source of truth, so
|
|
@@ -19,4 +19,4 @@ skill carries **no Edit and no Write tool**: the CLI is the source of truth, so
|
|
|
19
19
|
## What it does
|
|
20
20
|
|
|
21
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 `/
|
|
22
|
+
2. If it reads `NO ACTIVE SESSION`, tell the human to `/plumbbob:plan` to frame a goal.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: step
|
|
3
|
+
description: Revise the next increment just-in-time — sharpen the next undone step against what's now true, or (with input) re-cut, split, or add a step. Empty input runs an automatic sharpen. One at a time; the human approves.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
model: opus
|
|
6
|
+
allowed-tools: Read, Edit, Bash(plumbbob status:*)
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Plumbbob — revise the next step (the single-increment move)
|
|
10
|
+
|
|
11
|
+
Current session state (injected when this skill runs): !`plumbbob status 2>/dev/null || echo "plumbbob CLI not found - install the dep and re-run: npm i -g plumbbob && plumbbob init"`
|
|
12
|
+
|
|
13
|
+
`/plumbbob:plan` authors the **whole** step list up front, so `/plumbbob:step` is the
|
|
14
|
+
**just-in-time revision** move: it keeps the *next* undone step honest against what the
|
|
15
|
+
build has actually taught you, right before you `/plumbbob:build` it. (Framing the whole goal
|
|
16
|
+
is the separate `/plumbbob:plan` move.) It can also add or re-cut a step when scope genuinely
|
|
17
|
+
grew — but its everyday job is to sharpen, not to invent.
|
|
18
|
+
|
|
19
|
+
## Two ways to fire it
|
|
20
|
+
|
|
21
|
+
- **`/plumbbob:step` (no input) → automatic sharpen.** Re-examine the next undone step
|
|
22
|
+
against the completed code, the Decisions, the Constraints, and the build-log, then
|
|
23
|
+
make the obvious revisions to its **done-when** and **seam** so it matches reality —
|
|
24
|
+
e.g. a file moved, a decision narrowed the scope, an earlier step already did part of
|
|
25
|
+
it. This is the zero-effort "keep my next step in sync" move: if the human does
|
|
26
|
+
nothing else, the next step stays current.
|
|
27
|
+
- **`/plumbbob:step <what changed>` → directed revision.** Take the human's input and propose
|
|
28
|
+
the matching change: tighten the done-when, adjust the seam, split the step in two, or
|
|
29
|
+
add a new increment the plan was missing.
|
|
30
|
+
|
|
31
|
+
## What this skill does
|
|
32
|
+
|
|
33
|
+
1. **Read the plan and the reality.** Read `intent.md`'s Frame, Decisions, Constraints,
|
|
34
|
+
and the steps already done, plus the next undone step, to see what it *should* now be.
|
|
35
|
+
2. **Propose the revision** (or the new/split step): a one-line **title**, a **done-when**
|
|
36
|
+
`/plumbbob:verify` can validate, and a **seam** (exact paths, or a `dir/` grant). Keep it
|
|
37
|
+
small enough to verify in one review pass. Show the before/after so the human can see
|
|
38
|
+
what you changed and why.
|
|
39
|
+
3. **Get the human's OK**, then write it into `## Steps` in the standard format —
|
|
40
|
+
`N. [ ] <title> — **done when:** <criterion>` with a `- seam:` sub-line. Revise the
|
|
41
|
+
existing step in place; only append when you are genuinely adding an increment.
|
|
42
|
+
|
|
43
|
+
## The hard contracts
|
|
44
|
+
|
|
45
|
+
- **One verifiable increment.** Each step carries a done-when `/plumbbob:verify` can check
|
|
46
|
+
and a seam small enough to review in one pass.
|
|
47
|
+
- **Edit `## Steps` only**, in the standard format `status` and `build` parse — never
|
|
48
|
+
the Roadmap, never loose prose. A done step (`[x]`) is history; do not rewrite it.
|
|
49
|
+
- **The human approves the revision** before it lands. You propose; they decide.
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
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.
|
|
2
|
+
name: 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
4
|
disable-model-invocation: true
|
|
5
5
|
model: opus
|
|
6
|
-
allowed-tools: Read, Bash(
|
|
6
|
+
allowed-tools: Read, Bash(plumbbob status:*), Bash(plumbbob check:*), Bash(plumbbob checkpoint:*), Bash(git diff:*), Bash(git status:*)
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Plumbbob — verify a step (the tick)
|
|
10
10
|
|
|
11
|
-
Current session state (injected when this skill runs): !`
|
|
11
|
+
Current session state (injected when this skill runs): !`plumbbob status 2>/dev/null || echo "plumbbob CLI not found - install the dep and re-run: npm i -g plumbbob && plumbbob init"`
|
|
12
12
|
|
|
13
13
|
This is the **tick** — the one beat where the human is the clock. Whatever produced
|
|
14
|
-
the current diff — `/
|
|
14
|
+
the current diff — `/plumbbob:build`, your own hands, a vibe session, another harness —
|
|
15
15
|
this skill verifies it the same way: **it reads the diff, not the author** (D3).
|
|
16
16
|
|
|
17
17
|
## What this skill does, in order
|
|
18
18
|
|
|
19
|
-
1. **Check.** Run `
|
|
19
|
+
1. **Check.** Run `plumbbob check` (the heavy gate). If it comes back
|
|
20
20
|
**red**, stop here: report what failed and do **not** pause for approval — there
|
|
21
21
|
is nothing to approve yet. The human fixes it and re-invokes.
|
|
22
22
|
2. **Self-review** *(a single structured read, D16)*. Read `git diff` and
|
|
@@ -31,7 +31,7 @@ this skill verifies it the same way: **it reads the diff, not the author** (D3).
|
|
|
31
31
|
the human is the clock. Never checkpoint without it.
|
|
32
32
|
5. **Checkpoint** *(only after approval)*. Commit the work — the human's
|
|
33
33
|
commit-with-TIL skill for a rich message, or let `checkpoint` make the WIP commit
|
|
34
|
-
— then run `
|
|
34
|
+
— then run `plumbbob checkpoint` to record the SHA, flip the step to done,
|
|
35
35
|
and return to DESIGN. Do **not** bump the version or touch the changelog — that is
|
|
36
36
|
the human's `/version` call.
|
|
37
37
|
|
|
@@ -40,7 +40,7 @@ this skill verifies it the same way: **it reads the diff, not the author** (D3).
|
|
|
40
40
|
- **Never skip the pause.** Check → self-review → validate, then wait. Approval is
|
|
41
41
|
the only thing that triggers the checkpoint.
|
|
42
42
|
- **Read the diff, not the author** (D3). Verify identically whether the code was
|
|
43
|
-
built by `/
|
|
43
|
+
built by `/plumbbob:build`, by hand, vibed, or by another harness.
|
|
44
44
|
- **Red means stop, not pause.** A failing check is not an approval decision; report
|
|
45
45
|
it and end your turn.
|
|
46
46
|
- **You review; you do not build.** If the self-review finds a problem, surface it
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description:
|
|
2
|
+
name: wrap
|
|
3
|
+
description: Wrap up the build — write the report (what shipped, decisions, parked/harvested items, deferred tangents), then safely archive intent + build-log + report before clearing for a fresh goal. Archive-then-clear, never destroy. Report by default, no gate.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
model: opus
|
|
6
|
-
allowed-tools: Read, Write, Bash(
|
|
6
|
+
allowed-tools: Read, Write, Bash(plumbbob status:*), Bash(plumbbob wrap:*)
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
# Plumbbob —
|
|
9
|
+
# Plumbbob — wrap the build (the close-out)
|
|
10
10
|
|
|
11
|
-
Current session state (injected when this skill runs): !`
|
|
11
|
+
Current session state (injected when this skill runs): !`plumbbob status 2>/dev/null || echo "plumbbob CLI not found - install the dep and re-run: npm i -g plumbbob && plumbbob init"`
|
|
12
12
|
|
|
13
|
-
`/
|
|
13
|
+
`/plumbbob:wrap` ends the build: it captures what happened, archives it, and clears the
|
|
14
14
|
sidecar for the next goal. **Report by default** (D9) — no refuse-without-report gate,
|
|
15
15
|
and no separate docs phase.
|
|
16
16
|
|
|
@@ -23,11 +23,11 @@ and no separate docs phase.
|
|
|
23
23
|
- **Final status** — done or partial, and what is left.
|
|
24
24
|
- **Deferred tangents** — the harvested items that become future work.
|
|
25
25
|
This is the "yeah, I did that" artifact. Write it by default; the human may edit it.
|
|
26
|
-
2. **Archive & clear.** Run `
|
|
26
|
+
2. **Archive & clear.** Run `plumbbob wrap`, which appends the checkpoint SHAs
|
|
27
27
|
to the report, archives intent + build-log + report to
|
|
28
28
|
`.plumbbob/archive/<date>-<slug>/`, and clears the sidecar (STATE last). Git is not
|
|
29
29
|
touched.
|
|
30
|
-
3. **Point at the next goal** — `/
|
|
30
|
+
3. **Point at the next goal** — `/plumbbob:plan` to frame the next one.
|
|
31
31
|
|
|
32
32
|
## The hard contracts
|
|
33
33
|
|
package/templates/build-log.md
CHANGED
|
@@ -4,8 +4,8 @@ step boundaries. The antidote to "my plan got lost in the noise."
|
|
|
4
4
|
|
|
5
5
|
Steps : where you are. One step in flight at a time.
|
|
6
6
|
Park list : where ideas go so you do not chase them. CAPTURE, never act inline.
|
|
7
|
-
|
|
8
|
-
Log : the audit trail. Feeds /plumbbob
|
|
7
|
+
Harvest : the boundary ritual that keeps you on one branch.
|
|
8
|
+
Log : the audit trail. Feeds the /plumbbob:wrap report, then gets archived.
|
|
9
9
|
-->
|
|
10
10
|
|
|
11
11
|
# Build log — {{TITLE}}
|
|
@@ -16,7 +16,8 @@ step boundaries. The antidote to "my plan got lost in the noise."
|
|
|
16
16
|
## Steps
|
|
17
17
|
|
|
18
18
|
*(Mirror of intent.md's Steps, with live status. Only ONE step is in flight. A step
|
|
19
|
-
is done only after
|
|
19
|
+
is done only after a checkpoint — check green + checkpoint taken, via `/plumbbob:verify` or
|
|
20
|
+
`/plumbbob:build`.)*
|
|
20
21
|
|
|
21
22
|
- ☐ 1. <step>
|
|
22
23
|
|
|
@@ -24,31 +25,30 @@ is done only after `plumbbob done` — check green + checkpoint taken.)*
|
|
|
24
25
|
|
|
25
26
|
> Mid-step, every new problem / idea / "ooh what if" lands HERE, untouched, and you
|
|
26
27
|
> go straight back to the step. Acting the instant an idea arrives is the disease.
|
|
27
|
-
> Capture is one line (`/park` composes it
|
|
28
|
-
> only at the boundary.
|
|
28
|
+
> Capture is one line (`/plumbbob:park` composes it). Harvest happens only at the boundary.
|
|
29
29
|
|
|
30
|
-
##
|
|
30
|
+
## Harvest *(run `/plumbbob:harvest` at each step boundary, after green)*
|
|
31
31
|
|
|
32
32
|
Classify each parked item as exactly ONE. Naming it before acting is what keeps you
|
|
33
33
|
from sprawling across branches.
|
|
34
34
|
|
|
35
|
-
| Class | Meaning | Action
|
|
36
|
-
|
|
37
|
-
| **blocker** | Plan was wrong/incomplete; can't proceed |
|
|
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.
|
|
35
|
+
| Class | Meaning | Action |
|
|
36
|
+
|------------------|-------------------------------------------|---------------------------------|
|
|
37
|
+
| **blocker** | Plan was wrong/incomplete; can't proceed | `/plumbbob:revert`, fold into intent |
|
|
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
40
|
|
|
41
41
|
> Reality check: almost everything that *feels* like a pivot is a tangent. Require a
|
|
42
42
|
> failed assumption, not a shinier idea, before you pivot.
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
Harvest results this boundary:
|
|
45
45
|
|
|
46
46
|
- (none yet)
|
|
47
47
|
|
|
48
48
|
## Log
|
|
49
49
|
|
|
50
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
|
|
52
|
-
`plumbbob
|
|
51
|
+
did that — the LLM helped, but those were my calls." `/plumbbob:wrap` reads this for the
|
|
52
|
+
report; `plumbbob wrap` archives it under `.plumbbob/archive/`.)*
|
|
53
53
|
|
|
54
54
|
- <date> — <decision / event / what shipped this step>
|
package/templates/intent.md
CHANGED
|
@@ -33,25 +33,29 @@ open. If the implementor (you-later, or the LLM) has to guess, the doc failed.
|
|
|
33
33
|
## Decisions
|
|
34
34
|
|
|
35
35
|
*(One line each. Settled, not re-litigated in the chat. Grows as you resolve the
|
|
36
|
-
holes
|
|
36
|
+
holes `/plumbbob:refine` surfaces, and as blockers fold in during BUILD.)*
|
|
37
37
|
|
|
38
38
|
- D1: <decision> — *because* <the one reason that mattered>
|
|
39
39
|
|
|
40
40
|
## Constraints
|
|
41
41
|
|
|
42
|
-
*(Hard rules the build must honor.
|
|
42
|
+
*(Hard rules the build must honor. `/plumbbob:verify` and `/plumbbob:refine` read against these.)*
|
|
43
43
|
|
|
44
44
|
- C1: <e.g. functional/procedural only; no new dependencies>
|
|
45
45
|
|
|
46
46
|
## Steps
|
|
47
47
|
|
|
48
|
-
*(The build plan.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
*(The build plan. `/plumbbob:plan` authors the **whole list up front** — each step a small,
|
|
49
|
+
verifiable increment with its own **done-when** and **seam** (the paths it will touch,
|
|
50
|
+
which `/plumbbob:build` records in `.plumbbob/SEAM` for orientation — awareness, not a lock
|
|
51
|
+
in v2). Then drive `/plumbbob:build` until done. Later steps may be fuzzier than the first;
|
|
52
|
+
sharpen the next one just-in-time with `/plumbbob:step` (empty input auto-syncs it), and use
|
|
53
|
+
`/plumbbob:refine` to repair the whole plan when a blocker rewrites it.)*
|
|
52
54
|
|
|
53
55
|
1. [ ] <step> — **done when:** <criterion, ideally a test or check result>
|
|
54
56
|
- seam: `<file>`, `<file>`
|
|
57
|
+
2. [ ] <step> — **done when:** <criterion>
|
|
58
|
+
- seam: `<file>`
|
|
55
59
|
|
|
56
60
|
## Open questions
|
|
57
61
|
|
package/dist/lib/settings.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
// Claude Code settings.json hook registration (D27). A pure-TS JSON merge (zero
|
|
2
|
-
// runtime deps, node builtins only — C2): strip our entries, then re-add them, so
|
|
3
|
-
// a re-run is byte-identical. The same logic serves all three D27 scopes — only
|
|
4
|
-
// the settings file path and the command-path representation differ, both chosen
|
|
5
|
-
// by the `setup` verb. Functional/procedural, no classes (C1).
|
|
6
|
-
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
-
import { dirname } from 'node:path';
|
|
8
|
-
// A registration entry is "ours" iff one of its hook commands points into our
|
|
9
|
-
// hooks dir. This marker matches every command form we emit: the global
|
|
10
|
-
// `~/.claude/plumbbob/hooks/...` copy and the self-contained
|
|
11
|
-
// `$CLAUDE_PROJECT_DIR/node_modules/plumbbob/hooks/...` reference both contain
|
|
12
|
-
// `plumbbob/hooks/`, so uninstall finds either.
|
|
13
|
-
const OURS_MARKER = 'plumbbob/hooks/';
|
|
14
|
-
// The edit matcher covers all four editing tools (D5); v2 uses it for the
|
|
15
|
-
// post-edit feedback hook (the only edit-time hook left).
|
|
16
|
-
const EDIT_MATCHER = 'Edit|Write|MultiEdit|NotebookEdit';
|
|
17
|
-
// The single hook Plumbbob v2 registers: the PostToolUse light feedback (D11/D25).
|
|
18
|
-
// v1's PreToolUse muzzle + bash-guard are gone (D1/D13); the merge still STRIPS any
|
|
19
|
-
// leftover PreToolUse entry from an older install, it just no longer adds one. Two
|
|
20
|
-
// command forms:
|
|
21
|
-
// global — a direct absolute path to the +x copy under ~/.claude.
|
|
22
|
-
// self (viaSh) — `sh "<dir>/<file>"` against the package's hooks/ under
|
|
23
|
-
// node_modules, addressed through `$CLAUDE_PROJECT_DIR` so the
|
|
24
|
-
// committed/portable form carries no machine-absolute path and
|
|
25
|
-
// needs no execute bit (pnpm's store may not preserve one).
|
|
26
|
-
function registrationEntries(hooksDir, viaSh) {
|
|
27
|
-
const dir = hooksDir.endsWith('/') ? hooksDir.slice(0, -1) : hooksDir;
|
|
28
|
-
const command = (file) => (viaSh ? `sh "${dir}/${file}"` : `${dir}/${file}`);
|
|
29
|
-
const entry = (matcher, file) => ({
|
|
30
|
-
matcher,
|
|
31
|
-
hooks: [{ type: 'command', command: command(file) }],
|
|
32
|
-
});
|
|
33
|
-
return { PostToolUse: [entry(EDIT_MATCHER, 'post-edit.sh')] };
|
|
34
|
-
}
|
|
35
|
-
function isOurs(entry) {
|
|
36
|
-
return (entry.hooks ?? []).some((h) => h.command.includes(OURS_MARKER));
|
|
37
|
-
}
|
|
38
|
-
function stripOurs(list) {
|
|
39
|
-
return (list ?? []).filter((e) => !isOurs(e));
|
|
40
|
-
}
|
|
41
|
-
// Merge our registration into a parsed settings object (strip-then-add). Returns
|
|
42
|
-
// a new object; every unrelated key and unrelated hook is preserved, and key
|
|
43
|
-
// order is stable across runs so a second merge is byte-identical.
|
|
44
|
-
export function mergeRegistration(settings, hooksDir, viaSh = false) {
|
|
45
|
-
const entries = registrationEntries(hooksDir, viaSh);
|
|
46
|
-
const existing = settings.hooks ?? {};
|
|
47
|
-
const hooks = { ...existing };
|
|
48
|
-
// Strip any leftover v1 PreToolUse muzzle/bash-guard from an older install, but
|
|
49
|
-
// add back only the PostToolUse feedback hook — v2 registers no PreToolUse.
|
|
50
|
-
if (existing.PreToolUse !== undefined) {
|
|
51
|
-
hooks.PreToolUse = stripOurs(existing.PreToolUse);
|
|
52
|
-
}
|
|
53
|
-
hooks.PostToolUse = [...stripOurs(existing.PostToolUse), ...entries.PostToolUse];
|
|
54
|
-
return { ...settings, hooks };
|
|
55
|
-
}
|
|
56
|
-
// Remove only our entries from the two events we manage, leaving everything else
|
|
57
|
-
// untouched (uninstall, and the idempotent strip half of a re-install).
|
|
58
|
-
export function stripRegistration(settings) {
|
|
59
|
-
const existing = settings.hooks;
|
|
60
|
-
if (existing === undefined) {
|
|
61
|
-
return settings;
|
|
62
|
-
}
|
|
63
|
-
const hooks = { ...existing };
|
|
64
|
-
if (existing.PreToolUse !== undefined) {
|
|
65
|
-
hooks.PreToolUse = stripOurs(existing.PreToolUse);
|
|
66
|
-
}
|
|
67
|
-
if (existing.PostToolUse !== undefined) {
|
|
68
|
-
hooks.PostToolUse = stripOurs(existing.PostToolUse);
|
|
69
|
-
}
|
|
70
|
-
return { ...settings, hooks };
|
|
71
|
-
}
|
|
72
|
-
export function readSettings(path) {
|
|
73
|
-
try {
|
|
74
|
-
return JSON.parse(readFileSync(path, 'utf8'));
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return {};
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
export function writeSettings(path, settings) {
|
|
81
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
82
|
-
writeFileSync(path, `${JSON.stringify(settings, null, 2)}\n`);
|
|
83
|
-
}
|
package/dist/verbs/setup.js
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
// `plumbbob setup` — the installer. Two install shapes:
|
|
2
|
-
//
|
|
3
|
-
// self-contained (default when plumbbob is a project-local dep; --local /
|
|
4
|
-
// --project to force) — NOTHING is written under ~/.claude. The hooks are
|
|
5
|
-
// referenced in place at $CLAUDE_PROJECT_DIR/node_modules/plumbbob/hooks/,
|
|
6
|
-
// and the skills are copied into <repo>/.claude/skills/. A skill's bin
|
|
7
|
-
// invocation can NOT use $CLAUDE_PROJECT_DIR — that variable is defined only
|
|
8
|
-
// in Claude Code's hook context and expands EMPTY in a skill's bash — so
|
|
9
|
-
// setup resolves the bin at install time (see selfBin): --local bakes the
|
|
10
|
-
// absolute path to <repo>/node_modules/.bin/plumbbob (personal + untracked,
|
|
11
|
-
// so a machine path is fine); --project uses a bare `plumbbob` (committed +
|
|
12
|
-
// shared, so it stays portable — Claude Code resolves it from the project's
|
|
13
|
-
// node_modules/.bin, which it prepends to PATH). The registration lands in:
|
|
14
|
-
// --local (default) <repo>/.claude/settings.local.json — personal, untracked
|
|
15
|
-
// --project <repo>/.claude/settings.json — committable, enrolls the team
|
|
16
|
-
// This is the "run it from the project root, no global install" shape.
|
|
17
|
-
//
|
|
18
|
-
// global (--global, or the auto default when plumbbob is NOT a project-local
|
|
19
|
-
// dep) — the original behavior: copy the hooks to ~/.claude/plumbbob/hooks/
|
|
20
|
-
// and the skills to ~/.claude/skills/, register absolute command paths in
|
|
21
|
-
// ~/.claude/settings.json, and leave the skills calling a bare `plumbbob`.
|
|
22
|
-
//
|
|
23
|
-
// `--uninstall` strips the registration from whichever settings file the same
|
|
24
|
-
// resolution selects; installed/copied files are left in place. The hooks are
|
|
25
|
-
// session-gated, so any registration is safe in every repo (C7).
|
|
26
|
-
import { chmodSync, cpSync, existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
27
|
-
import { homedir } from 'node:os';
|
|
28
|
-
import { join, sep } from 'node:path';
|
|
29
|
-
import { fileURLToPath } from 'node:url';
|
|
30
|
-
import { findRepoRoot } from "../lib/git.js";
|
|
31
|
-
import { mergeRegistration, readSettings, stripRegistration, writeSettings } from "../lib/settings.js";
|
|
32
|
-
const HOOK_FILES = ['post-edit.sh'];
|
|
33
|
-
// The placeholder every skill carries in place of its `plumbbob` invocation;
|
|
34
|
-
// setup substitutes it with the form the chosen install shape resolves.
|
|
35
|
-
const BIN_PLACEHOLDER = '__PLUMBBOB_BIN__';
|
|
36
|
-
// The hook is registered in settings.json, whose `command` runs in Claude Code's
|
|
37
|
-
// hook context — the one place $CLAUDE_PROJECT_DIR is defined — so the portable
|
|
38
|
-
// project-root variable resolves correctly here. (A skill's bash does NOT get
|
|
39
|
-
// that variable; the skill bin is resolved separately, by selfBin.)
|
|
40
|
-
const SELF_HOOKS_DIR = '$CLAUDE_PROJECT_DIR/node_modules/plumbbob/hooks';
|
|
41
|
-
export function setup(cwd, args) {
|
|
42
|
-
const home = process.env.HOME ?? homedir();
|
|
43
|
-
const mode = resolveMode(cwd, home, args);
|
|
44
|
-
if (mode === null) {
|
|
45
|
-
process.stderr.write('plumbbob: a project-scoped install writes <repo>/.claude/, but this is not a git repository. ' +
|
|
46
|
-
'Run setup from inside the repo, or use --global to install under ~/.claude.\n');
|
|
47
|
-
return 1;
|
|
48
|
-
}
|
|
49
|
-
if (args.includes('--uninstall')) {
|
|
50
|
-
writeSettings(mode.settingsFile, stripRegistration(readSettings(mode.settingsFile)));
|
|
51
|
-
process.stdout.write(`plumbbob: removed the hook registration from ${mode.settingsFile}. The installed/copied hooks and skills were left in place.\n`);
|
|
52
|
-
return 0;
|
|
53
|
-
}
|
|
54
|
-
return mode.kind === 'global' ? installGlobal(home, mode.settingsFile) : installSelfContained(mode);
|
|
55
|
-
}
|
|
56
|
-
// The original shape: hooks + skills copied under ~/.claude, absolute command
|
|
57
|
-
// paths, skills calling a bare `plumbbob` (which the global npm install puts on
|
|
58
|
-
// PATH).
|
|
59
|
-
function installGlobal(home, settingsFile) {
|
|
60
|
-
const hooksDir = join(home, '.claude', 'plumbbob', 'hooks');
|
|
61
|
-
const skillsDir = join(home, '.claude', 'skills');
|
|
62
|
-
cpSync(packageDir('hooks'), hooksDir, { recursive: true });
|
|
63
|
-
for (const file of HOOK_FILES) {
|
|
64
|
-
chmodSync(join(hooksDir, file), 0o755);
|
|
65
|
-
}
|
|
66
|
-
cpSync(packageDir('skills'), skillsDir, { recursive: true });
|
|
67
|
-
substituteSkillBins(skillsDir, 'plumbbob');
|
|
68
|
-
writeSettings(settingsFile, mergeRegistration(readSettings(settingsFile), hooksDir));
|
|
69
|
-
process.stdout.write(`plumbbob: installed hooks → ${hooksDir}, skills → ${skillsDir}.\n` +
|
|
70
|
-
`plumbbob: registered the hooks in ${settingsFile} (global scope).\n` +
|
|
71
|
-
'plumbbob: restart Claude Code (or reload settings) for the hooks to take effect.\n' +
|
|
72
|
-
"plumbbob: the skills call a bare `plumbbob`; the npm install puts it (and `pb`) on PATH. From a dev checkout add `alias plumbbob='node <repo>/src/cli.ts'`.\n");
|
|
73
|
-
return 0;
|
|
74
|
-
}
|
|
75
|
-
// The self-contained shape: nothing under ~/.claude. Hooks are referenced in
|
|
76
|
-
// place inside node_modules; only the skills are copied into the repo (Claude
|
|
77
|
-
// Code discovers skills under .claude/skills/, never from node_modules), with
|
|
78
|
-
// their bin invocation rewritten to the project-local binary.
|
|
79
|
-
function installSelfContained(mode) {
|
|
80
|
-
const skillsDir = join(mode.repoRoot, '.claude', 'skills');
|
|
81
|
-
const bin = selfBin(mode);
|
|
82
|
-
cpSync(packageDir('skills'), skillsDir, { recursive: true });
|
|
83
|
-
substituteSkillBins(skillsDir, bin);
|
|
84
|
-
writeSettings(mode.settingsFile, mergeRegistration(readSettings(mode.settingsFile), SELF_HOOKS_DIR, true));
|
|
85
|
-
const note = existsSync(join(mode.repoRoot, 'node_modules', 'plumbbob', 'hooks'))
|
|
86
|
-
? ''
|
|
87
|
-
: 'plumbbob: note — node_modules/plumbbob/ is not present in this repo yet. Add plumbbob as a dependency (e.g. `pnpm add -D plumbbob`) so the registered hooks resolve.\n';
|
|
88
|
-
const scope = mode.settingsFile.endsWith('settings.local.json') ? 'local' : 'project';
|
|
89
|
-
process.stdout.write(`plumbbob: copied skills → ${skillsDir} (bin → ${bin}). Nothing was written under ~/.claude.\n` +
|
|
90
|
-
`plumbbob: referenced the hooks at ${SELF_HOOKS_DIR} and registered them in ${mode.settingsFile} (${scope} scope).\n` +
|
|
91
|
-
note +
|
|
92
|
-
'plumbbob: restart Claude Code (or reload settings) for the hooks to take effect.\n' +
|
|
93
|
-
'plumbbob: drive the workflow from the chat with the `pb-*` driver skills. Keep the transition verbs OUT of your settings allowlist — each driver skill self-authorizes its own verb, so a stray model-initiated transition still surfaces a permission prompt.\n');
|
|
94
|
-
return 0;
|
|
95
|
-
}
|
|
96
|
-
// Resolve the bin string baked into the copied skills. A skill's `!`...``
|
|
97
|
-
// injection and its allowed-tools prefix can NOT rely on $CLAUDE_PROJECT_DIR (a
|
|
98
|
-
// hooks-only variable that expands empty in a skill's bash context), so we
|
|
99
|
-
// resolve at install time. --local is personal and untracked, so bake the
|
|
100
|
-
// absolute path to the project-local binary — fully resolved, no runtime
|
|
101
|
-
// lookup. --project is committed and shared, so a machine-absolute path can't
|
|
102
|
-
// travel; fall back to a bare `plumbbob`, which Claude Code resolves from the
|
|
103
|
-
// project's node_modules/.bin (it prepends that to PATH). A repo path with
|
|
104
|
-
// whitespace can't sit unquoted in the injection or an allowed-tools prefix
|
|
105
|
-
// either, so use the PATH-resolved bare form there too.
|
|
106
|
-
function selfBin(mode) {
|
|
107
|
-
const committed = !mode.settingsFile.endsWith('settings.local.json');
|
|
108
|
-
const abs = join(mode.repoRoot, 'node_modules', '.bin', 'plumbbob');
|
|
109
|
-
return committed || /\s/.test(abs) ? 'plumbbob' : abs;
|
|
110
|
-
}
|
|
111
|
-
// Resolve which install shape and settings file to use. Explicit flags win;
|
|
112
|
-
// with no flag, auto-detect — a project-local plumbbob installs self-contained
|
|
113
|
-
// (personal, into settings.local.json), anything else installs global. Returns
|
|
114
|
-
// null when a project scope is required but cwd is not in a git repo.
|
|
115
|
-
function resolveMode(cwd, home, args) {
|
|
116
|
-
const globalFile = join(home, '.claude', 'settings.json');
|
|
117
|
-
if (args.includes('--global')) {
|
|
118
|
-
return { kind: 'global', settingsFile: globalFile };
|
|
119
|
-
}
|
|
120
|
-
const wantsProject = args.includes('--project');
|
|
121
|
-
const wantsLocal = args.includes('--local');
|
|
122
|
-
const auto = !wantsProject && !wantsLocal && isProjectLocal(cwd);
|
|
123
|
-
if (!wantsProject && !wantsLocal && !auto) {
|
|
124
|
-
return { kind: 'global', settingsFile: globalFile };
|
|
125
|
-
}
|
|
126
|
-
const repoRoot = findRepoRoot(cwd);
|
|
127
|
-
if (repoRoot === null) {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
const file = wantsProject ? 'settings.json' : 'settings.local.json';
|
|
131
|
-
return { kind: 'self', settingsFile: join(repoRoot, '.claude', file), repoRoot };
|
|
132
|
-
}
|
|
133
|
-
// plumbbob is project-local when its package directory lives inside this repo's
|
|
134
|
-
// node_modules (a `pnpm add` / `npm install` dependency), as opposed to a global
|
|
135
|
-
// install or a dev checkout.
|
|
136
|
-
function isProjectLocal(cwd) {
|
|
137
|
-
const repoRoot = findRepoRoot(cwd);
|
|
138
|
-
if (repoRoot === null) {
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
return packageRoot().startsWith(join(repoRoot, 'node_modules') + sep);
|
|
142
|
-
}
|
|
143
|
-
// Rewrite the bin placeholder in every copied skill (both the `!`...`` status
|
|
144
|
-
// pre-injection and the allowed-tools patterns) to the resolved invocation.
|
|
145
|
-
function substituteSkillBins(skillsDir, bin) {
|
|
146
|
-
for (const name of readdirSync(skillsDir)) {
|
|
147
|
-
const file = join(skillsDir, name, 'SKILL.md');
|
|
148
|
-
if (!statSync(join(skillsDir, name)).isDirectory() || !existsSync(file)) {
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
const src = readFileSync(file, 'utf8');
|
|
152
|
-
if (src.includes(BIN_PLACEHOLDER)) {
|
|
153
|
-
writeFileSync(file, src.split(BIN_PLACEHOLDER).join(bin));
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
// The plumbbob package root (parent of hooks/ and skills/), off this module's URL.
|
|
158
|
-
function packageRoot() {
|
|
159
|
-
return fileURLToPath(new URL('../../', import.meta.url));
|
|
160
|
-
}
|
|
161
|
-
function packageDir(name) {
|
|
162
|
-
return join(packageRoot(), name);
|
|
163
|
-
}
|