plumbbob 0.10.3 → 0.11.0
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 +8 -4
- package/dist/lib/agents.js +2 -2
- package/dist/lib/git.js +60 -5
- package/dist/lib/notice.js +140 -0
- package/dist/lib/orient.js +437 -8
- package/dist/lib/sidecar.js +28 -0
- package/dist/verbs/abandon.js +25 -8
- package/dist/verbs/agent.js +63 -31
- package/dist/verbs/build.js +25 -9
- package/dist/verbs/check.js +8 -3
- package/dist/verbs/checkpoint.js +96 -23
- package/dist/verbs/finish.js +33 -9
- package/dist/verbs/handoff.js +432 -63
- package/dist/verbs/park.js +11 -5
- package/dist/verbs/revert.js +16 -5
- package/dist/verbs/spike.js +61 -22
- package/dist/verbs/start.js +56 -17
- package/dist/verbs/use.js +26 -16
- package/package.json +1 -1
- package/skills/abandon/SKILL.md +2 -1
- package/skills/build/SKILL.md +116 -35
- package/skills/finish/SKILL.md +19 -8
- package/skills/park/SKILL.md +15 -1
- package/skills/plan/SKILL.md +14 -1
- package/skills/recover/SKILL.md +3 -2
- package/skills/revert/SKILL.md +2 -1
- package/skills/spike/SKILL.md +3 -2
- package/skills/status/SKILL.md +13 -5
- package/skills/verify/SKILL.md +127 -31
package/dist/verbs/revert.js
CHANGED
|
@@ -23,6 +23,8 @@ import { findRepoRoot, resetHard, untrackedPaths } from "../lib/git.js";
|
|
|
23
23
|
import { bumpStepStat, checkpointsPath, hasSession, resolveBuild, seamPath, sidecarDir, stepPath } from "../lib/sidecar.js";
|
|
24
24
|
import { isArtifactPath, matchesSeam } from "../lib/intent.js";
|
|
25
25
|
import { AT_BOUNDARY, syncBuildLogState } from "../lib/buildlogsync.js";
|
|
26
|
+
import { ending, notice, transition } from "../lib/notice.js";
|
|
27
|
+
import { driverPointer } from "./handoff.js";
|
|
26
28
|
/**
|
|
27
29
|
* Rewind to a recorded checkpoint and return the build to the boundary.
|
|
28
30
|
*
|
|
@@ -35,13 +37,13 @@ import { AT_BOUNDARY, syncBuildLogState } from "../lib/buildlogsync.js";
|
|
|
35
37
|
export function revert(cwd, args) {
|
|
36
38
|
const root = findRepoRoot(cwd);
|
|
37
39
|
if (root === null || !hasSession(root)) {
|
|
38
|
-
process.stderr.write(
|
|
40
|
+
process.stderr.write(notice({ fact: 'no active session', remedy: 'plumbbob start "<title>"' }));
|
|
39
41
|
return 1;
|
|
40
42
|
}
|
|
41
43
|
const { build: slug, rest } = resolveBuild(root, args);
|
|
42
44
|
const to = parseTo(rest);
|
|
43
45
|
if (to === 'invalid') {
|
|
44
|
-
process.stderr.write(
|
|
46
|
+
process.stderr.write(notice({ fact: 'revert --to needs a step number', remedy: 'plumbbob revert --to 2' }));
|
|
45
47
|
return 1;
|
|
46
48
|
}
|
|
47
49
|
const checkpoints = readCheckpoints(root, slug);
|
|
@@ -52,13 +54,13 @@ export function revert(cwd, args) {
|
|
|
52
54
|
else {
|
|
53
55
|
const entry = checkpoints.steps.find((e) => e.n === to);
|
|
54
56
|
if (entry === undefined) {
|
|
55
|
-
process.stderr.write(
|
|
57
|
+
process.stderr.write(notice({ fact: `no checkpoint recorded for step ${to}` }));
|
|
56
58
|
return 1;
|
|
57
59
|
}
|
|
58
60
|
sha = entry.sha;
|
|
59
61
|
}
|
|
60
62
|
if (sha === undefined) {
|
|
61
|
-
process.stderr.write(
|
|
63
|
+
process.stderr.write(notice({ fact: 'no baseline recorded in checkpoints', detail: ['nothing to revert to'] }));
|
|
62
64
|
return 1;
|
|
63
65
|
}
|
|
64
66
|
// Compute untracked-in-seam BEFORE the reset (reset --hard leaves untracked and
|
|
@@ -83,7 +85,16 @@ export function revert(cwd, args) {
|
|
|
83
85
|
// revert keeps intent edits, so intent's checkboxes stay the truth to reflect.
|
|
84
86
|
// Best-effort, like every build-log write.
|
|
85
87
|
syncBuildLogState(root, slug, AT_BOUNDARY);
|
|
86
|
-
|
|
88
|
+
// A step exit: nothing landed, so no Verdict rides above the pointer, and with
|
|
89
|
+
// the markers cleared it aims forward from the boundary.
|
|
90
|
+
process.stdout.write(ending({
|
|
91
|
+
lead: transition({
|
|
92
|
+
label: 'Reverted',
|
|
93
|
+
fact: `to ${sha.slice(0, 9)}`,
|
|
94
|
+
detail: ['park lines and intent edits preserved'],
|
|
95
|
+
}),
|
|
96
|
+
pointer: driverPointer(root, slug),
|
|
97
|
+
}));
|
|
87
98
|
return 0;
|
|
88
99
|
}
|
|
89
100
|
/**
|
package/dist/verbs/spike.js
CHANGED
|
@@ -21,6 +21,8 @@ import { basename, dirname, join, relative } from 'node:path';
|
|
|
21
21
|
import { findRepoRoot } from "../lib/git.js";
|
|
22
22
|
import { buildFolder, clearSpike, hasSession, inSpike, listSpikeReports, markSpike, nextSpikeReportPath, resolveBuild, stepPath, } from "../lib/sidecar.js";
|
|
23
23
|
import { readTemplate, stampTemplate } from "../lib/templates.js";
|
|
24
|
+
import { advisory, blocks, ending, notice, transition } from "../lib/notice.js";
|
|
25
|
+
import { driverPointer } from "./handoff.js";
|
|
24
26
|
// Worktree/branch names when the caller lists none: a fork defaults to two arms.
|
|
25
27
|
const DEFAULT_OPTIONS = ['a', 'b'];
|
|
26
28
|
/**
|
|
@@ -37,7 +39,7 @@ const VERDICT_PLACEHOLDER = '*(viable | not viable | partial';
|
|
|
37
39
|
export function spike(cwd, args) {
|
|
38
40
|
const root = findRepoRoot(cwd);
|
|
39
41
|
if (root === null || !hasSession(root)) {
|
|
40
|
-
process.stderr.write(
|
|
42
|
+
process.stderr.write(notice({ fact: 'no active session', remedy: 'plumbbob start "<title>"' }));
|
|
41
43
|
return 1;
|
|
42
44
|
}
|
|
43
45
|
const { build: buildSlug, rest } = resolveBuild(root, args);
|
|
@@ -61,14 +63,21 @@ export function spike(cwd, args) {
|
|
|
61
63
|
function spikeReport(root, buildSlug, positionals) {
|
|
62
64
|
const slug = sanitize(positionals[0] ?? '');
|
|
63
65
|
if (slug.length === 0) {
|
|
64
|
-
process.stderr.write(
|
|
66
|
+
process.stderr.write(notice({ fact: 'spike report needs a slug', remedy: 'plumbbob spike report "auth-store"' }));
|
|
65
67
|
return 1;
|
|
66
68
|
}
|
|
67
69
|
const inFlight = readInFlightStep(root, buildSlug);
|
|
68
70
|
const via = inFlight !== null ? `step ${inFlight}` : '/plumbbob:spike';
|
|
69
71
|
const path = scaffoldSpikeReport(root, buildSlug, slug, via);
|
|
70
|
-
process.stdout.write(
|
|
71
|
-
|
|
72
|
+
process.stdout.write(ending({
|
|
73
|
+
lead: transition({
|
|
74
|
+
label: 'Spike report',
|
|
75
|
+
fact: 'scaffolded',
|
|
76
|
+
detail: [relative(root, path)],
|
|
77
|
+
remedy: 'record Findings and the Verdict there, which is what closes a spike step',
|
|
78
|
+
}),
|
|
79
|
+
pointer: driverPointer(root, buildSlug),
|
|
80
|
+
}));
|
|
72
81
|
return 0;
|
|
73
82
|
}
|
|
74
83
|
/**
|
|
@@ -110,17 +119,20 @@ function readInFlightStep(root, buildSlug) {
|
|
|
110
119
|
*/
|
|
111
120
|
function spikeStart(root, buildSlug, positionals) {
|
|
112
121
|
if (inSpike(root, buildSlug)) {
|
|
113
|
-
process.stderr.write(
|
|
122
|
+
process.stderr.write(notice({ fact: 'already in a spike', remedy: 'plumbbob spike done to close it first' }));
|
|
114
123
|
return 1;
|
|
115
124
|
}
|
|
116
125
|
if (existsSync(stepPath(root, buildSlug))) {
|
|
117
|
-
process.stderr.write(
|
|
118
|
-
'
|
|
126
|
+
process.stderr.write(notice({
|
|
127
|
+
fact: 'a step is in flight',
|
|
128
|
+
detail: ['a spike is a deliberate fork from a settled boundary'],
|
|
129
|
+
remedy: 'checkpoint, abandon, or revert the current step first',
|
|
130
|
+
}));
|
|
119
131
|
return 1;
|
|
120
132
|
}
|
|
121
133
|
const slug = sanitize(positionals[0] ?? '');
|
|
122
134
|
if (slug.length === 0) {
|
|
123
|
-
process.stderr.write(
|
|
135
|
+
process.stderr.write(notice({ fact: 'spike needs a slug', remedy: 'plumbbob spike "auth-store" a b' }));
|
|
124
136
|
return 1;
|
|
125
137
|
}
|
|
126
138
|
const explicit = positionals.slice(1).map(sanitize).filter((o) => o.length > 0);
|
|
@@ -129,7 +141,11 @@ function spikeStart(root, buildSlug, positionals) {
|
|
|
129
141
|
for (const opt of options) {
|
|
130
142
|
const path = join(dirname(root), `${basename(root)}-spike-${slug}-${opt}`);
|
|
131
143
|
if (existsSync(path)) {
|
|
132
|
-
process.stderr.write(
|
|
144
|
+
process.stderr.write(notice({
|
|
145
|
+
fact: 'the spike worktree path already exists',
|
|
146
|
+
detail: [path],
|
|
147
|
+
remedy: 'remove it, or run plumbbob spike done first',
|
|
148
|
+
}));
|
|
133
149
|
return 1;
|
|
134
150
|
}
|
|
135
151
|
git(root, ['worktree', 'add', '-b', `spike/${slug}-${opt}`, path, 'HEAD']);
|
|
@@ -139,9 +155,25 @@ function spikeStart(root, buildSlug, positionals) {
|
|
|
139
155
|
// Scaffold the report NOW, while the worktrees live: findings accrue during the
|
|
140
156
|
// experiment, not from memory after the teardown. Provenance names the worktrees.
|
|
141
157
|
const report = scaffoldSpikeReport(root, buildSlug, slug, `/plumbbob:spike — worktrees (${options.join(', ')})`);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
158
|
+
// The lead states the fact; the throwaway worktrees are a list, so they ride
|
|
159
|
+
// as a readout beneath it rather than crowding the line, and the report the
|
|
160
|
+
// spike scaffolded is a second landing under the same lead.
|
|
161
|
+
process.stdout.write(ending({
|
|
162
|
+
lead: blocks([
|
|
163
|
+
transition({
|
|
164
|
+
label: 'Spike',
|
|
165
|
+
fact: 'opened',
|
|
166
|
+
detail: ['the main tree stays put', `${created.length} throwaway worktree${created.length === 1 ? '' : 's'}`],
|
|
167
|
+
}) + created.map((path) => ` ${path}`).join('\n'),
|
|
168
|
+
transition({
|
|
169
|
+
label: 'Spike report',
|
|
170
|
+
fact: 'scaffolded',
|
|
171
|
+
detail: [relative(root, report)],
|
|
172
|
+
remedy: 'record findings and the Verdict there, then run plumbbob spike done',
|
|
173
|
+
}),
|
|
174
|
+
]),
|
|
175
|
+
pointer: driverPointer(root, buildSlug),
|
|
176
|
+
}));
|
|
145
177
|
return 0;
|
|
146
178
|
}
|
|
147
179
|
/**
|
|
@@ -152,7 +184,7 @@ function spikeStart(root, buildSlug, positionals) {
|
|
|
152
184
|
*/
|
|
153
185
|
function spikeDone(root, buildSlug) {
|
|
154
186
|
if (!inSpike(root, buildSlug)) {
|
|
155
|
-
process.stderr.write(
|
|
187
|
+
process.stderr.write(notice({ fact: 'no active spike to close' }));
|
|
156
188
|
return 1;
|
|
157
189
|
}
|
|
158
190
|
// Check for an unrecorded verdict BEFORE teardown. The reports live in the
|
|
@@ -168,15 +200,22 @@ function spikeDone(root, buildSlug) {
|
|
|
168
200
|
}
|
|
169
201
|
clearSpike(root, buildSlug);
|
|
170
202
|
// Guidance, not a gate: a missing verdict is a nudge, and the spike still
|
|
171
|
-
// closes.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
203
|
+
// closes. It follows the line it qualifies, and names the reports so the
|
|
204
|
+
// human knows where to write the call. The pointer, read once the marker is
|
|
205
|
+
// cleared, aims back at the step the spike interrupted, or forward.
|
|
206
|
+
process.stdout.write(ending({
|
|
207
|
+
lead: transition({ label: 'Spike', fact: 'closed', detail: ['worktrees and branches removed'] }),
|
|
208
|
+
advisories: unfilled.length === 0
|
|
209
|
+
? []
|
|
210
|
+
: [
|
|
211
|
+
advisory({
|
|
212
|
+
fact: 'no verdict recorded',
|
|
213
|
+
detail: unfilled,
|
|
214
|
+
remedy: 'record which option won, and why, now the worktrees are gone',
|
|
215
|
+
}),
|
|
216
|
+
],
|
|
217
|
+
pointer: driverPointer(root, buildSlug),
|
|
218
|
+
}));
|
|
180
219
|
return 0;
|
|
181
220
|
}
|
|
182
221
|
/**
|
package/dist/verbs/start.js
CHANGED
|
@@ -10,6 +10,7 @@ import { readTemplate, stampTemplate } from "../lib/templates.js";
|
|
|
10
10
|
import { findRepoRoot, hasCommit, headSha, isDirty, isIgnored } from "../lib/git.js";
|
|
11
11
|
import { sidecarDir, buildDir, listBuilds, slugify, checkpointsPath, intentPath, buildLogPath, beginSession, hasSession, excludeControl, excludeSidecar, setGrant, stampTick, } from "../lib/sidecar.js";
|
|
12
12
|
import { settingsPath } from "../lib/settings.js";
|
|
13
|
+
import { advisory, ending, notice, transition } from "../lib/notice.js";
|
|
13
14
|
/**
|
|
14
15
|
* Human-readable echo of the default check gate, stamped into the templates'
|
|
15
16
|
* {{CHECK}} line.
|
|
@@ -37,36 +38,59 @@ export async function start(cwd, args) {
|
|
|
37
38
|
const local = args.includes('--local');
|
|
38
39
|
const title = (positionals[0] ?? '').trim();
|
|
39
40
|
const slug = (flagValue(args, '--slug') ?? datedSlug(title)).trim();
|
|
41
|
+
// Every advisory the session accrues on its way open, collected rather than
|
|
42
|
+
// printed: they belong inside the one ending below, in the order found.
|
|
43
|
+
const advisories = [];
|
|
40
44
|
if (title.length === 0) {
|
|
41
|
-
process.stderr.write(
|
|
45
|
+
process.stderr.write(notice({ fact: 'start needs a title', remedy: 'plumbbob start "what you are building"' }));
|
|
42
46
|
return 1;
|
|
43
47
|
}
|
|
44
48
|
const root = findRepoRoot(cwd);
|
|
45
49
|
if (root === null) {
|
|
46
|
-
process.stderr.write(
|
|
50
|
+
process.stderr.write(notice({
|
|
51
|
+
fact: 'not a git repository',
|
|
52
|
+
detail: ['plumbbob records a baseline commit'],
|
|
53
|
+
remedy: 'git init, then make an initial commit',
|
|
54
|
+
}));
|
|
47
55
|
return 1;
|
|
48
56
|
}
|
|
49
57
|
if (!hasCommit(root)) {
|
|
50
|
-
process.stderr.write(
|
|
58
|
+
process.stderr.write(notice({
|
|
59
|
+
fact: 'this repository has no commits yet',
|
|
60
|
+
remedy: 'make an initial commit, so start can record a baseline',
|
|
61
|
+
}));
|
|
51
62
|
return 1;
|
|
52
63
|
}
|
|
53
64
|
if (hasSession(root)) {
|
|
54
|
-
process.stderr.write(
|
|
65
|
+
process.stderr.write(notice({ fact: 'a session is already active here', remedy: 'plumbbob finish to close it before starting another' }));
|
|
55
66
|
return 1;
|
|
56
67
|
}
|
|
57
68
|
if (isDirty(root)) {
|
|
58
69
|
if (!allowDirty) {
|
|
59
|
-
process.stderr.write(
|
|
70
|
+
process.stderr.write(notice({
|
|
71
|
+
fact: 'the working tree is dirty',
|
|
72
|
+
remedy: 'commit or stash first, or plumbbob start --allow-dirty "<title>" to record HEAD as the baseline',
|
|
73
|
+
}));
|
|
60
74
|
return 1;
|
|
61
75
|
}
|
|
62
|
-
|
|
76
|
+
advisories.push(advisory({
|
|
77
|
+
fact: 'recording HEAD as the baseline with a dirty tree',
|
|
78
|
+
detail: ['--allow-dirty'],
|
|
79
|
+
remedy: 'a later revert to baseline will DISCARD the uncommitted work',
|
|
80
|
+
}));
|
|
63
81
|
}
|
|
64
82
|
if (!local && slug.length === 0) {
|
|
65
|
-
process.stderr.write(
|
|
83
|
+
process.stderr.write(notice({
|
|
84
|
+
fact: `could not derive a build slug from "${title}"`,
|
|
85
|
+
remedy: 'pass --slug <name>, or retitle with letters or digits',
|
|
86
|
+
}));
|
|
66
87
|
return 1;
|
|
67
88
|
}
|
|
68
89
|
if (!local && listBuilds(root).includes(slug)) {
|
|
69
|
-
process.stderr.write(
|
|
90
|
+
process.stderr.write(notice({
|
|
91
|
+
fact: `a build named "${slug}" already exists in .plumbbob/builds/`,
|
|
92
|
+
remedy: 'retitle, or pass --slug <name> to choose another',
|
|
93
|
+
}));
|
|
70
94
|
return 1;
|
|
71
95
|
}
|
|
72
96
|
const sha = headSha(root);
|
|
@@ -121,7 +145,6 @@ export async function start(cwd, args) {
|
|
|
121
145
|
// grant's lifetime is one turn.
|
|
122
146
|
setGrant(root, null);
|
|
123
147
|
stampTick(root, local ? null : slug);
|
|
124
|
-
process.stdout.write(`plumbbob: started "${title}" — baseline ${sha.slice(0, 9)}. Frame and decide in ${intentLocation}; \`build\` a step once the decisions are made.\n`);
|
|
125
148
|
// The tracked layout plants `builds/<slug>/` to ride the branch into the PR,
|
|
126
149
|
// but a repo whose own `.gitignore` excludes `.plumbbob/` overrides that, and
|
|
127
150
|
// an explicit `git add` of an ignored folder is a hard error. Detect it here,
|
|
@@ -132,9 +155,11 @@ export async function start(cwd, args) {
|
|
|
132
155
|
// Guidance only, and the session is already open: a probe hiccup stays silent
|
|
133
156
|
// rather than half-crashing a scaffolded session.
|
|
134
157
|
if (!local && sidecarIsIgnored(root, slug)) {
|
|
135
|
-
|
|
136
|
-
'
|
|
137
|
-
'
|
|
158
|
+
advisories.push(advisory({
|
|
159
|
+
fact: 'this repo gitignores .plumbbob/',
|
|
160
|
+
detail: ['the build folder cannot ride the branch', 'plan and finish commits will be record-only'],
|
|
161
|
+
remedy: 'unignore .plumbbob/builds/ before the first checkpoint to keep the record in the tree',
|
|
162
|
+
}));
|
|
138
163
|
}
|
|
139
164
|
// The plan-time gate probe: if checkride sees no code checks here, say so
|
|
140
165
|
// NOW (while the human is still deciding) instead of at the first
|
|
@@ -143,10 +168,24 @@ export async function start(cwd, args) {
|
|
|
143
168
|
// exit code, and a configured `check` or a probe hiccup stays silent.
|
|
144
169
|
const gate = await detectGate(root);
|
|
145
170
|
if (gate.configured === null && !gate.detected) {
|
|
146
|
-
|
|
147
|
-
'
|
|
148
|
-
'
|
|
171
|
+
advisories.push(advisory({
|
|
172
|
+
fact: 'the check gate sees no code checks in this repo',
|
|
173
|
+
detail: ['checkride detected no tools', 'checkpoints gate on it and would green vacuously'],
|
|
174
|
+
remedy: 'add {"check": "npm test"} to .plumbbob/settings.json while you are still planning',
|
|
175
|
+
}));
|
|
149
176
|
}
|
|
177
|
+
// No pointer: the plan skill owns the turn that follows, and there is no step
|
|
178
|
+
// yet to aim at, so the ending closes on the remedy line that says where to
|
|
179
|
+
// frame the build.
|
|
180
|
+
process.stdout.write(ending({
|
|
181
|
+
lead: transition({
|
|
182
|
+
label: 'Session',
|
|
183
|
+
fact: `started "${title}"`,
|
|
184
|
+
detail: [`baseline ${sha.slice(0, 9)}`],
|
|
185
|
+
remedy: `frame and decide in ${intentLocation}, then build a step`,
|
|
186
|
+
}),
|
|
187
|
+
advisories,
|
|
188
|
+
}));
|
|
150
189
|
return 0;
|
|
151
190
|
}
|
|
152
191
|
/**
|
|
@@ -167,11 +206,11 @@ function datedSlug(title) {
|
|
|
167
206
|
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${base}`;
|
|
168
207
|
}
|
|
169
208
|
/**
|
|
170
|
-
* Whether the tracked build folder is gitignored: the record-only
|
|
209
|
+
* Whether the tracked build folder is gitignored: the record-only advisory's
|
|
171
210
|
* predicate, probing exactly the path `checkpoint --plan` will try to stage.
|
|
172
211
|
*
|
|
173
212
|
* Best-effort: a probe that throws (a fatal `git check-ignore`) reads as
|
|
174
|
-
* not-ignored so the
|
|
213
|
+
* not-ignored so the advisory stays silent rather than crashing a session that
|
|
175
214
|
* is already open.
|
|
176
215
|
*/
|
|
177
216
|
function sidecarIsIgnored(root, slug) {
|
package/dist/verbs/use.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import { existsSync } from 'node:fs';
|
|
12
12
|
import { findRepoRoot } from "../lib/git.js";
|
|
13
13
|
import { activeBuild, hasSession, intentPath, listBuilds, setActiveBuild, stepPath } from "../lib/sidecar.js";
|
|
14
|
+
import { advisory, ending, notice, transition } from "../lib/notice.js";
|
|
15
|
+
import { driverPointer } from "./handoff.js";
|
|
14
16
|
/**
|
|
15
17
|
* Switch the active-build cursor to the named build.
|
|
16
18
|
*
|
|
@@ -20,35 +22,43 @@ import { activeBuild, hasSession, intentPath, listBuilds, setActiveBuild, stepPa
|
|
|
20
22
|
export function use(cwd, args) {
|
|
21
23
|
const root = findRepoRoot(cwd);
|
|
22
24
|
if (root === null || !hasSession(root)) {
|
|
23
|
-
process.stderr.write(
|
|
25
|
+
process.stderr.write(notice({ fact: 'no active session', remedy: 'plumbbob start "<title>"' }));
|
|
24
26
|
return 1;
|
|
25
27
|
}
|
|
26
28
|
const builds = listBuilds(root);
|
|
27
29
|
const target = args.find((a) => !a.startsWith('--'));
|
|
28
30
|
if (target === undefined || target.length === 0) {
|
|
29
|
-
process.stderr.write(
|
|
31
|
+
process.stderr.write(notice({ fact: 'use needs a build slug', detail: builds, remedy: 'plumbbob use <slug>' }));
|
|
30
32
|
return 1;
|
|
31
33
|
}
|
|
32
34
|
// Validate by the folder's intent.md, not the dir alone: an empty `builds/<slug>/`
|
|
33
35
|
// is not a resumable build, and the flat `--local` layout has no builds/ at all.
|
|
34
36
|
if (!existsSync(intentPath(root, target))) {
|
|
35
|
-
process.stderr.write(
|
|
37
|
+
process.stderr.write(notice({ fact: `no build named "${target}" in .plumbbob/builds/`, detail: builds }));
|
|
36
38
|
return 1;
|
|
37
39
|
}
|
|
38
40
|
const leaving = activeBuild(root);
|
|
39
|
-
|
|
40
|
-
process.stderr.write(`plumbbob: note — build "${leaving}" has a step in flight; its in-flight state is preserved ` +
|
|
41
|
-
`and resumes when you \`use ${leaving}\` again.\n`);
|
|
42
|
-
}
|
|
41
|
+
const left = leaving !== null && leaving !== target && existsSync(stepPath(root, leaving)) ? leaving : null;
|
|
43
42
|
setActiveBuild(root, target);
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
// The lead line, then the advisory it qualifies, then the pointer into the
|
|
44
|
+
// build just switched to: one fixed order for every ending, so the relay never
|
|
45
|
+
// has to guess which line leads.
|
|
46
|
+
process.stdout.write(ending({
|
|
47
|
+
lead: transition({
|
|
48
|
+
label: 'Active build',
|
|
49
|
+
fact: target,
|
|
50
|
+
detail: existsSync(stepPath(root, target)) ? ['a step is in flight'] : [],
|
|
51
|
+
}),
|
|
52
|
+
advisories: left === null
|
|
53
|
+
? []
|
|
54
|
+
: [
|
|
55
|
+
advisory({
|
|
56
|
+
fact: `build "${left}" has a step in flight`,
|
|
57
|
+
detail: ['its in-flight state is preserved'],
|
|
58
|
+
remedy: `plumbbob use ${left} to pick it back up`,
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
pointer: driverPointer(root, target),
|
|
62
|
+
}));
|
|
46
63
|
return 0;
|
|
47
64
|
}
|
|
48
|
-
/**
|
|
49
|
-
* A trailing " Builds: a, b." hint when any exist, so a bad or missing slug
|
|
50
|
-
* points the user straight at the valid choices.
|
|
51
|
-
*/
|
|
52
|
-
function buildsHint(builds) {
|
|
53
|
-
return builds.length > 0 ? ` Builds: ${builds.join(', ')}.` : '';
|
|
54
|
-
}
|
package/package.json
CHANGED
package/skills/abandon/SKILL.md
CHANGED
|
@@ -10,7 +10,7 @@ allowed-tools: Bash(plumbbob status:*), Bash(plumbbob abandon:*)
|
|
|
10
10
|
|
|
11
11
|
Current session state (injected when this skill runs): !`plumbbob status 2>/dev/null || echo "plumbbob CLI not on PATH in this session. Marketplace install: confirm the plugin is enabled in /plugin, then /reload-plugins. Skills-dir/global install: npm i -g plumbbob && plumbbob init."`
|
|
12
12
|
|
|
13
|
-
This is a **driver skill**: a chat-side trigger for the mechanical `plumbbob abandon` verb, so the whole workflow runs from the agent window instead of a terminal. It is `disable-model-invocation: true`: only the human fires it. It carries **no Edit and no Write tool**; its
|
|
13
|
+
This is a **driver skill**: a chat-side trigger for the mechanical `plumbbob abandon` verb, so the whole workflow runs from the agent window instead of a terminal. It is `disable-model-invocation: true`: only the human fires it. It carries **no Edit and no Write tool**; it shells the verb and relays its output verbatim, refusal included. A successful transition prints its whole ending, pointer and all, so one command is the whole turn. The CLI is the source of truth: never retry a refused transition, and never edit a file to work around one.
|
|
14
14
|
|
|
15
15
|
Abandon is the third exit from an in-flight step. `/plumbbob:verify` lands the step and `/plumbbob:revert` destroys its work; abandon drops the attempt and keeps the working-tree diff exactly where it is. The step keeps its `[ ]` in the plan, re-buildable later, and the diff stays yours to keep, rework, or commit by hand.
|
|
16
16
|
|
|
@@ -18,3 +18,4 @@ Abandon is the third exit from an in-flight step. `/plumbbob:verify` lands the s
|
|
|
18
18
|
|
|
19
19
|
1. Run `plumbbob abandon` via Bash. It clears the in-flight markers (`STEP`, `SEAM`, `TICK`, and the step's agent handoff ledger), appends an abandon line to the build log, and touches neither the working tree nor git.
|
|
20
20
|
2. Report the verb's output verbatim: the step it dropped, or any refusal. A step exit crosses the same boundary as a checkpoint, so abandon honors the same approval latch; a "no human turn since this step began" refusal is the pause working, not an error. End the turn, and the human's next turn is what lets a re-fired abandon land.
|
|
21
|
+
3. A successful abandon prints its own ending (the driver tier of the [turn anatomy](https://github.com/robmclarty/plumbbob/blob/main/docs/presentation.md)): the lead line, a blank line, and the pointer, which aims at the step just dropped, since it kept its `[ ]` in the plan. Relay that block whole and run no second command. The latch refusal above is not a transition; it carries no pointer, so relay it and end the turn.
|
package/skills/build/SKILL.md
CHANGED
|
@@ -104,31 +104,110 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
|
|
|
104
104
|
build the step and reach the pause *in one turn*, so under plumbbob's turn hook the
|
|
105
105
|
checkpoint at the end of that turn is **refused** ("no human turn since this step
|
|
106
106
|
began"), and that refusal **is** the pause working as designed. Do exactly what the
|
|
107
|
-
pause asks:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
and **stop at the boundary**. Landing
|
|
111
|
-
thing approval triggers), never a side
|
|
112
|
-
`/plumbbob:build` only ever starts the *next*
|
|
113
|
-
raw `git commit`**; the work plane stays
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
107
|
+
pause asks: write the detail file, run `plumbbob handoff`, paste its block, and **end
|
|
108
|
+
the turn there**. The human's next
|
|
109
|
+
message (their approval) is the human turn that lets the checkpoint land: on that turn
|
|
110
|
+
you run `plumbbob checkpoint <n>` (now allowed) and **stop at the boundary**. Landing
|
|
111
|
+
the checkpoint is its own deliberate beat (the thing approval triggers), never a side
|
|
112
|
+
effect of starting the next step, and `/plumbbob:build` only ever starts the *next*
|
|
113
|
+
step. **Never route around a refusal with a raw `git commit`**; the work plane stays
|
|
114
|
+
free, but the *record* is latched on purpose.
|
|
115
|
+
|
|
116
|
+
**Render the pause: write the file, run handoff, paste.** A build pause is a decision
|
|
117
|
+
turn, and the
|
|
118
|
+
[turn anatomy](https://github.com/robmclarty/plumbbob/blob/main/docs/presentation.md)
|
|
119
|
+
fixes it as one block. You author none of it in the chat. Your judgment goes into
|
|
120
|
+
`.plumbbob/detail.md`; `plumbbob handoff` renders the whole turn from that file and its
|
|
121
|
+
own measurements; you paste its output and write nothing before or after it. The rule is
|
|
122
|
+
positional on purpose: with the relay as the turn's whole text, no line is left for
|
|
123
|
+
"here is the pause", for reading the check verdict back in prose, or for a closing
|
|
124
|
+
courtesy. Each fact appears once, in its part.
|
|
125
|
+
|
|
126
|
+
*Write the detail file.* Before you call `plumbbob handoff`, overwrite
|
|
127
|
+
`.plumbbob/detail.md` for this step; never let a past step's detail linger. It is the
|
|
128
|
+
wire handoff parses, and it is the only path by which your judgment reaches the turn:
|
|
129
|
+
|
|
130
|
+
```markdown
|
|
131
|
+
# Detail · Step <N> · <the step title>
|
|
132
|
+
|
|
133
|
+
── recap · step <N> of <M> ──
|
|
134
|
+
done-when met
|
|
135
|
+
decisions honored: D1 (some-slug), D2 (another-slug)
|
|
136
|
+
constraints all honored
|
|
137
|
+
|
|
138
|
+
## Summary
|
|
139
|
+
|
|
140
|
+
<the outcome, not the activity: one sentence when one will do, a short
|
|
141
|
+
paragraph when the step needs explaining>
|
|
142
|
+
|
|
143
|
+
## 1 <the first highlight: one sentence, one move>
|
|
144
|
+
<the full story: what moved, why, what was tried and dropped>
|
|
145
|
+
|
|
146
|
+
## 2 ...
|
|
147
|
+
|
|
148
|
+
## Recommendation
|
|
149
|
+
|
|
150
|
+
<The move.> <The reason, one or two sentences.>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The three rows under the header rule are yours, and only those three; `check`, `seam`,
|
|
154
|
+
`diff`, and `spent` are measured by the CLI, and a row you wrote for them would be
|
|
155
|
+
overwritten. Keep the rows contiguous (the first blank line ends them). Each opens with a
|
|
156
|
+
verdict word from the closed set in the turn anatomy (`met`, `not met`, `drift`;
|
|
157
|
+
`honored`, `none exercised`, `bent`, `drift`; `all honored`, `bent`, `drift`); a cited
|
|
158
|
+
decision or constraint carries its slug (`C1 (no-new-deps)`, never a bare `C1`); a row
|
|
159
|
+
that cannot apply vanishes. A green row collapses to its word or its count and a red one
|
|
160
|
+
names the one offender in a short clause, which is what keeps the whole row inside the
|
|
161
|
+
fence's 80 columns (13 of label, 67 of value); two or more items break onto indented
|
|
162
|
+
continuation lines opening with `-`, and a red row's evidence onto an indented `→` line.
|
|
163
|
+
|
|
164
|
+
The `## Summary` lead and the `## <n>` section titles are the turn's opening block:
|
|
165
|
+
handoff prints the lead behind the `**Summary**:` label, appends the `(details: …)`
|
|
166
|
+
bracket, and renders the titles as the numbered highlights, so you type neither the path
|
|
167
|
+
nor the list. Five highlights at most, each one sentence and one move, plain English
|
|
168
|
+
first, a path in backticks only when the name itself is the news. The numbers are
|
|
169
|
+
handles: "expand 2" opens `## 2`, so every highlight has a section behind it with the
|
|
170
|
+
full story. A judgment or a flag (a stray the seam row will name, a decision you had to
|
|
171
|
+
bend, a doubt about the done-when) is one of those highlights, never a paragraph of its
|
|
172
|
+
own. The recommendation is the last section: the move you would take as its own
|
|
173
|
+
sentence, closed by a period, then the reason as a capitalized sentence or two. The lead
|
|
174
|
+
and the recommendation are flowing prose and handoff unwraps both to the renderer's
|
|
175
|
+
width, so never hard-wrap them; handoff also prepends the bold `**Recommendation**:`
|
|
176
|
+
label, which you never type. `checkpoint` folds this file into the commit body and then
|
|
177
|
+
truncates it, so the detail lands in the durable commit rather than piling up on disk.
|
|
178
|
+
|
|
179
|
+
*Paste the turn.* Run `plumbbob handoff` and paste its output whole, verbatim, at top
|
|
180
|
+
level, trailing blank line included, then end the turn. Never nest it inside a fence of
|
|
181
|
+
your own; it carries fences of its own and they cannot nest. It prints the Summary and
|
|
182
|
+
its highlights, the Readout fence (its measured `check`, `seam`, `diff`, and `spent`
|
|
183
|
+
rows folded with your three), a `diff` fence when the change is 20 lines or fewer, the
|
|
184
|
+
Verdict folded worst-of from the same rows, Next Up, Your Call, and your recommendation
|
|
185
|
+
last. The gate verdict's one home is that `check` row: no standalone verdict line
|
|
186
|
+
exists, and you never restate the verdict in prose. A narrowed run names the slots it
|
|
187
|
+
skipped there (`· without test`), and that named narrowing is the whole disclosure. The
|
|
188
|
+
notice checkride's Stop hook appends after your turn is not yours to relay or repeat;
|
|
189
|
+
the trailing blank line you kept is what lands it on its own line. Relayed CLI strings
|
|
190
|
+
keep their em-dashes; your own lines never use one, the write-versus-relay line of
|
|
191
|
+
[D78 (em-dash-ban)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d78).
|
|
192
|
+
|
|
193
|
+
*Then read the reply as an ask or a direction.* A message that **asks** ("expand 2",
|
|
194
|
+
"what does that mean?", "why did the seam row flag that?") is an expand: answer it from the detail file, from
|
|
195
|
+
`git diff`, or from `git show`, never from recall, then run `plumbbob handoff` again and
|
|
196
|
+
paste it. The step is still in flight, so it renders the same pause, and the Your Call
|
|
197
|
+
block stays the CLI's to render rather than yours to retype. A message that **directs**
|
|
198
|
+
is needs-work: take it as what to change, and nothing lands until the human says
|
|
199
|
+
`looks good`.
|
|
200
|
+
|
|
201
|
+
**Then hand off with the next model.** `plumbbob checkpoint` prints the whole boundary
|
|
202
|
+
ending itself: `**Checkpoint**: Step N complete (<sha>)`, the Verdict the step earned,
|
|
203
|
+
any advisory (a staged path outside the seam is the common one), and Next Up. Relay
|
|
204
|
+
that block whole, run no second command, and write nothing around it. It is the
|
|
205
|
+
orientation tier (no Your Call block, no recommendation), and it points at the next
|
|
206
|
+
undone step carrying that step's `- model:` recommendation (the plan's
|
|
207
|
+
smallest-model-that-fits call) so the human knows which `/model` to select before
|
|
208
|
+
running `/plumbbob:build` again. It is what carries the plan's suggestion across a fresh
|
|
209
|
+
context window, which inherits the session model, not the plan's. No `- model:` line
|
|
210
|
+
means any model will do. Guidance, never a gate.
|
|
132
211
|
|
|
133
212
|
## Running bound agents
|
|
134
213
|
|
|
@@ -181,10 +260,10 @@ and approves in the human's place**, and it **chains**:
|
|
|
181
260
|
bound agent returns `blocked` or `drift` (unblock-and-re-run, or `/plumbbob:refine`; an
|
|
182
261
|
agent cannot advance the loop), a new decision is needed, no planned steps
|
|
183
262
|
remain, or the top of a requested range is reached. **When `--auto` halts back to the
|
|
184
|
-
human,
|
|
185
|
-
|
|
186
|
-
`- model:` recommendation if it has one, so a fresh context window
|
|
187
|
-
to select.
|
|
263
|
+
human, end on the same turn the default pause does**: write the detail file, run
|
|
264
|
+
`plumbbob handoff`, and paste its block: the step just completed, the next undone step,
|
|
265
|
+
and that next step's `- model:` recommendation if it has one, so a fresh context window
|
|
266
|
+
knows which `/model` to select.
|
|
188
267
|
|
|
189
268
|
`--auto` and a step range are the only paths that checkpoint without a human pause, and
|
|
190
269
|
only because the human asked for it by name; a range re-imposes the pause at its top.
|
|
@@ -229,15 +308,17 @@ just the one more entry already in the halt list above.
|
|
|
229
308
|
who verifies and (bar `--auto`) the human is still the clock. See **§ Running bound
|
|
230
309
|
agents** for the mechanics.
|
|
231
310
|
- **A refused checkpoint is the pause, not an error.** Under plumbbob's turn
|
|
232
|
-
hook a same-turn `checkpoint` is refused by design:
|
|
233
|
-
block, **end the turn**; the human's approval on their next
|
|
311
|
+
hook a same-turn `checkpoint` is refused by design: write the file, run `plumbbob
|
|
312
|
+
handoff`, paste its block, **end the turn there**; the human's approval on their next
|
|
313
|
+
turn is what lets you land
|
|
234
314
|
it, and landing it is a deliberate beat, not a side effect of the next `/plumbbob:build`.
|
|
235
315
|
Never route around it with a raw `git commit`. An explicit `/plumbbob:build --auto` or a typed
|
|
236
316
|
step range in the human's own prompt are the only self-approvals; **never write `auto`
|
|
237
317
|
into a settings file yourself to unlock a checkpoint. A grant you mint is no grant (the
|
|
238
318
|
latch ignores it; [D67 (auto-not-a-grant)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d67)); ask the human to type
|
|
239
319
|
`/plumbbob:build --auto` again instead.**
|
|
240
|
-
- **Close with the next model.** When a step lands,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
320
|
+
- **Close with the next model.** When a step lands, `plumbbob checkpoint` prints the whole
|
|
321
|
+
boundary ending; relay it with nothing written around it and no second command. It cites
|
|
322
|
+
the completed step and the next undone step, and names that next step's `- model:`
|
|
323
|
+
recommendation if it has one, which is what a fresh context window needs to pick the
|
|
324
|
+
right `/model` before the next run. Guidance, never a gate.
|