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.
@@ -1,22 +1,21 @@
1
- // `plumbbob doctor` — diagnose a project's install end to end and print the fix
2
- // for anything broken. Read-only: it inspects, it never writes. The failure
3
- // class it exists for is SILENT a skill's status pre-injection resolving to an
4
- // empty/garbled bin (e.g. the pre-0.3 `$CLAUDE_PROJECT_DIR` form, a hooks-only
5
- // variable that expands empty in a skill's bash) leaves an empty dashboard with
6
- // no error. doctor names the problem and the remedy instead. Functional,
7
- // node builtins only (C1/C2).
8
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
1
+ // `plumbbob doctor` — diagnose the global plugin link end to end. Read-only: it
2
+ // inspects, it never writes. After `plumbbob init`, plumbbob lives as a symlink at
3
+ // ~/.claude/skills/plumbbob pointing at the installed package; Claude Code loads it
4
+ // in place as a plugin (skills `/plumbbob:*`, the post-edit hook from hooks.json).
5
+ // doctor verifies the link resolves to a package carrying the manifest, the skills,
6
+ // and the hook — and names the fix for anything missing. The failure class it
7
+ // exists for is SILENT (a `/plumbbob:status` that opens an empty dashboard because the
8
+ // plugin never linked). Functional, node builtins only (C1/C2).
9
+ import { existsSync, lstatSync, readdirSync, readlinkSync } from 'node:fs';
9
10
  import { homedir } from 'node:os';
10
- import { basename, join } from 'node:path';
11
+ import { join } from 'node:path';
11
12
  import { fileURLToPath } from 'node:url';
12
- import { findRepoRoot } from "../lib/git.js";
13
- import { readSettings } from "../lib/settings.js";
14
- // The plumbbob package's own skills/ dir (the canonical set setup installs). Off
15
- // this module's URL so it resolves the same from src/ (dev) and dist/ (published).
13
+ // The plumbbob package's own skills/ dir (the canonical set), off this module's
14
+ // URL so it resolves the same from src/ (dev) and dist/ (published).
16
15
  function packageDir(name) {
17
16
  return fileURLToPath(new URL(`../../${name}/`, import.meta.url));
18
17
  }
