plumbbob 0.11.3 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +4 -4
- package/dist/cli-core.js +11 -0
- package/dist/lib/latch.js +1 -1
- package/dist/lib/orient.js +233 -18
- package/dist/verbs/build.js +51 -12
- package/dist/verbs/checkpoint.js +17 -10
- package/dist/verbs/handoff.js +64 -13
- package/dist/verbs/order.js +102 -0
- package/dist/verbs/recover.js +31 -1
- package/dist/verbs/turn.js +7 -6
- package/node_modules/checkride/.claude-plugin/plugin.json +1 -1
- package/node_modules/checkride/CHANGELOG.md +117 -0
- package/node_modules/checkride/dist/adapters.d.ts.map +1 -1
- package/node_modules/checkride/dist/adapters.js +14 -10
- package/node_modules/checkride/dist/adapters.js.map +1 -1
- package/node_modules/checkride/dist/baseline/fallow.d.ts +4 -3
- package/node_modules/checkride/dist/baseline/fallow.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/fallow.js +15 -8
- package/node_modules/checkride/dist/baseline/fallow.js.map +1 -1
- package/node_modules/checkride/dist/gate.d.ts.map +1 -1
- package/node_modules/checkride/dist/gate.js +62 -44
- package/node_modules/checkride/dist/gate.js.map +1 -1
- package/node_modules/checkride/dist/init.js +3 -3
- package/node_modules/checkride/dist/init.js.map +1 -1
- package/node_modules/checkride/dist/orchestrator.d.ts.map +1 -1
- package/node_modules/checkride/dist/orchestrator.js +53 -8
- package/node_modules/checkride/dist/orchestrator.js.map +1 -1
- package/node_modules/checkride/dist/pack.js +3 -3
- package/node_modules/checkride/dist/security.d.ts +4 -2
- package/node_modules/checkride/dist/security.d.ts.map +1 -1
- package/node_modules/checkride/dist/security.js +42 -8
- package/node_modules/checkride/dist/security.js.map +1 -1
- package/node_modules/checkride/dist/triage/render.js +1 -1
- package/node_modules/checkride/dist/triage/render.js.map +1 -1
- package/node_modules/checkride/package.json +10 -10
- package/node_modules/checkride/skills/check/SKILL.md +4 -3
- package/node_modules/checkride/templates/shared/cspell.json +3 -24
- package/package.json +3 -3
- package/skills/build/SKILL.md +48 -26
- package/skills/finish/SKILL.md +5 -2
- package/skills/harvest/SKILL.md +4 -1
- package/skills/order/SKILL.md +50 -0
- package/skills/plan/SKILL.md +5 -2
- package/skills/refine/SKILL.md +6 -2
- package/skills/step/SKILL.md +8 -3
- package/skills/verify/SKILL.md +14 -8
- package/templates/intent.md +9 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plumbbob",
|
|
3
3
|
"displayName": "PlumbBob",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.12.1",
|
|
5
5
|
"description": "Attention-first build process — driver skills + a CLI that keep you the decider: guidance on the work, a latch on the record.",
|
|
6
6
|
"author": { "name": "Rob McLarty", "email": "hello@robmclarty.com", "url": "https://robmclarty.com" },
|
|
7
7
|
"homepage": "https://github.com/robmclarty/plumbbob#readme",
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<a href="https://robmclarty.github.io/plumbbob/api.html">API reference</a>
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
|
-
PlumbBob is a Claude Code plugin (
|
|
13
|
+
PlumbBob is a Claude Code plugin (fifteen skills and a small CLI) that runs
|
|
14
14
|
LLM-assisted coding as a loop you control. You and the model settle the plan in a file
|
|
15
15
|
*before* any code; then the model builds one small step at a time and **stops after
|
|
16
16
|
each one**, waiting for you to read the diff and approve it before anything is
|
|
@@ -221,7 +221,7 @@ nothing else:
|
|
|
221
221
|
| Skill <img alt="" width="110" height="1"> | Does |
|
|
222
222
|
| ------------------------------------ | ------ |
|
|
223
223
|
| `/plumbbob:plan` | plan the whole goal: open the session and author intent's Frame, Decisions, Constraints, **and all Steps** |
|
|
224
|
-
| `/plumbbob:build` | implement the next planned step, then verify it to the pause: `--auto` self-approves and chains to done; a range like `1-3` self-approves through step 3, then
|
|
224
|
+
| `/plumbbob:build` | implement the next planned step, then verify it to the pause: `--auto` self-approves and chains to done; a range like `1-3` self-approves through step 3, then stops |
|
|
225
225
|
| `/plumbbob:finish` | finish up: write the report, make the final commit, clear for a fresh goal |
|
|
226
226
|
|
|
227
227
|
**Plan-shaping moves**: optional, for when the plan needs work mid-flight:
|
|
@@ -334,7 +334,7 @@ that matters.
|
|
|
334
334
|
> under [`docs/evals/`](docs/evals/): the route the latch owns (*no checkpoint over a red
|
|
335
335
|
> check under pressure*) went from **2/5 prose-only to 5/5 latched** on the first sweep
|
|
336
336
|
> ([2026-07-11](docs/evals/2026-07-11.md)), every sweep since has held it and closed the
|
|
337
|
-
> two gaps that one surfaced, and the latest ([2026-09-
|
|
337
|
+
> two gaps that one surfaced, and the latest ([2026-09-17](docs/evals/2026-09-17.md))
|
|
338
338
|
> scores the shape of the turn itself.
|
|
339
339
|
|
|
340
340
|
## Documentation
|
|
@@ -346,7 +346,7 @@ Each doc answers one question (in rough reading order for a new user):
|
|
|
346
346
|
- *Show me the artifacts it leaves behind.* → [`examples/`](examples/): that same session's finished build folder, file by file.
|
|
347
347
|
- *Should I / can I / what about…?* → [`docs/faq.md`](docs/faq.md): the adoption questions, answered straight.
|
|
348
348
|
- *What is each method for?* → [`docs/techniques.md`](docs/techniques.md): steps, seams, the pause, park/harvest, spikes.
|
|
349
|
-
- *What does each skill do?* → [`docs/skills-reference.md`](docs/skills-reference.md): all
|
|
349
|
+
- *What does each skill do?* → [`docs/skills-reference.md`](docs/skills-reference.md): all fifteen skills: inputs, effects, when to reach for each.
|
|
350
350
|
- *What does a turn look like, and who renders which part?* → [`docs/presentation.md`](docs/presentation.md): the anatomy of the pause, the boundary, and the driver turns.
|
|
351
351
|
- *How do I plug in my own agent?* → [`docs/agents.md`](docs/agents.md): the subprocess envelope, the manifest, `harness.json`, and working examples (including a local-model reviewer via Ollama).
|
|
352
352
|
- *How do I get a local model reviewing my steps?* → [`docs/local-model-review.md`](docs/local-model-review.md): the ollama-reviewer example walked end to end, install to every-pause review.
|
package/dist/cli-core.js
CHANGED
|
@@ -14,6 +14,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
14
14
|
import { start } from "./verbs/start.js";
|
|
15
15
|
import { status } from "./verbs/status.js";
|
|
16
16
|
import { park } from "./verbs/park.js";
|
|
17
|
+
import { order } from "./verbs/order.js";
|
|
17
18
|
import { build } from "./verbs/build.js";
|
|
18
19
|
import { handoff } from "./verbs/handoff.js";
|
|
19
20
|
import { check } from "./verbs/check.js";
|
|
@@ -140,6 +141,14 @@ const VERBS = [
|
|
|
140
141
|
tolerateUnknownFlags: true,
|
|
141
142
|
notes: 'Refuses (exit 1) with no session, empty text, or no `## Park list` section.',
|
|
142
143
|
},
|
|
144
|
+
{
|
|
145
|
+
name: 'order',
|
|
146
|
+
description: 'set the build order: the undone steps in the sequence to build them',
|
|
147
|
+
synopsis: ['order <n> [<n>…]', 'order --reset'],
|
|
148
|
+
args: [{ name: '<n>…', gloss: 'undone step numbers in the sequence to build them; the rest follow in document order' }],
|
|
149
|
+
flags: [{ name: '--reset', gloss: 'drop the line, so the plan reads in document order again' }],
|
|
150
|
+
notes: 'Writes the one numeric line under `## Build order` in intent.md, which status, build, checkpoint, and handoff all read. Refuses (exit 1) with no session, no numbers, a number the plan lacks, a repeat, or a step already checkpointed.',
|
|
151
|
+
},
|
|
143
152
|
{
|
|
144
153
|
name: 'spike',
|
|
145
154
|
description: 'worktree experiment + report for a genuine fork',
|
|
@@ -340,6 +349,8 @@ async function dispatch(verb, cwd, rest) {
|
|
|
340
349
|
return status(cwd, rest);
|
|
341
350
|
case 'park':
|
|
342
351
|
return park(cwd, rest);
|
|
352
|
+
case 'order':
|
|
353
|
+
return order(cwd, rest);
|
|
343
354
|
case 'build':
|
|
344
355
|
return build(cwd, rest);
|
|
345
356
|
case 'handoff':
|
package/dist/lib/latch.js
CHANGED
|
@@ -112,7 +112,7 @@ export function checkLatch(root, step) {
|
|
|
112
112
|
/**
|
|
113
113
|
* The current GRANT, or null when the file is absent or unparseable.
|
|
114
114
|
*/
|
|
115
|
-
function readGrant(root) {
|
|
115
|
+
export function readGrant(root) {
|
|
116
116
|
try {
|
|
117
117
|
return parseGrant(readFileSync(grantPath(root), 'utf8'));
|
|
118
118
|
}
|
package/dist/lib/orient.js
CHANGED
|
@@ -15,11 +15,15 @@ function sectionLines(content, heading) {
|
|
|
15
15
|
if (start === -1) {
|
|
16
16
|
return { offset: 0, lines: [] };
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
return { offset: start + 1, lines: lines.slice(start + 1, sectionEnd(lines, start)) };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The index of the line that ends the section opened at `heading`: the next
|
|
22
|
+
* `## ` heading, or one past the last line when none follows.
|
|
23
|
+
*/
|
|
24
|
+
function sectionEnd(lines, heading) {
|
|
25
|
+
const end = lines.findIndex((l, i) => i > heading && l.startsWith('## '));
|
|
26
|
+
return end === -1 ? lines.length : end;
|
|
23
27
|
}
|
|
24
28
|
/**
|
|
25
29
|
* The build title: the first `# ` heading in intent.md, or null when absent.
|
|
@@ -70,6 +74,166 @@ export function parseSteps(intent) {
|
|
|
70
74
|
return { n: s.n, done: s.done, title: s.title, planned: /done when/i.test(block), doneWhen, model, line: offset + s.idx + 1 };
|
|
71
75
|
});
|
|
72
76
|
}
|
|
77
|
+
// A build-order line: step numbers and commas, nothing else. A trailing comma
|
|
78
|
+
// is tolerated, since a hand edit leaves one behind.
|
|
79
|
+
const BUILD_ORDER_LINE = /^\d+(?:\s*,\s*\d+)*\s*,?$/;
|
|
80
|
+
/**
|
|
81
|
+
* The build order declared under `## Build order`: the step numbers on the
|
|
82
|
+
* first line that is nothing but numbers and commas, in the sequence written.
|
|
83
|
+
*
|
|
84
|
+
* The line is the plan's own statement of what to build next once the
|
|
85
|
+
* numbering stops carrying it (a refined plan appends a step 7 that has to
|
|
86
|
+
* land before step 5), and `plumbbob order` is the verb that writes it. The
|
|
87
|
+
* template's guidance, a comment, and an absent section all read as `[]`,
|
|
88
|
+
* which every picker takes to mean document order. Numbers come back
|
|
89
|
+
* verbatim, repeats and unknowns included: `orderSteps` does the cleaning, and
|
|
90
|
+
* `recover` wants the raw read to say which number the plan lacks.
|
|
91
|
+
*/
|
|
92
|
+
export function parseBuildOrder(intent) {
|
|
93
|
+
const line = sectionLines(intent, '## Build order')
|
|
94
|
+
.lines.map((l) => l.trim())
|
|
95
|
+
.find((l) => BUILD_ORDER_LINE.test(l));
|
|
96
|
+
if (line === undefined) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
return line
|
|
100
|
+
.split(',')
|
|
101
|
+
.map((n) => n.trim())
|
|
102
|
+
.filter((n) => n.length > 0)
|
|
103
|
+
.map(Number);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The steps in build order: the numbers `order` lists first, in that sequence,
|
|
107
|
+
* then every step it leaves out, in document order.
|
|
108
|
+
*
|
|
109
|
+
* A listed number takes the first step in document order that carries it, and
|
|
110
|
+
* a repeat of the number is ignored, so a doubled entry cannot double a step; a
|
|
111
|
+
* number no step carries is dropped. An empty order is the identity, which is
|
|
112
|
+
* what a plan with no `## Build order` section reads as. The steps themselves
|
|
113
|
+
* never change, only the sequence, so `n`, `line`, and `done` stay what
|
|
114
|
+
* `parseSteps` read.
|
|
115
|
+
*/
|
|
116
|
+
export function orderSteps(steps, order) {
|
|
117
|
+
const taken = new Set();
|
|
118
|
+
const ordered = [];
|
|
119
|
+
for (const n of order) {
|
|
120
|
+
const step = steps.find((s) => s.n === n);
|
|
121
|
+
if (step !== undefined && !taken.has(step)) {
|
|
122
|
+
taken.add(step);
|
|
123
|
+
ordered.push(step);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return [...ordered, ...steps.filter((s) => !taken.has(s))];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The plan's steps in build order: `parseSteps` merged with `parseBuildOrder`.
|
|
130
|
+
*
|
|
131
|
+
* The one call every next-step picker makes, so the dashboard's `← next`, a
|
|
132
|
+
* bare `plumbbob build`, `checkpoint`'s fallback, and the card's Next Up
|
|
133
|
+
* cannot disagree about which step comes next.
|
|
134
|
+
*/
|
|
135
|
+
export function parseOrderedSteps(intent) {
|
|
136
|
+
return orderSteps(parseSteps(intent), parseBuildOrder(intent));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The undone steps that come before step `n` in `ordered`: what an explicit
|
|
140
|
+
* `build n` skips. Empty when `n` is not in the list.
|
|
141
|
+
*/
|
|
142
|
+
export function skippedBefore(ordered, n) {
|
|
143
|
+
const at = ordered.findIndex((s) => s.n === n);
|
|
144
|
+
return at === -1 ? [] : ordered.slice(0, at).filter((s) => !s.done);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The reminder the card and the dashboard render: the undone step numbers
|
|
148
|
+
* (minus `exclude`, the step a pause is about) in build order, when that
|
|
149
|
+
* sequence departs from document order; `[]` when the numbering already
|
|
150
|
+
* carries it, so a plan nobody re-sequenced renders nothing new.
|
|
151
|
+
*/
|
|
152
|
+
export function remainingOrder(steps, order, exclude) {
|
|
153
|
+
const left = (list) => list.filter((s) => !s.done && s.n !== exclude).map((s) => s.n);
|
|
154
|
+
const byOrder = left(orderSteps(steps, order));
|
|
155
|
+
const byDocument = left(steps);
|
|
156
|
+
return byOrder.some((n, i) => n !== byDocument[i]) ? byOrder : [];
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The build order as a phrase: the next `cap` step numbers in sequence, the
|
|
160
|
+
* rest folded to a count (`and 2 more`), the way a long detail degrades on
|
|
161
|
+
* every one-liner; '' for an empty order. With no cap, the whole sequence,
|
|
162
|
+
* which is the dashboard's row.
|
|
163
|
+
*/
|
|
164
|
+
export function buildOrderPhrase(order, cap) {
|
|
165
|
+
if (order.length === 0) {
|
|
166
|
+
return '';
|
|
167
|
+
}
|
|
168
|
+
const shown = cap === undefined ? order : order.slice(0, cap);
|
|
169
|
+
const rest = order.length - shown.length;
|
|
170
|
+
return `build order ${shown.join(', ')}${rest > 0 ? ` (and ${rest} more)` : ''}`;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Write `order` as the one numeric line under `## Build order`: in place of
|
|
174
|
+
* the line already there, else last in the section, else in a new section
|
|
175
|
+
* opened right after `## Steps`, so the sequence sits beside the steps it
|
|
176
|
+
* sequences. Mechanical bookkeeping for `plumbbob order`, the way
|
|
177
|
+
* `markStepDone` is for `checkpoint`.
|
|
178
|
+
*/
|
|
179
|
+
export function setBuildOrder(intent, order) {
|
|
180
|
+
const lines = intent.split('\n');
|
|
181
|
+
const text = order.join(', ');
|
|
182
|
+
const heading = lines.findIndex((l) => l.trim() === '## Build order');
|
|
183
|
+
if (heading !== -1) {
|
|
184
|
+
const end = sectionEnd(lines, heading);
|
|
185
|
+
const at = lines.findIndex((l, i) => i > heading && i < end && BUILD_ORDER_LINE.test(l.trim()));
|
|
186
|
+
if (at !== -1) {
|
|
187
|
+
lines[at] = text;
|
|
188
|
+
return lines.join('\n');
|
|
189
|
+
}
|
|
190
|
+
insertAfterContent(lines, heading, end, [text]);
|
|
191
|
+
return lines.join('\n');
|
|
192
|
+
}
|
|
193
|
+
const steps = lines.findIndex((l) => l.trim() === '## Steps');
|
|
194
|
+
const end = steps === -1 ? lines.length : sectionEnd(lines, steps);
|
|
195
|
+
insertAfterContent(lines, steps, end, ['## Build order', '', text]);
|
|
196
|
+
return lines.join('\n');
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Splice `block` in after the last non-blank line before `end` (and after
|
|
200
|
+
* `from`), one blank line on each side, without doubling a blank that is
|
|
201
|
+
* already there.
|
|
202
|
+
*/
|
|
203
|
+
function insertAfterContent(lines, from, end, block) {
|
|
204
|
+
let last = end - 1;
|
|
205
|
+
while (last > from && (lines[last] ?? '').trim() === '') {
|
|
206
|
+
last -= 1;
|
|
207
|
+
}
|
|
208
|
+
const at = last + 1;
|
|
209
|
+
const followed = at < lines.length && (lines[at] ?? '').trim() === '';
|
|
210
|
+
lines.splice(at, 0, '', ...block, ...(followed ? [] : ['']));
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Drop the numeric line under `## Build order`, and the section itself when
|
|
214
|
+
* nothing but blank lines is left in it, so the plan reads in document order
|
|
215
|
+
* again. A plan with no line is returned unchanged.
|
|
216
|
+
*/
|
|
217
|
+
export function clearBuildOrder(intent) {
|
|
218
|
+
const lines = intent.split('\n');
|
|
219
|
+
const heading = lines.findIndex((l) => l.trim() === '## Build order');
|
|
220
|
+
if (heading === -1) {
|
|
221
|
+
return intent;
|
|
222
|
+
}
|
|
223
|
+
const at = lines.findIndex((l, i) => i > heading && i < sectionEnd(lines, heading) && BUILD_ORDER_LINE.test(l.trim()));
|
|
224
|
+
if (at === -1) {
|
|
225
|
+
return intent;
|
|
226
|
+
}
|
|
227
|
+
lines.splice(at, 1);
|
|
228
|
+
if ((lines[at - 1] ?? '').trim() === '' && (at >= lines.length || (lines[at] ?? '').trim() === '')) {
|
|
229
|
+
lines.splice(at - 1, 1);
|
|
230
|
+
}
|
|
231
|
+
const body = lines.slice(heading + 1, sectionEnd(lines, heading));
|
|
232
|
+
if (body.every((l) => l.trim() === '')) {
|
|
233
|
+
lines.splice(heading, body.length + 1);
|
|
234
|
+
}
|
|
235
|
+
return lines.join('\n');
|
|
236
|
+
}
|
|
73
237
|
/**
|
|
74
238
|
* Flip step N's `[ ]` checkbox to `[x]` within the `## Steps` section.
|
|
75
239
|
*
|
|
@@ -118,15 +282,43 @@ export function parseOpenQuestions(intent) {
|
|
|
118
282
|
}).length;
|
|
119
283
|
}
|
|
120
284
|
/**
|
|
121
|
-
* Count the
|
|
285
|
+
* Count the park list: the `- [ ]` lines still open, and every checklist line
|
|
286
|
+
* under `## Park list`, harvested `- [x]` ones included.
|
|
122
287
|
*
|
|
123
288
|
* A parked item is a mid-build idea the `park` verb appends as a flat checklist
|
|
124
|
-
* line for later triage; `/plumbbob:harvest` flips a triaged one to `- [x]
|
|
125
|
-
*
|
|
126
|
-
*
|
|
289
|
+
* line for later triage; `/plumbbob:harvest` flips a triaged one to `- [x]`, so
|
|
290
|
+
* the pair reads `parked 2 of 7 open` and a harvest shows as the gap between
|
|
291
|
+
* the two. The `(none yet)` placeholder and the blockquote instructions never
|
|
292
|
+
* match.
|
|
293
|
+
*/
|
|
294
|
+
export function parseParkTotals(buildLog) {
|
|
295
|
+
let open = 0;
|
|
296
|
+
let total = 0;
|
|
297
|
+
for (const line of sectionLines(buildLog, '## Park list').lines) {
|
|
298
|
+
const m = /^-\s+\[([ xX])\]\s+\S/.exec(line.trim());
|
|
299
|
+
if (m === null) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
total += 1;
|
|
303
|
+
if (m[1] === ' ') {
|
|
304
|
+
open += 1;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return { open, total };
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Count the open parked items alone: `- [ ]` lines under `## Park list`.
|
|
127
311
|
*/
|
|
128
312
|
export function parseParked(buildLog) {
|
|
129
|
-
return
|
|
313
|
+
return parseParkTotals(buildLog).open;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* The park count as both surfaces say it: `parked 2 of 7 open` once anything
|
|
317
|
+
* has been parked, the open count against everything ever parked; `parked 0`
|
|
318
|
+
* with nothing on the list yet.
|
|
319
|
+
*/
|
|
320
|
+
export function parkedPhrase(totals) {
|
|
321
|
+
return totals.total === 0 ? 'parked 0' : `parked ${totals.open} of ${totals.total} open`;
|
|
130
322
|
}
|
|
131
323
|
/**
|
|
132
324
|
* Count the constraints declared under `## Constraints`: the section's
|
|
@@ -225,7 +417,7 @@ function nextMove(spiking, steps, inFlight, parked, requested) {
|
|
|
225
417
|
if (target.done) {
|
|
226
418
|
return `build step ${target.n} — explicitly requested (already checkpointed)`;
|
|
227
419
|
}
|
|
228
|
-
const skipped = steps
|
|
420
|
+
const skipped = skippedBefore(steps, target.n).length;
|
|
229
421
|
const notes = [];
|
|
230
422
|
if (skipped > 0) {
|
|
231
423
|
notes.push(`skips ${skipped} undone step${skipped === 1 ? '' : 's'}`);
|
|
@@ -256,13 +448,19 @@ function nextMove(spiking, steps, inFlight, parked, requested) {
|
|
|
256
448
|
*/
|
|
257
449
|
export function orient(input) {
|
|
258
450
|
const steps = parseSteps(input.intent);
|
|
259
|
-
|
|
451
|
+
// The build order re-sequences the picks and nothing else: the step block
|
|
452
|
+
// still renders in document order, with the marker showing where the
|
|
453
|
+
// sequence actually goes.
|
|
454
|
+
const order = parseBuildOrder(input.intent);
|
|
455
|
+
const ordered = orderSteps(steps, order);
|
|
456
|
+
const parks = parseParkTotals(input.buildLog);
|
|
260
457
|
// The target the dashboard details: the explicitly requested step when it
|
|
261
458
|
// exists in the plan, else the next undone. A requested number that names no
|
|
262
459
|
// planned step carries nothing here; the next-move line reports the mismatch
|
|
263
460
|
// while the rest of the dashboard renders as usual.
|
|
264
461
|
const requestedStep = input.requested === null ? undefined : steps.find((s) => s.n === input.requested);
|
|
265
|
-
const
|
|
462
|
+
const nextUndone = ordered.find((s) => !s.done);
|
|
463
|
+
const target = requestedStep ?? nextUndone;
|
|
266
464
|
const seamParse = target === undefined ? null : parseStepSeam(input.intent, target.n);
|
|
267
465
|
const phase = input.spiking ? 'SPIKE' : input.inFlight !== null ? 'BUILD' : 'DESIGN';
|
|
268
466
|
return {
|
|
@@ -270,9 +468,12 @@ export function orient(input) {
|
|
|
270
468
|
phase,
|
|
271
469
|
steps,
|
|
272
470
|
lastCheckpoint: parseLastCheckpoint(input.checkpoints),
|
|
273
|
-
parked,
|
|
471
|
+
parked: parks.open,
|
|
472
|
+
parkedTotal: parks.total,
|
|
274
473
|
openQuestions: parseOpenQuestions(input.intent),
|
|
275
|
-
next: nextMove(input.spiking,
|
|
474
|
+
next: nextMove(input.spiking, ordered, input.inFlight, parks.open, input.requested),
|
|
475
|
+
nextUndone: nextUndone?.n ?? null,
|
|
476
|
+
buildOrder: remainingOrder(steps, order, null),
|
|
276
477
|
requested: requestedStep?.n ?? null,
|
|
277
478
|
nextDoneWhen: target?.doneWhen ?? null,
|
|
278
479
|
nextSeam: seamParse !== null && seamParse.ok ? seamParse.seam : [],
|
|
@@ -741,15 +942,25 @@ export function parseSections(detail) {
|
|
|
741
942
|
export function parseRecommendation(detail) {
|
|
742
943
|
return sectionProse(detail, /^##\s+recommendation\s*$/i);
|
|
743
944
|
}
|
|
945
|
+
/**
|
|
946
|
+
* The step `.plumbbob/detail.md` was written for, read off its
|
|
947
|
+
* `# Detail · Step <n> · <title>` header, or null when the first line is not a
|
|
948
|
+
* step header (a plan pause's detail, or a file the model wrote without one).
|
|
949
|
+
*/
|
|
950
|
+
export function parseDetailStep(detail) {
|
|
951
|
+
const first = detail.trimStart().split('\n', 1)[0] ?? '';
|
|
952
|
+
const m = /^#\s+Detail\b.*?\bStep\s+(\d+)\b/.exec(first);
|
|
953
|
+
return m === null ? null : Number(m[1]);
|
|
954
|
+
}
|
|
744
955
|
/**
|
|
745
956
|
* Render an Orientation as the plain-text dashboard `status` prints.
|
|
746
957
|
*/
|
|
747
958
|
export function formatOrientation(o) {
|
|
748
959
|
const doneCount = o.steps.filter((s) => s.done).length;
|
|
749
|
-
const nextUndone = o.steps.find((s) =>
|
|
960
|
+
const nextUndone = o.nextUndone === null ? undefined : o.steps.find((s) => s.n === o.nextUndone);
|
|
750
961
|
// One arrow, always: an explicitly requested step takes the marker and the
|
|
751
962
|
// detail rows, so the injected state never argues with the invocation. With
|
|
752
|
-
// no (valid) request the next undone step keeps them, as ever.
|
|
963
|
+
// no (valid) request the next undone step in build order keeps them, as ever.
|
|
753
964
|
const requestedStep = o.requested === null ? undefined : o.steps.find((s) => s.n === o.requested);
|
|
754
965
|
const marked = requestedStep ?? nextUndone;
|
|
755
966
|
const stepLines = o.steps.map((s) => {
|
|
@@ -782,6 +993,9 @@ export function formatOrientation(o) {
|
|
|
782
993
|
const receipts = o.outOfBand > 0
|
|
783
994
|
? [`${o.outOfBand} commit${o.outOfBand === 1 ? '' : 's'} since the last checkpoint landed outside plumbbob's ledger.`]
|
|
784
995
|
: [];
|
|
996
|
+
// The build order, whole, only when it departs from the numbering: the
|
|
997
|
+
// dashboard is the reference surface, so nothing is folded to a count here.
|
|
998
|
+
const orderRow = o.buildOrder.length > 0 ? [`build order ${o.buildOrder.join(', ')}`] : [];
|
|
785
999
|
return [
|
|
786
1000
|
`PlumbBob — ${o.title ?? '(untitled)'} [${o.phase}]`,
|
|
787
1001
|
'',
|
|
@@ -789,7 +1003,8 @@ export function formatOrientation(o) {
|
|
|
789
1003
|
'',
|
|
790
1004
|
cpLine,
|
|
791
1005
|
...receipts,
|
|
792
|
-
|
|
1006
|
+
...orderRow,
|
|
1007
|
+
`${parkedPhrase({ open: o.parked, total: o.parkedTotal })} · open questions ${o.openQuestions}`,
|
|
793
1008
|
'',
|
|
794
1009
|
`next → ${o.next}`,
|
|
795
1010
|
].join('\n');
|
package/dist/verbs/build.js
CHANGED
|
@@ -8,16 +8,18 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
8
8
|
import { findRepoRoot } from "../lib/git.js";
|
|
9
9
|
import { hasSession, intentPath, resolveBuild, seamPath, stampStepStat, stampTick, stepPath } from "../lib/sidecar.js";
|
|
10
10
|
import { parseStepSeam } from "../lib/intent.js";
|
|
11
|
-
import {
|
|
11
|
+
import { parseOrderedSteps, skippedBefore } from "../lib/orient.js";
|
|
12
12
|
import { stepLabel, syncBuildLogState } from "../lib/buildlogsync.js";
|
|
13
|
+
import { readGrant } from "../lib/latch.js";
|
|
13
14
|
import { notice } from "../lib/notice.js";
|
|
14
15
|
/**
|
|
15
16
|
* Enter step n (or the next undone step) and write its SEAM/STEP markers.
|
|
16
17
|
*
|
|
17
18
|
* Refuses a missing session, a malformed step argument, an `N-M` range (a
|
|
18
19
|
* `/plumbbob:build` skill feature, not a CLI one), and a seam that fails to parse.
|
|
19
|
-
* On entry it stamps the turn ledger and the step's start time,
|
|
20
|
-
*
|
|
20
|
+
* On entry it stamps the turn ledger and the step's start time, re-renders the
|
|
21
|
+
* build-log's Current step line, and names the step when it is the last one a
|
|
22
|
+
* self-approval grant reaches.
|
|
21
23
|
*/
|
|
22
24
|
export function build(cwd, args) {
|
|
23
25
|
const root = findRepoRoot(cwd);
|
|
@@ -28,12 +30,13 @@ export function build(cwd, args) {
|
|
|
28
30
|
const { build: slug, rest } = resolveBuild(root, args);
|
|
29
31
|
const raw = rest.find((a) => !a.startsWith('--'));
|
|
30
32
|
// A step range like `1-3` is a `/plumbbob:build` skill affordance (auto-approve
|
|
31
|
-
// through the range, then
|
|
32
|
-
// in-flight step at a time. Name it rather than bounce off the
|
|
33
|
+
// every step through the range's top, then stop), not a CLI capability: the CLI
|
|
34
|
+
// records one in-flight step at a time. Name it rather than bounce off the
|
|
35
|
+
// generic usage.
|
|
33
36
|
if (raw !== undefined && /^\d+-\d*$/.test(raw)) {
|
|
34
37
|
process.stderr.write(notice({
|
|
35
38
|
fact: 'build takes one step number',
|
|
36
|
-
detail: [`\`${raw}\` is a /plumbbob:build range, which auto-approves through
|
|
39
|
+
detail: [`\`${raw}\` is a /plumbbob:build range, which auto-approves through its top and then stops`],
|
|
37
40
|
remedy: `plumbbob build ${raw.split('-')[0]}`,
|
|
38
41
|
}));
|
|
39
42
|
return 1;
|
|
@@ -45,7 +48,9 @@ export function build(cwd, args) {
|
|
|
45
48
|
return 1;
|
|
46
49
|
}
|
|
47
50
|
const intent = readFileSync(intentPath(root, slug), 'utf8');
|
|
48
|
-
|
|
51
|
+
// In build order: the sequence `## Build order` declares when a refined plan
|
|
52
|
+
// has one, else the numbering, so this pick and the dashboard's agree.
|
|
53
|
+
const steps = parseOrderedSteps(intent);
|
|
49
54
|
// No argument ⇒ enter the next undone step in intent.md (the same idiom
|
|
50
55
|
// `checkpoint` uses), so a bare `plumbbob build` advances the loop without the
|
|
51
56
|
// skill re-deriving the step in prose. Every step checkpointed ⇒ a `/plumbbob:step`
|
|
@@ -86,17 +91,20 @@ export function build(cwd, args) {
|
|
|
86
91
|
const title = steps.find((s) => s.n === step)?.title ?? null;
|
|
87
92
|
syncBuildLogState(root, slug, stepLabel(step, title));
|
|
88
93
|
// An explicit jump past undone work says so out loud: the entered step and
|
|
89
|
-
// the count it skips
|
|
90
|
-
//
|
|
91
|
-
|
|
94
|
+
// the count it skips (the undone steps ahead of it in build order) read back
|
|
95
|
+
// to the human (and to the transcript), so a deliberate `build 22` and a
|
|
96
|
+
// confused one look different on the page.
|
|
97
|
+
const skipped = raw === undefined ? 0 : skippedBefore(steps, step).length;
|
|
92
98
|
const picked = raw === undefined
|
|
93
99
|
? ['next undone']
|
|
94
100
|
: skipped > 0
|
|
95
101
|
? ['explicitly requested', `skips ${skipped} undone step${skipped === 1 ? '' : 's'}`]
|
|
96
102
|
: [];
|
|
97
|
-
// Two lines
|
|
98
|
-
//
|
|
103
|
+
// Two lines (three on the last step a grant reaches), one colon each, then
|
|
104
|
+
// the seam as a plain readout beneath the notice that frames it: the paths
|
|
105
|
+
// are a list, and a list is not a one-liner.
|
|
99
106
|
process.stdout.write(notice({ fact: `building step ${step}`, detail: picked }) +
|
|
107
|
+
lastGrantedNotice(readGrant(root), steps, step) +
|
|
100
108
|
notice({
|
|
101
109
|
fact: 'the seam is orientation, not a lock',
|
|
102
110
|
detail: [`${parsed.seam.length} path${parsed.seam.length === 1 ? '' : 's'}`],
|
|
@@ -104,3 +112,34 @@ export function build(cwd, args) {
|
|
|
104
112
|
`${parsed.seam.map((path) => ` ${path}`).join('\n')}\n`);
|
|
105
113
|
return 0;
|
|
106
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* The line naming the last step a self-approval grant reaches this turn, or ''
|
|
117
|
+
* for any other step and when no grant is live.
|
|
118
|
+
*
|
|
119
|
+
* That step lands like every step before it, and then the turn ends on its
|
|
120
|
+
* checkpoint: a clean halt. Saying so as the step is entered puts the fact in
|
|
121
|
+
* front of the model before it chooses between landing the step and holding it
|
|
122
|
+
* at a pause, which is the choice a clean halt kept getting wrong. A range
|
|
123
|
+
* reaches its top, or the last undone step under its top when the plan ends
|
|
124
|
+
* first, or when the build order puts a number past the top ahead of the rest
|
|
125
|
+
* (a range names plan numbers, and the skill halts before one past its top);
|
|
126
|
+
* `--auto` reaches the plan's last undone step.
|
|
127
|
+
*/
|
|
128
|
+
function lastGrantedNotice(grant, steps, step) {
|
|
129
|
+
if (grant === null) {
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
const remedy = 'land it on green, then stop at the boundary';
|
|
133
|
+
if (grant.kind === 'range') {
|
|
134
|
+
if (step === grant.ceiling) {
|
|
135
|
+
return notice({ fact: `step ${step} is the top of the range you granted`, remedy });
|
|
136
|
+
}
|
|
137
|
+
const after = steps.slice(steps.findIndex((s) => s.n === step) + 1).find((s) => !s.done);
|
|
138
|
+
const more = after !== undefined && after.n <= grant.ceiling;
|
|
139
|
+
return step < grant.ceiling && !more
|
|
140
|
+
? notice({ fact: `step ${step} is the last undone step in the range you granted`, remedy })
|
|
141
|
+
: '';
|
|
142
|
+
}
|
|
143
|
+
const more = steps.some((s) => !s.done && s.n !== step);
|
|
144
|
+
return more ? '' : notice({ fact: `step ${step} is the last undone step in the plan`, remedy });
|
|
145
|
+
}
|
package/dist/verbs/checkpoint.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// `/plumbbob:build` run, your own hands, a vibe session, or another harness all
|
|
4
4
|
// checkpoint identically. It does NOT require a STEP marker (the flat control
|
|
5
5
|
// file recording the step in flight): the step is whatever you pass, else the
|
|
6
|
-
// in-flight STEP, else the next undone step in intent.md. It evaluates the
|
|
6
|
+
// in-flight STEP, else the next undone step in intent.md's build order. It evaluates the
|
|
7
7
|
// approval latch, gates on a green check, commits any pending work (or records
|
|
8
8
|
// the existing HEAD when the tree is already clean: the human's commit skill
|
|
9
9
|
// may have committed first), records the SHA, flips the intent checkbox to
|
|
@@ -16,7 +16,7 @@ import { activeBuild, buildFolder, buildLogPath, buildScope, bumpStepStat, check
|
|
|
16
16
|
import { runCheck } from "../lib/check.js";
|
|
17
17
|
import { readCommitBody } from "../lib/commitbody.js";
|
|
18
18
|
import { checkLatch } from "../lib/latch.js";
|
|
19
|
-
import { markStepDone, parseSteps } from "../lib/orient.js";
|
|
19
|
+
import { markStepDone, parseDetailStep, parseOrderedSteps, parseSteps } from "../lib/orient.js";
|
|
20
20
|
import { parseBuildScope, parseStepSeam, scopeDrift } from "../lib/intent.js";
|
|
21
21
|
import { conventionalSubject, subjectFromTitle, withMarker } from "../lib/commitmsg.js";
|
|
22
22
|
import { appendToSection, checkpointLogLine, logEntry, planLogLine } from "../lib/buildlog.js";
|
|
@@ -87,7 +87,7 @@ export async function checkpoint(cwd, args) {
|
|
|
87
87
|
// tracked ledger is the archive, and it rides the branch where a commit body
|
|
88
88
|
// would not survive the squash); the commit body keeps its marker and the
|
|
89
89
|
// lead prose only.
|
|
90
|
-
const record = stepRecord(root, activeBuild(root), step, readDetail(root) ?? '');
|
|
90
|
+
const record = stepRecord(root, activeBuild(root), step, readDetail(root, step) ?? '');
|
|
91
91
|
let sha;
|
|
92
92
|
// The drift advisory is read off the index (staged, before the commit clears
|
|
93
93
|
// it) but printed inside the ending below, where the fixed order puts it: the
|
|
@@ -165,7 +165,7 @@ function checkpointPlan(root, args) {
|
|
|
165
165
|
appendFileSync(checkpointsPath(root), `plan ${sha}\n`);
|
|
166
166
|
// The cold read that approved the plan is the Log's first entry, beneath a
|
|
167
167
|
// dated plan line; the detail file is then spent, the same as at a step.
|
|
168
|
-
const pointer = logPlan(root, sha, planRecord(readDetail(root) ?? ''));
|
|
168
|
+
const pointer = logPlan(root, sha, planRecord(readDetail(root, null) ?? ''));
|
|
169
169
|
clearDetail(root);
|
|
170
170
|
// Landing the plan consumes `start`'s entry stamp: a later hand-built diff
|
|
171
171
|
// (no `build <n>`) must find no stale TICK and stay guidance-governed.
|
|
@@ -200,7 +200,7 @@ function planSubject(root) {
|
|
|
200
200
|
/**
|
|
201
201
|
* Resolve the step being checkpointed, or null when none can be determined.
|
|
202
202
|
*
|
|
203
|
-
* Explicit arg > in-flight STEP file > first undone step in
|
|
203
|
+
* Explicit arg > in-flight STEP file > first undone step in build order. A `-m`
|
|
204
204
|
* value is a message, never a step: `checkpoint -m "2"` must not read as
|
|
205
205
|
* step 2.
|
|
206
206
|
*/
|
|
@@ -214,7 +214,7 @@ function resolveStep(root, args) {
|
|
|
214
214
|
return inFlight;
|
|
215
215
|
}
|
|
216
216
|
try {
|
|
217
|
-
return
|
|
217
|
+
return parseOrderedSteps(readFileSync(intentPath(root), 'utf8')).find((s) => !s.done)?.n ?? null;
|
|
218
218
|
}
|
|
219
219
|
catch {
|
|
220
220
|
return null;
|
|
@@ -444,16 +444,23 @@ function fallbackBody(root, step) {
|
|
|
444
444
|
}
|
|
445
445
|
/**
|
|
446
446
|
* The in-flight step's detail from `.plumbbob/detail.md`, or null when the file
|
|
447
|
-
* is absent or
|
|
447
|
+
* is absent, empty, or written for another step.
|
|
448
448
|
*
|
|
449
449
|
* The model overwrites it before every pause; checkpoint reads it once here for
|
|
450
450
|
* the Log's record, and the caller clears it afterward. A missing or blank file
|
|
451
|
-
* records nothing
|
|
451
|
+
* records nothing, and so does one whose header names a different step: that
|
|
452
|
+
* file is a leftover, written after its own step had already landed, and
|
|
453
|
+
* recording it would file one step's story under another. The plan commit
|
|
454
|
+
* passes no step and takes the file as it is.
|
|
452
455
|
*/
|
|
453
|
-
function readDetail(root) {
|
|
456
|
+
function readDetail(root, step) {
|
|
454
457
|
try {
|
|
455
458
|
const raw = readFileSync(detailPath(root), 'utf8').trim();
|
|
456
|
-
|
|
459
|
+
if (raw.length === 0) {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
const written = step === null ? null : parseDetailStep(raw);
|
|
463
|
+
return written === null || written === step ? raw : null;
|
|
457
464
|
}
|
|
458
465
|
catch {
|
|
459
466
|
return null;
|