plumbbob 0.8.18 → 0.9.1
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/README.md +50 -44
- package/dist/cli-core.js +35 -15
- package/dist/lib/agents.js +230 -112
- package/dist/lib/buildlog.js +50 -30
- package/dist/lib/buildlogsync.js +21 -12
- package/dist/lib/check.js +69 -35
- package/dist/lib/commitmsg.js +53 -32
- package/dist/lib/git.js +86 -31
- package/dist/lib/intent.js +91 -31
- package/dist/lib/latch.js +70 -39
- package/dist/lib/orient.js +78 -39
- package/dist/lib/plugins.js +11 -8
- package/dist/lib/settings.js +65 -32
- package/dist/lib/sidecar.js +320 -150
- package/dist/lib/templates.js +17 -6
- package/dist/verbs/agent.js +116 -55
- package/dist/verbs/build.js +27 -12
- package/dist/verbs/check.js +33 -7
- package/dist/verbs/checkpoint.js +153 -78
- package/dist/verbs/doctor.js +192 -95
- package/dist/verbs/finish.js +86 -45
- package/dist/verbs/handoff.js +56 -20
- package/dist/verbs/init.js +37 -12
- package/dist/verbs/park.js +14 -3
- package/dist/verbs/revert.js +64 -24
- package/dist/verbs/spike.js +89 -30
- package/dist/verbs/start.js +72 -38
- package/dist/verbs/status.js +44 -17
- package/dist/verbs/turn.js +88 -45
- package/dist/verbs/use.js +20 -9
- package/package.json +1 -1
- package/skills/{pb-build → build}/SKILL.md +25 -25
- package/skills/{pb-doctor → doctor}/SKILL.md +1 -1
- package/skills/{pb-finish → finish}/SKILL.md +3 -3
- package/skills/{pb-harvest → harvest}/SKILL.md +4 -4
- package/skills/{pb-park → park}/SKILL.md +2 -2
- package/skills/{pb-plan → plan}/SKILL.md +11 -11
- package/skills/{pb-refine → refine}/SKILL.md +7 -7
- package/skills/{pb-revert → revert}/SKILL.md +2 -2
- package/skills/{pb-spike → spike}/SKILL.md +4 -4
- package/skills/{pb-status → status}/SKILL.md +3 -3
- package/skills/{pb-step → step}/SKILL.md +9 -9
- package/skills/{pb-verify → verify}/SKILL.md +8 -8
- package/templates/build-log.md +7 -7
- package/templates/intent.md +8 -8
- package/templates/spike-report.md +1 -1
package/dist/verbs/doctor.js
CHANGED
|
@@ -1,27 +1,37 @@
|
|
|
1
|
-
// `plumbbob doctor` —
|
|
1
|
+
// `plumbbob doctor` — the health report, five sections under one verb.
|
|
2
2
|
//
|
|
3
3
|
// 1. The global plugin link (read-only). After `plumbbob init`, plumbbob lives as a
|
|
4
4
|
// symlink at ~/.claude/skills/plumbbob pointing at the installed package; Claude
|
|
5
5
|
// Code loads it in place as a plugin (skills `/plumbbob:*`, the post-edit hook from
|
|
6
6
|
// hooks.json). doctor verifies the link resolves to a package carrying the manifest,
|
|
7
7
|
// the skills, and the hook — and names the fix for anything missing. The failure
|
|
8
|
-
// class it exists for is SILENT (a `/
|
|
8
|
+
// class it exists for is SILENT (a `/plumbbob:status` that opens an empty dashboard
|
|
9
9
|
// because the plugin never linked).
|
|
10
10
|
//
|
|
11
|
-
// 2. The repo sidecar layout.
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// the
|
|
11
|
+
// 2. The repo sidecar layout. plumbbob keeps its per-repo state in a `.plumbbob/`
|
|
12
|
+
// sidecar: a tracked artifact plane (settings.json plus one `builds/<slug>/` folder
|
|
13
|
+
// per build — intent, build-log, checkpoints, report — that rides its branch into
|
|
14
|
+
// the PR) and a git-excluded control plane of per-worktree files. A repo scaffolded
|
|
15
|
+
// by an older plumbbob carries the legacy FLAT sidecar instead (`.plumbbob/intent.md`,
|
|
16
|
+
// `config`, `archive/`), fully git-excluded, whose record dies with
|
|
17
|
+
// `git worktree remove`. doctor detects it and, with `--migrate`, moves it into the
|
|
18
|
+
// tracked `builds/<slug>/` layout: archive entries and the active session become
|
|
19
|
+
// build folders, `config` becomes `settings.json`, and the whole move is STAGED but
|
|
20
|
+
// never committed — the human owns that commit.
|
|
19
21
|
//
|
|
20
|
-
// 3.
|
|
21
|
-
// command
|
|
22
|
-
// "detected but tool missing" is the footgun this exists for.
|
|
22
|
+
// 3. Agent validation. Every resolvable user-authored agent gets its manifest and its
|
|
23
|
+
// `command` statically checked, so a broken agent surfaces here rather than mid-build.
|
|
23
24
|
//
|
|
24
|
-
//
|
|
25
|
+
// 4. The check gate. When a `check` setting overrides checkride (the default heavy
|
|
26
|
+
// check), doctor names the command; otherwise it runs checkride's own doctor and
|
|
27
|
+
// prints the slot/adapter table — "detected but tool missing" is the footgun this
|
|
28
|
+
// exists for.
|
|
29
|
+
//
|
|
30
|
+
// 5. The approval latch. Reports whether the harness's turn ledger is live (checkpoint
|
|
31
|
+
// can latch on a recorded human turn) or dormant (guidance only) — dormant is a
|
|
32
|
+
// state, never a failure.
|
|
33
|
+
//
|
|
34
|
+
// Functional, node builtins plus checkride.
|
|
25
35
|
import { accessSync, constants, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
26
36
|
import { homedir } from 'node:os';
|
|
27
37
|
import { dirname, isAbsolute, join } from 'node:path';
|
|
@@ -33,12 +43,18 @@ import { marketplacePlumbbob } from "../lib/plugins.js";
|
|
|
33
43
|
import { findRepoRoot, gitPath, stagePath } from "../lib/git.js";
|
|
34
44
|
import { buildDir, excludeControl, listBuilds, readTurn, setActiveBuild, sidecarDir, slugify } from "../lib/sidecar.js";
|
|
35
45
|
import { resolveBoolean, resolveString, settingsPath } from "../lib/settings.js";
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Resolve a directory shipped inside the plumbbob package (e.g. its skills/).
|
|
48
|
+
*
|
|
49
|
+
* Resolved off this module's URL so it lands the same from src/ (dev) and
|
|
50
|
+
* dist/ (published).
|
|
51
|
+
*/
|
|
38
52
|
function packageDir(name) {
|
|
39
53
|
return fileURLToPath(new URL(`../../${name}/`, import.meta.url));
|
|
40
54
|
}
|
|
41
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Skill directories (those carrying a SKILL.md) under `dir`.
|
|
57
|
+
*/
|
|
42
58
|
function listSkills(dir) {
|
|
43
59
|
try {
|
|
44
60
|
return readdirSync(dir).filter((n) => existsSync(join(dir, n, 'SKILL.md')));
|
|
@@ -47,8 +63,10 @@ function listSkills(dir) {
|
|
|
47
63
|
return [];
|
|
48
64
|
}
|
|
49
65
|
}
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the dir the plugin link points at: a symlink's target, or the path
|
|
68
|
+
* itself if a real directory was copied there. null if nothing is linked.
|
|
69
|
+
*/
|
|
52
70
|
function linkedPackage(link) {
|
|
53
71
|
try {
|
|
54
72
|
const st = lstatSync(link);
|
|
@@ -61,6 +79,12 @@ function linkedPackage(link) {
|
|
|
61
79
|
return null;
|
|
62
80
|
}
|
|
63
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* The link-install checks: manifest, skill set, and post-edit hook, each with its fix.
|
|
84
|
+
*
|
|
85
|
+
* `shipped` is the canonical skill list read from the package itself; an installed
|
|
86
|
+
* count below it means a stale or partial link, and every fix is the same re-link.
|
|
87
|
+
*/
|
|
64
88
|
function buildChecks(link, pkg, shipped) {
|
|
65
89
|
const installed = listSkills(join(pkg, 'skills'));
|
|
66
90
|
return [
|
|
@@ -76,7 +100,15 @@ function buildChecks(link, pkg, shipped) {
|
|
|
76
100
|
: { ok: false, label: 'hook missing (hooks/hooks.json)', fix: 're-link: plumbbob init' },
|
|
77
101
|
];
|
|
78
102
|
}
|
|
79
|
-
|
|
103
|
+
/**
|
|
104
|
+
* The plugin-link diagnostic: how plumbbob is installed into Claude Code, as checks.
|
|
105
|
+
*
|
|
106
|
+
* Two install kinds are recognized: the marketplace plugin (one green line — skills
|
|
107
|
+
* load and the CLI rides the plugin's bin/, nothing to verify piecemeal) and the
|
|
108
|
+
* init-style link at ~/.claude/skills/plumbbob (verified piece by piece). Both at
|
|
109
|
+
* once is a collision — two plugins named plumbbob fight over /plumbbob:* — reported
|
|
110
|
+
* first, with the keep-one fix.
|
|
111
|
+
*/
|
|
80
112
|
function pluginChecks() {
|
|
81
113
|
const home = process.env.HOME ?? homedir();
|
|
82
114
|
const link = join(home, '.claude', 'skills', 'plumbbob');
|
|
@@ -98,7 +130,9 @@ function pluginChecks() {
|
|
|
98
130
|
}
|
|
99
131
|
return checks;
|
|
100
132
|
}
|
|
101
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Read a directory's immediate sub-directory names, sorted; [] when it is absent.
|
|
135
|
+
*/
|
|
102
136
|
function subdirs(dir) {
|
|
103
137
|
try {
|
|
104
138
|
return readdirSync(dir, { withFileTypes: true })
|
|
@@ -110,11 +144,15 @@ function subdirs(dir) {
|
|
|
110
144
|
return [];
|
|
111
145
|
}
|
|
112
146
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Detect a legacy flat sidecar, or null when the repo is on the current layout
|
|
149
|
+
* (or has no sidecar).
|
|
150
|
+
*
|
|
151
|
+
* `config` and `archive/` are unambiguous legacy markers — the builds/ layout has
|
|
152
|
+
* neither. A flat `intent.md` alone would also match today's `--local` layout, so it
|
|
153
|
+
* only counts as legacy when the repo is NOT already migrated (no `builds/`, no
|
|
154
|
+
* `settings.json`): that guard is what keeps `--local` untouched.
|
|
155
|
+
*/
|
|
118
156
|
export function inspectLegacy(root) {
|
|
119
157
|
const dir = sidecarDir(root);
|
|
120
158
|
if (!existsSync(dir))
|
|
@@ -127,7 +165,9 @@ export function inspectLegacy(root) {
|
|
|
127
165
|
return null;
|
|
128
166
|
return { config, archive, session };
|
|
129
167
|
}
|
|
130
|
-
|
|
168
|
+
/**
|
|
169
|
+
* The first `# Heading` in a flat intent.md — the build title the slug derives from.
|
|
170
|
+
*/
|
|
131
171
|
function titleFromIntent(path) {
|
|
132
172
|
try {
|
|
133
173
|
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
@@ -141,7 +181,9 @@ function titleFromIntent(path) {
|
|
|
141
181
|
}
|
|
142
182
|
return '';
|
|
143
183
|
}
|
|
144
|
-
|
|
184
|
+
/**
|
|
185
|
+
* The `check=<cmd>` line from a legacy `.plumbbob/config`, or null when absent.
|
|
186
|
+
*/
|
|
145
187
|
function configCheck(path) {
|
|
146
188
|
try {
|
|
147
189
|
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
@@ -155,9 +197,13 @@ function configCheck(path) {
|
|
|
155
197
|
}
|
|
156
198
|
return null;
|
|
157
199
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
200
|
+
/**
|
|
201
|
+
* A slug not already claimed by `taken`, suffixing `-2`, `-3`, … only on collision.
|
|
202
|
+
*
|
|
203
|
+
* `start` refuses a colliding slug outright — the human picks a new title — but
|
|
204
|
+
* migration is mechanically moving folders that already exist, so it disambiguates
|
|
205
|
+
* rather than aborting mid-move.
|
|
206
|
+
*/
|
|
161
207
|
function uniqueSlug(base, taken) {
|
|
162
208
|
const slug = base.length > 0 ? base : 'migrated-build';
|
|
163
209
|
if (!taken.has(slug))
|
|
@@ -167,7 +213,9 @@ function uniqueSlug(base, taken) {
|
|
|
167
213
|
n += 1;
|
|
168
214
|
return `${slug}-${n}`;
|
|
169
215
|
}
|
|
170
|
-
|
|
216
|
+
/**
|
|
217
|
+
* Move whichever of `names` exist from `from/` into `to/`.
|
|
218
|
+
*/
|
|
171
219
|
function moveInto(from, to, names) {
|
|
172
220
|
mkdirSync(to, { recursive: true });
|
|
173
221
|
for (const name of names) {
|
|
@@ -176,9 +224,15 @@ function moveInto(from, to, names) {
|
|
|
176
224
|
renameSync(src, join(to, name));
|
|
177
225
|
}
|
|
178
226
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Replace the legacy blanket `.plumbbob/` exclude with the narrowed control-plane
|
|
229
|
+
* patterns.
|
|
230
|
+
*
|
|
231
|
+
* The flat layout excluded the whole sidecar via the shared gitdir's info/exclude;
|
|
232
|
+
* dropping that line and writing the per-worktree control patterns is what lets the
|
|
233
|
+
* moved `builds/` and `settings.json` become trackable while the control files stay
|
|
234
|
+
* excluded.
|
|
235
|
+
*/
|
|
182
236
|
function narrowExcludes(root) {
|
|
183
237
|
const exclude = gitPath(root, 'info/exclude');
|
|
184
238
|
try {
|
|
@@ -195,14 +249,17 @@ function narrowExcludes(root) {
|
|
|
195
249
|
}
|
|
196
250
|
excludeControl(root);
|
|
197
251
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Perform the migration and return a human-readable list of what moved.
|
|
254
|
+
*
|
|
255
|
+
* STAGES the result but never commits — the human owns that commit. Call only when
|
|
256
|
+
* `inspectLegacy` reported a legacy layout; doctor prints the returned actions.
|
|
257
|
+
*/
|
|
201
258
|
export function migrateSidecar(root) {
|
|
202
259
|
const dir = sidecarDir(root);
|
|
203
260
|
const actions = [];
|
|
204
261
|
const taken = new Set(listBuilds(root));
|
|
205
|
-
// config → settings.json (
|
|
262
|
+
// config → settings.json (skipped when settings.json already exists).
|
|
206
263
|
const configPath = join(dir, 'config');
|
|
207
264
|
if (existsSync(configPath)) {
|
|
208
265
|
if (!existsSync(settingsPath(root))) {
|
|
@@ -217,8 +274,9 @@ export function migrateSidecar(root) {
|
|
|
217
274
|
}
|
|
218
275
|
rmSync(configPath, { force: true });
|
|
219
276
|
}
|
|
220
|
-
// The flat active session → its own build folder, and the cursor
|
|
221
|
-
// the one in-flight build
|
|
277
|
+
// The flat active session → its own build folder, and the active-build cursor
|
|
278
|
+
// (.plumbbob/STATE) points at it: it is the one in-flight build. Migrate it first
|
|
279
|
+
// so it keeps the slug from its title.
|
|
222
280
|
const flatIntent = join(dir, 'intent.md');
|
|
223
281
|
if (existsSync(flatIntent)) {
|
|
224
282
|
const slug = uniqueSlug(slugify(titleFromIntent(flatIntent)), taken);
|
|
@@ -243,9 +301,13 @@ export function migrateSidecar(root) {
|
|
|
243
301
|
actions.push('staged the move (builds/ + settings.json) — commit it yourself');
|
|
244
302
|
return actions;
|
|
245
303
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
304
|
+
/**
|
|
305
|
+
* The sidecar section: legacy detection plus the migrate offer, or the migration
|
|
306
|
+
* report under `--migrate`.
|
|
307
|
+
*
|
|
308
|
+
* Returns the lines to print and how many problems it found (an un-migrated legacy
|
|
309
|
+
* layout counts as one, so the exit code flags it).
|
|
310
|
+
*/
|
|
249
311
|
function sidecarReport(cwd, migrate) {
|
|
250
312
|
const root = findRepoRoot(cwd);
|
|
251
313
|
const legacy = root === null ? null : inspectLegacy(root);
|
|
@@ -270,16 +332,21 @@ function sidecarReport(cwd, migrate) {
|
|
|
270
332
|
lines.push(' (archive/ + the active session → builds/, config → settings.json; staged, never committed)');
|
|
271
333
|
return { lines, failed: 1 };
|
|
272
334
|
}
|
|
273
|
-
// --- agent validation
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
335
|
+
// --- agent validation ---
|
|
336
|
+
/**
|
|
337
|
+
* Interpreters recognized as the program of a manifest `command`: when the command
|
|
338
|
+
* is `sh <script>` / `node <script>` / …, the checkable artifact is the script
|
|
339
|
+
* argument (the interpreter reads it — the file need not carry +x), not the
|
|
340
|
+
* interpreter, which we trust to be on PATH.
|
|
341
|
+
*/
|
|
278
342
|
const INTERPRETERS = new Set(['sh', 'bash', 'zsh', 'dash', 'node', 'deno', 'bun', 'python', 'python3', 'ruby', 'perl']);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
343
|
+
/**
|
|
344
|
+
* Expand PLUMBBOB_AGENT_DIR in a command and split it into bare tokens
|
|
345
|
+
* (surrounding quotes stripped).
|
|
346
|
+
*
|
|
347
|
+
* A deliberately shallow split — enough for the static command check below, not a
|
|
348
|
+
* shell parser (which is neither possible nor wanted for a best-effort diagnostic).
|
|
349
|
+
*/
|
|
283
350
|
function commandTokens(command, agentDir) {
|
|
284
351
|
const expanded = command.replaceAll('${PLUMBBOB_AGENT_DIR}', agentDir).replaceAll('$PLUMBBOB_AGENT_DIR', agentDir);
|
|
285
352
|
return expanded
|
|
@@ -287,14 +354,17 @@ function commandTokens(command, agentDir) {
|
|
|
287
354
|
.filter((t) => t.length > 0)
|
|
288
355
|
.map((t) => t.replace(/^['"]/, '').replace(/['"]$/, ''));
|
|
289
356
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
357
|
+
/**
|
|
358
|
+
* Statically check a manifest `command` for the silent footguns: a script file that
|
|
359
|
+
* does not exist, or a directly-invoked one missing its +x bit.
|
|
360
|
+
*
|
|
361
|
+
* The command is a shell string spawned via `sh -c` with the repo root as cwd, so a
|
|
362
|
+
* relative path resolves against `root` and PLUMBBOB_AGENT_DIR points at the agent's
|
|
363
|
+
* own dir. Best-effort by design — a path-shaped program or an interpreter's script
|
|
364
|
+
* argument is checked; a bare command (a PATH binary, an inline `node -e` program)
|
|
365
|
+
* is trusted, because we cannot prove a shell line broken without running it.
|
|
366
|
+
* Returns the problem string, or null when nothing checkable is wrong.
|
|
367
|
+
*/
|
|
298
368
|
function checkCommand(command, agentDir, root) {
|
|
299
369
|
const tokens = commandTokens(command, agentDir);
|
|
300
370
|
if (tokens.length === 0)
|
|
@@ -323,21 +393,29 @@ function checkCommand(command, agentDir, root) {
|
|
|
323
393
|
}
|
|
324
394
|
return null;
|
|
325
395
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
396
|
+
/**
|
|
397
|
+
* One agent's problem, or null when it validates.
|
|
398
|
+
*
|
|
399
|
+
* A malformed or unsupported-contract manifest surfaces as its resolution error; an
|
|
400
|
+
* otherwise-valid agent is checked for a missing/non-executable command.
|
|
401
|
+
*/
|
|
329
402
|
function agentProblem(listing, root) {
|
|
330
403
|
if (!listing.resolution.ok)
|
|
331
404
|
return listing.resolution.error;
|
|
332
405
|
const { manifest, dir } = listing.resolution.agent;
|
|
333
406
|
return checkCommand(manifest.command, dir, root);
|
|
334
407
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
408
|
+
/**
|
|
409
|
+
* The agent-validation section: walk every resolvable user-authored agent across
|
|
410
|
+
* both tiers (project `.plumbbob/agents/` + personal `~/.plumbbob/agents/`) and flag
|
|
411
|
+
* anything broken.
|
|
412
|
+
*
|
|
413
|
+
* Flags a malformed agent.json, an unsupported contract version (both arrive as the
|
|
414
|
+
* listing's parse error), or a missing/non-executable command. A repo with no agents
|
|
415
|
+
* shows no section (like the sidecar one), keeping doctor quiet for the common case;
|
|
416
|
+
* outside a repo there is no project tier to anchor to, so the section is skipped
|
|
417
|
+
* entirely.
|
|
418
|
+
*/
|
|
341
419
|
function agentReport(cwd) {
|
|
342
420
|
const root = findRepoRoot(cwd);
|
|
343
421
|
if (root === null)
|
|
@@ -360,11 +438,15 @@ function agentReport(cwd) {
|
|
|
360
438
|
}
|
|
361
439
|
return { lines, failed };
|
|
362
440
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
441
|
+
/**
|
|
442
|
+
* The check-gate section: what gates `checkpoint`, and whether it can run.
|
|
443
|
+
*
|
|
444
|
+
* A configured `check` setting names the spawn override and asks nothing more;
|
|
445
|
+
* otherwise checkride's own doctor reports the slot/adapter table. Only rows
|
|
446
|
+
* checkride marks `required` count as problems — an empty slot ("no tool detected")
|
|
447
|
+
* is informational, not a failure, because the runtime gate already refuses a
|
|
448
|
+
* vacuous run.
|
|
449
|
+
*/
|
|
368
450
|
async function gateReport(cwd, env) {
|
|
369
451
|
const root = findRepoRoot(cwd);
|
|
370
452
|
if (root === null)
|
|
@@ -391,11 +473,10 @@ async function gateReport(cwd, env) {
|
|
|
391
473
|
if (c.required && c.status !== 'ok')
|
|
392
474
|
failed += 1;
|
|
393
475
|
}
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
// exact fix; `start` surfaces the same probe at plan time.
|
|
476
|
+
// Called out where the human can see it coming: no CODE checks detected means
|
|
477
|
+
// checkpoints would be gated only by checkride's always-on repo checks — either
|
|
478
|
+
// a vacuous refusal or, worse, a green that tested nothing. Informational, with
|
|
479
|
+
// the exact fix; `start` surfaces the same probe at plan time.
|
|
399
480
|
if (!gateDetectsTools(report.checks)) {
|
|
400
481
|
lines.push(' ○ gate: no code checks detected — checkpoints would be gated by the always-on repo checks alone;' +
|
|
401
482
|
' set {"check": "npm test"} in .plumbbob/settings.json, or add tool configs checkride can see');
|
|
@@ -407,8 +488,10 @@ async function gateReport(cwd, env) {
|
|
|
407
488
|
return { lines: [...lines, ` ✗ checkride doctor failed — ${message}`], failed: 1 };
|
|
408
489
|
}
|
|
409
490
|
}
|
|
410
|
-
|
|
411
|
-
|
|
491
|
+
/**
|
|
492
|
+
* One slot line: `✓ types ← tsc (6.0.3)`, `✗ dead ← fallow` with its install
|
|
493
|
+
* hint, or `○ spell — no tool detected` for an empty (skipping) slot.
|
|
494
|
+
*/
|
|
412
495
|
function toolRow(c) {
|
|
413
496
|
if (c.adapter === null || c.adapter === undefined) {
|
|
414
497
|
return ` ○ ${c.slot ?? c.name} — ${c.hint ?? 'no tool detected (slot skips)'}`;
|
|
@@ -418,15 +501,20 @@ function toolRow(c) {
|
|
|
418
501
|
const hint = c.status !== 'ok' && c.hint !== null ? `\n → ${c.hint}` : '';
|
|
419
502
|
return ` ${mark} ${c.slot ?? c.name} ← ${c.adapter}${version}${hint}`;
|
|
420
503
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
504
|
+
/**
|
|
505
|
+
* The approval-latch health probe: is the turn ledger live?
|
|
506
|
+
*
|
|
507
|
+
* The latch is `checkpoint`'s refusal to land a step until the harness has recorded
|
|
508
|
+
* a human turn since the step was entered. `.plumbbob/TURN` holds a count once the
|
|
509
|
+
* UserPromptSubmit hook has ticked at least once, so a present count means the latch
|
|
510
|
+
* is armed — its absence means the tick is guidance-only. Dormant is a legitimate
|
|
511
|
+
* state (a host with no hooks simply has no ledger), never a failure — so this
|
|
512
|
+
* section adds nothing to the problem count. The dormant hint names the hook because
|
|
513
|
+
* a missing ledger almost always means it never wired, and that hook rides in either
|
|
514
|
+
* install kind (the marketplace plugin's hooks/hooks.json or an init-style link).
|
|
515
|
+
* Outside a repo there is no worktree ledger to read, so the section is skipped
|
|
516
|
+
* entirely.
|
|
517
|
+
*/
|
|
430
518
|
function latchReport(cwd) {
|
|
431
519
|
const root = findRepoRoot(cwd);
|
|
432
520
|
if (root === null)
|
|
@@ -436,14 +524,23 @@ function latchReport(cwd) {
|
|
|
436
524
|
? ' ○ latch: dormant — guidance only (no turn ledger yet; it ticks on your first prompt when the UserPromptSubmit hook is wired — re-run after one to confirm)'
|
|
437
525
|
: ` ✓ latch: live (turn ${turn})`;
|
|
438
526
|
const lines = ['', 'plumbbob doctor — approval latch (D64)', line];
|
|
439
|
-
//
|
|
440
|
-
//
|
|
527
|
+
// The latch ignores a settings `auto`: a model can write that file, and a grant
|
|
528
|
+
// the model can forge is no grant — self-approval comes only from the human's
|
|
529
|
+
// typed `/build --auto`. Surface a set value so a human relying on it isn't
|
|
441
530
|
// silently changed — informational, never a problem.
|
|
442
531
|
if (resolveBoolean(root, 'auto', false)) {
|
|
443
|
-
lines.push(' ○ auto: set in settings but not a grant since D67 — self-approve per run with `/
|
|
532
|
+
lines.push(' ○ auto: set in settings but not a grant since D67 — self-approve per run with `/plumbbob:build --auto`');
|
|
444
533
|
}
|
|
445
534
|
return { lines, failed: 0 };
|
|
446
535
|
}
|
|
536
|
+
/**
|
|
537
|
+
* Run every doctor section, print the report, and return the exit code.
|
|
538
|
+
*
|
|
539
|
+
* Sections in order: plugin install, sidecar layout (migrating under `--migrate`),
|
|
540
|
+
* agents, check gate, approval latch. Exit 1 when any section reported a problem, so
|
|
541
|
+
* scripts can gate on it; the closing lines cover the restart and the PATH story
|
|
542
|
+
* either way. `env` injects a deterministic toolchain probe for tests (see GateEnv).
|
|
543
|
+
*/
|
|
447
544
|
export async function doctor(cwd, args = [], env) {
|
|
448
545
|
const checks = pluginChecks();
|
|
449
546
|
const out = ['plumbbob doctor — plugin install'];
|
|
@@ -463,7 +560,7 @@ export async function doctor(cwd, args = [], env) {
|
|
|
463
560
|
out.push(failed === 0
|
|
464
561
|
? 'plumbbob: all checks passed. If a skill still misbehaves, restart Claude Code (or /reload-plugins).'
|
|
465
562
|
: `plumbbob: ${failed} problem(s) — apply the → fixes, then restart Claude Code.`);
|
|
466
|
-
out.push('plumbbob: skills shell a bare `plumbbob`. The marketplace plugin puts it on PATH only inside a Claude Code session (via bin/) — there is no terminal `plumbbob`; for one (or the skills-dir install) run `npm i -g plumbbob`. In-session you can also run this as `/
|
|
563
|
+
out.push('plumbbob: skills shell a bare `plumbbob`. The marketplace plugin puts it on PATH only inside a Claude Code session (via bin/) — there is no terminal `plumbbob`; for one (or the skills-dir install) run `npm i -g plumbbob`. In-session you can also run this as `/plumbbob:doctor`. Sessions are per-project via `plumbbob start`.');
|
|
467
564
|
process.stdout.write(`${out.join('\n')}\n`);
|
|
468
565
|
return failed === 0 ? 0 : 1;
|
|
469
566
|
}
|