19
- // The pb-* skill directories under `dir` that actually carry a SKILL.md.
18
+ // Skill directories (those carrying a SKILL.md) under `dir`.
20
19
  function listSkills(dir) {
21
20
  try {
22
21
  return readdirSync(dir).filter((n) => existsSync(join(dir, n, 'SKILL.md')));
@@ -25,148 +24,51 @@ function listSkills(dir) {
25
24
  return [];
26
25
  }
27
26
  }
28
- // Pull the bin token out of a skill's status pre-injection the text between
29
- // `!`` and ` status`. Returns null if the skill carries no injection line.
30
- function injectionBin(body) {
31
- const m = body.match(/!`(.+?) status\b/);
32
- return m === null ? null : (m[1] ?? null);
33
- }
34
- // The distinct bin tokens the installed skills inject. They should be uniform; a
35
- // set surfaces a partial/failed setup that left some skills on the old token.
36
- function installedBins(skillsDir, skills) {
37
- const bins = skills
38
- .map((n) => injectionBin(readFileSync(join(skillsDir, n, 'SKILL.md'), 'utf8')))
39
- .filter((b) => b !== null);
40
- return [...new Set(bins)];
41
- }
42
- // Classify the injected bins: ok, or a list of human-readable problems. Catches
43
- // the unresolved placeholder (setup never substituted) and the legacy
44
- // $CLAUDE_PROJECT_DIR form (empty in a skill), and verifies an absolute bin
45
- // actually exists on disk. A bare `plumbbob` resolves from PATH — accepted.
46
- function binProblems(bins) {
47
- const problems = [];
48
- for (const bin of bins) {
49
- if (bin.includes('__PLUMBBOB_BIN__')) {
50
- problems.push('placeholder not substituted (setup did not finish)');
51
- }
52
- else if (bin.includes('$CLAUDE_PROJECT_DIR')) {
53
- problems.push('legacy $CLAUDE_PROJECT_DIR bin — expands empty in a skill (pre-0.3 install)');
54
- }
55
- else if (bin.startsWith('/') && !existsSync(bin)) {
56
- problems.push(`absolute bin not found on disk: ${bin}`);
27
+ // Resolve the dir the plugin link points at: a symlink's target, or the path
28
+ // itself if a real directory was copied there. null if nothing is linked.
29
+ function linkedPackage(link) {
30
+ try {
31
+ const st = lstatSync(link);
32
+ if (st.isSymbolicLink()) {
33
+ return readlinkSync(link);
57
34
  }
35
+ return st.isDirectory() ? link : null;
36
+ }
37
+ catch {
38
+ return null;
58
39
  }
59
- return problems;
60
- }
61
- // True iff any registered hook command points into our hooks dir (the marker the
62
- // settings module also uses for strip/merge).
63
- function hasOurHook(settings) {
64
- const hooks = settings.hooks ?? {};
65
- return Object.values(hooks).some((list) => (list ?? []).some((e) => (e.hooks ?? []).some((h) => (h.command ?? '').includes('plumbbob/hooks/'))));
66
- }
67
- // The repo-scoped (self-contained) install: skills under <repo>/.claude, the CLI
68
- // and hook in <repo>/node_modules, registration in either settings file.
69
- function selfChecks(repoRoot, shipped) {
70
- const skillsDir = join(repoRoot, '.claude', 'skills');
71
- const installed = listSkills(skillsDir);
72
- const bins = installedBins(skillsDir, installed);
73
- const problems = binProblems(bins);
74
- const localFile = join(repoRoot, '.claude', 'settings.local.json');
75
- const projFile = join(repoRoot, '.claude', 'settings.json');
76
- const registeredIn = [localFile, projFile].filter((f) => hasOurHook(readSettings(f)));
77
- const hookScript = join(repoRoot, 'node_modules', 'plumbbob', 'hooks', 'post-edit.sh');
78
- return [
79
- installed.length >= shipped.length
80
- ? { ok: true, label: `skills installed (${installed.length}) in .claude/skills` }
81
- : {
82
- ok: false,
83
- label: `skills incomplete (${installed.length}/${shipped.length}) in .claude/skills`,
84
- fix: 'rerun: pnpm exec plumbbob setup --local',
85
- },
86
- problems.length === 0
87
- ? { ok: true, label: `skill bin resolves (${bins.join(', ') || 'n/a'})` }
88
- : { ok: false, label: `skill bin broken — ${problems.join('; ')}`, fix: 'rerun: pnpm exec plumbbob setup --local' },
89
- existsSync(join(repoRoot, 'node_modules', 'plumbbob'))
90
- ? { ok: true, label: 'plumbbob dependency present (node_modules/plumbbob)' }
91
- : { ok: false, label: 'plumbbob is not a project dependency', fix: 'add it: pnpm add -D plumbbob' },
92
- existsSync(join(repoRoot, 'node_modules', '.bin', 'plumbbob'))
93
- ? { ok: true, label: 'CLI shim present (node_modules/.bin/plumbbob)' }
94
- : { ok: false, label: 'CLI shim missing (node_modules/.bin/plumbbob)', fix: 'reinstall deps: pnpm install' },
95
- registeredIn.length > 0
96
- ? { ok: true, label: `post-edit hook registered (${registeredIn.map((f) => basename(f)).join(', ')})` }
97
- : {
98
- ok: false,
99
- label: 'post-edit hook not registered in .claude/settings*.json',
100
- fix: 'rerun: pnpm exec plumbbob setup --local',
101
- },
102
- existsSync(hookScript)
103
- ? { ok: true, label: 'hook script resolves (node_modules/plumbbob/hooks/post-edit.sh)' }
104
- : {
105
- ok: false,
106
- label: 'hook script missing (node_modules/plumbbob/hooks/post-edit.sh)',
107
- fix: 'reinstall the dep so the registered hook resolves: pnpm add -D plumbbob',
108
- },
109
- ];
110
40
  }
111
- // The global install: skills + hook copied under ~/.claude, the CLI on PATH from
112
- // `npm i -g`. The bare on-PATH bin can't be probed from here without spawning, so
113
- // this scope verifies the copied artifacts and the registration.
114
- function globalChecks(home, shipped) {
115
- const skillsDir = join(home, '.claude', 'skills');
116
- const installed = listSkills(skillsDir);
117
- const problems = binProblems(installedBins(skillsDir, installed));
118
- const hookScript = join(home, '.claude', 'plumbbob', 'hooks', 'post-edit.sh');
41
+ function buildChecks(link, pkg, shipped) {
42
+ const installed = listSkills(join(pkg, 'skills'));
119
43
  return [
120
- installed.length >= shipped.length
121
- ? { ok: true, label: `skills installed (${installed.length}) in ~/.claude/skills` }
122
- : { ok: false, label: `skills incomplete (${installed.length}/${shipped.length})`, fix: 'rerun: plumbbob setup --global' },
123
- problems.length === 0
124
- ? { ok: true, label: 'skill bin resolves (plumbbob, on PATH from the global install)' }
125
- : { ok: false, label: `skill bin broken — ${problems.join('; ')}`, fix: 'rerun: plumbbob setup --global' },
126
- existsSync(hookScript)
127
- ? { ok: true, label: 'hook script present (~/.claude/plumbbob/hooks/post-edit.sh)' }
128
- : { ok: false, label: 'hook script missing under ~/.claude/plumbbob/hooks', fix: 'rerun: plumbbob setup --global' },
129
- hasOurHook(readSettings(join(home, '.claude', 'settings.json')))
130
- ? { ok: true, label: 'post-edit hook registered (~/.claude/settings.json)' }
131
- : { ok: false, label: 'post-edit hook not registered (~/.claude/settings.json)', fix: 'rerun: plumbbob setup --global' },
44
+ { ok: true, label: `linked (${link} → ${pkg})` },
45
+ existsSync(join(pkg, '.claude-plugin', 'plugin.json'))
46
+ ? { ok: true, label: 'plugin manifest present (.claude-plugin/plugin.json)' }
47
+ : { ok: false, label: 'plugin manifest missing — the link does not point at a plumbbob package', fix: 're-link: plumbbob init' },
48
+ installed.length >= shipped.length && shipped.length > 0
49
+ ? { ok: true, label: `skills present (${installed.length}) load as /plumbbob:*` }
50
+ : { ok: false, label: `skills incomplete (${installed.length}/${shipped.length})`, fix: 're-link: plumbbob init' },
51
+ existsSync(join(pkg, 'hooks', 'hooks.json'))
52
+ ? { ok: true, label: 'post-edit hook present (hooks/hooks.json, auto-registers)' }
53
+ : { ok: false, label: 'hook missing (hooks/hooks.json)', fix: 're-link: plumbbob init' },
132
54
  ];
133
55
  }
134
- export function doctor(cwd) {
56
+ export function doctor() {
135
57
  const home = process.env.HOME ?? homedir();
58
+ const link = join(home, '.claude', 'skills', 'plumbbob');
136
59
  const shipped = listSkills(packageDir('skills'));
137
- const repoRoot = findRepoRoot(cwd);
138
- const repoSkills = repoRoot === null ? [] : listSkills(join(repoRoot, '.claude', 'skills'));
139
- const globalSkills = listSkills(join(home, '.claude', 'skills'));
140
- const out = [`plumbbob doctor — ${repoRoot ?? cwd}`];
141
- let checks;
142
- if (repoSkills.length > 0 && repoRoot !== null) {
143
- out.push('install shape: self-contained (<repo>/.claude)');
144
- checks = selfChecks(repoRoot, shipped);
145
- }
146
- else if (globalSkills.length > 0) {
147
- out.push('install shape: global (~/.claude)');
148
- checks = globalChecks(home, shipped);
149
- }
150
- else {
151
- out.push('install shape: none detected');
152
- checks = [
153
- {
154
- ok: false,
155
- label: 'no pb-* skills found under <repo>/.claude/skills or ~/.claude/skills',
156
- fix: repoRoot === null
157
- ? 'global install: npm i -g plumbbob && plumbbob setup --global'
158
- : 'project install: pnpm add -D plumbbob && pnpm exec plumbbob setup --local',
159
- },
160
- ];
161
- }
60
+ const pkg = linkedPackage(link);
61
+ const checks = pkg === null ? [{ ok: false, label: `not linked — no plugin at ${link}`, fix: 'run: plumbbob init' }] : buildChecks(link, pkg, shipped);
62
+ const out = ['plumbbob doctor — global plugin install'];
162
63
  for (const c of checks) {
163
64
  out.push(c.ok ? ` ✓ ${c.label}` : ` ✗ ${c.label}\n → ${c.fix}`);
164
65
  }
165
66
  const failed = checks.filter((c) => !c.ok).length;
166
67
  out.push('');
167
68
  out.push(failed === 0
168
- ? 'plumbbob: all checks passed. If a skill still misbehaves, restart Claude Code to reload settings.'
169
- : `plumbbob: ${failed} problem(s) found — apply the → fixes above, then restart Claude Code.`);
69
+ ? 'plumbbob: all checks passed. If a skill still misbehaves, restart Claude Code (or /reload-plugins).'
70
+ : `plumbbob: ${failed} problem(s) — apply the → fixes, then restart Claude Code.`);
71
+ out.push('plumbbob: skills shell a bare `plumbbob` — ensure it is on PATH (`npm i -g plumbbob`). Sessions are per-project via `plumbbob start`.');
170
72
  process.stdout.write(`${out.join('\n')}\n`);
171
73
  return failed === 0 ? 0 : 1;
172
74
  }
@@ -0,0 +1,81 @@
1
+ // `plumbbob init [--uninstall]` — link plumbbob into Claude Code as an in-place
2
+ // skills-directory plugin. This is the whole install: it symlinks the installed
3
+ // package into ~/.claude/skills/plumbbob, where Claude Code discovers it as
4
+ // `plumbbob@skills-dir` — the skills load namespaced (`/plumbbob:*`) and the
5
+ // post-edit hook auto-registers from hooks/hooks.json. Global-only by design:
6
+ // plumbbob is a personal tool (like firecrawl/gh), and install scope is NOT
7
+ // session scope — sessions stay per-project via `plumbbob start`. Idempotent +
8
+ // reversible (`--uninstall` drops the link); it NEVER writes settings.json.
9
+ // Functional, node builtins only (C1/C2).
10
+ import { lstatSync, mkdirSync, readlinkSync, rmSync, symlinkSync } from 'node:fs';
11
+ import { homedir } from 'node:os';
12
+ import { join } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ // The installed package root (parent of .claude-plugin/, skills/, hooks/, dist/),
15
+ // off this module's URL — the global install when run as the published bin, the
16
+ // checkout in dev. No trailing slash, so it compares clean against a readlink.
17
+ function packageRoot() {
18
+ return fileURLToPath(new URL('../../', import.meta.url)).replace(/\/+$/, '');
19
+ }
20
+ function linkPath(home) {
21
+ return join(home, '.claude', 'skills', 'plumbbob');
22
+ }
23
+ // lstat-based existence: true even for a broken symlink (existsSync follows the
24
+ // link and would miss one whose target moved).
25
+ function present(path) {
26
+ try {
27
+ lstatSync(path);
28
+ return true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ // The symlink's target (no trailing slash), or null if `path` is not a symlink.
35
+ function symlinkTarget(path) {
36
+ try {
37
+ return lstatSync(path).isSymbolicLink() ? readlinkSync(path).replace(/\/+$/, '') : null;
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ export function init(args) {
44
+ const home = process.env.HOME ?? homedir();
45
+ const link = linkPath(home);
46
+ const target = packageRoot();
47
+ if (args.includes('--uninstall')) {
48
+ return uninstall(link);
49
+ }
50
+ mkdirSync(join(home, '.claude', 'skills'), { recursive: true });
51
+ if (present(link)) {
52
+ const current = symlinkTarget(link);
53
+ if (current === null) {
54
+ process.stderr.write(`plumbbob: ${link} already exists and is not a plumbbob link. Move or remove it, then re-run \`plumbbob init\`.\n`);
55
+ return 1;
56
+ }
57
+ if (current === target) {
58
+ process.stdout.write(`plumbbob: already linked (${link} → ${target}). Nothing to do.\n`);
59
+ return 0;
60
+ }
61
+ rmSync(link); // a stale link from an earlier install location — repoint it
62
+ }
63
+ symlinkSync(target, link);
64
+ process.stdout.write(`plumbbob: linked ${link} → ${target}.\n` +
65
+ 'plumbbob: Claude Code loads it as a plugin — skills as `/plumbbob:*`, the post-edit hook auto-registered from hooks.json. Restart Claude Code (or /reload-plugins) to activate.\n' +
66
+ 'plumbbob: nothing else under ~ is touched and settings.json is left alone. Sessions are per-project — run `plumbbob start "<goal>"` in any repo, then `plumbbob doctor` to verify.\n');
67
+ return 0;
68
+ }
69
+ function uninstall(link) {
70
+ if (!present(link)) {
71
+ process.stdout.write(`plumbbob: nothing to uninstall — no link at ${link}.\n`);
72
+ return 0;
73
+ }
74
+ if (symlinkTarget(link) === null) {
75
+ process.stderr.write(`plumbbob: ${link} is not a plumbbob link (a real directory/file) — leaving it untouched.\n`);
76
+ return 1;
77
+ }
78
+ rmSync(link);
79
+ process.stdout.write(`plumbbob: unlinked ${link}. Restart Claude Code to drop the plugin.\n`);
80
+ return 0;
81
+ }
@@ -1,6 +1,6 @@
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/,
1
+ // `plumbbob wrap` (D9) — the v2 close-out, replacing the v1 four-verb
2
+ // finish ceremony. It archives intent + build-log + report
3
+ // (the `/plumbbob:wrap` skill writes the report by default) under .plumbbob/archive/,
4
4
  // clears the active files, and deletes the control state (STATE last). Unlike v1
