plumbbob 0.5.3 → 0.6.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 +9 -0
- package/dist/cli-core.js +4 -0
- package/dist/lib/agents.js +498 -0
- package/dist/lib/intent.js +98 -8
- package/dist/lib/settings.js +17 -0
- package/dist/lib/sidecar.js +39 -2
- package/dist/verbs/agent.js +312 -0
- package/dist/verbs/checkpoint.js +2 -1
- package/dist/verbs/doctor.js +96 -3
- package/dist/verbs/start.js +16 -3
- package/dist/verbs/status.js +64 -2
- package/node_modules/checkride/CHANGELOG.md +139 -0
- package/node_modules/checkride/LICENSE +21 -0
- package/node_modules/checkride/README.md +179 -0
- package/node_modules/checkride/dist/adapters.d.ts +56 -0
- package/node_modules/checkride/dist/adapters.d.ts.map +1 -0
- package/node_modules/checkride/dist/adapters.js +219 -0
- package/node_modules/checkride/dist/adapters.js.map +1 -0
- package/node_modules/checkride/dist/cli.d.ts +29 -0
- package/node_modules/checkride/dist/cli.d.ts.map +1 -0
- package/node_modules/checkride/dist/cli.js +227 -0
- package/node_modules/checkride/dist/cli.js.map +1 -0
- package/node_modules/checkride/dist/config.d.ts +74 -0
- package/node_modules/checkride/dist/config.d.ts.map +1 -0
- package/node_modules/checkride/dist/config.js +126 -0
- package/node_modules/checkride/dist/config.js.map +1 -0
- package/node_modules/checkride/dist/doctor.d.ts +76 -0
- package/node_modules/checkride/dist/doctor.d.ts.map +1 -0
- package/node_modules/checkride/dist/doctor.js +299 -0
- package/node_modules/checkride/dist/doctor.js.map +1 -0
- package/node_modules/checkride/dist/index.d.ts +18 -0
- package/node_modules/checkride/dist/index.d.ts.map +1 -0
- package/node_modules/checkride/dist/index.js +13 -0
- package/node_modules/checkride/dist/index.js.map +1 -0
- package/node_modules/checkride/dist/init.d.ts +64 -0
- package/node_modules/checkride/dist/init.d.ts.map +1 -0
- package/node_modules/checkride/dist/init.js +489 -0
- package/node_modules/checkride/dist/init.js.map +1 -0
- package/node_modules/checkride/dist/links.d.ts +23 -0
- package/node_modules/checkride/dist/links.d.ts.map +1 -0
- package/node_modules/checkride/dist/links.js +125 -0
- package/node_modules/checkride/dist/links.js.map +1 -0
- package/node_modules/checkride/dist/orchestrator.d.ts +96 -0
- package/node_modules/checkride/dist/orchestrator.d.ts.map +1 -0
- package/node_modules/checkride/dist/orchestrator.js +234 -0
- package/node_modules/checkride/dist/orchestrator.js.map +1 -0
- package/node_modules/checkride/package.json +76 -0
- package/node_modules/checkride/templates/flat/fallow.toml +26 -0
- package/node_modules/checkride/templates/flat/pnpm-workspace.yaml +6 -0
- package/node_modules/checkride/templates/flat/tsconfig.json +9 -0
- package/node_modules/checkride/templates/hybrid/fallow.toml +42 -0
- package/node_modules/checkride/templates/hybrid/pnpm-workspace.yaml +6 -0
- package/node_modules/checkride/templates/hybrid/tsconfig.json +12 -0
- package/node_modules/checkride/templates/monorepo/fallow.toml +44 -0
- package/node_modules/checkride/templates/monorepo/pnpm-workspace.yaml +7 -0
- package/node_modules/checkride/templates/monorepo/tsconfig.json +7 -0
- package/node_modules/checkride/templates/shared/cspell.json +35 -0
- package/node_modules/checkride/templates/shared/gitignore +10 -0
- package/node_modules/checkride/templates/shared/markdownlint-cli2.jsonc +11 -0
- package/node_modules/checkride/templates/shared/npmrc +1 -0
- package/node_modules/checkride/templates/shared/oxlintrc.json +33 -0
- package/node_modules/checkride/templates/shared/rules/no-class.yml +8 -0
- package/node_modules/checkride/templates/shared/rules/no-deep-sibling-import.yml +15 -0
- package/node_modules/checkride/templates/shared/rules/no-default-export.yml +9 -0
- package/node_modules/checkride/templates/shared/rules/require-js-extension.yml +21 -0
- package/node_modules/checkride/templates/shared/sgconfig.yml +2 -0
- package/node_modules/checkride/templates/shared/tsconfig.base.json +28 -0
- package/node_modules/checkride/templates/shared/vitest.config.ts.template +27 -0
- package/package.json +4 -1
- package/skills/pb-build/SKILL.md +42 -7
- package/skills/pb-finish/SKILL.md +1 -1
- package/skills/pb-harvest/SKILL.md +1 -1
- package/skills/pb-park/SKILL.md +1 -1
- package/skills/pb-plan/SKILL.md +25 -4
- package/skills/pb-refine/SKILL.md +1 -1
- package/skills/pb-revert/SKILL.md +1 -1
- package/skills/pb-spike/SKILL.md +1 -1
- package/skills/pb-status/SKILL.md +1 -1
- package/skills/pb-step/SKILL.md +10 -2
- package/skills/pb-verify/SKILL.md +21 -6
package/dist/lib/intent.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// step, a missing seam, or more than one seam line,
|
|
1
|
+
// The intent parser. `build <n>` reads the nth step under `## Steps` and extracts
|
|
2
|
+
// its seam — the backtick-wrapped paths on the single `seam:` sub-line. The seam
|
|
3
|
+
// half is strict by design (it gates git behavior): it refuses precisely on globs,
|
|
4
|
+
// absolute paths, a missing step, a missing seam, or more than one seam line,
|
|
5
|
+
// rather than guessing. The best-effort scrapers below (title, done-when,
|
|
6
|
+
// decisions, constraints) feed an agent's context, not a gate, so they never
|
|
7
|
+
// refuse — they return what they can and report the lines they skipped (D61).
|
|
5
8
|
const GLOB_CHARS = /[*?[\]{}]/;
|
|
6
|
-
|
|
9
|
+
function sliceStep(content, step) {
|
|
7
10
|
const lines = content.split('\n');
|
|
8
11
|
const stepsIdx = lines.findIndex((l) => l.trim() === '## Steps');
|
|
9
12
|
if (stepsIdx === -1) {
|
|
10
|
-
return
|
|
13
|
+
return { ok: false, error: 'intent.md has no "## Steps" section.' };
|
|
11
14
|
}
|
|
12
15
|
let sectionEnd = lines.findIndex((l, i) => i > stepsIdx && l.startsWith('## '));
|
|
13
16
|
if (sectionEnd === -1) {
|
|
@@ -22,12 +25,19 @@ export function parseStepSeam(content, step) {
|
|
|
22
25
|
}
|
|
23
26
|
const pos = itemStarts.findIndex((s) => s.n === step);
|
|
24
27
|
if (pos === -1) {
|
|
25
|
-
return
|
|
28
|
+
return { ok: false, error: `intent.md has no step ${step} under "## Steps".` };
|
|
26
29
|
}
|
|
27
30
|
const itemStart = itemStarts[pos]?.idx ?? stepsIdx;
|
|
28
31
|
const nextStart = itemStarts[pos + 1]?.idx;
|
|
29
32
|
const itemEnd = nextStart === undefined ? sectionEnd : nextStart;
|
|
30
|
-
|
|
33
|
+
return { ok: true, itemLines: lines.slice(itemStart, itemEnd) };
|
|
34
|
+
}
|
|
35
|
+
export function parseStepSeam(content, step) {
|
|
36
|
+
const slice = sliceStep(content, step);
|
|
37
|
+
if (!slice.ok) {
|
|
38
|
+
return fail(slice.error);
|
|
39
|
+
}
|
|
40
|
+
const itemLines = slice.itemLines;
|
|
31
41
|
const seamLines = itemLines
|
|
32
42
|
.map((l, i) => ({ l, i }))
|
|
33
43
|
.filter(({ l }) => /^\s*-\s*seam:/.test(l))
|
|
@@ -102,6 +112,86 @@ export function scopeDrift(paths, seam) {
|
|
|
102
112
|
}
|
|
103
113
|
return paths.filter((p) => !matchesSeam(p, seam) && !isArtifactPath(p));
|
|
104
114
|
}
|
|
115
|
+
const DONE_WHEN = /\*\*done when:\*\*/i;
|
|
116
|
+
export function parseStepMeta(content, step) {
|
|
117
|
+
const slice = sliceStep(content, step);
|
|
118
|
+
if (!slice.ok) {
|
|
119
|
+
return { title: '', doneWhen: '' };
|
|
120
|
+
}
|
|
121
|
+
// Gather the opener and its wrapped continuation lines, stopping at the first
|
|
122
|
+
// sub-bullet (`- seam:` and friends), then flatten to one line.
|
|
123
|
+
const collected = [];
|
|
124
|
+
for (let i = 0; i < slice.itemLines.length; i++) {
|
|
125
|
+
const line = slice.itemLines[i] ?? '';
|
|
126
|
+
if (i > 0 && /^\s*-\s/.test(line)) {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
collected.push(line.trim());
|
|
130
|
+
}
|
|
131
|
+
const body = collected.join(' ').replace(/^\d+\.\s*(?:\[[ xX]\]\s*)?/, '').trim();
|
|
132
|
+
const m = DONE_WHEN.exec(body);
|
|
133
|
+
if (m === null) {
|
|
134
|
+
return { title: stripTrailingDash(body), doneWhen: '' };
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
title: stripTrailingDash(body.slice(0, m.index)),
|
|
138
|
+
doneWhen: body.slice(m.index + m[0].length).trim(),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// The build title — the first `# ` heading. '' when absent.
|
|
142
|
+
export function parseBuildTitle(content) {
|
|
143
|
+
for (const line of content.split('\n')) {
|
|
144
|
+
const m = /^#\s+(.+)$/.exec(line);
|
|
145
|
+
if (m) {
|
|
146
|
+
return (m[1] ?? '').trim();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return '';
|
|
150
|
+
}
|
|
151
|
+
export function scrapeBullets(content, heading) {
|
|
152
|
+
const lines = content.split('\n');
|
|
153
|
+
const start = lines.findIndex((l) => l.trim() === heading);
|
|
154
|
+
if (start === -1) {
|
|
155
|
+
return { items: [], skipped: [] };
|
|
156
|
+
}
|
|
157
|
+
let end = lines.findIndex((l, i) => i > start && l.startsWith('## '));
|
|
158
|
+
if (end === -1) {
|
|
159
|
+
end = lines.length;
|
|
160
|
+
}
|
|
161
|
+
const items = [];
|
|
162
|
+
const skipped = [];
|
|
163
|
+
let current = null;
|
|
164
|
+
const flush = () => {
|
|
165
|
+
if (current !== null) {
|
|
166
|
+
items.push(current.join(' '));
|
|
167
|
+
current = null;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
for (let i = start + 1; i < end; i++) {
|
|
171
|
+
const line = lines[i] ?? '';
|
|
172
|
+
const bullet = /^-\s+(.*)$/.exec(line);
|
|
173
|
+
if (bullet !== null) {
|
|
174
|
+
flush();
|
|
175
|
+
current = [(bullet[1] ?? '').trim()];
|
|
176
|
+
}
|
|
177
|
+
else if (line.trim() === '') {
|
|
178
|
+
flush();
|
|
179
|
+
}
|
|
180
|
+
else if (current !== null && /^\s+\S/.test(line)) {
|
|
181
|
+
current.push(line.trim());
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
skipped.push(line);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
flush();
|
|
188
|
+
return { items, skipped };
|
|
189
|
+
}
|
|
190
|
+
// Trim a trailing em-dash or hyphen (the `Title —` separator) plus surrounding
|
|
191
|
+
// space, so a title never carries the marker that introduced its done-when.
|
|
192
|
+
function stripTrailingDash(text) {
|
|
193
|
+
return text.replace(/\s*[—-]+\s*$/, '').trim();
|
|
194
|
+
}
|
|
105
195
|
function fail(error) {
|
|
106
196
|
return { ok: false, error };
|
|
107
197
|
}
|
package/dist/lib/settings.js
CHANGED
|
@@ -53,6 +53,23 @@ export function resolveBoolean(root, key, fallback, flag) {
|
|
|
53
53
|
const value = resolveSetting(root, key, flag);
|
|
54
54
|
return typeof value === 'boolean' ? value : fallback;
|
|
55
55
|
}
|
|
56
|
+
// Resolve a non-negative integer setting (e.g. `agentTimeout`, D51). A missing
|
|
57
|
+
// rung, or one holding a non-finite / negative / non-integer number, yields the
|
|
58
|
+
// caller's fallback rather than a garbage timeout — 0 means "no timeout", so a
|
|
59
|
+
// broken value must never silently become one.
|
|
60
|
+
export function resolveNumber(root, key, fallback, flag) {
|
|
61
|
+
const value = resolveSetting(root, key, flag);
|
|
62
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : fallback;
|
|
63
|
+
}
|
|
64
|
+
// Resolve an object-valued setting (e.g. the `agents` slot-binding defaults, D57)
|
|
65
|
+
// across the same ladder. The first defined, object-typed rung wins; a missing
|
|
66
|
+
// rung, or one holding a non-object / array / scalar, yields {} rather than
|
|
67
|
+
// garbage — mirrors the string/boolean/number resolvers above. No flag rung: the
|
|
68
|
+
// caller (`agent run`) merges these under the per-build harness, not over a flag.
|
|
69
|
+
export function resolveRecord(root, key) {
|
|
70
|
+
const value = resolveSetting(root, key, undefined);
|
|
71
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : {};
|
|
72
|
+
}
|
|
56
73
|
// Read one key from the untracked local overlay ONLY — no project or built-in
|
|
57
74
|
// fallback. The `activeBuild` cursor lives here and must never resolve from the
|
|
58
75
|
// tracked settings.json (it is per-worktree state, not a shared default).
|
package/dist/lib/sidecar.js
CHANGED
|
@@ -27,8 +27,9 @@ export function buildDir(root, slug) {
|
|
|
27
27
|
// Derive a filesystem-safe slug from a build title: lowercased, every run of
|
|
28
28
|
// non-alphanumerics collapsed to a single hyphen, trimmed of leading/trailing
|
|
29
29
|
// hyphens. The CLI stays dumb and explicit (D38) — collision handling belongs to
|
|
30
|
-
// the caller (`start` refuses rather than silently suffixing `-2
|
|
31
|
-
//
|
|
30
|
+
// the caller (`start` refuses rather than silently suffixing `-2`, and prepends
|
|
31
|
+
// the `YYYY-MM-DD-` date that keeps `listBuilds`' lexical sort chronological). A
|
|
32
|
+
// title with no alphanumerics yields `''`, which the caller must reject or override.
|
|
32
33
|
export function slugify(title) {
|
|
33
34
|
return title
|
|
34
35
|
.toLowerCase()
|
|
@@ -125,6 +126,39 @@ export function buildLogPath(root, slug) {
|
|
|
125
126
|
export function reportPath(root, slug) {
|
|
126
127
|
return join(artifactDir(root, slug), 'report.md');
|
|
127
128
|
}
|
|
129
|
+
// handoff.json is the agent-run ledger (D47): each `agent run` appends its
|
|
130
|
+
// validated envelope here so a later invocation can thread earlier results into
|
|
131
|
+
// the next call's `context[]` — the CLI itself is memoryless between runs, and
|
|
132
|
+
// the file survives context-window compaction where inline stdout does not. It is
|
|
133
|
+
// in-flight control state like STEP/SEAM: untracked (excludeControl), scoped to
|
|
134
|
+
// the current step, cleared when the step checkpoints (`clearHandoff`).
|
|
135
|
+
export function handoffPath(root, slug) {
|
|
136
|
+
return join(artifactDir(root, slug), 'handoff.json');
|
|
137
|
+
}
|
|
138
|
+
// Append one entry to the build's handoff.json, creating the file (as a JSON
|
|
139
|
+
// array) when absent and tolerating a malformed existing file by starting fresh —
|
|
140
|
+
// a corrupt ledger must never wedge a run. Pretty-printed so it stays readable.
|
|
141
|
+
export function appendHandoff(root, slug, entry) {
|
|
142
|
+
const path = handoffPath(root, slug);
|
|
143
|
+
let entries = [];
|
|
144
|
+
try {
|
|
145
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
146
|
+
if (Array.isArray(parsed))
|
|
147
|
+
entries = parsed;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
entries = [];
|
|
151
|
+
}
|
|
152
|
+
entries.push(entry);
|
|
153
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
154
|
+
writeFileSync(path, `${JSON.stringify(entries, null, 2)}\n`);
|
|
155
|
+
}
|
|
156
|
+
// Remove the build's handoff.json — the step-scoped ledger is cleared when the
|
|
157
|
+
// step checkpoints (D47), the same beat that clears STEP/SEAM. Absent file is a
|
|
158
|
+
// no-op (`force`).
|
|
159
|
+
export function clearHandoff(root, slug) {
|
|
160
|
+
rmSync(handoffPath(root, slug), { force: true });
|
|
161
|
+
}
|
|
128
162
|
// A session exists iff STATE exists. Deleting STATE (at finish) is what flips the
|
|
129
163
|
// repo back to "no session" — so it is the single source of truth for "is there
|
|
130
164
|
// a session". `start` calls beginSession; `finish` removes the file.
|
|
@@ -177,6 +211,9 @@ export function excludeControl(root) {
|
|
|
177
211
|
`${DIRNAME}/builds/*/STEP`,
|
|
178
212
|
`${DIRNAME}/builds/*/SEAM`,
|
|
179
213
|
`${DIRNAME}/builds/*/SPIKE`,
|
|
214
|
+
// The agent-run handoff ledger (D47) is step-scoped in-flight state, not a
|
|
215
|
+
// tracked artifact — it must never ride a step commit into the PR.
|
|
216
|
+
`${DIRNAME}/builds/*/handoff.json`,
|
|
180
217
|
// The checkride gate (D32) writes raw tool output to `.check/`; checkpoint's
|
|
181
218
|
// stageAll must never sweep it into a step commit.
|
|
182
219
|
'.check/',
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// `plumbbob agent <subcommand>` — the doorway to user-authored agents (D39/D41).
|
|
2
|
+
// `agent list` walks the two tiers and prints each resolvable agent. `agent run
|
|
3
|
+
// <name> [--step N] [--mode before|build|after]` (D60) composes the StepContext,
|
|
4
|
+
// spawns the manifest command, streams its stderr live, captures and validates
|
|
5
|
+
// the child's envelope, re-emits it on this verb's own stdout (machine) with the
|
|
6
|
+
// human summary on stderr (D46/D47), lands `parked[]` through the build-log (D44),
|
|
7
|
+
// and appends the envelope to the step-scoped handoff ledger (D47). There is no
|
|
8
|
+
// code path here to checkpoint, flip a step, or chain agents — the identity
|
|
9
|
+
// invariant (C6) holds by construction. A thin read-write shell: resolution,
|
|
10
|
+
// composition, and spawn mechanics live in lib/agents.ts.
|
|
11
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { findRepoRoot } from "../lib/git.js";
|
|
14
|
+
import { SLOTS, composeStepContext, formatAgentList, isSlot, listAgents, parseSlotBindings, readHarnessFile, resolveAgent, resolveSlotAgents, runAgent, } from "../lib/agents.js";
|
|
15
|
+
import { appendToSection } from "../lib/buildlog.js";
|
|
16
|
+
import { resolveBoolean, resolveNumber, resolveRecord } from "../lib/settings.js";
|
|
17
|
+
import { appendHandoff, buildFolder, buildLogPath, hasSession, intentPath, resolveBuild, stepPath } from "../lib/sidecar.js";
|
|
18
|
+
export async function agent(cwd, args = []) {
|
|
19
|
+
const [sub, ...rest] = args;
|
|
20
|
+
if (sub === 'list')
|
|
21
|
+
return list(cwd, rest);
|
|
22
|
+
if (sub === 'run')
|
|
23
|
+
return run(cwd, rest);
|
|
24
|
+
const known = 'Available subcommands: list, run.';
|
|
25
|
+
const message = sub === undefined ? `plumbbob agent <subcommand>. ${known}` : `plumbbob: unknown 'agent' subcommand '${sub}'. ${known}`;
|
|
26
|
+
process.stderr.write(`${message}\n`);
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
function list(cwd, _args) {
|
|
30
|
+
const root = findRepoRoot(cwd);
|
|
31
|
+
if (root === null) {
|
|
32
|
+
process.stderr.write('plumbbob: not inside a git repository.\n');
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
process.stdout.write(`${formatAgentList(listAgents(root))}\n`);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
// `agent run`: with a name (or `--agent` flag) run exactly that agent, failing
|
|
39
|
+
// loud on a miss (D54); with no name run the step's harness-bound agents for the
|
|
40
|
+
// requested slot (D42). Either way this composes the StepContext, spawns, and
|
|
41
|
+
// applies side effects — never advancing the loop.
|
|
42
|
+
async function run(cwd, args) {
|
|
43
|
+
const root = findRepoRoot(cwd);
|
|
44
|
+
if (root === null || !hasSession(root)) {
|
|
45
|
+
process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n');
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
const { build: slug, rest } = resolveBuild(root, args);
|
|
49
|
+
const parsed = parseRunArgs(rest);
|
|
50
|
+
if (typeof parsed === 'string') {
|
|
51
|
+
process.stderr.write(`${parsed}\n`);
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
const step = parsed.step ?? readStep(root, slug);
|
|
55
|
+
if (step === null) {
|
|
56
|
+
process.stderr.write('plumbbob: no step to run against — pass --step N, or `plumbbob build N` first.\n');
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
// A `--agent <path>` flag still needs a name — it labels an explicit run (the
|
|
60
|
+
// handoff ledger and the human summary key on the name), it does not name one.
|
|
61
|
+
if (parsed.name === undefined && parsed.flagPath !== undefined) {
|
|
62
|
+
process.stderr.write('plumbbob: --agent needs an agent name too, for the run label. Try: plumbbob agent run reviewer --agent ./path --step 3.\n');
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
// Explicit ask (a name, above the bindings — D57): run exactly it, fail loud on
|
|
66
|
+
// a miss. No name: resolve the harness bindings for the slot and run them.
|
|
67
|
+
if (parsed.name !== undefined) {
|
|
68
|
+
return runOne(root, slug, step, {
|
|
69
|
+
name: parsed.name,
|
|
70
|
+
flagPath: parsed.flagPath,
|
|
71
|
+
mode: parsed.mode,
|
|
72
|
+
ambient: false,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return runBound(root, slug, step, parsed.mode);
|
|
76
|
+
}
|
|
77
|
+
async function runOne(root, slug, step, spec) {
|
|
78
|
+
const resolution = resolveAgent(root, spec.name, spec.flagPath !== undefined ? { flagPath: spec.flagPath } : {});
|
|
79
|
+
if (!resolution.ok) {
|
|
80
|
+
return degrade(spec.ambient, `plumbbob: ${resolution.error}`, `plumbbob: bound agent "${spec.name}" did not resolve — ${resolution.error} Skipping (D54 — the loop works without it).`);
|
|
81
|
+
}
|
|
82
|
+
const { manifest, dir } = resolution.agent;
|
|
83
|
+
const resolved = resolveMode(spec.mode, manifest);
|
|
84
|
+
if (!resolved.ok) {
|
|
85
|
+
return degrade(spec.ambient, resolved.error, `plumbbob: bound agent "${spec.name}" — ${resolved.error.replace(/^plumbbob:\s*/, '')} Skipping (D54).`);
|
|
86
|
+
}
|
|
87
|
+
const mode = resolved.mode;
|
|
88
|
+
let intent;
|
|
89
|
+
try {
|
|
90
|
+
intent = readFileSync(intentPath(root, slug), 'utf8');
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
process.stderr.write('plumbbob: no intent.md for the active build — cannot compose the agent input.\n');
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
const composed = composeStepContext({
|
|
97
|
+
intent,
|
|
98
|
+
slug: slug ?? '',
|
|
99
|
+
step,
|
|
100
|
+
mode,
|
|
101
|
+
settings: {
|
|
102
|
+
auto: resolveBoolean(root, 'auto', false),
|
|
103
|
+
agentTimeout: resolveNumber(root, 'agentTimeout', 0),
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
if (!composed.ok) {
|
|
107
|
+
process.stderr.write(`plumbbob: ${composed.error}\n`);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
for (const warning of composed.warnings) {
|
|
111
|
+
process.stderr.write(`${warning}\n`);
|
|
112
|
+
}
|
|
113
|
+
const result = await runAgent({
|
|
114
|
+
root,
|
|
115
|
+
command: manifest.command,
|
|
116
|
+
agentDir: dir,
|
|
117
|
+
input: composed.input,
|
|
118
|
+
timeoutSeconds: resolveNumber(root, 'agentTimeout', 0),
|
|
119
|
+
});
|
|
120
|
+
return report(root, slug, spec.name, mode, step, result);
|
|
121
|
+
}
|
|
122
|
+
// No name given: run the harness-bound agents for the requested slot (D42). The
|
|
123
|
+
// bindings merge the ladder — per-step over harness defaults over settings-level
|
|
124
|
+
// defaults (D57). A missing bound agent degrades to a warning (D54); an absent
|
|
125
|
+
// harness, or one that binds nothing to this slot, is a clean no-op. Each bound
|
|
126
|
+
// agent runs in turn; the batch exits 1 if any agent that actually ran failed.
|
|
127
|
+
async function runBound(root, slug, step, modeFlag) {
|
|
128
|
+
if (modeFlag === undefined) {
|
|
129
|
+
process.stderr.write('plumbbob: agent run needs an agent name, or --mode <slot> to run the step\'s bound agents.\n');
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
if (!isSlot(modeFlag)) {
|
|
133
|
+
process.stderr.write(`plumbbob: unknown --mode '${modeFlag}' — slots are ${SLOTS.join(', ')}.\n`);
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
const harness = readHarnessFile(join(buildFolder(root, slug), 'harness.json'));
|
|
137
|
+
if (harness !== null && !harness.ok) {
|
|
138
|
+
process.stderr.write(`plumbbob: ${harness.error}\n`);
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
const settingsDefaults = parseSlotBindings(resolveRecord(root, 'agents'));
|
|
142
|
+
const names = resolveSlotAgents({
|
|
143
|
+
harness: harness?.harness ?? null,
|
|
144
|
+
settingsDefaults,
|
|
145
|
+
step,
|
|
146
|
+
slot: modeFlag,
|
|
147
|
+
});
|
|
148
|
+
if (names.length === 0) {
|
|
149
|
+
process.stderr.write(`plumbbob: no agents bound to the '${modeFlag}' slot for step ${step} — nothing to run.\n`);
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
let worst = 0;
|
|
153
|
+
for (const name of names) {
|
|
154
|
+
const code = await runOne(root, slug, step, { name, flagPath: undefined, mode: modeFlag, ambient: true });
|
|
155
|
+
if (code !== 0)
|
|
156
|
+
worst = code;
|
|
157
|
+
}
|
|
158
|
+
return worst;
|
|
159
|
+
}
|
|
160
|
+
// Emit the loud error or the soft warning by whether this run was an explicit ask
|
|
161
|
+
// (name/flag) or an ambient harness binding (D54/D54), and return the matching
|
|
162
|
+
// code — a hard miss stops with 1, a degraded one warns and returns 0 so a batch
|
|
163
|
+
// of bound agents carries on.
|
|
164
|
+
function degrade(ambient, hard, soft) {
|
|
165
|
+
process.stderr.write(`${ambient ? soft : hard}\n`);
|
|
166
|
+
return ambient ? 0 : 1;
|
|
167
|
+
}
|
|
168
|
+
// Split `run`'s argv (with `--build` already stripped by resolveBuild) into the
|
|
169
|
+
// agent name and the value flags. A value flag missing its value, or `--step`
|
|
170
|
+
// given a non-number, is a loud error (returned as a string) rather than a silent
|
|
171
|
+
// default. Unknown `--flags` are ignored — the point is the named agent.
|
|
172
|
+
function parseRunArgs(args) {
|
|
173
|
+
const positionals = [];
|
|
174
|
+
let step;
|
|
175
|
+
let mode;
|
|
176
|
+
let flagPath;
|
|
177
|
+
for (let i = 0; i < args.length; i++) {
|
|
178
|
+
const arg = args[i];
|
|
179
|
+
if (arg === '--step') {
|
|
180
|
+
const value = args[++i];
|
|
181
|
+
if (value === undefined || !/^\d+$/.test(value) || Number(value) < 1) {
|
|
182
|
+
return 'plumbbob: --step needs a positive step number.';
|
|
183
|
+
}
|
|
184
|
+
step = Number(value);
|
|
185
|
+
}
|
|
186
|
+
else if (arg === '--mode') {
|
|
187
|
+
const value = args[++i];
|
|
188
|
+
if (value === undefined)
|
|
189
|
+
return `plumbbob: --mode needs a slot (${SLOTS.join(', ')}).`;
|
|
190
|
+
mode = value;
|
|
191
|
+
}
|
|
192
|
+
else if (arg === '--agent') {
|
|
193
|
+
const value = args[++i];
|
|
194
|
+
if (value === undefined)
|
|
195
|
+
return 'plumbbob: --agent needs a path to an agent directory.';
|
|
196
|
+
flagPath = value;
|
|
197
|
+
}
|
|
198
|
+
else if (!arg.startsWith('--')) {
|
|
199
|
+
positionals.push(arg);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { name: positionals[0], step, mode, flagPath };
|
|
203
|
+
}
|
|
204
|
+
// Resolve the slot the agent runs in. An explicit `--mode` must name a real slot
|
|
205
|
+
// AND one the manifest declares — an undeclared slot is refused loud (D54). With
|
|
206
|
+
// no `--mode`, a single-slot agent uses its only slot; a multi-slot agent must be
|
|
207
|
+
// told which one (step 5's harness bindings pick the slot from the lifecycle
|
|
208
|
+
// point; a bare `run` cannot guess).
|
|
209
|
+
function resolveMode(flag, manifest) {
|
|
210
|
+
if (flag !== undefined) {
|
|
211
|
+
if (!isSlot(flag))
|
|
212
|
+
return { ok: false, error: `plumbbob: unknown --mode '${flag}' — slots are ${SLOTS.join(', ')}.` };
|
|
213
|
+
if (!manifest.slots.includes(flag)) {
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
error: `plumbbob: agent "${manifest.name}" does not declare the '${flag}' slot (it declares ${manifest.slots.join(', ')}).`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return { ok: true, mode: flag };
|
|
220
|
+
}
|
|
221
|
+
if (manifest.slots.length === 1)
|
|
222
|
+
return { ok: true, mode: manifest.slots[0] };
|
|
223
|
+
return {
|
|
224
|
+
ok: false,
|
|
225
|
+
error: `plumbbob: agent "${manifest.name}" declares multiple slots (${manifest.slots.join(', ')}) — pass --mode <slot>.`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// Map the run outcome to reporting and side effects (D46). A failed run — non-zero
|
|
229
|
+
// exit, out-of-contract stdout, timeout, interrupt, or a shell that never
|
|
230
|
+
// started — reports on stderr and stops with NO side effects (the envelope of a
|
|
231
|
+
// failed child is not authoritative). A clean run lands `parked[]` (D44), records
|
|
232
|
+
// the envelope in the handoff ledger (D47), prints the human summary on stderr,
|
|
233
|
+
// and re-emits the machine envelope on stdout — nothing else, ever (the stream
|
|
234
|
+
// discipline: stdout carries the envelope alone).
|
|
235
|
+
function report(root, slug, name, mode, step, result) {
|
|
236
|
+
if (!result.ok) {
|
|
237
|
+
process.stderr.write(`${failureLine(name, result)}\n`);
|
|
238
|
+
return 1;
|
|
239
|
+
}
|
|
240
|
+
const { envelope } = result;
|
|
241
|
+
applyParked(root, slug, envelope.parked);
|
|
242
|
+
appendHandoff(root, slug, { agent: name, mode, step, envelope });
|
|
243
|
+
process.stderr.write(humanSummary(name, mode, envelope));
|
|
244
|
+
process.stdout.write(`${JSON.stringify(envelope)}\n`);
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
function failureLine(name, result) {
|
|
248
|
+
switch (result.reason) {
|
|
249
|
+
case 'exit':
|
|
250
|
+
return `plumbbob: agent "${name}" exited ${result.code} — failed run, stopping. No side effects applied.`;
|
|
251
|
+
case 'contract':
|
|
252
|
+
return `plumbbob: agent "${name}" is out of contract — ${result.error}`;
|
|
253
|
+
case 'timeout':
|
|
254
|
+
return `plumbbob: agent "${name}" timed out after ${result.seconds}s — killed.`;
|
|
255
|
+
case 'interrupted':
|
|
256
|
+
return `plumbbob: interrupted — killed agent "${name}".`;
|
|
257
|
+
case 'spawn':
|
|
258
|
+
return `plumbbob: could not spawn agent "${name}" — ${result.error}`;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Land each parked concern through the build-log's Park list (D44 — the agent
|
|
262
|
+
// never writes .plumbbob/ itself). A build-log with no "## Park list" section, or
|
|
263
|
+
// no build-log at all, warns once rather than losing the lines silently.
|
|
264
|
+
function applyParked(root, slug, parked) {
|
|
265
|
+
if (parked.length === 0)
|
|
266
|
+
return;
|
|
267
|
+
const path = buildLogPath(root, slug);
|
|
268
|
+
let content;
|
|
269
|
+
try {
|
|
270
|
+
content = readFileSync(path, 'utf8');
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
process.stderr.write(`plumbbob: no build-log.md — could not park ${parked.length} line(s) the agent returned.\n`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
for (const line of parked) {
|
|
277
|
+
const updated = appendToSection(content, 'Park list', `- [ ] ${line}`);
|
|
278
|
+
if (updated === null) {
|
|
279
|
+
process.stderr.write(`plumbbob: no "## Park list" in build-log.md — could not park: ${line}\n`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
content = updated;
|
|
283
|
+
process.stderr.write(`plumbbob: parked — ${line}\n`);
|
|
284
|
+
}
|
|
285
|
+
writeFileSync(path, content);
|
|
286
|
+
}
|
|
287
|
+
// The human-facing summary on stderr (D47): a headline plus, for a halt, the
|
|
288
|
+
// route the skills name (D52) — `blocked` unblocks and re-runs, `drift` sends the
|
|
289
|
+
// plan to /pb-refine. The machine envelope on stdout carries the same status for
|
|
290
|
+
// the calling skill; this is the terminal read.
|
|
291
|
+
function humanSummary(name, mode, envelope) {
|
|
292
|
+
const head = `plumbbob: agent "${name}" (${mode}) — ${envelope.status}: ${envelope.summary}\n`;
|
|
293
|
+
const notes = envelope.notes.length > 0 ? ` notes: ${envelope.notes}\n` : '';
|
|
294
|
+
if (envelope.status === 'blocked') {
|
|
295
|
+
return `${head} blocked — the agent couldn't finish; unblock and re-run.\n${notes}`;
|
|
296
|
+
}
|
|
297
|
+
if (envelope.status === 'drift') {
|
|
298
|
+
return `${head} drift — the plan no longer matches reality; /pb-refine before continuing.\n${notes}`;
|
|
299
|
+
}
|
|
300
|
+
return head;
|
|
301
|
+
}
|
|
302
|
+
// The in-flight step for this build (the STEP marker `build` wrote), or null when
|
|
303
|
+
// none is set — the caller then requires an explicit `--step`.
|
|
304
|
+
function readStep(root, slug) {
|
|
305
|
+
try {
|
|
306
|
+
const raw = readFileSync(stepPath(root, slug), 'utf8').trim();
|
|
307
|
+
return /^\d+$/.test(raw) ? Number(raw) : null;
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
package/dist/verbs/checkpoint.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// the same way.
|
|
10
10
|
import { appendFileSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
11
|
import { commit, findRepoRoot, headSha, isDirty, stageAll, stagePath, stagedPaths, stagedStat } from "../lib/git.js";
|
|
12
|
-
import { buildFolder, buildLogPath, checkpointsPath, hasSession, intentPath, seamPath, stepPath } from "../lib/sidecar.js";
|
|
12
|
+
import { buildFolder, buildLogPath, checkpointsPath, clearHandoff, hasSession, intentPath, seamPath, stepPath, } from "../lib/sidecar.js";
|
|
13
13
|
import { runCheck } from "../lib/check.js";
|
|
14
14
|
import { markStepDone, parseSteps, parseTitle } from "../lib/orient.js";
|
|
15
15
|
import { parseStepSeam, scopeDrift } from "../lib/intent.js";
|
|
@@ -50,6 +50,7 @@ export async function checkpoint(cwd, args) {
|
|
|
50
50
|
logCheckpoint(root, step, sha);
|
|
51
51
|
rmSync(seamPath(root), { force: true });
|
|
52
52
|
rmSync(stepPath(root), { force: true });
|
|
53
|
+
clearHandoff(root); // the agent-run ledger is step-scoped (D47) — clear it with the markers.
|
|
53
54
|
process.stdout.write(`plumbbob: step ${step} checkpointed — ${sha.slice(0, 9)}. Back at the boundary.\n`);
|
|
54
55
|
return 0;
|
|
55
56
|
}
|
package/dist/verbs/doctor.js
CHANGED
|
@@ -22,11 +22,12 @@
|
|
|
22
22
|
// "detected but tool missing" is the footgun this exists for.
|
|
23
23
|
//
|
|
24
24
|
// Functional, node builtins plus checkride (C1/C2).
|
|
25
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { accessSync, constants, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
26
26
|
import { homedir } from 'node:os';
|
|
27
|
-
import { dirname, join } from 'node:path';
|
|
27
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
28
28
|
import { fileURLToPath } from 'node:url';
|
|
29
29
|
import { runDoctor } from 'checkride';
|
|
30
|
+
import { listAgents } from "../lib/agents.js";
|
|
30
31
|
import { marketplacePlumbbob } from "../lib/plugins.js";
|
|
31
32
|
import { findRepoRoot, gitPath, stagePath } from "../lib/git.js";
|
|
32
33
|
import { buildDir, excludeControl, listBuilds, sidecarDir, slugify } from "../lib/sidecar.js";
|
|
@@ -264,6 +265,96 @@ function sidecarReport(cwd, migrate) {
|
|
|
264
265
|
lines.push(' (archive/ + the active session → builds/, config → settings.json; staged, never committed)');
|
|
265
266
|
return { lines, failed: 1 };
|
|
266
267
|
}
|
|
268
|
+
// --- agent validation (D48) ---
|
|
269
|
+
// Interpreters recognized as the program of a manifest `command`: when the command
|
|
270
|
+
// is `sh <script>` / `node <script>` / …, the checkable artifact is the script
|
|
271
|
+
// argument (the interpreter reads it — the file need not carry +x), not the
|
|
272
|
+
// interpreter, which we trust to be on PATH.
|
|
273
|
+
const INTERPRETERS = new Set(['sh', 'bash', 'zsh', 'dash', 'node', 'deno', 'bun', 'python', 'python3', 'ruby', 'perl']);
|
|
274
|
+
// Expand PLUMBBOB_AGENT_DIR (D49) in a command and split it into bare tokens
|
|
275
|
+
// (surrounding quotes stripped). A deliberately shallow split — enough for the
|
|
276
|
+
// static command check below, not a shell parser (which is neither possible nor
|
|
277
|
+
// wanted for a best-effort diagnostic).
|
|
278
|
+
function commandTokens(command, agentDir) {
|
|
279
|
+
const expanded = command.replaceAll('${PLUMBBOB_AGENT_DIR}', agentDir).replaceAll('$PLUMBBOB_AGENT_DIR', agentDir);
|
|
280
|
+
return expanded
|
|
281
|
+
.split(/\s+/)
|
|
282
|
+
.filter((t) => t.length > 0)
|
|
283
|
+
.map((t) => t.replace(/^['"]/, '').replace(/['"]$/, ''));
|
|
284
|
+
}
|
|
285
|
+
// Statically check a manifest `command` for the footgun D48 names: a script file
|
|
286
|
+
// that does not exist, or a directly-invoked one missing its +x bit. The command
|
|
287
|
+
// is a shell string spawned via `sh -c` at the repo root (D49), so a relative path
|
|
288
|
+
// resolves against `root` and PLUMBBOB_AGENT_DIR points at the agent's own dir.
|
|
289
|
+
// Best-effort by design — a path-shaped program or an interpreter's script
|
|
290
|
+
// argument is checked; a bare command (a PATH binary, an inline `node -e` program)
|
|
291
|
+
// is trusted, because we cannot prove a shell line broken without running it.
|
|
292
|
+
// Returns the problem string, or null when nothing checkable is wrong.
|
|
293
|
+
function checkCommand(command, agentDir, root) {
|
|
294
|
+
const tokens = commandTokens(command, agentDir);
|
|
295
|
+
if (tokens.length === 0)
|
|
296
|
+
return null;
|
|
297
|
+
const resolve = (p) => (isAbsolute(p) ? p : join(root, p));
|
|
298
|
+
const program = tokens[0];
|
|
299
|
+
// A path-shaped program is invoked directly, so it must exist AND be executable.
|
|
300
|
+
if (program.includes('/')) {
|
|
301
|
+
const path = resolve(program);
|
|
302
|
+
if (!existsSync(path))
|
|
303
|
+
return `command program ${program} does not exist`;
|
|
304
|
+
try {
|
|
305
|
+
accessSync(path, constants.X_OK);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return `command program ${program} is not executable — chmod +x it`;
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
// An interpreter reads its script argument, so the script need only exist.
|
|
313
|
+
if (INTERPRETERS.has(program)) {
|
|
314
|
+
const script = tokens.slice(1).find((t) => t.includes('/'));
|
|
315
|
+
if (script !== undefined && !existsSync(resolve(script))) {
|
|
316
|
+
return `command script ${script} does not exist`;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
// One agent's problem (D48), or null when it validates: a malformed or
|
|
322
|
+
// unsupported-contract manifest surfaces as its resolution error; an
|
|
323
|
+
// otherwise-valid agent is checked for a missing/non-executable command.
|
|
324
|
+
function agentProblem(listing, root) {
|
|
325
|
+
if (!listing.resolution.ok)
|
|
326
|
+
return listing.resolution.error;
|
|
327
|
+
const { manifest, dir } = listing.resolution.agent;
|
|
328
|
+
return checkCommand(manifest.command, dir, root);
|
|
329
|
+
}
|
|
330
|
+
// The agent-validation section (D48): walk every resolvable agent across both
|
|
331
|
+
// tiers (project + personal) and flag a malformed agent.json, an unsupported
|
|
332
|
+
// contract version (both arrive as the listing's parse error), or a
|
|
333
|
+
// missing/non-executable command. A repo with no agents shows no section (like the
|
|
334
|
+
// sidecar one), keeping doctor quiet for the common case; outside a repo there is
|
|
335
|
+
// no project tier to anchor to, so the section is skipped entirely.
|
|
336
|
+
function agentReport(cwd) {
|
|
337
|
+
const root = findRepoRoot(cwd);
|
|
338
|
+
if (root === null)
|
|
339
|
+
return { lines: [], failed: 0 };
|
|
340
|
+
const listings = listAgents(root);
|
|
341
|
+
if (listings.length === 0)
|
|
342
|
+
return { lines: [], failed: 0 };
|
|
343
|
+
const lines = ['', 'plumbbob doctor — agents (D48)'];
|
|
344
|
+
let failed = 0;
|
|
345
|
+
for (const listing of listings) {
|
|
346
|
+
const problem = agentProblem(listing, root);
|
|
347
|
+
if (problem === null) {
|
|
348
|
+
const slots = listing.resolution.ok ? ` [${listing.resolution.agent.manifest.slots.join(', ')}]` : '';
|
|
349
|
+
lines.push(` ✓ ${listing.name} (${listing.origin})${slots}`);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
lines.push(` ✗ ${listing.name} (${listing.origin}) — ${problem}`);
|
|
353
|
+
failed += 1;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return { lines, failed };
|
|
357
|
+
}
|
|
267
358
|
// The check-gate section (D32): a configured `check` setting names the spawn
|
|
268
359
|
// override and asks nothing more; otherwise checkride's own doctor reports the
|
|
269
360
|
// slot/adapter table. Only rows checkride marks `required` count as problems —
|
|
@@ -319,9 +410,11 @@ export async function doctor(cwd, args = []) {
|
|
|
319
410
|
}
|
|
320
411
|
const sidecar = sidecarReport(cwd, args.includes('--migrate'));
|
|
321
412
|
out.push(...sidecar.lines);
|
|
413
|
+
const agents = agentReport(cwd);
|
|
414
|
+
out.push(...agents.lines);
|
|
322
415
|
const gate = await gateReport(cwd);
|
|
323
416
|
out.push(...gate.lines);
|
|
324
|
-
const failed = checks.filter((c) => !c.ok).length + sidecar.failed + gate.failed;
|
|
417
|
+
const failed = checks.filter((c) => !c.ok).length + sidecar.failed + agents.failed + gate.failed;
|
|
325
418
|
out.push('');
|
|
326
419
|
out.push(failed === 0
|
|
327
420
|
? 'plumbbob: all checks passed. If a skill still misbehaves, restart Claude Code (or /reload-plugins).'
|