plumbbob 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +15 -10
- package/dist/cli-core.js +25 -3
- package/dist/lib/git.js +20 -8
- package/dist/lib/notice.js +15 -1
- package/dist/lib/orient.js +266 -19
- package/dist/lib/sidecar.js +149 -3
- package/dist/verbs/abandon.js +19 -14
- package/dist/verbs/build.js +45 -22
- package/dist/verbs/checkpoint.js +34 -25
- package/dist/verbs/doctor.js +23 -4
- package/dist/verbs/finish.js +21 -17
- package/dist/verbs/handoff.js +235 -65
- package/dist/verbs/order.js +110 -0
- package/dist/verbs/park.js +6 -2
- package/dist/verbs/recover.js +31 -1
- package/dist/verbs/relay.js +306 -0
- package/dist/verbs/revert.js +17 -11
- package/dist/verbs/spike.js +50 -38
- package/dist/verbs/start.js +13 -9
- package/dist/verbs/status.js +2 -2
- package/dist/verbs/turn.js +14 -7
- package/dist/verbs/use.js +21 -17
- package/hooks/hooks.json +7 -0
- 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 +47 -20
- package/skills/harvest/SKILL.md +4 -1
- package/skills/order/SKILL.md +50 -0
- package/skills/park/SKILL.md +7 -0
- package/skills/plan/SKILL.md +4 -1
- package/skills/refine/SKILL.md +6 -2
- package/skills/step/SKILL.md +8 -3
- package/skills/verify/SKILL.md +42 -22
- package/templates/intent.md +9 -0
package/dist/lib/sidecar.js
CHANGED
|
@@ -213,6 +213,21 @@ export function stepPath(root, slug) {
|
|
|
213
213
|
export function clearStep(root, slug) {
|
|
214
214
|
rmSync(stepPath(root, slug), { force: true });
|
|
215
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* PAUSE: the stamp of the step pause `handoff` last rendered whole, the step
|
|
218
|
+
* number and a hash of what the card showed. While a later render matches it,
|
|
219
|
+
* handoff prints the one-line reminder instead of the card.
|
|
220
|
+
*/
|
|
221
|
+
export function pausePath(root, slug) {
|
|
222
|
+
return join(artifactDir(root, slug), 'PAUSE');
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Remove the PAUSE stamp: a checkpoint, a revert, or an abandon ends the pause
|
|
226
|
+
* it recorded, so the next pause renders whole. Absent is a no-op.
|
|
227
|
+
*/
|
|
228
|
+
export function clearPause(root, slug) {
|
|
229
|
+
rmSync(pausePath(root, slug), { force: true });
|
|
230
|
+
}
|
|
216
231
|
/**
|
|
217
232
|
* The build's `checkpoints` ledger: the recorded SHAs of landed plan and step
|
|
218
233
|
* commits, one line each.
|
|
@@ -536,6 +551,131 @@ export function readTurn(root) {
|
|
|
536
551
|
return null;
|
|
537
552
|
}
|
|
538
553
|
}
|
|
554
|
+
// --- The relay record: `.plumbbob/RELAY`, the ending the CLI printed last, so a
|
|
555
|
+
// Stop hook can check that the turn closed on it. The verb that prints an
|
|
556
|
+
// ending records it here: it knows what it printed, where a hook scraping
|
|
557
|
+
// stdout would have to guess which command ran. Flat like TURN, because the
|
|
558
|
+
// turn it is checked against is the session's, not a build's. A later ending
|
|
559
|
+
// overwrites an earlier one, so a turn is judged by the last thing it printed. ---
|
|
560
|
+
// The tiers of docs/presentation.md, which decide how a turn has to carry an
|
|
561
|
+
// ending: a decision ending closes the turn, an orientation ending appears in
|
|
562
|
+
// it, and a driver ending is recorded only so that the last ending wins.
|
|
563
|
+
const RELAY_TIERS = ['decision', 'orientation', 'driver'];
|
|
564
|
+
// Which ending printed: handoff's six forms first, then one per transition. It
|
|
565
|
+
// is what the check's reason names when the ending went missing.
|
|
566
|
+
const RELAY_RENDERS = [
|
|
567
|
+
'card',
|
|
568
|
+
'reminder',
|
|
569
|
+
'plan',
|
|
570
|
+
'boundary',
|
|
571
|
+
'pointer',
|
|
572
|
+
'driver',
|
|
573
|
+
'checkpoint',
|
|
574
|
+
'plan-commit',
|
|
575
|
+
'park',
|
|
576
|
+
'revert',
|
|
577
|
+
'abandon',
|
|
578
|
+
'spike-open',
|
|
579
|
+
'spike-report',
|
|
580
|
+
'spike-done',
|
|
581
|
+
'use',
|
|
582
|
+
'order',
|
|
583
|
+
'finish',
|
|
584
|
+
'start',
|
|
585
|
+
];
|
|
586
|
+
/**
|
|
587
|
+
* RELAY: the ending the CLI printed last, at the flat sidecar root.
|
|
588
|
+
*/
|
|
589
|
+
export function relayPath(root) {
|
|
590
|
+
return join(root, DIRNAME, 'RELAY');
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Record an ending in RELAY, stamped with the current TURN: a header of
|
|
594
|
+
* `key value` lines, a blank line, then the text exactly as it printed.
|
|
595
|
+
*
|
|
596
|
+
* Written only while TURN holds a count, the same dormancy `stampTick` keeps: a
|
|
597
|
+
* host with no turn hook has no turn to check the ending against. Best-effort
|
|
598
|
+
* by contract, since the ending has already reached stdout and a failed record
|
|
599
|
+
* must not change it or the exit code.
|
|
600
|
+
*/
|
|
601
|
+
export function writeRelay(root, relay) {
|
|
602
|
+
const turn = readTurn(root);
|
|
603
|
+
if (turn === null)
|
|
604
|
+
return;
|
|
605
|
+
// A hold the check already spent on this turn survives the ending the retry
|
|
606
|
+
// reprints, or a turn whose retry missed again would be held a second time.
|
|
607
|
+
const held = readRelay(root)?.held === turn ? { held: turn } : {};
|
|
608
|
+
try {
|
|
609
|
+
writeFileSync(relayPath(root), formatRelay({ ...relay, turn, ...held }));
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
// A read-only tree or a directory in the way: the ending already printed.
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Stamp the record with the turn the check held open over it, so the check
|
|
617
|
+
* holds at most once per turn. False when the stamp could not be written, and
|
|
618
|
+
* the check then holds nothing, since a hold it cannot count could repeat.
|
|
619
|
+
*/
|
|
620
|
+
export function holdRelay(root, record) {
|
|
621
|
+
try {
|
|
622
|
+
writeFileSync(relayPath(root), formatRelay({ ...record, held: record.turn }));
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/** A record as RELAY stores it: the header's `key value` lines, a blank line, then the text. */
|
|
630
|
+
function formatRelay(record) {
|
|
631
|
+
const header = [`turn ${record.turn}`, `tier ${record.tier}`, `render ${record.render}`];
|
|
632
|
+
if (record.command !== undefined)
|
|
633
|
+
header.push(`command ${record.command}`);
|
|
634
|
+
if (record.held !== undefined)
|
|
635
|
+
header.push(`held ${record.held}`);
|
|
636
|
+
return `${header.join('\n')}\n\n${record.text}`;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* The recorded ending, or null when RELAY is absent or its header does not
|
|
640
|
+
* parse: a record the check cannot read is no record.
|
|
641
|
+
*/
|
|
642
|
+
export function readRelay(root) {
|
|
643
|
+
let raw;
|
|
644
|
+
try {
|
|
645
|
+
raw = readFileSync(relayPath(root), 'utf8');
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
const split = raw.indexOf('\n\n');
|
|
651
|
+
if (split === -1)
|
|
652
|
+
return null;
|
|
653
|
+
const fields = new Map();
|
|
654
|
+
for (const line of raw.slice(0, split).split('\n')) {
|
|
655
|
+
const space = line.indexOf(' ');
|
|
656
|
+
if (space > 0)
|
|
657
|
+
fields.set(line.slice(0, space), line.slice(space + 1));
|
|
658
|
+
}
|
|
659
|
+
const turn = count(fields.get('turn'));
|
|
660
|
+
const tier = RELAY_TIERS.find((t) => t === fields.get('tier'));
|
|
661
|
+
const render = RELAY_RENDERS.find((r) => r === fields.get('render'));
|
|
662
|
+
if (turn === null || tier === undefined || render === undefined)
|
|
663
|
+
return null;
|
|
664
|
+
const command = fields.get('command');
|
|
665
|
+
const held = count(fields.get('held'));
|
|
666
|
+
return {
|
|
667
|
+
turn,
|
|
668
|
+
tier,
|
|
669
|
+
render,
|
|
670
|
+
...(command === undefined ? {} : { command }),
|
|
671
|
+
...(held === null ? {} : { held }),
|
|
672
|
+
text: raw.slice(split + 2),
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
/** A header field read as a bare count, or null when it is absent or anything else. */
|
|
676
|
+
function count(field) {
|
|
677
|
+
return field !== undefined && /^\d+$/.test(field) ? Number(field) : null;
|
|
678
|
+
}
|
|
539
679
|
/**
|
|
540
680
|
* Walk up from `cwd` to the nearest ancestor with an active session (a
|
|
541
681
|
* `.plumbbob/STATE` file), or null when there is none.
|
|
@@ -600,16 +740,22 @@ export function excludeControl(root) {
|
|
|
600
740
|
`${DIRNAME}/TURN`,
|
|
601
741
|
`${DIRNAME}/GRANT`,
|
|
602
742
|
// The in-flight step's detail file: flat per-session control the model
|
|
603
|
-
// overwrites before every pause. checkpoint
|
|
604
|
-
// and then clears it, so its archive is the
|
|
605
|
-
// excluding it keeps stageAll from ever sweeping it into a step commit.
|
|
743
|
+
// overwrites before every pause. checkpoint records it in the build-log's
|
|
744
|
+
// Log and then clears it, so its archive is the tracked ledger, never this
|
|
745
|
+
// path; excluding it keeps stageAll from ever sweeping it into a step commit.
|
|
606
746
|
`${DIRNAME}/detail.md`,
|
|
747
|
+
// The ending the CLI printed last, for the Stop check: per-worktree
|
|
748
|
+
// control, rewritten by every ending, and never a step commit's to carry.
|
|
749
|
+
`${DIRNAME}/RELAY`,
|
|
607
750
|
`${DIRNAME}/builds/*/STEP`,
|
|
608
751
|
`${DIRNAME}/builds/*/SEAM`,
|
|
609
752
|
`${DIRNAME}/builds/*/SPIKE`,
|
|
610
753
|
// The entry stamp is in-flight control like STEP/SEAM: checkpoint's
|
|
611
754
|
// stageAll must never sweep it into a step commit.
|
|
612
755
|
`${DIRNAME}/builds/*/TICK`,
|
|
756
|
+
// The pause stamp is in-flight control too: what handoff last showed, so
|
|
757
|
+
// a repeat prints the reminder. It must never ride a step commit.
|
|
758
|
+
`${DIRNAME}/builds/*/PAUSE`,
|
|
613
759
|
// The agent-run handoff ledger is step-scoped in-flight state, not a
|
|
614
760
|
// tracked artifact: it must never ride a step commit into the PR.
|
|
615
761
|
`${DIRNAME}/builds/*/handoff.json`,
|
package/dist/verbs/abandon.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// `plumbbob abandon`: the third exit from an in-flight step. Checkpoint lands
|
|
2
2
|
// the step and revert destroys the work; abandon drops the attempt but keeps the
|
|
3
3
|
// working-tree diff exactly where it is. Its only writes are the in-flight
|
|
4
|
-
// control markers it clears (STEP, SEAM, TICK, and the
|
|
5
|
-
// ledger), a build-log line, and the stats receipt: it
|
|
6
|
-
// working tree nor git nor the intent checkbox, so the step
|
|
7
|
-
// re-buildable with its diff still in the tree for the human
|
|
8
|
-
// commit by hand.
|
|
4
|
+
// control markers it clears (STEP, SEAM, TICK, the PAUSE stamp, and the
|
|
5
|
+
// step-scoped handoff ledger), a build-log line, and the stats receipt: it
|
|
6
|
+
// touches neither the working tree nor git nor the intent checkbox, so the step
|
|
7
|
+
// stays planned and re-buildable with its diff still in the tree for the human
|
|
8
|
+
// to keep, rework, or commit by hand.
|
|
9
9
|
//
|
|
10
10
|
// A step exit is a boundary crossing, so abandon honors the same approval latch
|
|
11
11
|
// as checkpoint. That is not decoration: abandon clears the entry stamp the
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
// control-state clears; abandon itself never calls the raw fs deleters.
|
|
16
16
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
17
17
|
import { findRepoRoot } from "../lib/git.js";
|
|
18
|
-
import { bumpStepStat, buildLogPath, clearHandoff, clearSeam, clearStep, clearTick, hasSession, intentPath, resolveBuild, stepPath, } from "../lib/sidecar.js";
|
|
18
|
+
import { bumpStepStat, buildLogPath, clearHandoff, clearPause, clearSeam, clearStep, clearTick, hasSession, intentPath, resolveBuild, stepPath, } from "../lib/sidecar.js";
|
|
19
19
|
import { checkLatch } from "../lib/latch.js";
|
|
20
20
|
import { abandonLogLine, appendToSection } from "../lib/buildlog.js";
|
|
21
21
|
import { parseSteps } from "../lib/orient.js";
|
|
22
|
-
import { ending, notice, transition } from "../lib/notice.js";
|
|
22
|
+
import { ending, notice, printEnding, transition } from "../lib/notice.js";
|
|
23
23
|
import { driverPointer } from "./handoff.js";
|
|
24
24
|
/**
|
|
25
25
|
* Drop the in-flight step while keeping its work: latch, clear the markers,
|
|
@@ -55,18 +55,23 @@ export function abandon(cwd, args) {
|
|
|
55
55
|
clearSeam(root, slug);
|
|
56
56
|
clearTick(root, slug);
|
|
57
57
|
clearHandoff(root, slug);
|
|
58
|
+
clearPause(root, slug);
|
|
58
59
|
bumpStepStat(root, slug, step, 'abandons');
|
|
59
60
|
logAbandon(root, slug, step);
|
|
60
61
|
// A step exit: with the step back at `[ ]` and its markers gone, the pointer
|
|
61
62
|
// aims forward from the boundary, at the very step just dropped.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
printEnding(root, {
|
|
64
|
+
tier: 'driver',
|
|
65
|
+
render: 'abandon',
|
|
66
|
+
text: ending({
|
|
67
|
+
lead: transition({
|
|
68
|
+
label: 'Abandoned',
|
|
69
|
+
fact: `Step ${step}`,
|
|
70
|
+
detail: ['work kept in the tree', 'the step stays planned'],
|
|
71
|
+
}),
|
|
72
|
+
pointer: driverPointer(root, slug),
|
|
67
73
|
}),
|
|
68
|
-
|
|
69
|
-
}));
|
|
74
|
+
});
|
|
70
75
|
return 0;
|
|
71
76
|
}
|
|
72
77
|
/**
|
package/dist/verbs/build.js
CHANGED
|
@@ -8,7 +8,7 @@ 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
13
|
import { readGrant } from "../lib/latch.js";
|
|
14
14
|
import { notice } from "../lib/notice.js";
|
|
@@ -18,8 +18,8 @@ import { notice } from "../lib/notice.js";
|
|
|
18
18
|
* Refuses a missing session, a malformed step argument, an `N-M` range (a
|
|
19
19
|
* `/plumbbob:build` skill feature, not a CLI one), and a seam that fails to parse.
|
|
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
|
|
22
|
-
*
|
|
21
|
+
* build-log's Current step line, and names the step when it is the last one a
|
|
22
|
+
* self-approval grant reaches.
|
|
23
23
|
*/
|
|
24
24
|
export function build(cwd, args) {
|
|
25
25
|
const root = findRepoRoot(cwd);
|
|
@@ -48,7 +48,9 @@ export function build(cwd, args) {
|
|
|
48
48
|
return 1;
|
|
49
49
|
}
|
|
50
50
|
const intent = readFileSync(intentPath(root, slug), 'utf8');
|
|
51
|
-
|
|
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);
|
|
52
54
|
// No argument ⇒ enter the next undone step in intent.md (the same idiom
|
|
53
55
|
// `checkpoint` uses), so a bare `plumbbob build` advances the loop without the
|
|
54
56
|
// skill re-deriving the step in prose. Every step checkpointed ⇒ a `/plumbbob:step`
|
|
@@ -89,30 +91,20 @@ export function build(cwd, args) {
|
|
|
89
91
|
const title = steps.find((s) => s.n === step)?.title ?? null;
|
|
90
92
|
syncBuildLogState(root, slug, stepLabel(step, title));
|
|
91
93
|
// An explicit jump past undone work says so out loud: the entered step and
|
|
92
|
-
// the count it skips
|
|
93
|
-
//
|
|
94
|
-
|
|
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;
|
|
95
98
|
const picked = raw === undefined
|
|
96
99
|
? ['next undone']
|
|
97
100
|
: skipped > 0
|
|
98
101
|
? ['explicitly requested', `skips ${skipped} undone step${skipped === 1 ? '' : 's'}`]
|
|
99
102
|
: [];
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
// it at a pause, which is the choice a range kept getting wrong.
|
|
104
|
-
const grant = readGrant(root);
|
|
105
|
-
const top = grant?.kind === 'range' && grant.ceiling === step
|
|
106
|
-
? notice({
|
|
107
|
-
fact: `step ${step} is the top of the range you granted`,
|
|
108
|
-
remedy: 'land it on green, then stop at the boundary',
|
|
109
|
-
})
|
|
110
|
-
: '';
|
|
111
|
-
// Two lines (three at a range's top), one colon each, then the seam as a
|
|
112
|
-
// plain readout beneath the notice that frames it: the paths are a list, and
|
|
113
|
-
// a list is not a one-liner.
|
|
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.
|
|
114
106
|
process.stdout.write(notice({ fact: `building step ${step}`, detail: picked }) +
|
|
115
|
-
|
|
107
|
+
lastGrantedNotice(readGrant(root), steps, step) +
|
|
116
108
|
notice({
|
|
117
109
|
fact: 'the seam is orientation, not a lock',
|
|
118
110
|
detail: [`${parsed.seam.length} path${parsed.seam.length === 1 ? '' : 's'}`],
|
|
@@ -120,3 +112,34 @@ export function build(cwd, args) {
|
|
|
120
112
|
`${parsed.seam.map((path) => ` ${path}`).join('\n')}\n`);
|
|
121
113
|
return 0;
|
|
122
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
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
import { appendFileSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { relative } from 'node:path';
|
|
14
14
|
import { commit, findRepoRoot, headSha, isDirty, stageAll, stagePath, stagedPaths, stagedStat } from "../lib/git.js";
|
|
15
|
-
import { activeBuild, buildFolder, buildLogPath, buildScope, bumpStepStat, checkpointsPath, clearDetail, clearHandoff, clearTick, detailPath, hasSession, intentPath, readStats, refreshExcludes, seamPath, stampStepStat, stepPath, } from "../lib/sidecar.js";
|
|
15
|
+
import { activeBuild, buildFolder, buildLogPath, buildScope, bumpStepStat, checkpointsPath, clearDetail, clearHandoff, clearPause, clearTick, detailPath, hasSession, intentPath, readStats, refreshExcludes, seamPath, stampStepStat, stepPath, } from "../lib/sidecar.js";
|
|
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, parseDetailStep, 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";
|
|
23
23
|
import { AT_BOUNDARY, syncBuildLogState } from "../lib/buildlogsync.js";
|
|
24
|
-
import { advisory, ending, notice, transition } from "../lib/notice.js";
|
|
24
|
+
import { advisory, ending, notice, printEnding, transition } from "../lib/notice.js";
|
|
25
25
|
import { boundaryEnding, planRecord, stepRecord } from "./handoff.js";
|
|
26
26
|
/**
|
|
27
27
|
* Land a step: latch, check gate, commit, record, return to the boundary.
|
|
@@ -116,17 +116,22 @@ export async function checkpoint(cwd, args) {
|
|
|
116
116
|
rmSync(stepPath(root), { force: true });
|
|
117
117
|
clearHandoff(root); // the agent-run handoff ledger is step-scoped; clear it with the markers.
|
|
118
118
|
clearTick(root); // the step's entry stamp is spent; the next `build <n>` re-stamps.
|
|
119
|
+
clearPause(root); // the pause it stamped is over; the next step's pause renders whole.
|
|
119
120
|
// The whole ending, one block: the lead line, the Verdict folded over what
|
|
120
121
|
// just landed, the advisories this checkpoint alone can state, and the
|
|
121
122
|
// pointer. Read after the flip and the ledger append, so the fold and the
|
|
122
123
|
// pointer both see the boundary the human is standing at.
|
|
123
124
|
const landed = boundaryEnding(root, activeBuild(root), step);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
printEnding(root, {
|
|
126
|
+
tier: 'orientation',
|
|
127
|
+
render: 'checkpoint',
|
|
128
|
+
text: ending({
|
|
129
|
+
lead: transition({ label: 'Checkpoint', fact: `Step ${step} complete`, detail: leadDetail(sha, pointer) }),
|
|
130
|
+
verdict: landed.verdict,
|
|
131
|
+
advisories: [drift, flipNotice],
|
|
132
|
+
pointer: landed.pointer,
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
130
135
|
return 0;
|
|
131
136
|
}
|
|
132
137
|
/**
|
|
@@ -172,19 +177,23 @@ function checkpointPlan(root, args) {
|
|
|
172
177
|
clearTick(root);
|
|
173
178
|
// No Verdict: the plan commit measures nothing. The pointer aims at the step
|
|
174
179
|
// the build starts on, which is what `handoff` renders from a null step.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
180
|
+
printEnding(root, {
|
|
181
|
+
tier: 'orientation',
|
|
182
|
+
render: 'plan-commit',
|
|
183
|
+
text: ending({
|
|
184
|
+
lead: transition({ label: 'Plan', fact: 'committed', detail: leadDetail(sha, pointer) }),
|
|
185
|
+
advisories: staged
|
|
186
|
+
? []
|
|
187
|
+
: [
|
|
188
|
+
advisory({
|
|
189
|
+
fact: 'the plan rides the commit message',
|
|
190
|
+
detail: ['record-only', '.plumbbob/ is gitignored, so the files stay untracked'],
|
|
191
|
+
remedy: 'unignore .plumbbob/builds/ to keep the record in the tree',
|
|
192
|
+
}),
|
|
193
|
+
],
|
|
194
|
+
pointer: boundaryEnding(root, activeBuild(root), null).pointer,
|
|
195
|
+
}),
|
|
196
|
+
});
|
|
188
197
|
return 0;
|
|
189
198
|
}
|
|
190
199
|
/**
|
|
@@ -200,7 +209,7 @@ function planSubject(root) {
|
|
|
200
209
|
/**
|
|
201
210
|
* Resolve the step being checkpointed, or null when none can be determined.
|
|
202
211
|
*
|
|
203
|
-
* Explicit arg > in-flight STEP file > first undone step in
|
|
212
|
+
* Explicit arg > in-flight STEP file > first undone step in build order. A `-m`
|
|
204
213
|
* value is a message, never a step: `checkpoint -m "2"` must not read as
|
|
205
214
|
* step 2.
|
|
206
215
|
*/
|
|
@@ -214,7 +223,7 @@ function resolveStep(root, args) {
|
|
|
214
223
|
return inFlight;
|
|
215
224
|
}
|
|
216
225
|
try {
|
|
217
|
-
return
|
|
226
|
+
return parseOrderedSteps(readFileSync(intentPath(root), 'utf8')).find((s) => !s.done)?.n ?? null;
|
|
218
227
|
}
|
|
219
228
|
catch {
|
|
220
229
|
return null;
|
package/dist/verbs/doctor.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
//
|
|
3
3
|
// 1. The global plugin link (read-only). After `plumbbob init`, plumbbob lives as a
|
|
4
4
|
// symlink at ~/.claude/skills/plumbbob pointing at the installed package; Claude
|
|
5
|
-
// Code loads it in place as a plugin (skills `/plumbbob:*`, the
|
|
5
|
+
// Code loads it in place as a plugin (skills `/plumbbob:*`, the hooks from
|
|
6
6
|
// hooks.json). doctor verifies the link resolves to a package carrying the manifest,
|
|
7
|
-
// the skills, and the
|
|
7
|
+
// the skills, and the hooks, and names the fix for anything missing. The failure
|
|
8
8
|
// class it exists for is SILENT (a `/plumbbob:status` that opens an empty dashboard
|
|
9
9
|
// because the plugin never linked).
|
|
10
10
|
//
|
|
@@ -80,7 +80,7 @@ function linkedPackage(link) {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
/**
|
|
83
|
-
* The link-install checks: manifest, skill set, and
|
|
83
|
+
* The link-install checks: manifest, skill set, and hooks, each with its fix.
|
|
84
84
|
*
|
|
85
85
|
* `shipped` is the canonical skill list read from the package itself; an installed
|
|
86
86
|
* count below it means a stale or partial link, and every fix is the same re-link.
|
|
@@ -96,10 +96,29 @@ function buildChecks(link, pkg, shipped) {
|
|
|
96
96
|
? { ok: true, label: `skills present (${installed.length}) — load as /plumbbob:*` }
|
|
97
97
|
: { ok: false, label: `skills incomplete (${installed.length}/${shipped.length})`, fix: 're-link: plumbbob init' },
|
|
98
98
|
existsSync(join(pkg, 'hooks', 'hooks.json'))
|
|
99
|
-
? { ok: true, label: '
|
|
99
|
+
? { ok: true, label: hooksLabel(wiredEvents(join(pkg, 'hooks', 'hooks.json'))) }
|
|
100
100
|
: { ok: false, label: 'hook missing (hooks/hooks.json)', fix: 're-link: plumbbob init' },
|
|
101
101
|
];
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* The hooks line's label: every event the package wires, so a link that
|
|
105
|
+
* predates a hook reads as one at a glance. A file whose events cannot be read
|
|
106
|
+
* still counts as present, the way the line read before it named any.
|
|
107
|
+
*/
|
|
108
|
+
function hooksLabel(events) {
|
|
109
|
+
const named = events.length === 0 ? '' : `: ${events.join(', ')}`;
|
|
110
|
+
return `hooks present (hooks/hooks.json${named}; auto-registers)`;
|
|
111
|
+
}
|
|
112
|
+
/** The events a hooks.json wires, in file order, or none when it does not parse. */
|
|
113
|
+
function wiredEvents(path) {
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
116
|
+
return typeof parsed.hooks === 'object' && parsed.hooks !== null ? Object.keys(parsed.hooks) : [];
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
103
122
|
/**
|
|
104
123
|
* The plugin-link diagnostic: how plumbbob is installed into Claude Code, as checks.
|
|
105
124
|
*
|
package/dist/verbs/finish.js
CHANGED
|
@@ -17,7 +17,7 @@ import { readCommitBody } from "../lib/commitbody.js";
|
|
|
17
17
|
import { buildScope, checkpointsPath, clearTick, hasSession, intentPath, readStats, refreshExcludes, reportPath, resolveBuild, seamPath, setGrant, sidecarDir, spikePath, stepPath, } from "../lib/sidecar.js";
|
|
18
18
|
import { conventionalSubject, withMarker } from "../lib/commitmsg.js";
|
|
19
19
|
import { parseBuildScope } from "../lib/intent.js";
|
|
20
|
-
import { advisory, ending, notice, transition } from "../lib/notice.js";
|
|
20
|
+
import { advisory, ending, notice, printEnding, transition } from "../lib/notice.js";
|
|
21
21
|
// The pointer finish leaves the turn on. Every other one is `handoff`'s, but a
|
|
22
22
|
// finished session has no state left to render from, and past a closed build
|
|
23
23
|
// the only move is framing the next goal.
|
|
@@ -84,23 +84,27 @@ export function finish(cwd, args = []) {
|
|
|
84
84
|
// the turn's last text. finish renders that pointer itself: it just cleared
|
|
85
85
|
// the session `handoff` would read one from.
|
|
86
86
|
const where = slug === null ? '.plumbbob/' : `.plumbbob/builds/${slug}/`;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
printEnding(root, {
|
|
88
|
+
tier: 'orientation',
|
|
89
|
+
render: 'finish',
|
|
90
|
+
text: ending({
|
|
91
|
+
lead: transition({
|
|
92
|
+
label: 'Session',
|
|
93
|
+
fact: 'finished',
|
|
94
|
+
detail: [sha.slice(0, 9), `${where} rides your branch into the PR`],
|
|
95
|
+
}),
|
|
96
|
+
advisories: reported
|
|
97
|
+
? []
|
|
98
|
+
: [
|
|
99
|
+
advisory({
|
|
100
|
+
fact: 'no report.md found',
|
|
101
|
+
detail: ['finished without one', 'no gate here by design'],
|
|
102
|
+
remedy: '/plumbbob:finish normally writes the report first',
|
|
103
|
+
}),
|
|
104
|
+
],
|
|
105
|
+
pointer: NEXT_UP,
|
|
92
106
|
}),
|
|
93
|
-
|
|
94
|
-
? []
|
|
95
|
-
: [
|
|
96
|
-
advisory({
|
|
97
|
-
fact: 'no report.md found',
|
|
98
|
-
detail: ['finished without one', 'no gate here by design'],
|
|
99
|
-
remedy: '/plumbbob:finish normally writes the report first',
|
|
100
|
-
}),
|
|
101
|
-
],
|
|
102
|
-
pointer: NEXT_UP,
|
|
103
|
-
}));
|
|
107
|
+
});
|
|
104
108
|
return 0;
|
|
105
109
|
}
|
|
106
110
|
/**
|