5
5
  // `finish` there is NO refuse-without-report gate — guidance offers the artifact, it
6
6
  // does not wall the exit. Archive-then-clear, never destroy (C4); git untouched (C5).
@@ -9,7 +9,7 @@ import { join, relative } from 'node:path';
9
9
  import { findRepoRoot } from "../lib/git.js";
10
10
  import { buildLogPath, checkpointsPath, hasSession, intentPath, seamPath, sidecarDir, stepPath } from "../lib/sidecar.js";
11
11
  import { archiveSession, reportPath } from "../lib/archive.js";
12
- export function reset(cwd) {
12
+ export function wrap(cwd) {
13
13
  const root = findRepoRoot(cwd);
14
14
  if (root === null || !hasSession(root)) {
15
15
  process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n');
@@ -20,7 +20,7 @@ export function reset(cwd) {
20
20
  }
21
21
  else {
22
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');
23
+ '(/plumbbob:wrap normally writes the report first). No gate (D9).\n');
24
24
  }
25
25
  const archived = archiveSession(root);
26
26
  // Clear the active files — now safely archived — then the control state, STATE
@@ -31,8 +31,8 @@ export function reset(cwd) {
31
31
  rmSync(seamPath(root), { force: true });
32
32
  rmSync(stepPath(root), { force: true });
33
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');
34
+ process.stdout.write(`plumbbob: wrap — archived to ${relative(root, archived)}. Sidecar cleared. ` +
35
+ 'Run `/plumbbob:plan` (or `plumbbob start "<title>"`) to frame the next goal.\n');
36
36
  return 0;
37
37
  }
38
38
  // Append the recorded checkpoints (baseline + each `step n <sha>`) to the report so
@@ -0,0 +1,12 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "Edit|Write|MultiEdit|NotebookEdit",
6
+ "hooks": [
7
+ { "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/post-edit.sh\"" }
8
+ ]
9
+ }
10
+ ]
11
+ }
12
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "plumbbob",
3
- "version": "0.3.1",
4
- "description": "Attention-first build process: eight skills + a CLI that keep you the decider — guidance, not enforcement.",
3
+ "version": "0.4.2",
4
+ "description": "Attention-first build process: 11 skills + a CLI that keep you the decider — guidance, not enforcement.",
5
5
  "keywords": [
6
6
  "claude-code",
7
7
  "claude",
@@ -24,6 +24,7 @@
24
24
  "pb": "dist/cli.js"
25
25
  },
