plumbbob 0.13.4 → 0.13.6
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 +23 -23
- package/dist/cli-core.js +6 -5
- package/dist/lib/notice.js +42 -7
- package/dist/lib/orient.js +102 -23
- package/dist/lib/sidecar.js +53 -31
- package/dist/verbs/checkpoint.js +12 -12
- package/dist/verbs/finish.js +7 -7
- package/dist/verbs/handoff.js +368 -302
- package/dist/verbs/harvest.js +20 -21
- package/dist/verbs/relay.js +48 -6
- package/dist/verbs/turn.js +111 -31
- package/package.json +1 -1
- package/skills/build/SKILL.md +59 -52
- package/skills/finish/SKILL.md +10 -4
- package/skills/harvest/SKILL.md +26 -13
- package/skills/plan/SKILL.md +13 -9
- package/skills/run/SKILL.md +38 -24
- package/skills/verify/SKILL.md +48 -40
package/dist/verbs/harvest.js
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
// A stop's record is latched (D103 (stop-latched)). It refuses in the turn
|
|
11
11
|
// the stop card printed, since a run's long turn ends on proposals and that is
|
|
12
12
|
// where a model would carry straight on into recording its own; it refuses a
|
|
13
|
-
// range
|
|
14
|
-
//
|
|
13
|
+
// range no stop card showed, read off the range the card's STOP stamp names,
|
|
14
|
+
// though not a detail file rewritten since, which is a lean the human changed
|
|
15
|
+
// in a reply (D110 (reply-after-the-card)); and it refuses a range that no
|
|
16
|
+
// longer reads as landed, telling
|
|
15
17
|
// a step landed and reopened from one that joined the range after it landed,
|
|
16
18
|
// which moves to a range still to come. A harvest outside a stop (`# Detail · Harvest`) needs no stamp. A proposal
|
|
17
19
|
// naming a handle the list lacks (`P9` with eight parked), or a class outside
|
|
@@ -24,9 +26,9 @@ import { ending, notice, printEnding, transition } from "../lib/notice.js";
|
|
|
24
26
|
import { appendHarvest, appendToSection, flipParks, harvestLines, logEntry, stopLogLine } from "../lib/buildlog.js";
|
|
25
27
|
import { checkStopLatch } from "../lib/latch.js";
|
|
26
28
|
import { parseBuildRanges, parseDetailStep, parseLastCheckpoint, parseOrderedSteps, stepShas } from "../lib/orient.js";
|
|
27
|
-
import { activeBuild, buildLogPath, checkpointsPath, clearDetail, clearStop,
|
|
29
|
+
import { activeBuild, buildLogPath, checkpointsPath, clearDetail, clearStop, detailPath, hasSession, intentPath, readStop, } from "../lib/sidecar.js";
|
|
28
30
|
import { hasStep } from "../lib/steplist.js";
|
|
29
|
-
import { boundaryEnding, harvestProposals, parkList, parseDetailRange, stopRecord } from "./handoff.js";
|
|
31
|
+
import { boundaryEnding, harvestProposals, isHarvestDetail, parkList, parseDetailRange, stopRecord, } from "./handoff.js";
|
|
30
32
|
// The three classes a park settles as, in the order the ending counts them.
|
|
31
33
|
const CLASSES = ['blocker', 'tangent', 'pivot'];
|
|
32
34
|
// Where a refusal about the proposals sends the model: the file it wrote them in.
|
|
@@ -57,7 +59,7 @@ export function harvest(cwd, _args) {
|
|
|
57
59
|
const parks = parkList(buildLog);
|
|
58
60
|
let stop = null;
|
|
59
61
|
if (item !== null) {
|
|
60
|
-
const found = stopRange(root, slug, intent,
|
|
62
|
+
const found = stopRange(root, slug, intent, item);
|
|
61
63
|
if (typeof found === 'string') {
|
|
62
64
|
process.stderr.write(found);
|
|
63
65
|
return 1;
|
|
@@ -101,26 +103,31 @@ export function harvest(cwd, _args) {
|
|
|
101
103
|
printEnding(root, {
|
|
102
104
|
tier: 'orientation',
|
|
103
105
|
render: 'harvest',
|
|
104
|
-
text: ending({ lead,
|
|
106
|
+
text: ending({ lead, state: boundaryEnding(root, slug, null).state }),
|
|
105
107
|
});
|
|
106
108
|
return 0;
|
|
107
109
|
}
|
|
108
110
|
/**
|
|
109
111
|
* The range a range's detail reviews, once the record may land: the plan
|
|
110
|
-
* holds the range, a stop card
|
|
112
|
+
* holds the range, a stop card for it has shown, a human turn has read it,
|
|
111
113
|
* and every step of the range has landed. Otherwise the refusal, with the move
|
|
112
114
|
* that unblocks it.
|
|
113
115
|
*
|
|
116
|
+
* The card need not have shown this detail file byte for byte: a reply that
|
|
117
|
+
* changed a lean rewrote the file without printing the card again, and the
|
|
118
|
+
* change is the human's own (D110 (reply-after-the-card)).
|
|
119
|
+
*
|
|
114
120
|
* An undone step in the range reads two ways, told apart by the ledger. One
|
|
115
121
|
* with a `step N <sha>` line landed and was reopened, so it lands again and
|
|
116
|
-
* the card
|
|
122
|
+
* the card prints again on `--stop --full`, since the range it reviews has
|
|
123
|
+
* changed under it. One with none joined the range after the card, a
|
|
117
124
|
* blocker folded into the range that just landed, and `handoff --stop` would
|
|
118
125
|
* refuse too, since the last landing no longer completes the range; so the
|
|
119
126
|
* remedy moves the step to a range still to come and runs the record again,
|
|
120
127
|
* which holds in the same reply while the detail file stays as the card
|
|
121
128
|
* showed it.
|
|
122
129
|
*/
|
|
123
|
-
function stopRange(root, slug, intent,
|
|
130
|
+
function stopRange(root, slug, intent, item) {
|
|
124
131
|
const plan = parseBuildRanges(intent);
|
|
125
132
|
const count = plan.ranges.length + plan.malformed.length;
|
|
126
133
|
const range = plan.ranges.find((r) => r.item === item);
|
|
@@ -129,22 +136,22 @@ function stopRange(root, slug, intent, detail, item) {
|
|
|
129
136
|
return refuse(`the plan has no range ${item}`, `write the range's number in the header of .plumbbob/detail.md`);
|
|
130
137
|
}
|
|
131
138
|
const stamp = readStop(root, slug);
|
|
132
|
-
if (stamp === null || stamp.range !== item
|
|
133
|
-
return refuse('no stop card showed
|
|
139
|
+
if (stamp === null || stamp.range !== item) {
|
|
140
|
+
return refuse('no stop card showed it', 'plumbbob handoff --stop, then end the turn on the card');
|
|
134
141
|
}
|
|
135
142
|
if (!checkStopLatch(root, stamp.turn)) {
|
|
136
143
|
return refuse("its card printed this turn, and no human turn has read it", "end the turn on the card; the human's reply is what records it");
|
|
137
144
|
}
|
|
138
145
|
const held = parseOrderedSteps(intent).filter((s) => hasStep(range.steps, s.n));
|
|
139
146
|
if (held.length === 0) {
|
|
140
|
-
return refuse('the plan holds none of its steps', 'land it, then plumbbob handoff --stop');
|
|
147
|
+
return refuse('the plan holds none of its steps', 'land it, then plumbbob handoff --stop --full');
|
|
141
148
|
}
|
|
142
149
|
const open = held.find((s) => !s.done);
|
|
143
150
|
if (open !== undefined) {
|
|
144
151
|
// A step the ledger landed was reopened since, and lands again; one it never
|
|
145
152
|
// landed was folded into the range after the card, and moves out instead.
|
|
146
153
|
return stepShas(readOr(checkpointsPath(root, slug))).has(open.n)
|
|
147
|
-
? refuse(`step ${open.n} is open again`, 'land it, then plumbbob handoff --stop')
|
|
154
|
+
? refuse(`step ${open.n} is open again`, 'land it, then plumbbob handoff --stop --full')
|
|
148
155
|
: refuse(`step ${open.n} joined it after it landed`, `move step ${open.n} to a range still to come, then plumbbob harvest`);
|
|
149
156
|
}
|
|
150
157
|
return { item, count, stop: range.stop };
|
|
@@ -183,14 +190,6 @@ function boundaryWords(root, slug) {
|
|
|
183
190
|
const last = parseLastCheckpoint(readOr(checkpointsPath(root, slug)));
|
|
184
191
|
return last === null ? 'before the first step landed' : `at the step ${last.n} boundary`;
|
|
185
192
|
}
|
|
186
|
-
/**
|
|
187
|
-
* Whether a detail file is a boundary harvest's wire, read off its
|
|
188
|
-
* `# Detail · Harvest` header.
|
|
189
|
-
*/
|
|
190
|
-
export function isHarvestDetail(detail) {
|
|
191
|
-
const first = detail.trimStart().split('\n', 1)[0] ?? '';
|
|
192
|
-
return /^#\s+Detail\s*·\s*Harvest\s*$/.test(first);
|
|
193
|
-
}
|
|
194
193
|
/**
|
|
195
194
|
* The refusal for a detail file that is no range's and no harvest's: a step's
|
|
196
195
|
* is checkpoint's to record, and an empty one, or one with no header, records nothing.
|
package/dist/verbs/relay.js
CHANGED
|
@@ -66,7 +66,9 @@ export function relayCheck(cwd, raw) {
|
|
|
66
66
|
// A host too old to send the final message has nothing to check against.
|
|
67
67
|
if (payload === null || typeof payload.last_assistant_message !== 'string')
|
|
68
68
|
return null;
|
|
69
|
-
|
|
69
|
+
// The session the payload names, else the one the process stands in: the
|
|
70
|
+
// rule the turn hook ticks by, so the two always read the same session.
|
|
71
|
+
const root = (typeof payload.cwd === 'string' ? findSessionRoot(payload.cwd) : null) ?? findSessionRoot(cwd);
|
|
70
72
|
if (root === null)
|
|
71
73
|
return null;
|
|
72
74
|
const record = readRelay(root);
|
|
@@ -135,13 +137,24 @@ function sameParts(relayed, printed, counted) {
|
|
|
135
137
|
}
|
|
136
138
|
/**
|
|
137
139
|
* The facts the CLI measured, as one comparable string: the Readout table,
|
|
138
|
-
* whitespace aside, and the Verdict
|
|
140
|
+
* whitespace aside, and the Verdict's fact. Every other line of a block may be
|
|
139
141
|
* reworded or reordered without the relay counting as missed.
|
|
140
142
|
*/
|
|
141
143
|
function factsOf(parts) {
|
|
142
144
|
const readout = parts.find((p) => p.label === 'Readout');
|
|
143
145
|
const verdict = parts.find((p) => p.label === 'Verdict');
|
|
144
|
-
return `${flat(readout === undefined ? '' : tableOf(readout))}\n${flat(verdict
|
|
146
|
+
return `${flat(readout === undefined ? '' : tableOf(readout))}\n${flat(verdict === undefined ? '' : verdictOf(verdict))}`;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The Verdict's fact: the second cell of the state table's header row, where
|
|
150
|
+
* a card carries it (D111 (state-table)), or the text after the label on the
|
|
151
|
+
* line an older ending printed. Read as the fact, not the line, so a relay
|
|
152
|
+
* that changed the spacing around the table's pipes still carries the Verdict
|
|
153
|
+
* it printed.
|
|
154
|
+
*/
|
|
155
|
+
function verdictOf(part) {
|
|
156
|
+
const first = part.lines[0] ?? '';
|
|
157
|
+
return stateRowOf(first)?.value ?? first.replace(/^\*\*Verdict\*\*:\s*/, '');
|
|
145
158
|
}
|
|
146
159
|
/** The `text` fence inside the Readout part, rails included, or '' when it has none. */
|
|
147
160
|
function tableOf(part) {
|
|
@@ -180,8 +193,8 @@ function blockCount(part) {
|
|
|
180
193
|
}
|
|
181
194
|
/**
|
|
182
195
|
* An ending's labeled parts, in order: each opens on a line that starts with a
|
|
183
|
-
* bold label and a colon,
|
|
184
|
-
* the first label is not part of any.
|
|
196
|
+
* bold label and a colon, or on a row of the state table, outside a fence, and
|
|
197
|
+
* runs to the next. Text before the first label is not part of any.
|
|
185
198
|
*/
|
|
186
199
|
function partsOf(text) {
|
|
187
200
|
const parts = [];
|
|
@@ -196,7 +209,7 @@ function partsOf(text) {
|
|
|
196
209
|
parts.at(-1)?.lines.push(line);
|
|
197
210
|
continue;
|
|
198
211
|
}
|
|
199
|
-
const label = rail === null ?
|
|
212
|
+
const label = rail === null ? labelOf(line) : undefined;
|
|
200
213
|
if (label !== undefined) {
|
|
201
214
|
parts.push({ label, lines: [line] });
|
|
202
215
|
}
|
|
@@ -206,6 +219,35 @@ function partsOf(text) {
|
|
|
206
219
|
}
|
|
207
220
|
return parts;
|
|
208
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* The label a line opens a part on, or undefined when it opens none: a bold
|
|
224
|
+
* label and a colon at the line's start, or a state-table row, whose first
|
|
225
|
+
* cell is its label. Each row of the table is a part of its own, so the
|
|
226
|
+
* table's rows are read in order as the labeled lines were.
|
|
227
|
+
*/
|
|
228
|
+
function labelOf(line) {
|
|
229
|
+
return /^\*\*([^*\n]+)\*\*:(?:\s|$)/.exec(line)?.[1] ?? stateRowOf(line)?.label;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* A state-table row's label and fact, or null for any other line: a table row
|
|
233
|
+
* of two cells, its label with the bold stripped. The delimiter row and the
|
|
234
|
+
* lean table's four-cell rows are no state row, so they stay inside the part
|
|
235
|
+
* above them.
|
|
236
|
+
*/
|
|
237
|
+
function stateRowOf(line) {
|
|
238
|
+
const cells = tableCells(line);
|
|
239
|
+
const [label, value] = cells ?? [];
|
|
240
|
+
if (cells === null || cells.length !== 2 || label === undefined || value === undefined || /^:?-{3,}:?$/.test(label))
|
|
241
|
+
return null;
|
|
242
|
+
return { label: label.replace(/^\*\*(.*)\*\*$/, '$1').trim(), value };
|
|
243
|
+
}
|
|
244
|
+
/** The cells of a markdown table row, split on unescaped pipes, or null when the line is not one. */
|
|
245
|
+
function tableCells(line) {
|
|
246
|
+
const text = line.trim();
|
|
247
|
+
if (text.length < 2 || !text.startsWith('|') || !text.endsWith('|'))
|
|
248
|
+
return null;
|
|
249
|
+
return text.slice(1, -1).split(/(?<!\\)\|/).map((c) => c.trim());
|
|
250
|
+
}
|
|
209
251
|
/**
|
|
210
252
|
* The one-line reason the model acts on and the human reads in full: what the
|
|
211
253
|
* turn missed, and the command or block that puts it back.
|
package/dist/verbs/turn.js
CHANGED
|
@@ -13,28 +13,37 @@
|
|
|
13
13
|
// range the run is landing. The harness also sends a background job's
|
|
14
14
|
// completion notice through this event, as a prompt the human never typed, so
|
|
15
15
|
// a notice ticks nothing, keeps the grant and the mark, and clears only the
|
|
16
|
-
// ending `RELAY` holds from an earlier turn.
|
|
17
|
-
//
|
|
18
|
-
// session (post-compaction, or a scripted `-p` turn where no skill prose
|
|
19
|
-
// context) still knows
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
16
|
+
// ending `RELAY` holds from an earlier turn. A typed prompt also emits ONE
|
|
17
|
+
// `UserPromptSubmit` additionalContext line when there is a beat to keep, so a
|
|
18
|
+
// fresh session (post-compaction, or a scripted `-p` turn where no skill prose
|
|
19
|
+
// is in context) still knows it: mid-step, a tangent is parked, not chased;
|
|
20
|
+
// at a pause or a stop whose card has shown, the message is the human's reply,
|
|
21
|
+
// answered in the model's own words and closed on the one line `handoff`
|
|
22
|
+
// prints, never on the card again (D110 (reply-after-the-card)). Guidance
|
|
23
|
+
// only: it blocks nothing. It ALWAYS exits 0 and any failure degrades to no
|
|
24
|
+
// output, so neither the tick nor the emit can ever wedge prompting.
|
|
23
25
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
24
26
|
import { parseBuildRanges, readRunArgs, resolveRun } from "../lib/orient.js";
|
|
25
|
-
import { clearRelay, findSessionRoot, inSpike, intentPath, notePrompt, readPrompt, readRun, setGrant, setRun, stepPath, turnPath, } from "../lib/sidecar.js";
|
|
27
|
+
import { activeBuild, clearRelay, findSessionRoot, inSpike, intentPath, notePrompt, readPause, readPrompt, readRun, setGrant, setRun, stepPath, turnPath, } from "../lib/sidecar.js";
|
|
26
28
|
import { hasStep, normalizeStepList, readBuildArgs } from "../lib/steplist.js";
|
|
29
|
+
import { waitingCard } from "./handoff.js";
|
|
27
30
|
/**
|
|
28
|
-
* The hook entry point: apply the prompt, then emit the
|
|
29
|
-
*
|
|
31
|
+
* The hook entry point: apply the prompt, then emit the nudge when it counted
|
|
32
|
+
* as a human turn.
|
|
30
33
|
*
|
|
34
|
+
* The session is the one the payload's `cwd` names when it names one, else
|
|
35
|
+
* the one the process stands in, the rule the `Stop` check follows too, so the
|
|
36
|
+
* turn that ticks and the turn that is judged are always the same session's.
|
|
31
37
|
* Always returns 0: a broken turn must never wedge a prompt, so any failure is
|
|
32
38
|
* swallowed and degrades to no output.
|
|
33
39
|
*/
|
|
34
40
|
export function turn(cwd, _args) {
|
|
35
41
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
const raw = readStdin();
|
|
43
|
+
const { cwd: named, prompt } = readPayload(raw);
|
|
44
|
+
const where = named !== null && findSessionRoot(named) !== null ? named : cwd;
|
|
45
|
+
if (applyTurn(where, raw)) {
|
|
46
|
+
const context = turnContext(where, prompt);
|
|
38
47
|
if (context !== null)
|
|
39
48
|
process.stdout.write(context);
|
|
40
49
|
}
|
|
@@ -45,32 +54,102 @@ export function turn(cwd, _args) {
|
|
|
45
54
|
return 0;
|
|
46
55
|
}
|
|
47
56
|
/**
|
|
48
|
-
* The `UserPromptSubmit` additionalContext payload
|
|
57
|
+
* The `UserPromptSubmit` additionalContext payload for the beat the session is
|
|
58
|
+
* on, or null when there is none to keep.
|
|
49
59
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
60
|
+
* Three beats, read off the control files. A step whose pause card has shown
|
|
61
|
+
* (STEP, and PAUSE naming that step's pause) is waiting for the human's call,
|
|
62
|
+
* so the message is the reply to it: answered or acted on in the model's own
|
|
63
|
+
* words, and closed on the one line `handoff` prints rather than the card
|
|
64
|
+
* again (D110 (reply-after-the-card)). A step in flight with no card yet is
|
|
65
|
+
* mid-build, where a tangent is a park and not an edit. A stop card waiting
|
|
66
|
+
* for its reply is the pause's beat at a stop. At a pause or a stop, a
|
|
67
|
+
* `prompt` asking to see the card (`show the pause`, the phrase the card's
|
|
68
|
+
* `expand` move teaches) gets the one instruction that matters for it: paste
|
|
69
|
+
* the `--full` output exactly as it printed, as the whole turn. Guidance only,
|
|
70
|
+
* since none of the build, run, or park prose may be in context after
|
|
71
|
+
* compaction or on a scripted turn. Exported because an in-process test cannot
|
|
72
|
+
* read the hook's real stdout.
|
|
58
73
|
*/
|
|
59
|
-
export function
|
|
74
|
+
export function turnContext(cwd, prompt = '') {
|
|
60
75
|
const root = findSessionRoot(cwd);
|
|
61
76
|
if (root === null)
|
|
62
77
|
return null;
|
|
78
|
+
const text = nudgeText(root, SHOW_REQUEST.test(prompt));
|
|
79
|
+
return text === null ? null : `${JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: text } })}\n`;
|
|
80
|
+
}
|
|
81
|
+
// A message asking to see the card again: `show the pause` and `show the stop`,
|
|
82
|
+
// the phrases the card's `expand` move teaches, and `show me the card` beside
|
|
83
|
+
// them. In a fresh session the model ran `--full` on that request and then
|
|
84
|
+
// retyped the card behind a line of its own, three runs in five of the
|
|
85
|
+
// 2026-10-27 eval, because nothing in its context said how to relay it.
|
|
86
|
+
const SHOW_REQUEST = /\bshow\s+(?:me\s+)?the\s+(?:pause|stop|card)\b/i;
|
|
87
|
+
/**
|
|
88
|
+
* The nudge's words for the beat the session is on, or null when it is on
|
|
89
|
+
* none: no step in flight and no stop card waiting. `showing` is a message
|
|
90
|
+
* that asks to see the card, which at a pause or a stop takes the show nudge.
|
|
91
|
+
*/
|
|
92
|
+
function nudgeText(root, showing) {
|
|
63
93
|
const step = inFlightStep(root);
|
|
64
|
-
if (step
|
|
94
|
+
if (step !== null) {
|
|
95
|
+
const shown = readPause(root);
|
|
96
|
+
if (shown?.kind !== 'pause' || shown.step !== step)
|
|
97
|
+
return parkNudge(step);
|
|
98
|
+
return showing ? showNudge(`step ${step}'s card`, 'plumbbob handoff --full') : pauseNudge(step);
|
|
99
|
+
}
|
|
100
|
+
const slug = activeBuild(root);
|
|
101
|
+
let intent = '';
|
|
102
|
+
try {
|
|
103
|
+
intent = readFileSync(intentPath(root, slug), 'utf8');
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
65
106
|
return null;
|
|
66
|
-
|
|
107
|
+
}
|
|
108
|
+
const card = waitingCard(root, slug, intent);
|
|
109
|
+
if (card === null)
|
|
110
|
+
return null;
|
|
111
|
+
return showing
|
|
112
|
+
? showNudge(`range ${card.range} of ${card.count}'s stop card`, 'plumbbob handoff --stop --full')
|
|
113
|
+
: stopNudge(card.range, card.count);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The nudge when the human asks to see a card again: the one command and how
|
|
117
|
+
* to relay what it prints, since the card is the whole turn and a retyped one
|
|
118
|
+
* is the miss the relay check holds.
|
|
119
|
+
*/
|
|
120
|
+
function showNudge(card, command) {
|
|
121
|
+
return (`plumbbob: the human asked to see ${card} again. ` +
|
|
122
|
+
`Run \`${command}\` and paste its output exactly as it printed, as the whole turn: ` +
|
|
123
|
+
`no line before it, nothing after it, and nothing in it retyped, reworded, or cut.`);
|
|
124
|
+
}
|
|
125
|
+
/** The mid-build nudge: a tangent is a park, not an edit, and the step lands on approval. */
|
|
126
|
+
function parkNudge(step) {
|
|
127
|
+
return (`plumbbob: step ${step} is in flight — a new idea or tangent is a park, not an edit: ` +
|
|
67
128
|
`capture it with \`plumbbob park "<one line>"\` and relay the block it prints whole, ` +
|
|
68
129
|
`with nothing written around it, since that block is the whole turn. ` +
|
|
69
130
|
`The one exception is a message that also asks something: answer the question first, ` +
|
|
70
131
|
`then relay the park's block after the answer and write nothing after it. ` +
|
|
71
132
|
`The block's pointer back at the step closes the turn either way, so run no \`plumbbob handoff\` after it. ` +
|
|
72
|
-
`The step stays open and lands when you run \`plumbbob checkpoint ${step}\` after approval
|
|
73
|
-
|
|
133
|
+
`The step stays open and lands when you run \`plumbbob checkpoint ${step}\` after approval.`);
|
|
134
|
+
}
|
|
135
|
+
/** The nudge at a pause whose card has shown: the message is the reply, and the card is not printed again. */
|
|
136
|
+
function pauseNudge(step) {
|
|
137
|
+
return (`plumbbob: step ${step}'s card has shown, and this message is the human's reply to it. ` +
|
|
138
|
+
`An approval lands the step: run \`plumbbob checkpoint ${step}\` and relay its ending. ` +
|
|
139
|
+
`Anything else is answered in your own words: answer the question, or make the change it asks for, ` +
|
|
140
|
+
`and when a change moves what the card said, write \`.plumbbob/detail.md\` again so the record matches what lands. ` +
|
|
141
|
+
`Print no card for it: run \`plumbbob handoff\` and end the turn on the one line it prints, with nothing after it. ` +
|
|
142
|
+
`Only when the human asks to see the card again, run \`plumbbob handoff --full\` and paste its output exactly as it printed, as the whole turn. ` +
|
|
143
|
+
`A tangent the human hands you is still a park: \`plumbbob park "<one line>"\`.`);
|
|
144
|
+
}
|
|
145
|
+
/** The nudge at a stop card waiting for its reply: the stop's version of the pause's. */
|
|
146
|
+
function stopNudge(range, count) {
|
|
147
|
+
return (`plumbbob: range ${range} of ${count}'s stop card has shown, and this message is the human's reply to it. ` +
|
|
148
|
+
`An approval records the stop: fold each confirmed blocker into the plan, then run \`plumbbob harvest\` and relay its ending. ` +
|
|
149
|
+
`Anything else is answered in your own words: answer the question, or make the change it asks for ` +
|
|
150
|
+
`(a lean is its line under the detail file's \`## Harvest\`). ` +
|
|
151
|
+
`Print no card for it: run \`plumbbob handoff --stop\` and end the turn on the one line it prints, with nothing after it. ` +
|
|
152
|
+
`Only when the human asks to see the card again, run \`plumbbob handoff --stop --full\` and paste its output exactly as it printed, as the whole turn.`);
|
|
74
153
|
}
|
|
75
154
|
/**
|
|
76
155
|
* The in-flight step number, or null when no STEP marker is present.
|
|
@@ -251,13 +330,13 @@ function readBuild(root) {
|
|
|
251
330
|
}
|
|
252
331
|
}
|
|
253
332
|
/**
|
|
254
|
-
* The `prompt` and `
|
|
255
|
-
* null when
|
|
333
|
+
* The `prompt`, `prompt_id`, and `cwd` fields of the UserPromptSubmit JSON, as
|
|
334
|
+
* '' and null when any is absent.
|
|
256
335
|
*
|
|
257
336
|
* Absent, unparseable, or non-string input contributes nothing rather than
|
|
258
337
|
* wedging the tool: the tick still lands (a human turn did occur), the empty
|
|
259
|
-
* prompt clears any GRANT,
|
|
260
|
-
* throws over bad stdin.
|
|
338
|
+
* prompt clears any GRANT, a missing id records none, and a missing `cwd`
|
|
339
|
+
* leaves the process's; the verb never throws over bad stdin.
|
|
261
340
|
*/
|
|
262
341
|
function readPayload(raw) {
|
|
263
342
|
try {
|
|
@@ -265,10 +344,11 @@ function readPayload(raw) {
|
|
|
265
344
|
return {
|
|
266
345
|
prompt: typeof parsed.prompt === 'string' ? parsed.prompt : '',
|
|
267
346
|
promptId: typeof parsed.prompt_id === 'string' && parsed.prompt_id !== '' ? parsed.prompt_id : null,
|
|
347
|
+
cwd: typeof parsed.cwd === 'string' && parsed.cwd !== '' ? parsed.cwd : null,
|
|
268
348
|
};
|
|
269
349
|
}
|
|
270
350
|
catch {
|
|
271
|
-
return { prompt: '', promptId: null };
|
|
351
|
+
return { prompt: '', promptId: null, cwd: null };
|
|
272
352
|
}
|
|
273
353
|
}
|
|
274
354
|
/**
|
package/package.json
CHANGED
package/skills/build/SKILL.md
CHANGED
|
@@ -168,19 +168,23 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
|
|
|
168
168
|
fence's 80 columns (13 of label, 67 of value); two or more items break onto indented
|
|
169
169
|
continuation lines opening with `-`, and a red row's evidence onto an indented `→` line.
|
|
170
170
|
|
|
171
|
-
The `## Summary` lead
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
171
|
+
The `## Summary` lead opens the card: handoff prints it behind the `**Summary**:` label.
|
|
172
|
+
The `## <n>` sections stay in the detail file, where `expand 2` opens `## 2` and the
|
|
173
|
+
build log's record keeps them; the card prints none of them and closes instead on a
|
|
174
|
+
`[more details](…)` link to the file, which handoff writes, so you type no path. Five
|
|
175
|
+
sections at most, each titled with one sentence and one move, plain English first, a
|
|
176
|
+
path in backticks only when the name itself is the news. A judgment or a flag (a stray
|
|
177
|
+
the seam row will name, a decision you had to bend, a doubt about the done-when) is one
|
|
178
|
+
of those sections, and the card carries it where it bears on the call: a bent row's `→`
|
|
179
|
+
line points at it, and your recommendation names it when it changes the move. It is
|
|
180
|
+
never a paragraph of its own. The recommendation is the last section: the move you would take as its own
|
|
180
181
|
sentence, closed by a period, then the reason as a capitalized sentence or two. The lead
|
|
181
182
|
and the recommendation are flowing prose and handoff unwraps both to the renderer's
|
|
182
183
|
width, so never hard-wrap them; handoff also prepends the bold `**Recommendation**:`
|
|
183
|
-
label, which you never type.
|
|
184
|
+
label, which you never type. A shell command you want the human to run never rides
|
|
185
|
+
inside a sentence: end the sentence on a colon and give the command a fenced `sh` block
|
|
186
|
+
of its own, one command to a line, which handoff keeps line for line. A slash command
|
|
187
|
+
or a path stays in backticks in the sentence. `checkpoint` records this file beneath the step's dated
|
|
184
188
|
line in the build-log's `## Log` and then truncates it, so the detail lands in the
|
|
185
189
|
tracked ledger rather than piling up on disk.
|
|
186
190
|
|
|
@@ -192,14 +196,15 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
|
|
|
192
196
|
|
|
193
197
|
*Paste the turn.* Run `plumbbob handoff` and paste its output whole, verbatim, at top
|
|
194
198
|
level, trailing blank line included, then end the turn. Never nest it inside a fence of
|
|
195
|
-
your own; it carries fences of its own and they cannot nest. It prints the
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
rows,
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
your own; it carries fences of its own and they cannot nest. It prints the Summary, the
|
|
200
|
+
Readout fence (its measured `check`, `seam`, `diff`, and `spent` rows folded with your
|
|
201
|
+
three, the changed files listed beneath `diff`), a `diff` fence when the change is 20
|
|
202
|
+
lines or fewer, Your Call, the state table (the Verdict folded worst-of from the same
|
|
203
|
+
rows, the Now row naming the step in flight, Next Up, and the Range, Order, and Parked
|
|
204
|
+
rows when they apply), your recommendation, and the `more details` link last. Run it
|
|
205
|
+
once: a second run at the same pause prints the one-line reminder instead of the card,
|
|
206
|
+
so if that line comes back where the card belongs (you ran handoff earlier in this
|
|
207
|
+
turn), run `plumbbob handoff --full` and paste that. The gate verdict's one home is that `check`
|
|
203
208
|
row: no standalone verdict line exists, and you never restate the verdict in prose. A
|
|
204
209
|
narrowed run names the slots it skipped there (`· without test`), and that named
|
|
205
210
|
narrowing is the whole disclosure. The notice checkride's Stop hook appends after your
|
|
@@ -208,37 +213,39 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
|
|
|
208
213
|
one, the write-versus-relay line of
|
|
209
214
|
[D78 (em-dash-ban)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d78).
|
|
210
215
|
|
|
211
|
-
*Then
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
the
|
|
216
|
+
*Then answer the reply in your own words.* The card shows once
|
|
217
|
+
([D110 (reply-after-the-card)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d110)):
|
|
218
|
+
every later turn at this pause is your reply, closed by one line. A message that
|
|
219
|
+
**asks** ("expand 2", "what does that mean?", "why did the seam row flag that?") is an
|
|
220
|
+
expand: answer it from the detail file, from `git diff`, or from the build-log's
|
|
221
|
+
`## Log` for an older step, never from recall. A message that **directs** is needs-work:
|
|
222
|
+
make the change, run the check, and say what moved and what the check found; when the
|
|
223
|
+
change moves what the card said (a judgment row, the Summary, the recommendation), write
|
|
224
|
+
the detail file again so the record matches what lands. Nothing lands until the human
|
|
225
|
+
says `looks good`. Either way, then run `plumbbob handoff` and paste its output beneath
|
|
226
|
+
your reply, whole and verbatim: at a pause whose card has shown it prints one line,
|
|
227
|
+
`**Next Up**: Back to Step <N> of <M> - <title>`, whatever changed since, so your reply
|
|
228
|
+
stays on screen. The turn is your reply, then that line, and nothing after it. The CLI
|
|
229
|
+
picks the form, never you: paste whichever one it prints, and never retype the card.
|
|
230
|
+
|
|
231
|
+
The card comes back one way: the human asks to see it (`show the pause`, or any wording
|
|
232
|
+
of it). Then run `plumbbob handoff --full` and paste its output whole, with nothing
|
|
233
|
+
written around it. Never run `--full` to read the card for yourself, after a compaction
|
|
234
|
+
or in a fresh session; what it prints is the turn's ending, and the relay check holds a
|
|
235
|
+
turn that does not close on it. Read the detail file and `plumbbob status` instead. A
|
|
236
|
+
tangent the human hands you at the pause is still a park.
|
|
232
237
|
|
|
233
238
|
**Then hand off with the next model.** `plumbbob checkpoint` prints the whole boundary
|
|
234
|
-
ending itself: `**Checkpoint**: Step N complete (<sha>)`,
|
|
235
|
-
|
|
236
|
-
this landing completes a range (
|
|
237
|
-
the build stops here),
|
|
238
|
-
(the relay check holds a boundary that dropped
|
|
239
|
-
nothing around it.
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
ending itself: `**Checkpoint**: Step N complete (<sha>)`, any advisory (a staged path
|
|
240
|
+
outside the seam is the common one), and the state table: the Verdict the step earned,
|
|
241
|
+
the Stop row when this landing completes a range (`| Stop | Range 3 of 5 landed - <its
|
|
242
|
+
stop> |`, the reason the build stops here), Next Up, and the rows beneath it. Relay that
|
|
243
|
+
block whole, the table with the rest (the relay check holds a boundary that dropped a
|
|
244
|
+
row), run no second command, and write nothing around it. A later turn at this boundary
|
|
245
|
+
is your reply; a `plumbbob handoff` there prints Next Up's one line, since the boundary
|
|
246
|
+
has shown. It is the
|
|
247
|
+
orientation tier (no Your Call block, no recommendation), and its Next Up points at the
|
|
248
|
+
next undone step carrying that step's `- model:` recommendation (the plan's
|
|
242
249
|
smallest-model-that-fits call) so the human knows which `/model` to select before
|
|
243
250
|
running `/plumbbob:build` again. It is what carries the plan's suggestion across a fresh
|
|
244
251
|
context window, which inherits the session model, not the plan's. No `- model:` line
|
|
@@ -376,13 +383,13 @@ out, or the next step in build order is one the range does not name),
|
|
|
376
383
|
the next one. A run's range ends on the stop card, which lists every open park with a
|
|
377
384
|
proposed class, and the reply that settles the card records them (the run skill's
|
|
378
385
|
section on the reply at a stop). A range landed here, by a typed range, ends on the plain
|
|
379
|
-
boundary instead; when the human asks what comes next there with parks open (the
|
|
380
|
-
`
|
|
386
|
+
boundary instead; when the human asks what comes next there with parks open (the table's
|
|
387
|
+
`Parked` row, `2 of 7 open`), the move is `/plumbbob:harvest`, then the run the card names.
|
|
381
388
|
- **After an early stop, settle the step, then rerun what is left.** A halt on trouble
|
|
382
389
|
inside a range ends at that step's pause, and the grant ends with the turn. Once the
|
|
383
|
-
human approves and the step lands, the
|
|
384
|
-
what is left of the range (
|
|
385
|
-
the human types to carry on; `/plumbbob:build 8-9` lands the same steps by hand.
|
|
390
|
+
human approves and the step lands, the state table's Range row names the run that lands
|
|
391
|
+
what is left of the range (`| Range | 3 of 5: /plumbbob:run (steps 8-9) |`), and that is
|
|
392
|
+
what the human types to carry on; `/plumbbob:build 8-9` lands the same steps by hand.
|
|
386
393
|
|
|
387
394
|
## The hard contracts
|
|
388
395
|
|
package/skills/finish/SKILL.md
CHANGED
|
@@ -41,7 +41,13 @@ record.
|
|
|
41
41
|
- `## Decisions and why`: the settled calls that shaped the build (the *why* behind
|
|
42
42
|
the log). Copy each one's `D# (slug)` from `intent.md` (never a bare `D#`) so
|
|
43
43
|
the report reads without flipping back to the intent.
|
|
44
|
-
- `## Parked & harvested`: what was captured and how each was classified
|
|
44
|
+
- `## Parked & harvested`: what was captured and how each was classified, as the
|
|
45
|
+
table the stop card's leans use, with the settled class where the card had the lean
|
|
46
|
+
([D112 (lean-table)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d112)):
|
|
47
|
+
`| Park | What it is | Why it matters | Settled |`, one row per park, its handle and a
|
|
48
|
+
short title, the park's text as the Park list holds it, why it matters (the why the
|
|
49
|
+
stop's record kept, or your own clause for a park harvested before the table), and
|
|
50
|
+
the class it settled as with its reason.
|
|
45
51
|
- `## Final status`: done or partial, and what is left.
|
|
46
52
|
- `## Deferred tangents`: the harvested items that become future work.
|
|
47
53
|
Stop there. `plumbbob finish` appends `## Checkpoints` (the baseline and step SHAs) and
|
|
@@ -74,9 +80,9 @@ record.
|
|
|
74
80
|
the `plumbbob finish` marker subject only.
|
|
75
81
|
4. **Point at the next goal.** Relay `plumbbob finish`'s output verbatim, blank lines
|
|
76
82
|
included: its lead line, which names the branch the tracked folder now rides into the
|
|
77
|
-
PR, any advisory beneath it, and
|
|
78
|
-
is the orientation tier's close
|
|
79
|
-
[turn anatomy](https://github.com/robmclarty/plumbbob/blob/main/docs/presentation.md)):
|
|
83
|
+
PR, any advisory beneath it, and its state table, whose one row is
|
|
84
|
+
`| **Next Up** | Nothing planned - /plumbbob:plan |`. That is the orientation tier's close
|
|
85
|
+
(the [turn anatomy](https://github.com/robmclarty/plumbbob/blob/main/docs/presentation.md)):
|
|
80
86
|
the lead line, then the forward pointer. `finish` renders that pointer itself rather
|
|
81
87
|
than leaving it to `plumbbob handoff`, because finish has just cleared the session
|
|
82
88
|
handoff would read one from. Relay the block and write nothing after it.
|