plumbbob 0.11.0 → 0.11.2
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 +108 -51
- package/dist/lib/buildlog.js +22 -0
- package/dist/lib/check.js +76 -6
- package/dist/lib/orient.js +53 -8
- package/dist/lib/sidecar.js +6 -6
- package/dist/verbs/checkpoint.js +58 -31
- package/dist/verbs/handoff.js +75 -1
- package/package.json +1 -1
- package/skills/build/SKILL.md +4 -3
- package/skills/plan/SKILL.md +51 -6
- package/skills/verify/SKILL.md +3 -3
- package/templates/build-log.md +4 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plumbbob",
|
|
3
3
|
"displayName": "PlumbBob",
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.2",
|
|
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
|
@@ -29,6 +29,45 @@ Ridgeline runs large, settled work autonomously without you, PlumbBob keeps you
|
|
|
29
29
|
driver's seat for the work your judgment has to steer. This repository was built using
|
|
30
30
|
PlumbBob itself, running its own loop.
|
|
31
31
|
|
|
32
|
+
## Who it's for
|
|
33
|
+
|
|
34
|
+
- **You already build with a coding agent and end the day reviewing a wall of diff.**
|
|
35
|
+
PlumbBob turns that wall into steps you approve one at a time, each small enough to
|
|
36
|
+
read as an editor.
|
|
37
|
+
- **You are skeptical of AI-written code.** Nothing lands without a green check, a
|
|
38
|
+
self-review against the plan, and your say-so, and the CLI refuses to record a step
|
|
39
|
+
the model approved for itself.
|
|
40
|
+
- **You would rather not learn a tool.** Everything it writes is markdown and ordinary
|
|
41
|
+
git commits in your own repo; the skills are slash commands, and `/plumbbob:status`
|
|
42
|
+
always names the next one. A [glossary](docs/glossary.md) covers the words it mints.
|
|
43
|
+
- **Not for a typo, and not for a rewrite.** A one-line fix needs no session, and a
|
|
44
|
+
large, settled build wants an autonomous harness such as Ridgeline. The loop earns
|
|
45
|
+
its keep in between: a feature, a bug, a refactor.
|
|
46
|
+
|
|
47
|
+
## The loop in one picture
|
|
48
|
+
|
|
49
|
+
<p align="center">
|
|
50
|
+
<img src="docs/media/loop-one-step.svg" alt="One step of the loop: /plumbbob:build implements step 1 and stops at the pause, the human says looks good, the step is checkpointed" width="760">
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
Everything the model must stay true to lives in one file, the build's `intent.md`
|
|
54
|
+
(under `.plumbbob/builds/<slug>/`), written before any code: the **Frame** (the problem, what done looks like, what you're
|
|
55
|
+
explicitly *not* doing), the **Decisions** and **Constraints** (the settled calls, each
|
|
56
|
+
with its *because*), and the **Steps**: a numbered list where every step carries a
|
|
57
|
+
**done-when** (a checkable finish line, ideally a test) and a **seam** (the files it's
|
|
58
|
+
expected to touch).
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
/plumbbob:plan decide everything on paper — frame, decisions, all steps (once)
|
|
62
|
+
per step:
|
|
63
|
+
/plumbbob:build implement the next step → run checks → self-review → PAUSE
|
|
64
|
+
(looks good) → the step is committed as a checkpoint; fire /plumbbob:build again
|
|
65
|
+
/plumbbob:finish report what shipped and why, final commit, clear (once)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Nothing is locked and nothing refuses you: the loop does a step's labor, pulls up to
|
|
69
|
+
the line, and waits for you to advance it. **You are the clock.**
|
|
70
|
+
|
|
32
71
|
## What a prompt can't replicate
|
|
33
72
|
|
|
34
73
|
A system prompt can *ask* a model to plan first and stop for review. What it can't do is
|
|
@@ -55,30 +94,10 @@ The planning surface is the on-ramp; these five are the floor under it. And the
|
|
|
55
94
|
payoff is [measured, not asserted](#why-not-just), not just claimed. **Guidance on the
|
|
56
95
|
work, a latch on the record.**
|
|
57
96
|
|
|
58
|
-
## The loop in one picture
|
|
59
|
-
|
|
60
97
|
<p align="center">
|
|
61
|
-
<img src="
|
|
98
|
+
<img src="docs/media/build-record-in-pr.svg" alt="A pull request's file list with the build folder beside the source" width="760">
|
|
62
99
|
</p>
|
|
63
100
|
|
|
64
|
-
Everything the model must stay true to lives in one file, the build's `intent.md`
|
|
65
|
-
(under `.plumbbob/builds/<slug>/`), written before any code: the **Frame** (the problem, what done looks like, what you're
|
|
66
|
-
explicitly *not* doing), the **Decisions** and **Constraints** (the settled calls, each
|
|
67
|
-
with its *because*), and the **Steps**: a numbered list where every step carries a
|
|
68
|
-
**done-when** (a checkable finish line, ideally a test) and a **seam** (the files it's
|
|
69
|
-
expected to touch).
|
|
70
|
-
|
|
71
|
-
```text
|
|
72
|
-
/plumbbob:plan decide everything on paper — frame, decisions, all steps (once)
|
|
73
|
-
per step:
|
|
74
|
-
/plumbbob:build implement the next step → run checks → self-review → PAUSE
|
|
75
|
-
(approve) → the step is committed as a checkpoint; fire /plumbbob:build again
|
|
76
|
-
/plumbbob:finish report what shipped and why, final commit, clear (once)
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Nothing is locked and nothing refuses you: the loop does a step's labor, pulls up to
|
|
80
|
-
the line, and waits for you to advance it. **You are the clock.**
|
|
81
|
-
|
|
82
101
|
## Install
|
|
83
102
|
|
|
84
103
|
PlumbBob installs **once, globally** (like `gh` or your dotfiles). Two ways work;
|
|
@@ -115,20 +134,68 @@ In Claude Code, inside any git repo with a clean tree:
|
|
|
115
134
|
path to a spec file. Together you fill the build's `intent.md`. No code is written
|
|
116
135
|
yet.
|
|
117
136
|
2. **Build.** Fire `/plumbbob:build`. It implements the next undone step, runs the
|
|
118
|
-
heavy check gate, reviews its own diff against the plan, and stops
|
|
137
|
+
heavy check gate, reviews its own diff against the plan, and stops. What it stops on
|
|
138
|
+
is one block with a fixed shape (the [turn anatomy](docs/presentation.md)): the CLI
|
|
139
|
+
measures the check, the seam, and the diff, the model supplies only the judgment, and
|
|
140
|
+
nothing is written around it.
|
|
141
|
+
|
|
142
|
+
````markdown
|
|
143
|
+
**Summary**: A per-IP token bucket now refuses the 6th attempt inside a minute. (details: `.plumbbob/detail.md`)
|
|
144
|
+
|
|
145
|
+
1. The bucket lives in memory, keyed by IP, refilling five tokens a minute.
|
|
146
|
+
2. The clock is injected, so the test advances time instead of sleeping on it.
|
|
147
|
+
|
|
148
|
+
**Readout**: Step 1 - Add a token-bucket limiter
|
|
119
149
|
|
|
120
150
|
```text
|
|
121
|
-
|
|
151
|
+
check green: 7 of 7 checks
|
|
152
|
+
done-when met
|
|
153
|
+
decisions 2 of 2 honored
|
|
154
|
+
constraints 1 of 1 honored
|
|
155
|
+
seam held: 2 of 2 declared, no strays
|
|
156
|
+
diff +61 -3 across 2 files
|
|
157
|
+
spent 22 min · 2 turns · 41s gate · green first run
|
|
122
158
|
```
|
|
123
159
|
|
|
160
|
+
**Verdict**: ● Plumb
|
|
161
|
+
|
|
162
|
+
**Next Up**: Step 2 of 3 - Wire the limiter into POST /login (model: **Sonnet**, details: `.plumbbob/builds/2026-07-03-rate-limit-the-login-endpoint/intent.md:20`)
|
|
163
|
+
|
|
164
|
+
**Your Call**:
|
|
165
|
+
|
|
166
|
+
- `looks good` → I checkpoint Step 1; back to the boundary
|
|
167
|
+
- `expand`, or any question → I show more of what is there; nothing changes
|
|
168
|
+
- anything that reads as direction → I take it as what to change; nothing lands until you approve
|
|
169
|
+
- `revert` → I wind the work back to the last checkpoint
|
|
170
|
+
|
|
171
|
+
**Recommendation**: Approve it. The gate is green, the seam held, and every call the step made is one the plan already decided.
|
|
172
|
+
````
|
|
173
|
+
|
|
174
|
+

|
|
175
|
+
|
|
124
176
|
(The check gate is [checkride](https://www.npmjs.com/package/checkride), one
|
|
125
177
|
run across the tools your repo already configures: types, lint, tests, dead
|
|
126
178
|
code, docs. To gate through your own command instead, set the `"check"` key in
|
|
127
|
-
`.plumbbob/settings.json`, for example `"check": "npm test"
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
179
|
+
`.plumbbob/settings.json`, for example `"check": "npm test"`; in a repo where
|
|
180
|
+
checkride finds no tools at all, `start` says so up front and names that key.)
|
|
181
|
+
3. **Reply.** Say `looks good` and it commits the step as a checkpoint, marks it done,
|
|
182
|
+
and returns to the boundary with the same three lines every landed step gets:
|
|
183
|
+
|
|
184
|
+
```text
|
|
185
|
+
**Checkpoint**: Step 1 complete (a1b2c3d4e, details: `.plumbbob/builds/2026-07-03-rate-limit-the-login-endpoint/build-log.md:44`)
|
|
186
|
+
|
|
187
|
+
**Verdict**: ● Plumb
|
|
188
|
+
|
|
189
|
+
**Next Up**: Step 2 of 3 - Wire the limiter into POST /login (model: **Sonnet**, details: `.plumbbob/builds/2026-07-03-rate-limit-the-login-endpoint/intent.md:20`)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+

|
|
193
|
+
|
|
194
|
+
Ask instead (`expand 2`, or any question) and it shows more of what is there and
|
|
195
|
+
changes nothing; direct it and it takes that as the fix, and nothing lands until you
|
|
196
|
+
say `looks good`. Then fire `/plumbbob:build` again for the next step: re-firing it
|
|
197
|
+
*is* the clock tick. Whenever you lose the thread, `/plumbbob:status` shows where you
|
|
198
|
+
are and names the next move.
|
|
132
199
|
4. **Finish.** When the last step is done, `/plumbbob:finish` writes a report of what shipped
|
|
133
200
|
and why into the build's tracked folder, makes the final commit, and clears the slate
|
|
134
201
|
for the next goal. The folder rides the branch into the PR: the build's record merges
|
|
@@ -187,8 +254,9 @@ All fourteen, with inputs and effects, are in
|
|
|
187
254
|
[`docs/skills-reference.md`](docs/skills-reference.md).
|
|
188
255
|
|
|
189
256
|
Under the skills ships a lean `plumbbob` CLI (the mechanical verbs the
|
|
190
|
-
skills shell out to),
|
|
191
|
-
|
|
257
|
+
skills shell out to), three hooks (a non-blocking post-edit lint, a `git commit`
|
|
258
|
+
ask-hook while a step is in flight, and the per-prompt turn ledger the approval latch
|
|
259
|
+
reads), and a `.plumbbob/` sidecar you can open and edit by hand at any time: a tracked
|
|
192
260
|
`builds/<slug>/` folder per build (`intent.md`, `build-log.md`, `checkpoints`, `report.md`)
|
|
193
261
|
that rides the branch into the PR, plus an untracked control plane (`settings.local.json`,
|
|
194
262
|
the session sentinel, the in-flight markers).
|
|
@@ -260,37 +328,26 @@ you, reading the diff. And when a raw commit does slip the ledger, `status` says
|
|
|
260
328
|
checkpoint record makes recovery one command; cheap recovery, not a cage, is the control
|
|
261
329
|
that matters.
|
|
262
330
|
|
|
263
|
-
> The latch's payoff is measured, not asserted
|
|
264
|
-
> ([`test/evals/`](test/evals/)) sweeps
|
|
265
|
-
> shipped latch,
|
|
266
|
-
>
|
|
267
|
-
>
|
|
268
|
-
> (
|
|
269
|
-
>
|
|
270
|
-
>
|
|
271
|
-
> write into settings, a legal side door. The re-sweep
|
|
272
|
-
> ([opus, 2026-07-18](docs/evals/2026-07-18.md), plumbbob 0.8.7) reproduced the win
|
|
273
|
-
> (*no checkpoint over a red check* still **5/5 latched**) and pinned both gaps. Both are
|
|
274
|
-
> now closed. The side door went by construction: [D67 (auto-not-a-grant)](docs/decisions.md#d67)
|
|
275
|
-
> (self-approval is human-typed only) retired the settings `auto` grant, so the one
|
|
276
|
-
> approval a model could forge is gone. Park capture was never the latch's to reach,
|
|
277
|
-
> prose-governed by design ([D10 (pause-not-lock)](docs/decisions.md#d10)/[D13 (no-edit-guards)](docs/decisions.md#d13)),
|
|
278
|
-
> and it failed because the guidance couldn't reach a fresh session at all; the turn hook
|
|
279
|
-
> now injects one line when a step is in flight, and the third sweep
|
|
280
|
-
> ([opus, 2026-07-27](docs/evals/2026-07-27.md), plumbbob 0.9.0) measures it at **5/5**,
|
|
281
|
-
> alongside every other contract. That sweep ran the latched arm only, so its numbers are
|
|
282
|
-
> a regression check on the shipped configuration, not a fresh baseline delta: the
|
|
283
|
-
> 2/5→5/5 headline above remains the 2026-07-11 measurement.
|
|
331
|
+
> The latch's payoff is measured, not asserted. A headless skill-eval harness
|
|
332
|
+
> ([`test/evals/`](test/evals/)) sweeps the loop with a prose-only baseline against the
|
|
333
|
+
> shipped latch, every assertion a mechanical read of git state, and commits each receipt
|
|
334
|
+
> under [`docs/evals/`](docs/evals/): the route the latch owns (*no checkpoint over a red
|
|
335
|
+
> check under pressure*) went from **2/5 prose-only to 5/5 latched** on the first sweep
|
|
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-03](docs/evals/2026-09-03.md))
|
|
338
|
+
> scores the shape of the turn itself.
|
|
284
339
|
|
|
285
340
|
## Documentation
|
|
286
341
|
|
|
287
342
|
Each doc answers one question (in rough reading order for a new user):
|
|
288
343
|
|
|
289
344
|
- *What does a session actually look like?* → [`docs/happy-path.md`](docs/happy-path.md): one goal walked end to end; read this first.
|
|
345
|
+
- *What do the words mean?* → [`docs/glossary.md`](docs/glossary.md): every term the loop mints, one line each, with where to read more.
|
|
290
346
|
- *Show me the artifacts it leaves behind.* → [`examples/`](examples/): that same session's finished build folder, file by file.
|
|
291
347
|
- *Should I / can I / what about…?* → [`docs/faq.md`](docs/faq.md): the adoption questions, answered straight.
|
|
292
348
|
- *What is each method for?* → [`docs/techniques.md`](docs/techniques.md): steps, seams, the pause, park/harvest, spikes.
|
|
293
349
|
- *What does each skill do?* → [`docs/skills-reference.md`](docs/skills-reference.md): all fourteen skills: inputs, effects, when to reach for each.
|
|
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.
|
|
294
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).
|
|
295
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.
|
|
296
353
|
- *How do I install it, exactly?* → [`docs/install.md`](docs/install.md): the full guide and the agent-neutral roadmap.
|
package/dist/lib/buildlog.js
CHANGED
|
@@ -118,6 +118,28 @@ export function checkpointLogLine(date, step, sha, title, stats = null) {
|
|
|
118
118
|
const titled = title !== null && title.length > 0 ? `${head} — ${title}` : head;
|
|
119
119
|
return stats !== null && stats.length > 0 ? `${titled} (${stats})` : titled;
|
|
120
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* The Log line `checkpoint --plan` writes when the plan lands: dated, with the
|
|
123
|
+
* short SHA. The cold read's record rides beneath it, so the first entry of a
|
|
124
|
+
* build's history is the plan and the read that approved it.
|
|
125
|
+
*/
|
|
126
|
+
export function planLogLine(date, sha) {
|
|
127
|
+
return `- ${date} — plan committed · ${sha.slice(0, 9)}`;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* A Log entry: the dated line and, beneath it, the record of what landed (the
|
|
131
|
+
* pause as the human approved it), blank-line separated and indented two
|
|
132
|
+
* spaces so it nests as the line's own content. Blank lines inside the record
|
|
133
|
+
* stay bare rather than carrying the indent. A null record is the bare line,
|
|
134
|
+
* which is what a hand-built step with no detail file gets.
|
|
135
|
+
*/
|
|
136
|
+
export function logEntry(line, record) {
|
|
137
|
+
if (record === null || record.trim().length === 0) {
|
|
138
|
+
return line;
|
|
139
|
+
}
|
|
140
|
+
const nested = record.split('\n').map((l) => (l.trim().length === 0 ? '' : ` ${l}`));
|
|
141
|
+
return [line, '', ...nested].join('\n');
|
|
142
|
+
}
|
|
121
143
|
/**
|
|
122
144
|
* The Log line `abandon` writes for a dropped-but-kept step: the first
|
|
123
145
|
* non-checkpoint event the log records. Dated, names the step (its title when
|
package/dist/lib/check.js
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// settings.local.json, the untracked personal overlay → settings.json, the
|
|
5
5
|
// tracked project defaults) is a shell command → spawn it verbatim, so any
|
|
6
6
|
// repo can gate through anything (test fixtures point it at `true`/`false`
|
|
7
|
-
// to keep throwaway repos deterministic)
|
|
7
|
+
// to keep throwaway repos deterministic), and leave the same
|
|
8
|
+
// `.check/summary.json` checkride would, so the pause's readout can
|
|
9
|
+
// measure the gate whichever path ran it;
|
|
8
10
|
// - no setting at all → checkride, our sibling package, imported
|
|
9
11
|
// programmatically rather than spawned: the typed summary comes back
|
|
10
12
|
// in-process, failing slots are reported with their `.check/` raw-output
|
|
@@ -13,7 +15,9 @@
|
|
|
13
15
|
// Checkride's stream discipline holds: human progress goes to stderr. Exit 0 is
|
|
14
16
|
// green, 1 is red, 2 means the harness itself broke: reported distinctly,
|
|
15
17
|
// because a misconfigured gate must not read as broken code (both still block).
|
|
16
|
-
import {
|
|
18
|
+
import { spawn } from 'node:child_process';
|
|
19
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
17
21
|
import { runChecks, runDoctor } from 'checkride';
|
|
18
22
|
import { gateIsRunningFor, withGateMarker } from "./reentry.js";
|
|
19
23
|
import { resolveString } from "./settings.js";
|
|
@@ -103,17 +107,83 @@ export async function runCheck(root, flags = {}, commandFlag) {
|
|
|
103
107
|
const command = resolveString(root, 'check', '', commandFlag);
|
|
104
108
|
return withGateMarker(root, () => (command.length > 0 ? runCommand(root, command, flags) : runCheckride(root, flags)));
|
|
105
109
|
}
|
|
110
|
+
/** Where the configured command's combined output lands, relative to `.check/`. */
|
|
111
|
+
const OVERRIDE_OUTPUT = 'check.stdout.txt';
|
|
112
|
+
/** The longest a command may run as the summary's check name before it is cut with an ellipsis. */
|
|
113
|
+
const OVERRIDE_NAME_BUDGET = 40;
|
|
106
114
|
/**
|
|
107
115
|
* The spawn override: stream the configured command's own output to the
|
|
108
|
-
* terminal
|
|
109
|
-
* interpretation.
|
|
116
|
+
* terminal as it arrives, keep a copy under `.check/`, and return its exit
|
|
117
|
+
* code. The command is trusted verbatim, no interpretation.
|
|
118
|
+
*
|
|
119
|
+
* The run leaves the same `summary.json` checkride would, with the command as
|
|
120
|
+
* its one check: the pause's readout measures its `check` row from that file
|
|
121
|
+
* and offers `looks good` only on a measured green, so a gate that left no
|
|
122
|
+
* summary rendered no row and withheld the move at every pause in a repo that
|
|
123
|
+
* gates through its own command.
|
|
110
124
|
*/
|
|
111
125
|
function runCommand(root, command, flags) {
|
|
112
126
|
if (hasFlags(flags)) {
|
|
113
127
|
process.stderr.write(`plumbbob: check flags only narrow the checkride gate — ignored for the configured command '${command}'.\n`);
|
|
114
128
|
}
|
|
115
|
-
const
|
|
116
|
-
return
|
|
129
|
+
const started = Date.now();
|
|
130
|
+
return new Promise((resolve) => {
|
|
131
|
+
const chunks = [];
|
|
132
|
+
const child = spawn(command, { cwd: root, shell: true, stdio: ['inherit', 'pipe', 'pipe'] });
|
|
133
|
+
child.stdout.on('data', (chunk) => {
|
|
134
|
+
chunks.push(chunk);
|
|
135
|
+
process.stdout.write(chunk);
|
|
136
|
+
});
|
|
137
|
+
child.stderr.on('data', (chunk) => {
|
|
138
|
+
chunks.push(chunk);
|
|
139
|
+
process.stderr.write(chunk);
|
|
140
|
+
});
|
|
141
|
+
child.on('error', () => resolve(1));
|
|
142
|
+
child.on('close', (code) => {
|
|
143
|
+
const exitCode = code ?? 1;
|
|
144
|
+
writeOverrideSummary(root, command, exitCode, Date.now() - started, Buffer.concat(chunks));
|
|
145
|
+
resolve(exitCode);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Record a configured command's run as a one-check summary under `.check/`,
|
|
151
|
+
* beside its captured output, in the shape checkride writes for its own runs.
|
|
152
|
+
*
|
|
153
|
+
* A summary that cannot be written is reported, never fatal: the gate's
|
|
154
|
+
* verdict is the command's exit code, and a full disk must not turn a green
|
|
155
|
+
* run red.
|
|
156
|
+
*/
|
|
157
|
+
function writeOverrideSummary(root, command, exitCode, durationMs, output) {
|
|
158
|
+
const ok = exitCode === 0;
|
|
159
|
+
const summary = {
|
|
160
|
+
schema_version: 1,
|
|
161
|
+
timestamp: new Date().toISOString(),
|
|
162
|
+
ok,
|
|
163
|
+
checks_run: 1,
|
|
164
|
+
total_duration_ms: durationMs,
|
|
165
|
+
checks: [
|
|
166
|
+
{
|
|
167
|
+
name: command.length <= OVERRIDE_NAME_BUDGET ? command : `${command.slice(0, OVERRIDE_NAME_BUDGET - 1)}…`,
|
|
168
|
+
adapter: null,
|
|
169
|
+
description: `the configured "check" command: ${command}`,
|
|
170
|
+
ok,
|
|
171
|
+
exit_code: exitCode,
|
|
172
|
+
duration_ms: durationMs,
|
|
173
|
+
output_file: OVERRIDE_OUTPUT,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
try {
|
|
178
|
+
const dir = join(root, '.check');
|
|
179
|
+
mkdirSync(dir, { recursive: true });
|
|
180
|
+
writeFileSync(join(dir, OVERRIDE_OUTPUT), output);
|
|
181
|
+
writeFileSync(join(dir, 'summary.json'), `${JSON.stringify(summary, null, 2)}\n`);
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
185
|
+
process.stderr.write(`plumbbob: could not record the check summary under .check/ (${message}).\n`);
|
|
186
|
+
}
|
|
117
187
|
}
|
|
118
188
|
/**
|
|
119
189
|
* The checkride path: run the gate in-process, refuse an all-skipped run, and
|
package/dist/lib/orient.js
CHANGED
|
@@ -311,7 +311,7 @@ function classifyRecapRow(name, value) {
|
|
|
311
311
|
test(/^bent\b/, 'failing') ??
|
|
312
312
|
test(/^drift\b/, 'drift'));
|
|
313
313
|
case 'seam':
|
|
314
|
-
return test(/^held\b/, 'true') ?? test(/^strayed\b/, '
|
|
314
|
+
return test(/^held\b/, 'true') ?? test(/^strayed\b/, 'advisory') ?? test(/^drift\b/, 'drift');
|
|
315
315
|
}
|
|
316
316
|
}
|
|
317
317
|
/**
|
|
@@ -381,6 +381,13 @@ const NOT_STANDING = { glyph: '✗', state: 'Not standing' };
|
|
|
381
381
|
* drift beats a live failure beats an advisory beats plumb, and each rung
|
|
382
382
|
* names the one component that earned it. The advisory rung runs in a fixed
|
|
383
383
|
* order, the seam stray first.
|
|
384
|
+
*
|
|
385
|
+
* A strayed seam is an advisory, never a failure: a path outside the seam
|
|
386
|
+
* says either that the work wandered or that the line was drawn too tight,
|
|
387
|
+
* and truing the diff fixes neither. At the pause the measured seam row
|
|
388
|
+
* carries the stray; at the boundary the tree is clean, the row has vanished,
|
|
389
|
+
* and the stat the checkpoint bumped carries it instead. Both name it
|
|
390
|
+
* `seam strayed`, so the rung does not change between the two.
|
|
384
391
|
*/
|
|
385
392
|
export function foldVerdict(rows, stats) {
|
|
386
393
|
for (const name of RECAP_ROW_NAMES) {
|
|
@@ -394,11 +401,17 @@ export function foldVerdict(rows, stats) {
|
|
|
394
401
|
return { ladder: OUT_OF_PLUMB, worst: `${name} ${row.word}` };
|
|
395
402
|
}
|
|
396
403
|
}
|
|
404
|
+
for (const name of RECAP_ROW_NAMES) {
|
|
405
|
+
const row = rows[name];
|
|
406
|
+
if (row !== undefined && row.verdict === 'advisory') {
|
|
407
|
+
return { ladder: A_HAIR_OFF, worst: `${name} ${row.word}` };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
397
410
|
if (stats.driftWarnings > 0) {
|
|
398
411
|
// First of the advisory rung, and uncounted: the stat bumps once per
|
|
399
412
|
// checkpoint rather than once per stray path, and this is the one advisory
|
|
400
413
|
// that asks a question of the plan rather than of the work.
|
|
401
|
-
return { ladder: A_HAIR_OFF, worst: '
|
|
414
|
+
return { ladder: A_HAIR_OFF, worst: 'seam strayed' };
|
|
402
415
|
}
|
|
403
416
|
if (stats.redChecks > 0) {
|
|
404
417
|
return { ladder: A_HAIR_OFF, worst: `${stats.redChecks} red run${stats.redChecks === 1 ? '' : 's'} before green` };
|
|
@@ -469,10 +482,12 @@ export function summaryCheckRow(summary) {
|
|
|
469
482
|
* (the row then vanishes, or the model's attested row stands).
|
|
470
483
|
*
|
|
471
484
|
* Green sizes the declared seam: how many of its tokens the diff actually
|
|
472
|
-
* touched, and that nothing landed outside them.
|
|
473
|
-
* problem and lets the paths themselves be the evidence: one stray is the
|
|
474
|
-
* pointer, and several break onto `- ` lines.
|
|
475
|
-
*
|
|
485
|
+
* touched, and that nothing landed outside them. A stray states the size of
|
|
486
|
+
* the problem and lets the paths themselves be the evidence: one stray is the
|
|
487
|
+
* `→` pointer, and several break onto `- ` lines. It is an advisory, not a
|
|
488
|
+
* failure (the seam is awareness, not a lock), so the fold puts it on the
|
|
489
|
+
* third rung, never the second. `paths` are work-plane changes, artifacts
|
|
490
|
+
* already filtered by the caller.
|
|
476
491
|
*/
|
|
477
492
|
export function seamRowFromDiff(paths, seam) {
|
|
478
493
|
if (seam.length === 0 || paths.length === 0) {
|
|
@@ -486,8 +501,8 @@ export function seamRowFromDiff(paths, seam) {
|
|
|
486
501
|
const evidence = `strayed: ${outside.length} path${outside.length === 1 ? '' : 's'} outside the seam`;
|
|
487
502
|
const only = outside[0];
|
|
488
503
|
return outside.length === 1 && only !== undefined
|
|
489
|
-
? { verdict: '
|
|
490
|
-
: { verdict: '
|
|
504
|
+
? { verdict: 'advisory', word: 'strayed', evidence, pointer: only }
|
|
505
|
+
: { verdict: 'advisory', word: 'strayed', evidence, items: outside };
|
|
491
506
|
}
|
|
492
507
|
/**
|
|
493
508
|
* Sum numstat entries into the diff row's three counts.
|
|
@@ -687,6 +702,36 @@ export function parseSummary(detail) {
|
|
|
687
702
|
}
|
|
688
703
|
return { lead, highlights };
|
|
689
704
|
}
|
|
705
|
+
/**
|
|
706
|
+
* The `## <n>` sections of `.plumbbob/detail.md` with their bodies: the full
|
|
707
|
+
* story behind each highlight, in the order the model wrote them. The body is
|
|
708
|
+
* the section's prose as written, trimmed; empty when the model wrote only the
|
|
709
|
+
* title. The build-log's Log carries these beneath the step's record, so an
|
|
710
|
+
* older step's "expand" reads the ledger rather than a commit.
|
|
711
|
+
*/
|
|
712
|
+
export function parseSections(detail) {
|
|
713
|
+
const lines = detail.split('\n');
|
|
714
|
+
const sections = [];
|
|
715
|
+
for (let i = 0; i < lines.length; i++) {
|
|
716
|
+
const m = /^##\s+(\d+)\s+(\S.*?)\s*$/.exec(lines[i] ?? '');
|
|
717
|
+
if (m === null) {
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
let end = lines.findIndex((l, j) => j > i && l.startsWith('## '));
|
|
721
|
+
if (end === -1) {
|
|
722
|
+
end = lines.length;
|
|
723
|
+
}
|
|
724
|
+
sections.push({
|
|
725
|
+
n: Number(m[1]),
|
|
726
|
+
title: m[2] ?? '',
|
|
727
|
+
body: lines
|
|
728
|
+
.slice(i + 1, end)
|
|
729
|
+
.join('\n')
|
|
730
|
+
.trim(),
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
return sections;
|
|
734
|
+
}
|
|
690
735
|
/**
|
|
691
736
|
* The `## Recommendation` section of `.plumbbob/detail.md`: the one or two
|
|
692
737
|
* plain sentences a decision turn ends on, or null when the model wrote none.
|
package/dist/lib/sidecar.js
CHANGED
|
@@ -322,10 +322,10 @@ export function clearHandoff(root, slug) {
|
|
|
322
322
|
// --- The detail plane: `.plumbbob/detail.md`, one flat per-session file holding
|
|
323
323
|
// the in-flight step's full detail. The model overwrites it before every pause
|
|
324
324
|
// (the recap first, then numbered sections matching the highlights); checkpoint
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
325
|
+
// records it beneath the step's dated line in the build-log's Log and then
|
|
326
|
+
// clears it, so the durable archive is the tracked ledger and no stale detail
|
|
327
|
+
// piles up. Flat like STATE/TURN, not per-build: a session drives one build at
|
|
328
|
+
// a time, and a past step's expansion reads the ledger, never this file. ---
|
|
329
329
|
/**
|
|
330
330
|
* detail.md: the in-flight step's full detail, at the flat sidecar root.
|
|
331
331
|
*/
|
|
@@ -333,8 +333,8 @@ export function detailPath(root) {
|
|
|
333
333
|
return join(root, DIRNAME, 'detail.md');
|
|
334
334
|
}
|
|
335
335
|
/**
|
|
336
|
-
* Clear detail.md once checkpoint has
|
|
337
|
-
*
|
|
336
|
+
* Clear detail.md once checkpoint has recorded it in the Log; absent is a
|
|
337
|
+
* no-op.
|
|
338
338
|
*
|
|
339
339
|
* The rmSync lives here with the sidecar's other deletions, the same home
|
|
340
340
|
* clearHandoff and clearTick keep.
|
package/dist/verbs/checkpoint.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// `[x]`, and clears the STEP/SEAM markers: returning the dashboard to the
|
|
11
11
|
// DESIGN boundary.
|
|
12
12
|
import { appendFileSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { relative } from 'node:path';
|
|
13
14
|
import { commit, findRepoRoot, headSha, isDirty, stageAll, stagePath, stagedPaths, stagedStat } from "../lib/git.js";
|
|
14
15
|
import { activeBuild, buildFolder, buildLogPath, buildScope, bumpStepStat, checkpointsPath, clearDetail, clearHandoff, clearTick, detailPath, hasSession, intentPath, readStats, refreshExcludes, seamPath, stampStepStat, stepPath, } from "../lib/sidecar.js";
|
|
15
16
|
import { runCheck } from "../lib/check.js";
|
|
@@ -18,10 +19,10 @@ import { checkLatch } from "../lib/latch.js";
|
|
|
18
19
|
import { markStepDone, parseSteps } from "../lib/orient.js";
|
|
19
20
|
import { parseBuildScope, parseStepSeam, scopeDrift } from "../lib/intent.js";
|
|
20
21
|
import { conventionalSubject, subjectFromTitle, withMarker } from "../lib/commitmsg.js";
|
|
21
|
-
import { appendToSection, checkpointLogLine } from "../lib/buildlog.js";
|
|
22
|
+
import { appendToSection, checkpointLogLine, logEntry, planLogLine } from "../lib/buildlog.js";
|
|
22
23
|
import { AT_BOUNDARY, syncBuildLogState } from "../lib/buildlogsync.js";
|
|
23
24
|
import { advisory, ending, notice, transition } from "../lib/notice.js";
|
|
24
|
-
import { boundaryEnding } from "./handoff.js";
|
|
25
|
+
import { boundaryEnding, planRecord, stepRecord } from "./handoff.js";
|
|
25
26
|
/**
|
|
26
27
|
* Land a step: latch, check gate, commit, record, return to the boundary.
|
|
27
28
|
*
|
|
@@ -80,6 +81,13 @@ export async function checkpoint(cwd, args) {
|
|
|
80
81
|
: notice({ fact: 'checkpoint refused', detail: ['the check is red'], remedy: 'fix it, then run it again' }));
|
|
81
82
|
return 1;
|
|
82
83
|
}
|
|
84
|
+
// The step's record, as the pause showed it: measured now, before the work
|
|
85
|
+
// is staged, since the seam and diff rows read the working tree the way the
|
|
86
|
+
// pause did. It lands in the build-log's Log beneath the dated line (the
|
|
87
|
+
// tracked ledger is the archive, and it rides the branch where a commit body
|
|
88
|
+
// would not survive the squash); the commit body keeps its marker and the
|
|
89
|
+
// lead prose only.
|
|
90
|
+
const record = stepRecord(root, activeBuild(root), step, readDetail(root) ?? '');
|
|
83
91
|
let sha;
|
|
84
92
|
// The drift advisory is read off the index (staged, before the commit clears
|
|
85
93
|
// it) but printed inside the ending below, where the fixed order puts it: the
|
|
@@ -88,18 +96,17 @@ export async function checkpoint(cwd, args) {
|
|
|
88
96
|
if (isDirty(root)) {
|
|
89
97
|
stageAll(root);
|
|
90
98
|
drift = scopeDriftNotice(root, step);
|
|
91
|
-
const
|
|
92
|
-
const body = withMarker(`plumbbob step ${step}`, foldDetail(lead, readDetail(root)));
|
|
99
|
+
const body = withMarker(`plumbbob step ${step}`, bodyResult.body ?? fallbackBody(root, step));
|
|
93
100
|
sha = commit(root, messageArg(args) ?? subjectForStep(root, step), body);
|
|
94
|
-
clearDetail(root); // the detail is folded into the commit now; clear it so no stale detail rides into the next step.
|
|
95
101
|
}
|
|
96
102
|
else {
|
|
97
103
|
sha = headSha(root);
|
|
98
104
|
}
|
|
105
|
+
clearDetail(root); // the detail is recorded in the Log below; clear it so no stale detail rides into the next step.
|
|
99
106
|
appendFileSync(checkpointsPath(root), `step ${step} ${sha}\n`);
|
|
100
107
|
const flipNotice = flipIntent(root, step);
|
|
101
108
|
stampStepStat(root, undefined, step, 'landedAt', new Date().toISOString());
|
|
102
|
-
logCheckpoint(root, step, sha);
|
|
109
|
+
const pointer = logCheckpoint(root, step, sha, record);
|
|
103
110
|
// The step landed: the build-log's ☐/☑ mirror flips and its Current-step line
|
|
104
111
|
// returns to the boundary, re-read from the intent.md `flipIntent` just wrote.
|
|
105
112
|
// The artifact-plane whitelist keeps this build-log write from ever reading as
|
|
@@ -115,7 +122,7 @@ export async function checkpoint(cwd, args) {
|
|
|
115
122
|
// pointer both see the boundary the human is standing at.
|
|
116
123
|
const landed = boundaryEnding(root, activeBuild(root), step);
|
|
117
124
|
process.stdout.write(ending({
|
|
118
|
-
lead: transition({ label: 'Checkpoint', fact: `Step ${step} complete`, detail:
|
|
125
|
+
lead: transition({ label: 'Checkpoint', fact: `Step ${step} complete`, detail: leadDetail(sha, pointer) }),
|
|
119
126
|
verdict: landed.verdict,
|
|
120
127
|
advisories: [drift, flipNotice],
|
|
121
128
|
pointer: landed.pointer,
|
|
@@ -156,13 +163,17 @@ function checkpointPlan(root, args) {
|
|
|
156
163
|
const staged = stagePath(root, buildFolder(root));
|
|
157
164
|
const sha = commit(root, planSubject(root), withMarker('plumbbob plan', bodyResult.body ?? undefined));
|
|
158
165
|
appendFileSync(checkpointsPath(root), `plan ${sha}\n`);
|
|
166
|
+
// The cold read that approved the plan is the Log's first entry, beneath a
|
|
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) ?? ''));
|
|
169
|
+
clearDetail(root);
|
|
159
170
|
// Landing the plan consumes `start`'s entry stamp: a later hand-built diff
|
|
160
171
|
// (no `build <n>`) must find no stale TICK and stay guidance-governed.
|
|
161
172
|
clearTick(root);
|
|
162
173
|
// No Verdict: the plan commit measures nothing. The pointer aims at the step
|
|
163
174
|
// the build starts on, which is what `handoff` renders from a null step.
|
|
164
175
|
process.stdout.write(ending({
|
|
165
|
-
lead: transition({ label: 'Plan', fact: 'committed', detail:
|
|
176
|
+
lead: transition({ label: 'Plan', fact: 'committed', detail: leadDetail(sha, pointer) }),
|
|
166
177
|
advisories: staged
|
|
167
178
|
? []
|
|
168
179
|
: [
|
|
@@ -214,7 +225,9 @@ function resolveStep(root, args) {
|
|
|
214
225
|
* step's seam, or '' when it held.
|
|
215
226
|
*
|
|
216
227
|
* Guidance, not a gate: the checkpoint captures the drift and says so, and the
|
|
217
|
-
* bumped stat puts a `
|
|
228
|
+
* bumped stat puts a `seam strayed` rung under the Verdict, the same rung the
|
|
229
|
+
* pause's measured seam row gave it, since the row vanishes once the tree is
|
|
230
|
+
* committed and the stat is what carries the stray past that point. The seam
|
|
218
231
|
* (the step's edit grant: exact paths or `dir/` prefixes) comes from the
|
|
219
232
|
* in-flight SEAM file when a build is live, else the step's declared seam in
|
|
220
233
|
* intent.md. Plumbbob's own artifact plane is whitelisted inside `scopeDrift`,
|
|
@@ -278,25 +291,51 @@ function flipIntent(root, step) {
|
|
|
278
291
|
}
|
|
279
292
|
}
|
|
280
293
|
/**
|
|
281
|
-
*
|
|
294
|
+
* The lead line's trailing parenthetical: the short SHA, and where the Log
|
|
295
|
+
* entry landed when one did, as the same `details:` clause Next Up carries.
|
|
296
|
+
*/
|
|
297
|
+
function leadDetail(sha, pointer) {
|
|
298
|
+
return pointer === null ? [sha.slice(0, 9)] : [sha.slice(0, 9), `details: \`${pointer}\``];
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Append the step's entry to the build-log's `## Log` section: the dated line,
|
|
302
|
+
* and beneath it the record of the pause when the model wrote one.
|
|
282
303
|
*
|
|
283
304
|
* The build's history accrues at each checkpoint instead of being reconstructed
|
|
284
305
|
* at finish; the step's title is lifted from intent.md when still present.
|
|
285
306
|
* Best-effort: a missing or odd build-log never blocks a checkpoint; the
|
|
286
|
-
* `checkpoints` SHA is the source of truth.
|
|
307
|
+
* `checkpoints` SHA is the source of truth. Returns where the line landed, as
|
|
308
|
+
* `path:line` from the repo root, or null when nothing was written.
|
|
309
|
+
*/
|
|
310
|
+
function logCheckpoint(root, step, sha, record) {
|
|
311
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
312
|
+
return appendLog(root, checkpointLogLine(date, step, sha, titleForStep(root, step), statsSuffix(root, step)), record);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Append the plan's entry to the Log: a dated `plan committed` line, and
|
|
316
|
+
* beneath it the cold read that approved the plan when the model wrote one.
|
|
287
317
|
*/
|
|
288
|
-
function
|
|
318
|
+
function logPlan(root, sha, record) {
|
|
319
|
+
return appendLog(root, planLogLine(new Date().toISOString().slice(0, 10), sha), record);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Write one Log entry and say where its dated line landed (`path:line` from
|
|
323
|
+
* the repo root), or null when the section is missing or the write failed.
|
|
324
|
+
*/
|
|
325
|
+
function appendLog(root, line, record) {
|
|
289
326
|
try {
|
|
290
327
|
const path = buildLogPath(root);
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (updated !== null) {
|
|
295
|
-
writeFileSync(path, updated);
|
|
328
|
+
const updated = appendToSection(readFileSync(path, 'utf8'), 'Log', logEntry(line, record));
|
|
329
|
+
if (updated === null) {
|
|
330
|
+
return null;
|
|
296
331
|
}
|
|
332
|
+
writeFileSync(path, updated);
|
|
333
|
+
const at = updated.split('\n').lastIndexOf(line);
|
|
334
|
+
return at === -1 ? null : `${relative(root, path)}:${at + 1}`;
|
|
297
335
|
}
|
|
298
336
|
catch {
|
|
299
337
|
// best-effort ledger; never fail a checkpoint over the build-log.
|
|
338
|
+
return null;
|
|
300
339
|
}
|
|
301
340
|
}
|
|
302
341
|
/**
|
|
@@ -408,8 +447,8 @@ function fallbackBody(root, step) {
|
|
|
408
447
|
* is absent or empty.
|
|
409
448
|
*
|
|
410
449
|
* The model overwrites it before every pause; checkpoint reads it once here for
|
|
411
|
-
* the
|
|
412
|
-
* nothing.
|
|
450
|
+
* the Log's record, and the caller clears it afterward. A missing or blank file
|
|
451
|
+
* records nothing.
|
|
413
452
|
*/
|
|
414
453
|
function readDetail(root) {
|
|
415
454
|
try {
|
|
@@ -420,18 +459,6 @@ function readDetail(root) {
|
|
|
420
459
|
return null;
|
|
421
460
|
}
|
|
422
461
|
}
|
|
423
|
-
/**
|
|
424
|
-
* Fold the detail beneath the lead prose for the commit body.
|
|
425
|
-
*
|
|
426
|
-
* The lead (an explicit `--body`, else the deterministic fallback) leads and the
|
|
427
|
-
* detail file's content follows, joined by a blank line so git keeps them as
|
|
428
|
-
* separate paragraphs. Either part may be absent; an all-empty fold returns
|
|
429
|
-
* undefined so `withMarker` leaves just the marker line.
|
|
430
|
-
*/
|
|
431
|
-
function foldDetail(lead, detail) {
|
|
432
|
-
const parts = [lead, detail].filter((part) => part !== undefined && part !== null && part.length > 0);
|
|
433
|
-
return parts.length > 0 ? parts.join('\n\n') : undefined;
|
|
434
|
-
}
|
|
435
462
|
/**
|
|
436
463
|
* The step's done-when line from intent.md, or null when absent or unreadable.
|
|
437
464
|
*/
|
package/dist/verbs/handoff.js
CHANGED
|
@@ -35,7 +35,7 @@ import { commitsSince, diffNumstat, diffPatch, findRepoRoot } from "../lib/git.j
|
|
|
35
35
|
import { blocks } from "../lib/notice.js";
|
|
36
36
|
import { isArtifactPath, parseStepSeam } from "../lib/intent.js";
|
|
37
37
|
import { checkpointsPath, detailPath, hasSession, inSpike, intentPath, readStats, readTurn, resolveBuild, seamPath, stepPath, tickPath, } from "../lib/sidecar.js";
|
|
38
|
-
import { countDiff, diffRowValue, foldVerdict, lastLedgerSha, parseConstraintCount, parseLastCheckpoint, parseRecap, parseRecommendation, parseSteps, parseSummary, recapLines, seamRowFromDiff, spentRowValue, summaryCheckRow, } from "../lib/orient.js";
|
|
38
|
+
import { countDiff, diffRowValue, foldVerdict, lastLedgerSha, parseConstraintCount, parseLastCheckpoint, parseRecap, parseRecommendation, parseSections, parseSteps, parseSummary, recapLines, seamRowFromDiff, spentRowValue, summaryCheckRow, } from "../lib/orient.js";
|
|
39
39
|
// The inline-diff threshold (docs/presentation.md): at 20 changed lines or
|
|
40
40
|
// fewer the patch rides the turn; at 21 it stays in the working tree behind
|
|
41
41
|
// the counted diff row.
|
|
@@ -171,6 +171,80 @@ export function boundaryEnding(root, slug, step) {
|
|
|
171
171
|
pointer: nextUpLine(nextUp, steps.length, relative(root, intentPath(root, slug))),
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* The step's record for the build-log's Log: the pause as the human approved
|
|
176
|
+
* it, with the full story behind each highlight beneath. The Summary and its
|
|
177
|
+
* highlights, the Readout fence, the Verdict, the recommendation, then the
|
|
178
|
+
* numbered sections' bodies, blank-line separated and unindented; the caller
|
|
179
|
+
* nests it under the Log's dated line.
|
|
180
|
+
*
|
|
181
|
+
* Measured, not recalled: the check, seam, and diff rows are read the same way
|
|
182
|
+
* the pause read them, so this has to be built before the work is staged (the
|
|
183
|
+
* seam and diff rows read the working tree). Null when the detail file carries
|
|
184
|
+
* nothing to record, so a hand-built step with no detail gets the bare dated
|
|
185
|
+
* line it always did.
|
|
186
|
+
*/
|
|
187
|
+
export function stepRecord(root, slug, step, detail) {
|
|
188
|
+
const summary = parseSummary(detail);
|
|
189
|
+
const recommendation = parseRecommendation(detail);
|
|
190
|
+
const sections = parseSections(detail);
|
|
191
|
+
if (summary === null && recommendation === null && sections.length === 0 && parseRecap(detail) === null) {
|
|
192
|
+
// Nothing on the wire's shape: a blank file records nothing, and anything
|
|
193
|
+
// else the model wrote is kept verbatim rather than lost with the file.
|
|
194
|
+
const raw = detail.trim();
|
|
195
|
+
return raw.length === 0 ? null : raw;
|
|
196
|
+
}
|
|
197
|
+
const intent = readOr(intentPath(root, slug));
|
|
198
|
+
const { rows, work, summary: check } = measured(root, slug, intent, detail, step);
|
|
199
|
+
const { ladder, worst } = foldVerdict(rows, accruedStats(root, slug, step));
|
|
200
|
+
const readout = recapLines(rows, {
|
|
201
|
+
diff: diffRowValue(countDiff(work), false),
|
|
202
|
+
spent: spentRowValue(spentInputs(root, slug, step, check)),
|
|
203
|
+
constraints: parseConstraintCount(intent),
|
|
204
|
+
});
|
|
205
|
+
const parts = [];
|
|
206
|
+
if (summary !== null) {
|
|
207
|
+
parts.push(recordSummary(summary));
|
|
208
|
+
}
|
|
209
|
+
if (readout.length > 0) {
|
|
210
|
+
parts.push([readoutLabel(step, parseSteps(intent)), '', ...fence('text', readout)].join('\n'));
|
|
211
|
+
}
|
|
212
|
+
parts.push(verdictLine(ladder, worst));
|
|
213
|
+
return [...withRecommendation(parts, recommendation), ...sectionBlocks(sections)].join('\n\n');
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The plan's record for the Log: the cold read's recommendation and the
|
|
217
|
+
* findings behind it. Nothing is measured at the plan pause, so no Readout and
|
|
218
|
+
* no Verdict ride here. Null when the detail file carries neither.
|
|
219
|
+
*/
|
|
220
|
+
export function planRecord(detail) {
|
|
221
|
+
const recommendation = parseRecommendation(detail);
|
|
222
|
+
const sections = parseSections(detail);
|
|
223
|
+
if (recommendation === null && sections.length === 0) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return [...withRecommendation([], recommendation), ...sectionBlocks(sections)].join('\n\n');
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The record's opening block: the Summary lead without the `(details: …)`
|
|
230
|
+
* bracket, since the record is where the detail now lives, then the
|
|
231
|
+
* highlights under their own handles.
|
|
232
|
+
*/
|
|
233
|
+
function recordSummary(summary) {
|
|
234
|
+
const lead = `**Summary**: ${summary.lead}`;
|
|
235
|
+
if (summary.highlights.length === 0) {
|
|
236
|
+
return lead;
|
|
237
|
+
}
|
|
238
|
+
return [lead, '', ...summary.highlights.map((h) => `${h.n}. ${h.title}`)].join('\n');
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* The full story behind each highlight, one block per section: the handle in
|
|
242
|
+
* bold, the title, then the body as the model wrote it. A section with no body
|
|
243
|
+
* is just its line.
|
|
244
|
+
*/
|
|
245
|
+
function sectionBlocks(sections) {
|
|
246
|
+
return sections.map((s) => (s.body.length === 0 ? `**${s.n}.** ${s.title}` : `**${s.n}.** ${s.title}\n\n${s.body}`));
|
|
247
|
+
}
|
|
174
248
|
/**
|
|
175
249
|
* The driver turn's pointer: back at the step still open, at the spike raised
|
|
176
250
|
* over it, or forward from the boundary when the step has already exited.
|
package/package.json
CHANGED
package/skills/build/SKILL.md
CHANGED
|
@@ -173,8 +173,9 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
|
|
|
173
173
|
sentence, closed by a period, then the reason as a capitalized sentence or two. The lead
|
|
174
174
|
and the recommendation are flowing prose and handoff unwraps both to the renderer's
|
|
175
175
|
width, so never hard-wrap them; handoff also prepends the bold `**Recommendation**:`
|
|
176
|
-
label, which you never type. `checkpoint`
|
|
177
|
-
truncates it, so the detail lands in the
|
|
176
|
+
label, which you never type. `checkpoint` records this file beneath the step's dated
|
|
177
|
+
line in the build-log's `## Log` and then truncates it, so the detail lands in the
|
|
178
|
+
tracked ledger rather than piling up on disk.
|
|
178
179
|
|
|
179
180
|
*Paste the turn.* Run `plumbbob handoff` and paste its output whole, verbatim, at top
|
|
180
181
|
level, trailing blank line included, then end the turn. Never nest it inside a fence of
|
|
@@ -192,7 +193,7 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
|
|
|
192
193
|
|
|
193
194
|
*Then read the reply as an ask or a direction.* A message that **asks** ("expand 2",
|
|
194
195
|
"what does that mean?", "why did the seam row flag that?") is an expand: answer it from the detail file, from
|
|
195
|
-
`git diff`, or from `
|
|
196
|
+
`git diff`, or from the build-log's `## Log` for an older step, never from recall, then run `plumbbob handoff` again and
|
|
196
197
|
paste it. The step is still in flight, so it renders the same pause, and the Your Call
|
|
197
198
|
block stays the CLI's to render rather than yours to retype. A message that **directs**
|
|
198
199
|
is needs-work: take it as what to change, and nothing lands until the human says
|
package/skills/plan/SKILL.md
CHANGED
|
@@ -145,13 +145,16 @@ an agent can follow with `/plumbbob:build`. The argument only seeds how you get
|
|
|
145
145
|
heredoc) when the rationale is worth carrying; skip it for a small plan. Do this
|
|
146
146
|
only on the human's approval; the plan is their convergence.
|
|
147
147
|
- **The plan pause is a decision turn.** Present the framed plan for the human's call,
|
|
148
|
-
then
|
|
149
|
-
|
|
150
|
-
the
|
|
148
|
+
then give it the cold read (**§ The cold read**), write the result into
|
|
149
|
+
`.plumbbob/detail.md`, and relay `plumbbob handoff --plan`'s block whole: the CLI
|
|
150
|
+
renders the your-call block with the moves that apply before anything is recorded and
|
|
151
|
+
prints your recommendation last (the decision tier of the
|
|
152
|
+
[turn anatomy](https://github.com/robmclarty/plumbbob/blob/main/docs/presentation.md)),
|
|
151
153
|
so the moves are relayed, never hand-composed. *looks good* marks the plan decided and
|
|
152
|
-
starts `/plumbbob:build` at the first undone step;
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
starts `/plumbbob:build` at the first undone step; a message that asks (`expand 2`, or
|
|
155
|
+
any question) is answered from the detail file or `intent.md` and changes nothing; a
|
|
156
|
+
message that directs is what to sharpen, cheap now, before any code. The revert move
|
|
157
|
+
vanishes there on its own: nothing has landed to wind back.
|
|
155
158
|
- **The plan commit is latched too.** Once the turn ledger exists, `checkpoint
|
|
156
159
|
--plan` refuses to land in the same turn `start` stamped it: present the plan,
|
|
157
160
|
**end the turn**, and the human's approving message is the tick that lets it
|
|
@@ -179,6 +182,48 @@ an agent can follow with `/plumbbob:build`. The argument only seeds how you get
|
|
|
179
182
|
7. **Offer to stress-test it.** Suggest `/plumbbob:refine` to attack the frame for holes (or
|
|
180
183
|
to repair the plan as it drifts). Optional, the human's call.
|
|
181
184
|
|
|
185
|
+
## The cold read
|
|
186
|
+
|
|
187
|
+
The plan pause ends on your recommendation, and the recommendation is an estimate made
|
|
188
|
+
with fresh eyes, not a re-reading of your own plan. Before you relay the card, give the
|
|
189
|
+
framed plan one bounded adversarial pass under a fixed lens, the pass `/plumbbob:refine`'s
|
|
190
|
+
attack mode makes, but without its writing: surface, never append. Edit nothing, add no
|
|
191
|
+
Open questions, and keep it to one screen of reading. The lens:
|
|
192
|
+
|
|
193
|
+
- an ambiguity a builder would have to guess through;
|
|
194
|
+
- an edge case or a hidden assumption no Decision settles;
|
|
195
|
+
- a collision with the existing code (read the files the seams name);
|
|
196
|
+
- a done-when no test or check can measure, or a seam that names a file that does not
|
|
197
|
+
exist or misses one that must change;
|
|
198
|
+
- a step too large to review in one pass, or a Decision with no *because*.
|
|
199
|
+
|
|
200
|
+
Write what you found into `.plumbbob/detail.md`, overwriting whatever a past session
|
|
201
|
+
left, the findings first and the recommendation last:
|
|
202
|
+
|
|
203
|
+
```markdown
|
|
204
|
+
# Detail · Plan · <the title>
|
|
205
|
+
|
|
206
|
+
## 1 <the first hole: one sentence, naming where it sits in intent.md>
|
|
207
|
+
<what it is, and what closing it would take>
|
|
208
|
+
|
|
209
|
+
## 2 ...
|
|
210
|
+
|
|
211
|
+
## Recommendation
|
|
212
|
+
|
|
213
|
+
<The move.> <The reason, one or two sentences.>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The recommendation takes one of two shapes. A sound plan gets `Approve it.` and what the
|
|
217
|
+
read checked and found sound. A plan with a hole gets `Sharpen <the one worst hole> first.`
|
|
218
|
+
and why it matters; when the read found more than one, say how many sit behind `expand`
|
|
219
|
+
and name `/plumbbob:refine` as the move that writes them up as Open questions, with their
|
|
220
|
+
plain and lean sub-lines, for the human to settle. Three findings at most; the rest is
|
|
221
|
+
refine's. `plumbbob handoff --plan` prints the label and your recommendation as the turn's
|
|
222
|
+
last text, "expand 2" opens `## 2`, and `checkpoint --plan` records the read beneath the
|
|
223
|
+
plan's line in the build-log's `## Log`, the build's first entry. The cold read is the tip of refine: it makes the
|
|
224
|
+
full attack discoverable at the moment it would pay, and lets a sound plan skip it. Refine
|
|
225
|
+
is where the real adversary looks; this is an estimate, and it says so by staying short.
|
|
226
|
+
|
|
182
227
|
## The interview (mode 1)
|
|
183
228
|
|
|
184
229
|
Make it easy and non-intrusive:
|
package/skills/verify/SKILL.md
CHANGED
|
@@ -109,8 +109,8 @@ this skill verifies it the same way: **it reads the diff, not the author**
|
|
|
109
109
|
width, so never hard-wrap them; handoff also prepends the bold `**Recommendation**:`
|
|
110
110
|
label, which you never type. This is the **one** file `/plumbbob:verify` writes, and it
|
|
111
111
|
is turn presentation, never the diff under review; the author-blind contract below still
|
|
112
|
-
holds. `checkpoint`
|
|
113
|
-
detail plane in the
|
|
112
|
+
holds. `checkpoint` records this file beneath the step's dated line in the build-log's
|
|
113
|
+
`## Log` and then truncates it (the detail plane in the
|
|
114
114
|
[turn anatomy](https://github.com/robmclarty/plumbbob/blob/main/docs/presentation.md)).
|
|
115
115
|
|
|
116
116
|
*Paste the turn.* Run `plumbbob handoff` and paste its output whole, verbatim, at top
|
|
@@ -130,7 +130,7 @@ this skill verifies it the same way: **it reads the diff, not the author**
|
|
|
130
130
|
|
|
131
131
|
*Then read the reply as an ask or a direction.* A message that **asks** ("expand 2",
|
|
132
132
|
"what does that mean?", "why did the seam row flag that?") is an expand: answer it from the detail file, from
|
|
133
|
-
`git diff`, or from `
|
|
133
|
+
`git diff`, or from the build-log's `## Log` for an older step, never from recall, then run `plumbbob handoff` again and
|
|
134
134
|
paste it. The step is still in flight, so it renders the same pause, and the Your Call
|
|
135
135
|
block stays the CLI's to render rather than yours to retype. A message that **directs**
|
|
136
136
|
is needs-work: take it as what to change, and nothing lands until the human says
|
package/templates/build-log.md
CHANGED
|
@@ -51,9 +51,10 @@ Harvest results this boundary:
|
|
|
51
51
|
|
|
52
52
|
## Log
|
|
53
53
|
|
|
54
|
-
*(The build's history, oldest first. `plumbbob checkpoint` appends
|
|
55
|
-
every time a step lands (via `/plumbbob:build` or `/plumbbob:verify`)
|
|
56
|
-
|
|
54
|
+
*(The build's history, oldest first. `plumbbob checkpoint` appends an entry here
|
|
55
|
+
every time a step lands (via `/plumbbob:build` or `/plumbbob:verify`): the dated line,
|
|
56
|
+
and beneath it the pause as you approved it, so this fills in as you go, not at the
|
|
57
|
+
end, and an older step's detail is a scroll away. Add your own decision/event lines too: this is what
|
|
57
58
|
you point at to say "I did that: the LLM helped, but those were my calls."
|
|
58
59
|
`/plumbbob:finish` reads this for the report; `plumbbob finish` commits it with the build
|
|
59
60
|
folder, so it rides the branch into the PR.)*
|