26
26
  "files": [
27
+ ".claude-plugin",
27
28
  "dist",
28
29
  "hooks",
29
30
  "skills",
@@ -57,6 +58,6 @@
57
58
  "check:astgrep": "ast-grep scan",
58
59
  "check:test": "vitest run",
59
60
  "check:knip": "knip",
60
- "check:md": "markdownlint --config .markdownlint.jsonc \"**/*.md\" --ignore node_modules"
61
+ "check:md": "markdownlint --config .markdownlint.jsonc \"**/*.md\" --ignore node_modules --ignore research"
61
62
  }
62
63
  }
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: build
3
+ description: The optional engine — read the next planned step from intent, implement it (its done-when, seam, Decisions, Constraints), then verify it through to the approval pause. Skip it to build by hand/vibed/another harness. `--auto` self-approves and chains to done.
4
+ disable-model-invocation: true
5
+ model: opus
6
+ allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob build:*), Bash(plumbbob check:*), Bash(plumbbob checkpoint:*), Bash(git diff:*)
7
+ ---
8
+
9
+ # Plumbbob — build a step (the optional engine)
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
+ 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 `/plumbbob: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.
18
+
19
+ Since `/plumbbob:plan` lays down the whole step list up front, the happy path is to fire
20
+ `/plumbbob:build` once per step until done — each run builds the next undone step and stops
21
+ at the pause for your approval. **Re-firing `/plumbbob:build` is itself the clock tick.**
22
+
23
+ ## What this skill does, in order
24
+
25
+ 1. **Pick the step.** Use the number you were invoked with (e.g. `/plumbbob:build 4`), else
26
+ the next undone, planned step in `.plumbbob/intent.md`. If there is no planned step
27
+ to build, stop and tell the human to `/plumbbob:step` first.
28
+ 2. **Enter the step.** Run `plumbbob build <n>` (records the in-flight STEP +
29
+ SEAM so `/plumbbob:status` shows the step in flight; in v2 the seam is awareness, not a
30
+ lock).
31
+ 3. **Read the plan.** Read the step's **done-when**, its **seam**, and the
32
+ **Decisions** and **Constraints** in `intent.md`. Build to *that* — the deciding
33
+ already happened, off the chat.
34
+ 4. **Implement** the step, and only that step, staying within the declared seam. A
35
+ new problem or "ooh what if" that surfaces mid-build is a `/plumbbob:park`, **not** an
36
+ edit — capture it and stay on the step. If you genuinely cannot finish without
37
+ touching more than the seam, that is scope drift: surface it to the human rather
38
+ than sprawling.
39
+ 5. **Verify, through to the pause.** Run the verify tick exactly as `/plumbbob:verify`
40
+ does: `plumbbob check` → self-review the diff against the done-when, the
41
+ Decisions, and the Constraints (a single structured read, D16) → validate → **PAUSE
42
+ for the human's approval** → only on approval, checkpoint with
43
+ `plumbbob checkpoint`. Do **not** bump the version or changelog — that is
44
+ the human's `/version` call.
45
+
46
+ ## `--auto` — let the agent be the clock (opt-in)
47
+
48
+ `/plumbbob:build --auto` is the explicit escape hatch when the human wants unattended
49
+ progress instead of approving each step. It does the same work, but **the agent reviews
50
+ and approves in the human's place**, and it **chains**:
51
+
52
+ - Build the next step → `check` → self-review → **if the check is green AND the
53
+ self-review finds no done-when / Decision / Constraint mismatch, checkpoint** and move
54
+ straight on to the next planned step. Repeat.
55
+ - **Stop and hand back to the human** the moment any of these is true: the check is red,
56
+ the self-review finds a mismatch (surface exactly what, and do not checkpoint it), a
57
+ new decision is needed, or no planned steps remain.
58
+
59
+ `--auto` is the only path that checkpoints without a human pause, and only because the
60
+ human asked for it by name. The default — no flag — always ends at the pause.
61
+
62
+ ## The hard contracts
63
+
64
+ - **Optional, never required.** The loop works without this skill; `/plumbbob:verify`
65
+ checkpoints a hand-built or vibed diff just the same (D3).
66
+ - **Build the decided step, not a new one.** Implement what `intent.md` settled. A
67
+ new idea mid-build is a `/plumbbob:park`, not an edit.
68
+ - **Default ends at the pause.** Implement → verify → wait for approval; never
69
+ checkpoint without it. Only an explicit `--auto` lets the agent approve in your place,
70
+ and it still halts on a red check or any mismatch.
@@ -1,16 +1,16 @@
1
1
  ---
2
- name: pb-harvest
2
+ name: harvest
3
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
4
  disable-model-invocation: true
5
5
  model: opus
6
- allowed-tools: Read, Edit, Bash(__PLUMBBOB_BIN__ status:*)
6
+ allowed-tools: Read, Edit, Bash(plumbbob status:*)
7
7
  ---
8
8
 
9
9
  # Plumbbob — harvest the park list
10
10
 
11
- Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status 2>/dev/null || echo "plumbbob CLI not found - install the dep and re-run: npx plumbbob setup"`
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
- `/pb-harvest` is the complement of `/pb-park` (D7): you parked ideas as seeds during a
13
+ `/plumbbob:harvest` is the complement of `/plumbbob:park` (D7): you parked ideas as seeds during a
14
14
  build; now, at a boundary, you harvest them — decide what each one is.
