plumbbob 0.9.2 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli-core.js +279 -22
- package/dist/verbs/agent.js +3 -2
- package/dist/verbs/check.js +6 -2
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plumbbob",
|
|
3
3
|
"displayName": "PlumbBob",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.3",
|
|
5
5
|
"description": "Attention-first build process — driver skills + a CLI that keep you the decider: guidance on the work, a latch on the record.",
|
|
6
6
|
"author": { "name": "Rob McLarty", "email": "hello@robmclarty.com", "url": "https://robmclarty.com" },
|
|
7
7
|
"homepage": "https://github.com/robmclarty/plumbbob#readme",
|
package/dist/cli-core.js
CHANGED
|
@@ -26,31 +26,260 @@ import { init } from "./verbs/init.js";
|
|
|
26
26
|
import { doctor } from "./verbs/doctor.js";
|
|
27
27
|
import { agent } from "./verbs/agent.js";
|
|
28
28
|
import { turn } from "./verbs/turn.js";
|
|
29
|
-
/**
|
|
29
|
+
/** Shared across every verb that resolves through the active-build cursor. */
|
|
30
|
+
const BUILD_FLAG = {
|
|
31
|
+
name: '--build',
|
|
32
|
+
value: '<slug>',
|
|
33
|
+
gloss: 'target a specific build instead of the active cursor',
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The full CLI spec, in the order the help table prints.
|
|
37
|
+
*
|
|
38
|
+
* One table drives three things: the top-level verb table, per-verb `--help`,
|
|
39
|
+
* and the unknown-flag refusal in `run`. Keeping it as data rather than inline
|
|
40
|
+
* writes is the deliberate exception to the write-at-the-call-site convention —
|
|
41
|
+
* a synopsis has to be readable by the validator, not just printable.
|
|
42
|
+
*/
|
|
30
43
|
const VERBS = [
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
{
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
{
|
|
45
|
+
name: 'start',
|
|
46
|
+
description: 'scaffold .plumbbob/; open the session; record the baseline commit',
|
|
47
|
+
synopsis: ['start "<title>" [--slug <name>] [--local] [--allow-dirty]'],
|
|
48
|
+
args: [{ name: '<title>', gloss: 'what you are building (quoted)' }],
|
|
49
|
+
flags: [
|
|
50
|
+
{ name: '--slug', value: '<name>', gloss: 'use this slug verbatim instead of the derived YYYY-MM-DD-<title-slug>' },
|
|
51
|
+
{ name: '--local', gloss: 'opt into the old fully-untracked flat layout' },
|
|
52
|
+
{ name: '--allow-dirty', gloss: 'record the current HEAD as baseline despite a dirty tree' },
|
|
53
|
+
],
|
|
54
|
+
notes: 'Refuses (exit 1) on an empty title, a slug collision, a non-git directory, a repo with no commits, an already-active session, or a dirty tree.',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'status',
|
|
58
|
+
description: 'print the orientation dashboard, or NO ACTIVE SESSION',
|
|
59
|
+
synopsis: ['status [--build <slug>]'],
|
|
60
|
+
flags: [BUILD_FLAG],
|
|
61
|
+
notes: 'Read-only; always exits 0. With no cursor it lists the available builds.',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'build',
|
|
65
|
+
description: 'write the seam from step n, or the next undone step (in-flight, not a lock)',
|
|
66
|
+
synopsis: ['build [<n>] [--build <slug>]'],
|
|
67
|
+
args: [{ name: '<n>', gloss: 'step to build (default: the next undone step)' }],
|
|
68
|
+
flags: [BUILD_FLAG],
|
|
69
|
+
notes: 'Refuses (exit 1) with no session, a non-numeric or < 1 step, or a seam it cannot parse.',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'handoff',
|
|
73
|
+
description: 'print the standardized build hand-off block (pause or post-checkpoint); read-only',
|
|
74
|
+
synopsis: ['handoff [<n>] [--build <slug>]'],
|
|
75
|
+
args: [{ name: '<n>', gloss: 'step to report on (default: the in-flight step, else the last checkpointed one)' }],
|
|
76
|
+
flags: [BUILD_FLAG],
|
|
77
|
+
notes: 'Read-only; refuses (exit 1) only with no session.',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'check',
|
|
81
|
+
description: 'run the heavy check and report; no state change',
|
|
82
|
+
synopsis: ['check [--bail] [--changed] [--all] [--only a,b] [--skip a,b] [--include a,b]'],
|
|
83
|
+
flags: [
|
|
84
|
+
{ name: '--bail', gloss: 'stop at the first red slot' },
|
|
85
|
+
{ name: '--changed', gloss: 'limit the run to changed files' },
|
|
86
|
+
{ name: '--all', gloss: 'run every slot' },
|
|
87
|
+
{ name: '--only', value: '<a,b>', gloss: 'run only these slots' },
|
|
88
|
+
{ name: '--skip', value: '<a,b>', gloss: 'run every slot except these' },
|
|
89
|
+
{ name: '--include', value: '<a,b>', gloss: 'add these slots to the run' },
|
|
90
|
+
],
|
|
91
|
+
notes: 'Exits 0 green, 1 red, 2 when the gate itself broke. The flags narrow a checkride run and are warned-and-ignored on the `check` settings-override path.',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'checkpoint',
|
|
95
|
+
description: 'gate on green, commit/record SHA, mark step done (executor-agnostic)',
|
|
96
|
+
synopsis: ['checkpoint [<n>] [-m <msg>] [--body]', 'checkpoint --plan [--body]'],
|
|
97
|
+
args: [{ name: '<n>', gloss: 'step to checkpoint (else the in-flight STEP, else the first undone step)' }],
|
|
98
|
+
flags: [
|
|
99
|
+
{ name: '--plan', gloss: "commit only the build's artifact folder as chore(<scope>): plan" },
|
|
100
|
+
{ name: '-m', value: '<msg>', gloss: 'override the CLI-owned Conventional subject' },
|
|
101
|
+
{ name: '--body', gloss: 'read the commit body from stdin (heredoc)' },
|
|
102
|
+
],
|
|
103
|
+
notes: 'Refuses (exit 1) with no session, no resolvable step, or a red check.',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'revert',
|
|
107
|
+
description: 'git reset --hard to a checkpoint SHA',
|
|
108
|
+
synopsis: ['revert [--to <n>] [--build <slug>]'],
|
|
109
|
+
flags: [
|
|
110
|
+
{ name: '--to', value: '<n>', gloss: "revert to step n's checkpoint (default: the last, else the baseline)" },
|
|
111
|
+
BUILD_FLAG,
|
|
112
|
+
],
|
|
113
|
+
notes: 'Refuses (exit 1) with no session, an invalid --to, or a step with no recorded checkpoint.',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'park',
|
|
117
|
+
description: 'append a raw line to the park list',
|
|
118
|
+
synopsis: ['park "<text>"'],
|
|
119
|
+
args: [{ name: '<text>', gloss: 'the note to park (free text; quote it)' }],
|
|
120
|
+
tolerateUnknownFlags: true,
|
|
121
|
+
notes: 'Refuses (exit 1) with no session, empty text, or no `## Park list` section.',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'spike',
|
|
125
|
+
description: 'worktree experiment + report for a genuine fork',
|
|
126
|
+
synopsis: ['spike "<slug>" [opt…] [--build <slug>]', 'spike report "<slug>"', 'spike done'],
|
|
127
|
+
args: [
|
|
128
|
+
{ name: '<slug>', gloss: 'name of the experiment' },
|
|
129
|
+
{ name: '[opt…]', gloss: 'the options to fork (default: a and b)' },
|
|
130
|
+
],
|
|
131
|
+
flags: [BUILD_FLAG],
|
|
132
|
+
notes: 'Refuses (exit 1) with no session, a step already in flight, an empty slug, or a worktree path that already exists.',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'use',
|
|
136
|
+
description: 're-point the active-build cursor and resume that build',
|
|
137
|
+
synopsis: ['use <slug>'],
|
|
138
|
+
args: [{ name: '<slug>', gloss: 'the build folder under .plumbbob/builds/ to resume' }],
|
|
139
|
+
notes: 'Refuses (exit 1) with an empty slug or a slug with no build folder; warns (but allows) leaving a step in flight.',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'finish',
|
|
143
|
+
description: 'close-out: report + final commit (no gate), clear the control state, close the session',
|
|
144
|
+
synopsis: ['finish [--body] [--build <slug>]'],
|
|
145
|
+
flags: [{ name: '--body', gloss: 'read the commit body from stdin (heredoc)' }, BUILD_FLAG],
|
|
146
|
+
notes: 'Refuses (exit 1) only with no session. There is no refuse-without-report gate.',
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'init',
|
|
150
|
+
description: 'link plumbbob into Claude Code as an in-place plugin (~/.claude/skills/plumbbob)',
|
|
151
|
+
synopsis: ['init [--uninstall] [--force]'],
|
|
152
|
+
flags: [
|
|
153
|
+
{ name: '--uninstall', gloss: 'drop the link' },
|
|
154
|
+
{ name: '--force', gloss: 'override the marketplace double-install guard' },
|
|
155
|
+
],
|
|
156
|
+
notes: 'Idempotent and global-only; it never writes settings.json. Restart Claude Code (or /reload-plugins) to activate.',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'doctor',
|
|
160
|
+
description: 'diagnose the plugin link + check gate; detect a legacy flat sidecar',
|
|
161
|
+
synopsis: ['doctor [--migrate]'],
|
|
162
|
+
flags: [{ name: '--migrate', gloss: 'move a legacy flat sidecar into builds/ (staged, not committed)' }],
|
|
163
|
+
notes: 'Exits 0 when everything passes, 1 when a check fails or an un-migrated legacy sidecar is present.',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'agent',
|
|
167
|
+
description: 'list or spawn a user-authored agent',
|
|
168
|
+
synopsis: [
|
|
169
|
+
'agent list',
|
|
170
|
+
'agent run <name> [--step N] [--mode before|build|after] [--agent <path>] [--build <slug>]',
|
|
171
|
+
'agent run --mode before|build|after [--step N]',
|
|
172
|
+
],
|
|
173
|
+
args: [
|
|
174
|
+
{ name: 'list', gloss: 'print every resolvable agent — name, origin tier, slots, description' },
|
|
175
|
+
{ name: 'run', gloss: 'spawn an agent through the doorway; no code path advances the loop' },
|
|
176
|
+
],
|
|
177
|
+
flags: [
|
|
178
|
+
{ name: '--step', value: '<n>', gloss: 'pick the step (else the in-flight STEP)' },
|
|
179
|
+
{ name: '--mode', value: '<slot>', gloss: 'the slot to run in: before, build, or after' },
|
|
180
|
+
{ name: '--agent', value: '<path>', gloss: 'point at an explicit agent directory (top of the ladder)' },
|
|
181
|
+
BUILD_FLAG,
|
|
182
|
+
],
|
|
183
|
+
notes: 'Exits 0 on a clean run, 1 on a failed one. See docs/agents.md for the author contract.',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'turn',
|
|
187
|
+
description: 'UserPromptSubmit hook machinery — tick the turn ledger from stdin (not a user verb)',
|
|
188
|
+
synopsis: ['turn'],
|
|
189
|
+
tolerateUnknownFlags: true,
|
|
190
|
+
notes: 'Reads the hook payload from stdin and always exits 0 — a broken hook must never wedge a prompt.',
|
|
191
|
+
},
|
|
46
192
|
];
|
|
47
193
|
/**
|
|
48
194
|
* Render the verb table for `plumbbob help`.
|
|
49
195
|
*/
|
|
50
196
|
export function formatHelp() {
|
|
51
197
|
const width = Math.max(...VERBS.map((v) => v.name.length));
|
|
52
|
-
const rows = VERBS.map((v) => ` ${v.name.padEnd(width)} ${v.
|
|
53
|
-
return [
|
|
198
|
+
const rows = VERBS.map((v) => ` ${v.name.padEnd(width)} ${v.description}`);
|
|
199
|
+
return [
|
|
200
|
+
'plumbbob — attention-first build process',
|
|
201
|
+
'',
|
|
202
|
+
'Usage: plumbbob <verb> [args]',
|
|
203
|
+
'',
|
|
204
|
+
'Verbs:',
|
|
205
|
+
...rows,
|
|
206
|
+
'',
|
|
207
|
+
"Run 'plumbbob <verb> --help' for a verb's arguments and flags.",
|
|
208
|
+
'',
|
|
209
|
+
].join('\n');
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Render the synopsis, argument, and flag block for `plumbbob <verb> --help`,
|
|
213
|
+
* or null when the name is not a verb.
|
|
214
|
+
*
|
|
215
|
+
* Arguments and flags share one aligned column so the reader scans a single
|
|
216
|
+
* list; a flag that takes a value prints as `--only <a,b>` so the spelling in
|
|
217
|
+
* the help is the spelling you type.
|
|
218
|
+
*/
|
|
219
|
+
export function formatVerbHelp(name) {
|
|
220
|
+
const spec = verbSpec(name);
|
|
221
|
+
if (spec === null)
|
|
222
|
+
return null;
|
|
223
|
+
const entries = [
|
|
224
|
+
...(spec.args ?? []).map((a) => ({ label: a.name, gloss: a.gloss })),
|
|
225
|
+
...(spec.flags ?? []).map((f) => ({
|
|
226
|
+
label: f.value === undefined ? f.name : `${f.name} ${f.value}`,
|
|
227
|
+
gloss: f.gloss,
|
|
228
|
+
})),
|
|
229
|
+
];
|
|
230
|
+
const width = Math.max(0, ...entries.map((e) => e.label.length));
|
|
231
|
+
const rows = entries.map((e) => ` ${e.label.padEnd(width)} ${e.gloss}`);
|
|
232
|
+
return [
|
|
233
|
+
...spec.synopsis.map((s) => `plumbbob ${s}`),
|
|
234
|
+
'',
|
|
235
|
+
spec.description,
|
|
236
|
+
...(rows.length > 0 ? ['', ...rows] : []),
|
|
237
|
+
...(spec.notes === undefined ? [] : ['', spec.notes]),
|
|
238
|
+
'',
|
|
239
|
+
`See: docs/cli-reference.md#${spec.name}`,
|
|
240
|
+
'',
|
|
241
|
+
].join('\n');
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Look up a verb's spec by name, or null when the name is not a verb.
|
|
245
|
+
*/
|
|
246
|
+
export function verbSpec(name) {
|
|
247
|
+
return VERBS.find((v) => v.name === name) ?? null;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Every verb name, in table order — the contract tests iterate this.
|
|
251
|
+
*/
|
|
252
|
+
export function verbNames() {
|
|
253
|
+
return VERBS.map((v) => v.name);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Walk a verb's argv against its spec: did it ask for help, and what did it
|
|
257
|
+
* spell wrong?
|
|
258
|
+
*
|
|
259
|
+
* Value-consuming flags swallow their next token, so a value that happens to
|
|
260
|
+
* look like a flag (`checkpoint -m "--help"`) is read as the value it is. Bare
|
|
261
|
+
* `--help`/`-h` are recognized for every verb without being declared.
|
|
262
|
+
*/
|
|
263
|
+
function scanFlags(spec, args) {
|
|
264
|
+
const known = new Map((spec.flags ?? []).map((f) => [f.name, f]));
|
|
265
|
+
const unknown = [];
|
|
266
|
+
let help = false;
|
|
267
|
+
for (let i = 0; i < args.length; i++) {
|
|
268
|
+
const arg = args[i];
|
|
269
|
+
const flag = known.get(arg);
|
|
270
|
+
if (flag !== undefined) {
|
|
271
|
+
if (flag.value !== undefined)
|
|
272
|
+
i++;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (arg === '--help' || arg === '-h') {
|
|
276
|
+
help = true;
|
|
277
|
+
}
|
|
278
|
+
else if (arg.startsWith('-') && arg.length > 1) {
|
|
279
|
+
unknown.push(arg);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return { help, unknown };
|
|
54
283
|
}
|
|
55
284
|
/**
|
|
56
285
|
* The package version, or 'unknown'.
|
|
@@ -116,21 +345,49 @@ async function dispatch(verb, cwd, rest) {
|
|
|
116
345
|
/**
|
|
117
346
|
* The CLI entrypoint minus the exit: parse argv, dispatch, return the exit code.
|
|
118
347
|
*
|
|
119
|
-
* `help` and `version` short-circuit before dispatch
|
|
120
|
-
*
|
|
121
|
-
*
|
|
348
|
+
* `help` and `version` short-circuit before dispatch, as do per-verb `--help`
|
|
349
|
+
* and the unknown-flag refusal. Screening flags *here* rather than in each verb
|
|
350
|
+
* is what makes it safe by construction: a mutating verb can no longer be
|
|
351
|
+
* reached by an argv it does not understand, which is how `checkpoint --help`
|
|
352
|
+
* used to commit a real checkpoint. Any thrown error becomes a one-line stderr
|
|
353
|
+
* report and exit 1, so the bin entry only ever exits with the code returned
|
|
354
|
+
* here.
|
|
122
355
|
*/
|
|
123
356
|
export async function run(argv) {
|
|
124
357
|
const verb = argv[0] ?? 'help';
|
|
125
358
|
const rest = argv.slice(1);
|
|
126
359
|
if (verb === 'help' || verb === '--help' || verb === '-h') {
|
|
127
|
-
|
|
360
|
+
// `plumbbob help <verb>` is the same page as `plumbbob <verb> --help` —
|
|
361
|
+
// both spellings get typed, so both work.
|
|
362
|
+
const topic = rest.find((a) => !a.startsWith('-'));
|
|
363
|
+
if (topic === undefined) {
|
|
364
|
+
process.stdout.write(`${formatHelp()}\n`);
|
|
365
|
+
return 0;
|
|
366
|
+
}
|
|
367
|
+
const help = formatVerbHelp(topic);
|
|
368
|
+
if (help === null) {
|
|
369
|
+
process.stderr.write(`plumbbob: unknown verb '${topic}'. Run 'plumbbob help' for the verb table.\n`);
|
|
370
|
+
return 1;
|
|
371
|
+
}
|
|
372
|
+
process.stdout.write(`${help}\n`);
|
|
128
373
|
return 0;
|
|
129
374
|
}
|
|
130
375
|
if (verb === 'version' || verb === '--version' || verb === '-v') {
|
|
131
376
|
process.stdout.write(`plumbbob ${readVersion()}\n`);
|
|
132
377
|
return 0;
|
|
133
378
|
}
|
|
379
|
+
const spec = verbSpec(verb);
|
|
380
|
+
if (spec !== null) {
|
|
381
|
+
const { help, unknown } = scanFlags(spec, rest);
|
|
382
|
+
if (help) {
|
|
383
|
+
process.stdout.write(`${formatVerbHelp(verb)}\n`);
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
386
|
+
if (unknown.length > 0 && spec.tolerateUnknownFlags !== true) {
|
|
387
|
+
process.stderr.write(`plumbbob: ${verb}: unknown flag '${unknown[0]}'. Run 'plumbbob ${verb} --help' for the flags.\n`);
|
|
388
|
+
return 1;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
134
391
|
try {
|
|
135
392
|
return await dispatch(verb, process.cwd(), rest);
|
|
136
393
|
}
|
package/dist/verbs/agent.js
CHANGED
|
@@ -215,8 +215,9 @@ function degrade(ambient, hard, soft) {
|
|
|
215
215
|
* agent name and the value flags.
|
|
216
216
|
*
|
|
217
217
|
* A value flag missing its value, or `--step` given a non-number, is a loud
|
|
218
|
-
* error (returned as a string) rather than a silent default.
|
|
219
|
-
*
|
|
218
|
+
* error (returned as a string) rather than a silent default. An undeclared
|
|
219
|
+
* `--flag` never arrives here — `run` screens argv against the verb spec in
|
|
220
|
+
* cli-core.ts and refuses before dispatch.
|
|
220
221
|
*/
|
|
221
222
|
function parseRunArgs(args) {
|
|
222
223
|
const positionals = [];
|
package/dist/verbs/check.js
CHANGED
|
@@ -32,8 +32,12 @@ export async function check(cwd, args = []) {
|
|
|
32
32
|
/**
|
|
33
33
|
* Map argv onto checkride's flags: bare booleans plus comma-separated slot lists.
|
|
34
34
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
35
|
+
* Only the declared flags are read here; an undeclared one never reaches this
|
|
36
|
+
* function, because `run` screens argv against the verb spec in cli-core.ts and
|
|
37
|
+
* refuses before dispatch. This used to tolerate unknown args on the grounds
|
|
38
|
+
* that running the gate mattered more than flag hygiene — but the same tolerance
|
|
39
|
+
* on the mutating verbs let `checkpoint --help` commit, so the screen is now
|
|
40
|
+
* central and uniform.
|
|
37
41
|
*/
|
|
38
42
|
function parseFlags(args) {
|
|
39
43
|
return {
|
package/package.json
CHANGED