plumbbob 0.13.6 → 0.13.8
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 +6 -6
- package/dist/cli-core.js +15 -1
- package/dist/lib/links.js +97 -0
- package/dist/lib/notice.js +1 -1
- package/dist/lib/orient.js +152 -11
- package/dist/lib/sidecar.js +43 -5
- package/dist/verbs/build.js +20 -12
- package/dist/verbs/checkpoint.js +8 -4
- package/dist/verbs/defer.js +96 -0
- package/dist/verbs/handoff.js +275 -97
- package/dist/verbs/harvest.js +15 -3
- package/dist/verbs/park.js +14 -4
- package/dist/verbs/recover.js +2 -2
- package/dist/verbs/relay.js +74 -6
- package/dist/verbs/start.js +40 -14
- package/dist/verbs/status.js +16 -4
- package/dist/verbs/turn.js +29 -12
- package/package.json +1 -1
- package/skills/build/SKILL.md +37 -19
- package/skills/harvest/SKILL.md +12 -3
- package/skills/park/SKILL.md +26 -9
- package/skills/plan/SKILL.md +43 -16
- package/skills/refine/SKILL.md +19 -5
- package/skills/run/SKILL.md +24 -10
- package/skills/status/SKILL.md +7 -0
- package/skills/verify/SKILL.md +12 -7
- package/templates/intent.md +19 -1
package/dist/verbs/park.js
CHANGED
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
// step it interrupted, so the relay is one command.
|
|
8
8
|
// Capture is not a state transition, so it runs in any context (terminal or
|
|
9
9
|
// in-session).
|
|
10
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
11
|
import { findRepoRoot } from "../lib/git.js";
|
|
12
|
-
import { activeBuild, hasSession, buildLogPath } from "../lib/sidecar.js";
|
|
12
|
+
import { activeBuild, hasSession, buildLogPath, intentPath } from "../lib/sidecar.js";
|
|
13
13
|
import { appendToSection } from "../lib/buildlog.js";
|
|
14
14
|
import { ending, notice, printEnding, transition } from "../lib/notice.js";
|
|
15
|
+
import { parseOpenQuestionRows } from "../lib/orient.js";
|
|
15
16
|
import { driverPointer } from "./handoff.js";
|
|
17
|
+
import { setAsideAdvisories } from "./defer.js";
|
|
16
18
|
/**
|
|
17
19
|
* Append the given text as one unchecked `- [ ]` line to the build-log's Park list.
|
|
18
20
|
*
|
|
@@ -41,12 +43,20 @@ export function park(cwd, args) {
|
|
|
41
43
|
return 1;
|
|
42
44
|
}
|
|
43
45
|
writeFileSync(path, updated);
|
|
46
|
+
const intent = intentPath(root);
|
|
47
|
+
const questions = parseOpenQuestionRows(existsSync(intent) ? readFileSync(intent, 'utf8') : '');
|
|
44
48
|
// A park interrupts a step without ending it, so the ending is the capture
|
|
45
|
-
// and the pointer back at what was interrupted.
|
|
49
|
+
// and the pointer back at what was interrupted. A park naming a question
|
|
50
|
+
// that still waits on the human says so, since the park list never moves
|
|
51
|
+
// the card's count (D123 (defer-records)).
|
|
46
52
|
printEnding(root, {
|
|
47
53
|
tier: 'driver',
|
|
48
54
|
render: 'park',
|
|
49
|
-
text: ending({
|
|
55
|
+
text: ending({
|
|
56
|
+
lead: transition({ label: 'Parked', fact: text }),
|
|
57
|
+
advisories: setAsideAdvisories(text, questions),
|
|
58
|
+
pointer: driverPointer(root, activeBuild(root)),
|
|
59
|
+
}),
|
|
50
60
|
});
|
|
51
61
|
return 0;
|
|
52
62
|
}
|
package/dist/verbs/recover.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { existsSync, readFileSync } from 'node:fs';
|
|
20
20
|
import { findRepoRoot } from "../lib/git.js";
|
|
21
21
|
import { parseBuildOrder, parseBuildRanges, parseOrderedSteps, parseSteps, rangeStanding } from "../lib/orient.js";
|
|
22
|
-
import { activeBuild,
|
|
22
|
+
import { activeBuild, clearHandoff, clearTick, grantPath, handoffPath, hasSession, intentPath, inSpike, listBuilds, planCommitted, readTurn, setActiveBuild, setGrant, stepPath, tickPath, } from "../lib/sidecar.js";
|
|
23
23
|
import { spikeBranches, spikeWorktrees } from "./spike.js";
|
|
24
24
|
/**
|
|
25
25
|
* Report every inconsistency in the control plane, and with `--fix` repair the
|
|
@@ -255,7 +255,7 @@ function leftoverFindings(root, slug, stepInFlight) {
|
|
|
255
255
|
// pause, and cleared by the checkpoint that lands either one. At the boundary
|
|
256
256
|
// with the plan already committed, neither pause is open, so a TICK left here
|
|
257
257
|
// is measuring a span that closed.
|
|
258
|
-
const planLanded =
|
|
258
|
+
const planLanded = planCommitted(root, slug);
|
|
259
259
|
if (!stepInFlight && planLanded && existsSync(tickPath(root, slug))) {
|
|
260
260
|
findings.push({
|
|
261
261
|
name: 'latch tick',
|
package/dist/verbs/relay.js
CHANGED
|
@@ -29,6 +29,14 @@
|
|
|
29
29
|
// stop card is a miss however whole the boundary was, since the card is the
|
|
30
30
|
// run's ending, and its reason names `plumbbob handoff --stop` as the retry.
|
|
31
31
|
//
|
|
32
|
+
// A reminder can close the turn and still leave the human without the reply.
|
|
33
|
+
// When the reply was written before a tool call and the final message is the
|
|
34
|
+
// reminder alone, a client that folds text written ahead of a tool call shows
|
|
35
|
+
// the line and nothing else (GitHub issue #12). That shape is held once too,
|
|
36
|
+
// by a reason that asks for the reply again and ends on the line to type. It
|
|
37
|
+
// names no command, since a retry that ran one would bury the reply the same
|
|
38
|
+
// way (D121 (reply-after-the-tools)).
|
|
39
|
+
//
|
|
32
40
|
// The final message comes from the payload's `last_assistant_message`, never
|
|
33
41
|
// from the transcript, which does not yet hold that message when Stop fires;
|
|
34
42
|
// the transcript supplies only the turn's earlier messages, which an
|
|
@@ -79,13 +87,53 @@ export function relayCheck(cwd, raw) {
|
|
|
79
87
|
// relayed: the card is the run's ending, and `handoff --stop` overwrites
|
|
80
88
|
// this record once it prints.
|
|
81
89
|
const carried = record.owes !== undefined ? false : carriedEnding(record, payload.last_assistant_message, payload);
|
|
90
|
+
if (carried === false)
|
|
91
|
+
return hold(root, record, reason(record));
|
|
92
|
+
// The reminder closed the turn, but the reply stood above a tool call.
|
|
93
|
+
if (carried === true && buriedReply(record, payload.last_assistant_message, payload))
|
|
94
|
+
return hold(root, record, buriedReason(record));
|
|
82
95
|
// Whole, or not this check's to judge: let the turn end.
|
|
83
|
-
|
|
84
|
-
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Stamp the once in `RELAY` and emit the hold with its reason, or nothing when
|
|
100
|
+
* the stamp cannot land.
|
|
101
|
+
*/
|
|
102
|
+
function hold(root, record, text) {
|
|
85
103
|
// A hold that cannot be counted could repeat, so it is no hold at all.
|
|
86
104
|
if (!holdRelay(root, record))
|
|
87
105
|
return null;
|
|
88
|
-
return `${JSON.stringify({ hookSpecificOutput: { hookEventName: 'Stop', additionalContext:
|
|
106
|
+
return `${JSON.stringify({ hookSpecificOutput: { hookEventName: 'Stop', additionalContext: text } })}\n`;
|
|
107
|
+
}
|
|
108
|
+
// The renders a reply at a card already shown closes on: the one line that
|
|
109
|
+
// points back at the pause or the stop.
|
|
110
|
+
const REMINDERS = new Set(['reminder', 'stop-reminder']);
|
|
111
|
+
/**
|
|
112
|
+
* Whether a reminder that closed the turn left the reply above a tool call:
|
|
113
|
+
* the final message is the reminder alone, with no words above its label,
|
|
114
|
+
* while the turn's earlier messages carried words. The printed line itself is
|
|
115
|
+
* not counted as words, since the transcript can already hold the final
|
|
116
|
+
* message when Stop fires. A transcript the check cannot read or scope is no
|
|
117
|
+
* evidence, and neither is a turn whose earlier messages said nothing.
|
|
118
|
+
*/
|
|
119
|
+
function buriedReply(record, last, payload) {
|
|
120
|
+
if (!REMINDERS.has(record.render) || leadOf(last).trim().length > 0)
|
|
121
|
+
return false;
|
|
122
|
+
const earlier = turnText(payload);
|
|
123
|
+
if (earlier === null)
|
|
124
|
+
return false;
|
|
125
|
+
return earlier.split(record.text.trim()).join('').trim().length > 0;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The one-line reason for a reply buried above a tool call: what happened, and
|
|
129
|
+
* the line to end on, with no command to run, since a retry that ran one would
|
|
130
|
+
* leave the reply above a tool call again.
|
|
131
|
+
*/
|
|
132
|
+
function buriedReason(record) {
|
|
133
|
+
return (`plumbbob: the turn ended on the ${DECISION_NAMES[record.render] ?? 'pause reminder'} it printed, ` +
|
|
134
|
+
'but the reply stood above a tool call, where a client can fold it out of sight. ' +
|
|
135
|
+
'Write the reply again, in your own words, with no tool call after it, ' +
|
|
136
|
+
`and end the turn on the same line, typed as it printed: ${record.text.trim()}`);
|
|
89
137
|
}
|
|
90
138
|
/**
|
|
91
139
|
* Whether the turn carried the recorded ending: true when it did, false when
|
|
@@ -197,7 +245,27 @@ function blockCount(part) {
|
|
|
197
245
|
* runs to the next. Text before the first label is not part of any.
|
|
198
246
|
*/
|
|
199
247
|
function partsOf(text) {
|
|
248
|
+
return readEnding(text).parts;
|
|
249
|
+
}
|
|
250
|
+
/** The text above an ending's first labeled part: the words a reply wrote above the line it closes on. */
|
|
251
|
+
function leadOf(text) {
|
|
252
|
+
return readEnding(text).lead.join('\n');
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* One walk over a stretch of text: the lines before its first labeled part,
|
|
256
|
+
* and the parts themselves, each run to the next, with a fence read as one
|
|
257
|
+
* stretch so a label inside it opens nothing.
|
|
258
|
+
*/
|
|
259
|
+
function readEnding(text) {
|
|
260
|
+
const lead = [];
|
|
200
261
|
const parts = [];
|
|
262
|
+
const keep = (line) => {
|
|
263
|
+
const last = parts.at(-1);
|
|
264
|
+
if (last === undefined)
|
|
265
|
+
lead.push(line);
|
|
266
|
+
else
|
|
267
|
+
last.lines.push(line);
|
|
268
|
+
};
|
|
201
269
|
let rail = null;
|
|
202
270
|
for (const line of text.split('\n')) {
|
|
203
271
|
const fence = /^\s*(`{3,})/.exec(line)?.[1];
|
|
@@ -206,7 +274,7 @@ function partsOf(text) {
|
|
|
206
274
|
}
|
|
207
275
|
else if (rail !== null && fence !== undefined && fence.length >= rail.length && line.trim() === fence) {
|
|
208
276
|
rail = null;
|
|
209
|
-
|
|
277
|
+
keep(line);
|
|
210
278
|
continue;
|
|
211
279
|
}
|
|
212
280
|
const label = rail === null ? labelOf(line) : undefined;
|
|
@@ -214,10 +282,10 @@ function partsOf(text) {
|
|
|
214
282
|
parts.push({ label, lines: [line] });
|
|
215
283
|
}
|
|
216
284
|
else {
|
|
217
|
-
|
|
285
|
+
keep(line);
|
|
218
286
|
}
|
|
219
287
|
}
|
|
220
|
-
return parts;
|
|
288
|
+
return { lead, parts };
|
|
221
289
|
}
|
|
222
290
|
/**
|
|
223
291
|
* The label a line opens a part on, or undefined when it opens none: a bold
|
package/dist/verbs/start.js
CHANGED
|
@@ -7,8 +7,8 @@ import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { detectGate } from "../lib/check.js";
|
|
9
9
|
import { readTemplate, stampTemplate } from "../lib/templates.js";
|
|
10
|
-
import { findRepoRoot, hasCommit, headSha, isDirty
|
|
11
|
-
import { sidecarDir, buildDir, listBuilds, slugify, checkpointsPath, intentPath, buildLogPath, beginSession, hasSession, excludeControl, excludeSidecar, setGrant, stampTick, } from "../lib/sidecar.js";
|
|
10
|
+
import { findRepoRoot, hasCommit, headSha, isDirty } from "../lib/git.js";
|
|
11
|
+
import { sidecarDir, buildDir, listBuilds, slugify, checkpointsPath, intentPath, buildLogPath, beginSession, hasSession, excludeControl, excludeSidecar, buildFolderIgnored, planCommitted, setGrant, stampTick, } from "../lib/sidecar.js";
|
|
12
12
|
import { settingsPath } from "../lib/settings.js";
|
|
13
13
|
import { advisory, ending, notice, printEnding, transition } from "../lib/notice.js";
|
|
14
14
|
/**
|
|
@@ -41,6 +41,12 @@ export async function start(cwd, args) {
|
|
|
41
41
|
// Every advisory the session accrues on its way open, collected rather than
|
|
42
42
|
// printed: they belong inside the one ending below, in the order found.
|
|
43
43
|
const advisories = [];
|
|
44
|
+
// `--local` on a session already open is the plan card's way out: keep this
|
|
45
|
+
// build untracked before its plan commit lands. It needs no title.
|
|
46
|
+
const open = local ? findRepoRoot(cwd) : null;
|
|
47
|
+
if (open !== null && hasSession(open)) {
|
|
48
|
+
return keepLocal(open);
|
|
49
|
+
}
|
|
44
50
|
if (title.length === 0) {
|
|
45
51
|
process.stderr.write(notice({ fact: 'start needs a title', remedy: 'plumbbob start "what you are building"' }));
|
|
46
52
|
return 1;
|
|
@@ -154,7 +160,7 @@ export async function start(cwd, args) {
|
|
|
154
160
|
// excludes the whole sidecar by design and says so, so skip the probe there.
|
|
155
161
|
// Guidance only, and the session is already open: a probe hiccup stays silent
|
|
156
162
|
// rather than half-crashing a scaffolded session.
|
|
157
|
-
if (!local &&
|
|
163
|
+
if (!local && buildFolderIgnored(root, slug)) {
|
|
158
164
|
advisories.push(advisory({
|
|
159
165
|
fact: 'this repo gitignores .plumbbob/',
|
|
160
166
|
detail: ['the build folder cannot ride the branch', 'plan and finish commits will be record-only'],
|
|
@@ -210,20 +216,40 @@ function datedSlug(title) {
|
|
|
210
216
|
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${base}`;
|
|
211
217
|
}
|
|
212
218
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
219
|
+
* Keep an open session untracked: `plumbbob start --local` typed while a build
|
|
220
|
+
* is open and its plan not yet committed, the way out the plan card offers
|
|
221
|
+
* (D117 (publish-on-the-card)).
|
|
215
222
|
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
223
|
+
* It writes the blanket `.plumbbob/` line to this clone's info/exclude, the
|
|
224
|
+
* line a `--local` start writes, so the plan and finish commits land
|
|
225
|
+
* record-only and nothing under `.plumbbob/` rides the branch. The folder
|
|
226
|
+
* keeps its place and its slug; only git's view of it changes. Refuses once the
|
|
227
|
+
* plan has landed, since the folder is in a commit by then and an exclude
|
|
228
|
+
* cannot take it back.
|
|
219
229
|
*/
|
|
220
|
-
function
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
230
|
+
function keepLocal(root) {
|
|
231
|
+
if (planCommitted(root)) {
|
|
232
|
+
process.stderr.write(notice({
|
|
233
|
+
fact: 'this session can no longer be kept local',
|
|
234
|
+
detail: ['its plan is already committed with the build folder'],
|
|
235
|
+
remedy: 'plumbbob finish to close it before starting another',
|
|
236
|
+
}));
|
|
237
|
+
return 1;
|
|
226
238
|
}
|
|
239
|
+
excludeSidecar(root);
|
|
240
|
+
printEnding(root, {
|
|
241
|
+
tier: 'driver',
|
|
242
|
+
render: 'start',
|
|
243
|
+
text: ending({
|
|
244
|
+
lead: transition({
|
|
245
|
+
label: 'Session',
|
|
246
|
+
fact: 'kept local',
|
|
247
|
+
detail: ['.plumbbob/ excluded in this clone', 'the plan and finish commits land record-only'],
|
|
248
|
+
remedy: 'plumbbob handoff --plan to show the plan card again',
|
|
249
|
+
}),
|
|
250
|
+
}),
|
|
251
|
+
});
|
|
252
|
+
return 0;
|
|
227
253
|
}
|
|
228
254
|
/**
|
|
229
255
|
* Read the value that follows a `--flag` in argv (for example `--slug my-build`), or
|
package/dist/verbs/status.js
CHANGED
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
// `--run`, it prints the run a `/plumbbob:run` invocation names above the
|
|
9
9
|
// dashboard, and names the waiting card again beneath the run's grant.
|
|
10
10
|
import { readFileSync } from 'node:fs';
|
|
11
|
-
import { join } from 'node:path';
|
|
11
|
+
import { join, relative } from 'node:path';
|
|
12
12
|
import { PLAN_COMMIT_MARKER, commitsSince, findRepoRoot } from "../lib/git.js";
|
|
13
13
|
import { SLOTS, readHarnessFile, resolveAgent, } from "../lib/agents.js";
|
|
14
14
|
import { buildFolder, buildLogPath, checkpointsPath, hasSession, inSpike, intentPath, listBuilds, resolveBuild, stepPath, } from "../lib/sidecar.js";
|
|
15
15
|
import { readGrant } from "../lib/latch.js";
|
|
16
|
-
import { formatOrientation, formatRunDashboard, lastLedgerSha, orient, parseRequestedStep, resolveRunArgs, } from "../lib/orient.js";
|
|
16
|
+
import { formatOrientation, formatRunDashboard, lastLedgerSha, orient, parseOpenQuestionRows, parseRequestedStep, resolveRunArgs, } from "../lib/orient.js";
|
|
17
17
|
import { normalizeStepList } from "../lib/steplist.js";
|
|
18
|
-
import {
|
|
18
|
+
import { blocks, notice } from "../lib/notice.js";
|
|
19
|
+
import { questionsPart, waitingCard } from "./handoff.js";
|
|
19
20
|
/**
|
|
20
21
|
* Read a file as utf8, or '' when it is missing or unreadable.
|
|
21
22
|
*/
|
|
@@ -67,7 +68,10 @@ function grantText(grant) {
|
|
|
67
68
|
* With `--run "<raw args>"` (how the `/plumbbob:run` skill passes its own),
|
|
68
69
|
* the dashboard prints beneath a header naming the run the text resolves to
|
|
69
70
|
* and the grant the turn hook minted, or carries the refusal in its pointer
|
|
70
|
-
* when the run names nothing it can walk.
|
|
71
|
+
* when the run names nothing it can walk. With `--questions` it prints the
|
|
72
|
+
* plan's open questions as the plan card tables them, and nothing else: the
|
|
73
|
+
* ones that wait on the human in the table, and the ones deferred to the work
|
|
74
|
+
* on a line beneath it.
|
|
71
75
|
*/
|
|
72
76
|
export function status(cwd, args = []) {
|
|
73
77
|
const root = findRepoRoot(cwd);
|
|
@@ -88,6 +92,14 @@ export function status(cwd, args = []) {
|
|
|
88
92
|
return 0;
|
|
89
93
|
}
|
|
90
94
|
}
|
|
95
|
+
if (rest.includes('--questions')) {
|
|
96
|
+
// The open questions alone, as the plan card tables them: the view a skill
|
|
97
|
+
// relays when the human asks what is still open (D118 (questions-table)),
|
|
98
|
+
// with the deferred ones on their own line (D120 (deferred-questions)).
|
|
99
|
+
const part = questionsPart(parseOpenQuestionRows(readOr(intentPath(root, slug))), relative(root, intentPath(root, slug)));
|
|
100
|
+
process.stdout.write(part === null ? notice({ fact: 'no open questions in this plan' }) : blocks([part]));
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
91
103
|
const inFlightRaw = readOr(stepPath(root, slug)).trim();
|
|
92
104
|
// Out-of-band commits (ones that landed outside the checkpoints ledger) are
|
|
93
105
|
// surfaced here, never blocked. Counting them needs git (orient is pure), so
|
package/dist/verbs/turn.js
CHANGED
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
// is in context) still knows it: mid-step, a tangent is parked, not chased;
|
|
20
20
|
// at a pause or a stop whose card has shown, the message is the human's reply,
|
|
21
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)).
|
|
22
|
+
// prints, never on the card again (D110 (reply-after-the-card)). Every tool
|
|
23
|
+
// call comes before that answer and none after it, since a client that folds
|
|
24
|
+
// text written ahead of a tool call would show the line and hide the answer
|
|
25
|
+
// (D121 (reply-after-the-tools)). Guidance
|
|
23
26
|
// only: it blocks nothing. It ALWAYS exits 0 and any failure degrades to no
|
|
24
27
|
// output, so neither the tick nor the emit can ever wedge prompting.
|
|
25
28
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
@@ -60,8 +63,9 @@ export function turn(cwd, _args) {
|
|
|
60
63
|
* Three beats, read off the control files. A step whose pause card has shown
|
|
61
64
|
* (STEP, and PAUSE naming that step's pause) is waiting for the human's call,
|
|
62
65
|
* 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
|
|
64
|
-
* again (D110 (reply-after-the-card)
|
|
66
|
+
* words after every tool call, and closed on the one line `handoff` prints
|
|
67
|
+
* rather than the card again (D110 (reply-after-the-card),
|
|
68
|
+
* D121 (reply-after-the-tools)). A step in flight with no card yet is
|
|
65
69
|
* mid-build, where a tangent is a park and not an edit. A stop card waiting
|
|
66
70
|
* for its reply is the pause's beat at a stop. At a pause or a stop, a
|
|
67
71
|
* `prompt` asking to see the card (`show the pause`, the phrase the card's
|
|
@@ -127,18 +131,23 @@ function parkNudge(step) {
|
|
|
127
131
|
return (`plumbbob: step ${step} is in flight — a new idea or tangent is a park, not an edit: ` +
|
|
128
132
|
`capture it with \`plumbbob park "<one line>"\` and relay the block it prints whole, ` +
|
|
129
133
|
`with nothing written around it, since that block is the whole turn. ` +
|
|
130
|
-
`The one exception is a message that also asks something: answer the question
|
|
131
|
-
`then
|
|
134
|
+
`The one exception is a message that also asks something: run \`plumbbob park\` first, then answer the question, ` +
|
|
135
|
+
`then end the turn on the park's block, typed as it printed, with no tool call after the answer. ` +
|
|
132
136
|
`The block's pointer back at the step closes the turn either way, so run no \`plumbbob handoff\` after it. ` +
|
|
133
137
|
`The step stays open and lands when you run \`plumbbob checkpoint ${step}\` after approval.`);
|
|
134
138
|
}
|
|
135
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* The nudge at a pause whose card has shown: the message is the reply, every
|
|
141
|
+
* tool call comes before it, and the card is not printed again.
|
|
142
|
+
*/
|
|
136
143
|
function pauseNudge(step) {
|
|
137
144
|
return (`plumbbob: step ${step}'s card has shown, and this message is the human's reply to it. ` +
|
|
138
145
|
`An approval lands the step: run \`plumbbob checkpoint ${step}\` and relay its ending. ` +
|
|
139
|
-
`Anything else is answered in your own words
|
|
140
|
-
`
|
|
141
|
-
`
|
|
146
|
+
`Anything else is answered in your own words, after every tool call and with none after it: ` +
|
|
147
|
+
`first read what you need and make the change it asks for, ` +
|
|
148
|
+
`write \`.plumbbob/detail.md\` again when a change moves what the card said, so the record matches what lands, ` +
|
|
149
|
+
`and run \`plumbbob handoff\` last among them, keeping the one line it prints. ` +
|
|
150
|
+
REPLY_LAST +
|
|
142
151
|
`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
152
|
`A tangent the human hands you is still a park: \`plumbbob park "<one line>"\`.`);
|
|
144
153
|
}
|
|
@@ -146,11 +155,19 @@ function pauseNudge(step) {
|
|
|
146
155
|
function stopNudge(range, count) {
|
|
147
156
|
return (`plumbbob: range ${range} of ${count}'s stop card has shown, and this message is the human's reply to it. ` +
|
|
148
157
|
`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
|
|
150
|
-
`(a lean is its line under the detail file's \`## Harvest\`)
|
|
151
|
-
`
|
|
158
|
+
`Anything else is answered in your own words, after every tool call and with none after it: ` +
|
|
159
|
+
`first read what you need and make the change it asks for (a lean is its line under the detail file's \`## Harvest\`), ` +
|
|
160
|
+
`and run \`plumbbob handoff --stop\` last among them, keeping the one line it prints. ` +
|
|
161
|
+
REPLY_LAST +
|
|
152
162
|
`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.`);
|
|
153
163
|
}
|
|
164
|
+
// How a reply at a card already shown ends, the same at a pause and a stop: the
|
|
165
|
+
// answer after the last tool call, then the line, because a client that folds
|
|
166
|
+
// text written before a tool call shows the line and hides the answer
|
|
167
|
+
// (D121 (reply-after-the-tools)).
|
|
168
|
+
const REPLY_LAST = `Then write your answer and end the turn on that line, typed as it printed, with no tool call after your answer: ` +
|
|
169
|
+
`a client that folds text written before a tool call would hide the answer and show the line alone. ` +
|
|
170
|
+
`Print no card for it. `;
|
|
154
171
|
/**
|
|
155
172
|
* The in-flight step number, or null when no STEP marker is present.
|
|
156
173
|
*
|
package/package.json
CHANGED
package/skills/build/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: build
|
|
|
3
3
|
description: "The default engine: read the next planned step from intent, implement it (its done-when, seam, Decisions, Constraints), then verify it through to the approval pause. Swappable: build by hand/vibed/another harness and run /plumbbob:verify instead. `--auto` self-approves and chains to done; a step range like `1-3`, or a list like `20,5-9`, self-approves exactly the steps it names, in build order, then stops."
|
|
4
4
|
argument-hint: "[step-number | step-range | step-list] [--auto]"
|
|
5
5
|
disable-model-invocation: true
|
|
6
|
-
allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob build:*), Bash(plumbbob handoff:*), Bash(plumbbob check:*), Bash(plumbbob checkpoint:*), Bash(plumbbob park:*), Bash(plumbbob agent:*), Bash(plumbbob spike:*), Bash(git diff:*)
|
|
6
|
+
allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob build:*), Bash(plumbbob handoff:*), Bash(plumbbob check:*), Bash(plumbbob checkpoint:*), Bash(plumbbob park:*), Bash(plumbbob defer:*), Bash(plumbbob agent:*), Bash(plumbbob spike:*), Bash(git diff:*)
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# PlumbBob: build a step (the default engine)
|
|
@@ -66,7 +66,13 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
|
|
|
66
66
|
capture it by running `plumbbob park "<one tidy line>"`: **saying "let's defer
|
|
67
67
|
that" writes nothing; only the park line in the build log is a capture.** When the
|
|
68
68
|
*human* hands you the tangent mid-build, their message **is** the approval to park
|
|
69
|
-
it; capture it right away, say what you parked, and stay on the step.
|
|
69
|
+
it; capture it right away, say what you parked, and stay on the step. An open question
|
|
70
|
+
the human sets aside to wait on other work is deferred, not parked: run
|
|
71
|
+
`plumbbob defer Q3 "<what settles it>"`, because the card counts questions by the
|
|
72
|
+
marker on each opener and never by the park list
|
|
73
|
+
([D123 (defer-records)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d123)).
|
|
74
|
+
When `plumbbob park` warns that a question it names still counts as one to answer,
|
|
75
|
+
defer it if the human said the work will settle it, and leave it to them otherwise. If you
|
|
70
76
|
genuinely cannot finish without touching more than the seam, that is scope drift:
|
|
71
77
|
surface it to the human rather than sprawling.
|
|
72
78
|
- **Bound agents can shape the build.** If `plumbbob status` shows a
|
|
@@ -186,7 +192,9 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
|
|
|
186
192
|
of its own, one command to a line, which handoff keeps line for line. A slash command
|
|
187
193
|
or a path stays in backticks in the sentence. `checkpoint` records this file beneath the step's dated
|
|
188
194
|
line in the build-log's `## Log` and then truncates it, so the detail lands in the
|
|
189
|
-
tracked ledger rather than piling up on disk.
|
|
195
|
+
tracked ledger rather than piling up on disk. Write a relative link from `.plumbbob/`, where
|
|
196
|
+
the file lives (`builds/<slug>/intent.md#d5`); the record re-aims it at the build folder.
|
|
197
|
+
A decision cited by its slug in plain text (`D5 (no-link-no-reconnect)`) needs no link at all.
|
|
190
198
|
|
|
191
199
|
**A bare range's pause is its stop.** When the injected dashboard's ranges block lists
|
|
192
200
|
this step as a bare range (`/plumbbob:build 5`, its row marked `▸`), the plan reserved a
|
|
@@ -200,8 +208,8 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
|
|
|
200
208
|
Readout fence (its measured `check`, `seam`, `diff`, and `spent` rows folded with your
|
|
201
209
|
three, the changed files listed beneath `diff`), a `diff` fence when the change is 20
|
|
202
210
|
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
|
|
204
|
-
rows when they apply), your recommendation, and the `more details` link last. Run it
|
|
211
|
+
rows, the Now row naming the step in flight, Next Up with the next range on its second
|
|
212
|
+
line, and the Order and Parked rows when they apply), your recommendation, and the `more details` link last. Run it
|
|
205
213
|
once: a second run at the same pause prints the one-line reminder instead of the card,
|
|
206
214
|
so if that line comes back where the card belongs (you ran handoff earlier in this
|
|
207
215
|
turn), run `plumbbob handoff --full` and paste that. The gate verdict's one home is that `check`
|
|
@@ -213,7 +221,7 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
|
|
|
213
221
|
one, the write-versus-relay line of
|
|
214
222
|
[D78 (em-dash-ban)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d78).
|
|
215
223
|
|
|
216
|
-
*Then answer the reply in your own words.* The card shows once
|
|
224
|
+
*Then answer the reply in your own words, after every tool call.* The card shows once
|
|
217
225
|
([D110 (reply-after-the-card)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d110)):
|
|
218
226
|
every later turn at this pause is your reply, closed by one line. A message that
|
|
219
227
|
**asks** ("expand 2", "what does that mean?", "why did the seam row flag that?") is an
|
|
@@ -222,11 +230,16 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
|
|
|
222
230
|
make the change, run the check, and say what moved and what the check found; when the
|
|
223
231
|
change moves what the card said (a judgment row, the Summary, the recommendation), write
|
|
224
232
|
the detail file again so the record matches what lands. Nothing lands until the human
|
|
225
|
-
says `looks good`. Either way,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
233
|
+
says `looks good`. Either way, every tool call comes before your reply and none after
|
|
234
|
+
it ([D121 (reply-after-the-tools)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d121)):
|
|
235
|
+
read, edit, run the check, write the detail file, and run `plumbbob handoff` last among
|
|
236
|
+
them. At a pause whose card has shown it prints one line,
|
|
237
|
+
`**Next Up**: Back to **Step N** on <Model> - <title>`, whatever changed since. Keep
|
|
238
|
+
that line, then write your reply, and end the turn on the line, whole and verbatim. The
|
|
239
|
+
turn is your tool calls, then your reply, then that line, and nothing after it: a
|
|
240
|
+
client that folds text written before a tool call (Zed, through the agent bridge) shows
|
|
241
|
+
the line and hides a reply written above the call. The CLI picks the form, never you:
|
|
242
|
+
paste whichever one it prints, and never retype the card.
|
|
230
243
|
|
|
231
244
|
The card comes back one way: the human asks to see it (`show the pause`, or any wording
|
|
232
245
|
of it). Then run `plumbbob handoff --full` and paste its output whole, with nothing
|
|
@@ -238,12 +251,13 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
|
|
|
238
251
|
**Then hand off with the next model.** `plumbbob checkpoint` prints the whole boundary
|
|
239
252
|
ending itself: `**Checkpoint**: Step N complete (<sha>)`, any advisory (a staged path
|
|
240
253
|
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
|
|
254
|
+
the Stop row when this landing completes a range (`| Stop | **Range 3** landed - <its
|
|
242
255
|
stop> |`, the reason the build stops here), Next Up, and the rows beneath it. Relay that
|
|
243
256
|
block whole, the table with the rest (the relay check holds a boundary that dropped a
|
|
244
257
|
row), run no second command, and write nothing around it. A later turn at this boundary
|
|
245
|
-
is your reply
|
|
246
|
-
has shown
|
|
258
|
+
is your reply, in the same order as at a pause: a `plumbbob handoff` there prints Next
|
|
259
|
+
Up's one line, since the boundary has shown, so run it last among your tool calls, then
|
|
260
|
+
write your reply and end on that line, with no tool call after it. It is the
|
|
247
261
|
orientation tier (no Your Call block, no recommendation), and its Next Up points at the
|
|
248
262
|
next undone step carrying that step's `- model:` recommendation (the plan's
|
|
249
263
|
smallest-model-that-fits call) so the human knows which `/model` to select before
|
|
@@ -371,9 +385,13 @@ out, or the next step in build order is one the range does not name),
|
|
|
371
385
|
- **A step inside the range won't build** (its seam won't parse): `plumbbob build <n>` fails
|
|
372
386
|
exactly as it always does; surface it and stop, the same as a red check. Do not skip past
|
|
373
387
|
the gap.
|
|
374
|
-
- **The range starts above the next undone step
|
|
375
|
-
|
|
376
|
-
|
|
388
|
+
- **The range starts above the next undone step** (the order reads `6, 4, 5` under
|
|
389
|
+
`/plumbbob:build 4-5`): the first step enters as a single-number jump like `/plumbbob:build 4`
|
|
390
|
+
would, with the skipped step noted. Once it lands, the next undone step in build order is
|
|
391
|
+
the one you jumped, which the range does not name, so the halt above fires there, and
|
|
392
|
+
`plumbbob build` names step 4 as the last undone step in the range, its detail naming step 6
|
|
393
|
+
as the step the walk halts before. The range that walks the whole stretch names the jumped
|
|
394
|
+
step too (`/plumbbob:build 6,4-5`).
|
|
377
395
|
- **The build order puts a step the range does not name inside it** (the order reads
|
|
378
396
|
`1, 4, 2, 3` under `/plumbbob:build 1-3`): the halt above fires before step 4, and
|
|
379
397
|
`plumbbob build` names step 1 as the last undone step in the range. The range that walks
|
|
@@ -387,8 +405,8 @@ out, or the next step in build order is one the range does not name),
|
|
|
387
405
|
`Parked` row, `2 of 7 open`), the move is `/plumbbob:harvest`, then the run the card names.
|
|
388
406
|
- **After an early stop, settle the step, then rerun what is left.** A halt on trouble
|
|
389
407
|
inside a range ends at that step's pause, and the grant ends with the turn. Once the
|
|
390
|
-
human approves and the step lands, the
|
|
391
|
-
what is left of the range (`| Range
|
|
408
|
+
human approves and the step lands, the range line beneath Next Up names the run that lands
|
|
409
|
+
what is left of the range (`| | **Range 3** on Opus: /plumbbob:run (steps 8-9) |`), and that is
|
|
392
410
|
what the human types to carry on; `/plumbbob:build 8-9` lands the same steps by hand.
|
|
393
411
|
|
|
394
412
|
## The hard contracts
|
package/skills/harvest/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: harvest
|
|
3
3
|
description: "Triage the park list at a step boundary: propose one class (blocker/tangent/pivot) for every open parked item at once, and once the human settles them in one reply, fold a confirmed blocker into intent and record the rest with plumbbob harvest. A run's stop card carries its own harvest."
|
|
4
4
|
disable-model-invocation: true
|
|
5
|
-
allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob order:*), Bash(plumbbob handoff:*), Bash(plumbbob harvest:*)
|
|
5
|
+
allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob order:*), Bash(plumbbob defer:*), Bash(plumbbob handoff:*), Bash(plumbbob harvest:*)
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# PlumbBob: harvest the park list
|
|
@@ -107,7 +107,16 @@ the call on each one theirs. The detail file you write first is the proposal; re
|
|
|
107
107
|
The verb's ending names each range it re-cut, and an advisory names any it left for
|
|
108
108
|
you; re-cut that one in the same confirmed edit, and `plumbbob status` afterward carries
|
|
109
109
|
a `⚠` row for any range still broken.
|
|
110
|
-
-
|
|
111
|
-
|
|
110
|
+
- A confirmed **tangent** that sets an open question aside (its park names `Q3`, and the
|
|
111
|
+
question waits on work still to come) defers that question as well: run
|
|
112
|
+
`plumbbob defer Q3 "<what settles it>"`. Say so in the proposal's reason
|
|
113
|
+
(`tangent: Q3 waits on step 9's numbers`), so the human's confirmation covers the
|
|
114
|
+
deferral. The park list never moves the Questions count, so a tangent alone leaves the
|
|
115
|
+
card reading a call still owed, and the record's ending warns of any question a tangent
|
|
116
|
+
names that still counts as one to answer
|
|
117
|
+
([D123 (defer-records)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d123)).
|
|
118
|
+
- Fold a confirmed blocker into `intent.md`, and defer each question a confirmed tangent
|
|
119
|
+
sets aside, before you run `plumbbob harvest`, so the record and the plan agree when it
|
|
120
|
+
lands.
|
|
112
121
|
- Never reclassify or resolve an item the human hasn't confirmed, and default every
|
|
113
122
|
uncertain item to **tangent**, never to blocker.
|
package/skills/park/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: "Compose one tidy tagged park line, get the human's OK in-turn, the
|
|
|
4
4
|
argument-hint: "[idea]"
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
model: haiku
|
|
7
|
-
allowed-tools: Bash(plumbbob status:*), Bash(plumbbob park:*)
|
|
7
|
+
allowed-tools: Bash(plumbbob status:*), Bash(plumbbob park:*), Bash(plumbbob defer:*)
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# PlumbBob: park an idea (capture, don't chase)
|
|
@@ -47,19 +47,36 @@ Then:
|
|
|
47
47
|
the capture plus that one pointer are the whole turn, and both are the CLI's to render,
|
|
48
48
|
never yours to compose from the dashboard. The capture is a one-beat interruption; the
|
|
49
49
|
step keeps its focus.
|
|
50
|
-
4. **A question beside the tangent is answered
|
|
51
|
-
asks something,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
4. **A question beside the tangent is answered after the capture.** When the human's
|
|
51
|
+
message also asks something, run `plumbbob park` first, then answer the question, then
|
|
52
|
+
end the turn on the park's block, typed as it printed, with no tool call after the
|
|
53
|
+
answer ([D121 (reply-after-the-tools)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d121)):
|
|
54
|
+
a client that folds text written before a tool call would show the block and hide the
|
|
55
|
+
answer. A message that is only a tangent ends on the block alone. Either way the
|
|
56
|
+
block's pointer back at the step closes the turn, so run no `plumbbob handoff` after
|
|
57
|
+
it. The answer is what the human asked for; dropping it to keep the block alone,
|
|
58
|
+
writing it above the capture, or stacking a second ending under the block are the ways
|
|
56
59
|
this goes wrong.
|
|
57
60
|
|
|
61
|
+
## A question set aside is deferred, not parked
|
|
62
|
+
|
|
63
|
+
When the idea is an open question from the plan that the human wants picked up later,
|
|
64
|
+
once other work lands (`leave Q3 until the latency numbers are in`), run
|
|
65
|
+
`plumbbob defer Q3 "<what settles it>"` in place of the park. The question's own opener
|
|
66
|
+
is where it waits, and the card counts it apart from the questions to answer; a park line
|
|
67
|
+
beside it would leave the card counting it as a call still owed
|
|
68
|
+
([D123 (defer-records)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d123)).
|
|
69
|
+
`plumbbob defer` prints the same kind of block, `**Deferred**: Q3 (<slug>) until <what
|
|
70
|
+
settles it>` and the pointer, and you relay it the same way. When `plumbbob park` warns that
|
|
71
|
+
a question it names still counts as one to answer, relay the block as printed; whether to
|
|
72
|
+
defer it is the human's call.
|
|
73
|
+
|
|
58
74
|
## The one hard contract
|
|
59
75
|
|
|
60
76
|
The capture itself is the **dumb CLI**, never an edit. This skill carries **no Edit and
|
|
61
|
-
no Write tool** on purpose: you may not append to `build-log.md` yourself
|
|
62
|
-
approval, then shell `plumbbob park
|
|
77
|
+
no Write tool** on purpose: you may not append to `build-log.md` yourself, nor mark a
|
|
78
|
+
question in `intent.md`. Compose, get approval, then shell `plumbbob park`, or
|
|
79
|
+
`plumbbob defer` for a question set aside; those are the only write paths. If approval
|
|
63
80
|
never comes, capture nothing. (When the human's **own message** states the idea, a
|
|
64
81
|
tangent they raise mid-build, that message **is** the approval: capture it directly
|
|
65
82
|
with `plumbbob park`, don't ask again. Deferring in words without the CLI line captures
|