15
15
 
16
16
  ## When to run it — wrong-state refusal
@@ -19,7 +19,7 @@ Harvest at a **step boundary**: after a step is checkpointed and you are back in
19
19
  DESIGN, not mid-step. Read the state injected above:
20
20
 
21
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`
22
+ - `BUILD` — a step is in flight; **refuse** and suggest finishing it with `/plumbbob:verify`
23
23
  before harvesting. Chasing parked items mid-step is the disease parking prevents.
24
24
  - `DESIGN` (and other settled states) — go ahead.
25
25
 
@@ -40,7 +40,7 @@ one line of reasoning, then **wait for the human to confirm or override**. Write
40
40
  **only after** per-item confirmation:
41
41
 
42
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`
43
+ - **Flip the harvested item** from `- [ ]` to `- [x]` in the Park list, so `/plumbbob:status`
44
44
  stops counting it as open.
45
45
  - A confirmed **blocker** also folds its decision into `intent.md`.
46
46
  - Never reclassify or resolve an item the human hasn't confirmed, and default every
@@ -1,16 +1,16 @@
1
1
  ---
2
- name: pb-park
2
+ name: park
3
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
4
  disable-model-invocation: true
5
5
  model: haiku
6
- allowed-tools: Bash(__PLUMBBOB_BIN__ status:*), Bash(__PLUMBBOB_BIN__ park:*)
6
+ allowed-tools: Bash(plumbbob status:*), Bash(plumbbob park:*)
7
7
  ---
