plumbbob 0.3.1 → 0.4.4
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 +202 -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
package/dist/verbs/doctor.js
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
|
-
// `plumbbob doctor` — diagnose
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
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 {
|
|
11
|
+
import { join } from 'node:path';
|
|
11
12
|
import { fileURLToPath } from 'node:url';
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
29
|
-
//
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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(
|
|
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
|
|
138
|
-
const
|
|
139
|
-
const
|
|
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
|
|
169
|
-
: `plumbbob: ${failed} problem(s)
|
|
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
|
|
2
|
-
//
|
|
3
|
-
// (the `/
|
|
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
|
|
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
|
-
'(/
|
|
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:
|
|
35
|
-
'Run `/
|
|
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
|
package/hooks/hooks.json
ADDED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plumbbob",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Attention-first build process:
|
|
3
|
+
"version": "0.4.4",
|
|
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:
|
|
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(
|
|
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): !`
|
|
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: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 `/
|
|
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 `/
|
|
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:
|
|
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(
|
|
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): !`
|
|
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: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 `
|
|
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 `
|
|
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.
|