8
8
 
9
9
  # Plumbbob — park an idea (capture, don't chase)
10
10
 
11
- Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status 2>/dev/null || echo "plumbbob CLI not found - install the dep and re-run: npx plumbbob setup"`
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
- `/pb-park` is the **capture** half of the loop; `/pb-harvest` is where parked items
13
+ `/plumbbob:park` is the **capture** half of the loop; `/plumbbob:harvest` is where parked items
14
14
  get triaged later (D7). Capturing the instant an idea arrives — instead of acting on
15
15
  it — is the whole point: it protects the step in flight.
16
16
 
@@ -29,12 +29,12 @@ later. Then:
29
29
 
30
30
  1. **Show the composed line to the human** and wait for **in-turn approval** — they
31
31
  confirm it as-is or edit the wording.
32
- 2. **Only after** that approval, capture it by running `__PLUMBBOB_BIN__ park "<the
32
+ 2. **Only after** that approval, capture it by running `plumbbob park "<the
33
33
  approved line>"` via Bash.
34
34
 
35
35
  ## The one hard contract
36
36
 
37
37
  The capture itself is the **dumb CLI**, never an edit. This skill carries **no Edit and
38
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
39
+ approval, then shell `plumbbob park` — that is the only write path. If approval
40
40
  never comes, capture nothing.
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: plan
3
+ description: "Frame a fresh goal and author the whole plan — Frame, Decisions, Constraints, and all Steps — before any code. Three input modes: no arg interviews you; a file path absorbs a spec; any other text expands your inline intent."
4
+ disable-model-invocation: true
5
+ model: opus
6
+ allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob start:*)
7
+ ---
8
+
9
+ # Plumbbob — plan a goal (the whole-goal 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` 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. By default it authors the
15
+ **complete plan, including all the Steps**, so the happy path afterward is just
16
+ `/plumbbob:build` until done. (Revising a single increment later is the separate `/plumbbob:step`
17
+ move; do not confuse the two.)
18
+
19
+ ## Three input modes (disambiguated for you — no quotes needed)
20
+
21
+ Look at the argument the human gave and pick the mode yourself:
22
+
23
+ 1. **No argument → interview.** Walk the human through a short, friendly Q&A to draw
24
+ the plan out of their head (see *The interview* below).
25
+ 2. **The argument is a path to a file that exists → absorb the spec.** Read that file
26
+ and distill it into `intent.md`, **retaining enough detail that `intent.md` stands
27
+ on its own** — don't just link to the source. Add a one-line provenance
28
+ (`*Source: <path>*`) and, for anything sizable, a `## Source` appendix preserving
29
+ the original text. (Probe with the `Read` tool; if it isn't a real file, fall to
30
+ mode 3.)
31
+ 3. **Any other text → expand the inline intent.** Treat the text as the human's
32
+ rough plan, expand it into the full `intent.md`, and ask only about what is
33
+ genuinely ambiguous.
34
+
35
+ All three modes converge on the **same artifact**: a complete, standalone `intent.md`
36
+ an agent can follow with `/plumbbob:build`. The argument only seeds how you get there.
37
+
38
+ ## What this skill does
39
+
40
+ 1. **Scaffold.** If there is no active session, run `plumbbob start "<title>"`
41
+ to create `.plumbbob/` (STATE=DESIGN, baseline recorded). If a session already
42
+ exists, say so and edit the existing `intent.md` rather than starting over.
43
+ 2. **Frame** (`.plumbbob/intent.md`), with the human: the **Problem** in plain words,
44
+ the **smallest thing** that solves it, what **done looks like**, and what you are
45
+ **explicitly NOT doing**. This is the human's convergence — propose wording, but
46
+ the human decides every line.
47
+ 3. **Decisions & Constraints.** Record the settled calls (one line each, with the
48
+ *because*) and the hard rules the build must honor. An unresolved hole goes to
49
+ **Open questions**, never guessed into a Decision.
50
+ 4. **Author the Steps.** Write the **full build plan** under `## Steps` — each step a
51
+ small, verifiable increment in the exact format the parser reads:
52
+
53
+ ```markdown
54
+ 1. [ ] <title> — **done when:** <criterion, ideally a test or check result>
55
+ - seam: `<file>`, `<file>`
56
+ ```
57
+
58
+ Every step needs a **done-when** `/plumbbob:verify` can check and a **seam** (the exact
59
+ paths it touches). Later steps may be fuzzier than the first — that's fine; they get
60
+ sharpened just-in-time when you reach them with `/plumbbob:step`. Keep each small enough to
61
+ verify in one review pass.
62
+ 5. **Offer to stress-test it.** Suggest `/plumbbob:refine` to attack the frame for holes (or
63
+ to repair the plan as it drifts). Optional, the human's call.
64
+
65
+ ## The interview (mode 1)
66
+
67
+ Make it easy and non-intrusive:
68
+
69
+ - **Triage size first.** A tiny change earns a 3-line Frame and one Step, fast — never
70
+ ceremony on a one-liner. Scale the questions to the work.
71
+ - **Propose, don't interrogate.** Offer concrete suggestions the human can **accept as-is
72
+ without typing** ("done-when: the 6th request in 60s returns 429 — good?"), while
73
+ taking arbitrary detail when they want to give it, including pointers to other files.
74
+ - **Let them double back.** They will revise as the picture sharpens; that's expected.
75
+ They can also edit `intent.md` by hand at any time, or call `/plumbbob:refine` to repair it.
76
+
77
+ ## The hard contracts
78
+
79
+ - **Deciding before code.** `/plumbbob:plan` writes `intent.md` only — never source.
80
+ - **The human converges.** You surface options and draft wording; the human picks.
81
+ An unresolved hole is an Open question, not a guessed Decision.
82
+ - **Stands on its own.** Whatever the input mode, the finished `intent.md` carries
83
+ enough detail to be followed without the chat or the external source.
84
+ - **Size to the work.** A small change fills Frame + a couple of Decisions + a step or
85
+ two and stops; ceremony on a one-liner is the failure mode, not thoroughness.