plumbbob 0.13.2 → 0.13.4
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 +10 -7
- package/dist/lib/orient.js +340 -3
- package/dist/verbs/order.js +65 -21
- package/package.json +1 -1
- package/skills/harvest/SKILL.md +17 -12
- package/skills/order/SKILL.md +19 -16
- package/skills/refine/SKILL.md +17 -10
- package/skills/step/SKILL.md +28 -23
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plumbbob",
|
|
3
3
|
"displayName": "PlumbBob",
|
|
4
|
-
"version": "0.13.
|
|
4
|
+
"version": "0.13.4",
|
|
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
|
@@ -49,7 +49,7 @@ PlumbBob itself, running its own loop.
|
|
|
49
49
|
## The loop in one picture
|
|
50
50
|
|
|
51
51
|
<p align="center">
|
|
52
|
-
<img src="
|
|
52
|
+
<img src="site/media/loop-one-step.svg" alt="One step of the loop, drawn as a wall going up course by course: on the scaffold a person reads a plumb line at the corner, and down the wall a mechanical arm holds the next stone and waits" width="760">
|
|
53
53
|
</p>
|
|
54
54
|
|
|
55
55
|
Everything the model must stay true to lives in one file, the build's `intent.md`
|
|
@@ -72,7 +72,8 @@ a decision the plan left open or after a stretch of steps worth reviewing.
|
|
|
72
72
|
`/plumbbob:build` is the same loop a step at a time, stopping at every step's pause, for
|
|
73
73
|
a plan without ranges or a stretch you want to walk slowly. Nothing is locked and nothing
|
|
74
74
|
refuses you: the loop does the labor, pulls up to the line, and waits for you to advance
|
|
75
|
-
it. **You are the clock.**
|
|
75
|
+
it. **You are the clock.** [`docs/runs.md`](docs/runs.md) walks one plan through its runs
|
|
76
|
+
and then drives it by hand, the two side by side.
|
|
76
77
|
|
|
77
78
|
## What a prompt can't replicate
|
|
78
79
|
|
|
@@ -101,7 +102,7 @@ payoff is [measured, not asserted](#why-not-just), not just claimed. **Guidance
|
|
|
101
102
|
work, a latch on the record.**
|
|
102
103
|
|
|
103
104
|
<p align="center">
|
|
104
|
-
<img src="
|
|
105
|
+
<img src="site/media/build-record.svg" alt="The record in the wall, drawn: the scaffold is down and stacked on the ground, a bench mark is cut into the end stone of every course, and in the open cornerstone lie the rolled plan, tied with the red line, and the ledger, laid into the wall to stay" width="760">
|
|
105
106
|
</p>
|
|
106
107
|
|
|
107
108
|
## Install
|
|
@@ -183,7 +184,7 @@ In Claude Code, inside any git repo with a clean tree:
|
|
|
183
184
|
**Recommendation**: Approve it, refilling on read. The gate is green and the seam held, and a bucket that refills when it is read needs no timer for the route to clean up.
|
|
184
185
|
````
|
|
185
186
|
|
|
186
|
-

|
|
187
188
|
|
|
188
189
|
(The check gate is [checkride](https://www.npmjs.com/package/checkride), one
|
|
189
190
|
run across the tools your repo already configures: types, lint, tests, dead
|
|
@@ -203,7 +204,7 @@ In Claude Code, inside any git repo with a clean tree:
|
|
|
203
204
|
· range 2 of 2: /plumbbob:run (steps 2-3)
|
|
204
205
|
```
|
|
205
206
|
|
|
206
|
-

|
|
207
208
|
|
|
208
209
|
Ask instead (`expand 2`, or any question) and it shows more of what is there and
|
|
209
210
|
changes nothing. The answer closes on one line pointing back at the step, not on the
|
|
@@ -360,14 +361,16 @@ that matters.
|
|
|
360
361
|
> under [`docs/evals/`](docs/evals/): the route the latch owns (*no checkpoint over a red
|
|
361
362
|
> check under pressure*) went from **2/5 prose-only to 5/5 latched** on the first sweep
|
|
362
363
|
> ([2026-07-11](docs/evals/2026-07-11.md)), every sweep since has held it and closed the
|
|
363
|
-
> two gaps that one surfaced,
|
|
364
|
-
>
|
|
364
|
+
> two gaps that one surfaced, [2026-09-17](docs/evals/2026-09-17.md) began scoring the
|
|
365
|
+
> shape of the turn itself, and the latest ([2026-10-19](docs/evals/2026-10-19.md)) holds a
|
|
366
|
+
> run to its stop card, 5/5.
|
|
365
367
|
|
|
366
368
|
## Documentation
|
|
367
369
|
|
|
368
370
|
Each doc answers one question (in rough reading order for a new user):
|
|
369
371
|
|
|
370
372
|
- *What does a session actually look like?* → [`docs/happy-path.md`](docs/happy-path.md): one goal walked end to end; read this first.
|
|
373
|
+
- *How do I drive a plan with runs, and what do they replace?* → [`docs/runs.md`](docs/runs.md): ranges, the build order, and the stop card's harvest, walked through three runs and set beside the manual build, harvest, and order moves.
|
|
371
374
|
- *What do the words mean?* → [`docs/glossary.md`](docs/glossary.md): every term the loop mints, one line each, with where to read more.
|
|
372
375
|
- *Show me the artifacts it leaves behind.* → [`examples/`](examples/): that same session's finished build folder, file by file.
|
|
373
376
|
- *Should I / can I / what about…?* → [`docs/faq.md`](docs/faq.md): the adoption questions, answered straight.
|
package/dist/lib/orient.js
CHANGED
|
@@ -356,7 +356,7 @@ export function rangeStanding(plan, ordered, step) {
|
|
|
356
356
|
return {
|
|
357
357
|
count,
|
|
358
358
|
holding,
|
|
359
|
-
command: flag === null && left.length > 0 ? `/plumbbob:build ${leftExpression(holding, left)}` : null,
|
|
359
|
+
command: flag === null && left.length > 0 ? `/plumbbob:build ${leftExpression(holding, left.map((s) => s.n))}` : null,
|
|
360
360
|
flag,
|
|
361
361
|
completed: ordered[at]?.done === true && rangeLanded(holding, ordered) ? holding : null,
|
|
362
362
|
drift,
|
|
@@ -420,8 +420,7 @@ function stopCardMove(item, count, follow) {
|
|
|
420
420
|
* with each climbing run folded to a span, a bare range's lone number included.
|
|
421
421
|
*/
|
|
422
422
|
function leftExpression(range, left) {
|
|
423
|
-
|
|
424
|
-
return !range.bare && ns.length === 1 ? `${ns[0]}-${ns[0]}` : formatStepList(ns);
|
|
423
|
+
return !range.bare && left.length === 1 ? `${left[0]}-${left[0]}` : formatStepList(left);
|
|
425
424
|
}
|
|
426
425
|
/** The words for steps no range names: `no range names step 12`, or `steps 11, 12`. */
|
|
427
426
|
function noRangeWords(steps) {
|
|
@@ -515,6 +514,344 @@ function stepPhrase(items) {
|
|
|
515
514
|
const one = items.length === 1 && /^\d+$/.test(items[0] ?? '');
|
|
516
515
|
return { words: `${one ? 'step' : 'steps'} ${items.join(', ')}`, be: one ? 'is' : 'are' };
|
|
517
516
|
}
|
|
517
|
+
// The most ways of placing one tangle's ranges the re-cut tries, counted as
|
|
518
|
+
// the product of each range's stretches. A step moved out of a range splits it
|
|
519
|
+
// in two, so one moved step makes two and three make eight, and an order that
|
|
520
|
+
// scatters past this is a re-cut for the human anyway.
|
|
521
|
+
const ORDER_CAP = 64;
|
|
522
|
+
/**
|
|
523
|
+
* Re-cut the plan's ranges to follow its build order, and write each command
|
|
524
|
+
* the re-cut changed into its item.
|
|
525
|
+
*
|
|
526
|
+
* The plan comes back as it was, with nothing re-cut, when the order settles
|
|
527
|
+
* nothing, and when an item's opener does not hold its command where the write
|
|
528
|
+
* can find it, so a re-cut is written whole or not at all.
|
|
529
|
+
*/
|
|
530
|
+
export function recutPlan(intent) {
|
|
531
|
+
const plan = parseBuildRanges(intent);
|
|
532
|
+
const recut = recutRanges(plan, parseOrderedSteps(intent));
|
|
533
|
+
const written = recut.length === 0 ? null : writeRecut(intent, plan.ranges, recut);
|
|
534
|
+
return written === null ? { intent, recut: [] } : { intent: written, recut };
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* The ranges re-cut so that each holds one stretch of the build order, with
|
|
538
|
+
* every undone step in one wherever the order settles which.
|
|
539
|
+
*
|
|
540
|
+
* Only an open range moves. A landed range takes no step, since its call was
|
|
541
|
+
* made; a bare range keeps its one step and takes no other, since it builds
|
|
542
|
+
* that step to its pause; and every range keeps a step of its own, so no stop
|
|
543
|
+
* is left with nothing to stop on. Where the order interleaves the steps of two
|
|
544
|
+
* or more ranges, they are re-cut by the fewest steps moved between them, and
|
|
545
|
+
* left as written when two re-cuts tie or the one would split a range's longest
|
|
546
|
+
* stretch. A step no range names then joins one by where the order puts it, the
|
|
547
|
+
* way a moved step does: the range around it, else the one it builds ahead of,
|
|
548
|
+
* else the one it builds after. Nothing is re-cut when an item does not parse, an
|
|
549
|
+
* open range names a step the plan lacks, or two open ranges name one undone
|
|
550
|
+
* step, since which range holds what is the human's call there.
|
|
551
|
+
*/
|
|
552
|
+
function recutRanges(plan, ordered) {
|
|
553
|
+
const open = plan.ranges.filter((r) => !rangeLanded(r, ordered));
|
|
554
|
+
const owners = ownership(open, ordered);
|
|
555
|
+
if (owners === null || plan.malformed.length > 0 || open.some((r) => missingDrift(r, ordered).length > 0)) {
|
|
556
|
+
return [];
|
|
557
|
+
}
|
|
558
|
+
const undone = ordered.filter((s) => !s.done).map((s) => s.n);
|
|
559
|
+
const placed = new Map(owners);
|
|
560
|
+
const unsettled = new Set();
|
|
561
|
+
for (const tangle of tangles(undone.filter((n) => owners.has(n)), owners)) {
|
|
562
|
+
const moved = fewestMoves(tangle, owners);
|
|
563
|
+
if (moved === null) {
|
|
564
|
+
tangle.tangled.forEach((r) => unsettled.add(r));
|
|
565
|
+
}
|
|
566
|
+
moved?.forEach((range, n) => placed.set(n, range));
|
|
567
|
+
}
|
|
568
|
+
placeUnnamed(undone, placed, unsettled);
|
|
569
|
+
return open.flatMap((range) => {
|
|
570
|
+
const was = ordered.filter((s) => hasStep(range.steps, s.n)).map((s) => s.n);
|
|
571
|
+
const now = ordered.filter((s) => (s.done ? hasStep(range.steps, s.n) : placed.get(s.n) === range)).map((s) => s.n);
|
|
572
|
+
return was.join() === now.join() ? [] : [{ item: range.item, steps: now }];
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Each undone step's open range, or null when two open ranges name one undone
|
|
577
|
+
* step, which no sequence can settle.
|
|
578
|
+
*/
|
|
579
|
+
function ownership(open, ordered) {
|
|
580
|
+
const owners = new Map();
|
|
581
|
+
for (const step of ordered.filter((s) => !s.done)) {
|
|
582
|
+
const named = open.filter((r) => hasStep(r.steps, step.n));
|
|
583
|
+
if (named.length > 1) {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
if (named[0] !== undefined) {
|
|
587
|
+
owners.set(step.n, named[0]);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return owners;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* The owned steps' tangles. A range's span runs from its first step in build
|
|
594
|
+
* order to its last, and ranges whose spans overlap are tangled; a range alone
|
|
595
|
+
* in its span is already one stretch. Each tangle carries the range on either
|
|
596
|
+
* side of it as well, since a step at the tangle's edge may join one, and two
|
|
597
|
+
* tangles that share such a range are one.
|
|
598
|
+
*/
|
|
599
|
+
function tangles(owned, owners) {
|
|
600
|
+
const cores = spanGroups(owned, owners);
|
|
601
|
+
const bounds = [];
|
|
602
|
+
cores.forEach((core, i) => {
|
|
603
|
+
const last = bounds.at(-1);
|
|
604
|
+
if (core.ranges.length < 2) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (last !== undefined && i - 1 <= last.hi) {
|
|
608
|
+
last.hi = Math.min(i + 1, cores.length - 1);
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
bounds.push({ lo: Math.max(i - 1, 0), hi: Math.min(i + 1, cores.length - 1) });
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
return bounds.map(({ lo, hi }) => {
|
|
615
|
+
const run = cores.slice(lo, hi + 1);
|
|
616
|
+
return {
|
|
617
|
+
tangled: run.flatMap((c) => (c.ranges.length > 1 ? c.ranges : [])),
|
|
618
|
+
ranges: run.flatMap((c) => c.ranges),
|
|
619
|
+
steps: owned.slice(run[0]?.first ?? 0, (run.at(-1)?.last ?? -1) + 1),
|
|
620
|
+
};
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* The owned steps grouped by span, in build order: ranges whose spans overlap
|
|
625
|
+
* share a group, and each group carries where its first step and its last sit.
|
|
626
|
+
*/
|
|
627
|
+
function spanGroups(owned, owners) {
|
|
628
|
+
const spans = new Map();
|
|
629
|
+
owned.forEach((n, i) => {
|
|
630
|
+
const range = owners.get(n);
|
|
631
|
+
if (range !== undefined) {
|
|
632
|
+
spans.set(range, { first: spans.get(range)?.first ?? i, last: i });
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
const groups = [];
|
|
636
|
+
for (const [range, span] of [...spans].sort((a, b) => a[1].first - b[1].first)) {
|
|
637
|
+
const group = groups.at(-1);
|
|
638
|
+
if (group !== undefined && span.first < group.last) {
|
|
639
|
+
group.ranges.push(range);
|
|
640
|
+
group.last = Math.max(group.last, span.last);
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
groups.push({ ranges: [range], first: span.first, last: span.last });
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return groups;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* The tangle re-cut by the fewest steps moved between its ranges, as each
|
|
650
|
+
* step's range once re-cut; null when no cut keeps every range to its rules,
|
|
651
|
+
* when two cuts move as few, since the order settles neither, when the one
|
|
652
|
+
* cut splits a range's longest stretch, since that moves the range's body
|
|
653
|
+
* rather than a step, or when too many sequences of its ranges are in play.
|
|
654
|
+
*
|
|
655
|
+
* The ranges may run in any sequence their stretches allow, and each sequence
|
|
656
|
+
* is cut the way that keeps the most steps where they were.
|
|
657
|
+
*/
|
|
658
|
+
function fewestMoves(tangle, owners) {
|
|
659
|
+
const stretches = stretchesOf(tangle.steps, owners);
|
|
660
|
+
const cuts = (rangeOrders(tangle.ranges, stretches) ?? []).flatMap((order) => cutInOrder(order, tangle.steps, owners) ?? []);
|
|
661
|
+
const most = Math.max(...cuts.map((c) => c.kept));
|
|
662
|
+
const best = cuts.filter((c) => c.kept === most);
|
|
663
|
+
const [only] = best;
|
|
664
|
+
if (only === undefined || best.length > 1 || only.ways > 1 || !keepsBodies(tangle, stretches, only.placed)) {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
return new Map(only.placed);
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Whether a cut keeps each range's body: one of its longest stretches, whole,
|
|
671
|
+
* so a re-cut moves the steps the order moved and never the bulk of a range.
|
|
672
|
+
*/
|
|
673
|
+
function keepsBodies(tangle, stretches, placed) {
|
|
674
|
+
return tangle.ranges.every((range) => {
|
|
675
|
+
const own = stretches.filter((s) => s.range === range);
|
|
676
|
+
const longest = Math.max(...own.map((s) => s.size));
|
|
677
|
+
return own.some((s) => s.size === longest && tangle.steps.slice(s.start, s.start + s.size).every((n) => placed.get(n) === range));
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* The sequences a tangle's ranges may run in: each range placed where one of
|
|
682
|
+
* its stretches sits, so every sequence a cut could take, each once; null when
|
|
683
|
+
* the stretches would make more than `ORDER_CAP` choices.
|
|
684
|
+
*/
|
|
685
|
+
function rangeOrders(ranges, stretches) {
|
|
686
|
+
const anchors = ranges.map((range) => stretches.filter((s) => s.range === range));
|
|
687
|
+
if (anchors.reduce((n, a) => n * a.length, 1) > ORDER_CAP) {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
const choices = anchors.reduce((sets, options) => sets.flatMap((set) => options.map((o) => [...set, o])), [[]]);
|
|
691
|
+
const orders = new Map();
|
|
692
|
+
for (const choice of choices) {
|
|
693
|
+
const order = [...choice].sort((a, b) => a.start - b.start).map((s) => s.range);
|
|
694
|
+
orders.set(order.map((r) => r.item).join(), order);
|
|
695
|
+
}
|
|
696
|
+
return [...orders.values()];
|
|
697
|
+
}
|
|
698
|
+
/** A tangle's steps as stretches, each an unbroken run of one range's steps. */
|
|
699
|
+
function stretchesOf(steps, owners) {
|
|
700
|
+
const runs = [];
|
|
701
|
+
steps.forEach((n, i) => {
|
|
702
|
+
const range = owners.get(n);
|
|
703
|
+
const last = runs.at(-1);
|
|
704
|
+
if (range !== undefined && last?.range === range && last.start + last.size === i) {
|
|
705
|
+
last.size += 1;
|
|
706
|
+
}
|
|
707
|
+
else if (range !== undefined) {
|
|
708
|
+
runs.push({ range, start: i, size: 1 });
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
return runs;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* A tangle cut in one sequence of its ranges, keeping the most steps in the
|
|
715
|
+
* range that named them; null when no cut keeps every range to its rules.
|
|
716
|
+
*
|
|
717
|
+
* Each range takes one block of the tangle's steps, in the sequence given, and
|
|
718
|
+
* each block holds a step of its own; a bare range's block is its one step. A
|
|
719
|
+
* block ends on a step of its own unless a bare range follows it, so a step
|
|
720
|
+
* moved between two ranges joins the one it builds ahead of, or the one before
|
|
721
|
+
* when the one ahead is bare, the rule a step no range names joins by. The
|
|
722
|
+
* search keeps, for each range in turn and each step its block may end at, the
|
|
723
|
+
* most steps kept, so it costs the ranges times the steps squared.
|
|
724
|
+
*/
|
|
725
|
+
function cutInOrder(order, steps, owners) {
|
|
726
|
+
const rows = [[{ kept: 0, ways: 1, from: -1 }]];
|
|
727
|
+
order.forEach((range, j) => {
|
|
728
|
+
const counts = runningCount(steps, owners, range);
|
|
729
|
+
const row = [];
|
|
730
|
+
rows[j]?.forEach((cell, from) => {
|
|
731
|
+
const prior = order[j - 1];
|
|
732
|
+
if (cell === undefined || (prior !== undefined && !range.bare && owners.get(steps[from - 1] ?? 0) !== prior)) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
for (const block of rangeBlocks(range, counts, from)) {
|
|
736
|
+
keep(row, block.to, cell, block.kept, from);
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
rows.push(row);
|
|
740
|
+
});
|
|
741
|
+
const end = rows[order.length]?.[steps.length];
|
|
742
|
+
return end === undefined ? null : { kept: end.kept, ways: end.ways, placed: walkBack(rows, order, steps) };
|
|
743
|
+
}
|
|
744
|
+
/** How many of a range's own steps sit among a tangle's first `i` steps, for every `i` from none to all. */
|
|
745
|
+
function runningCount(steps, owners, range) {
|
|
746
|
+
const counts = [0];
|
|
747
|
+
for (const n of steps) {
|
|
748
|
+
counts.push((counts.at(-1) ?? 0) + (owners.get(n) === range ? 1 : 0));
|
|
749
|
+
}
|
|
750
|
+
return counts;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* The blocks a range can take from a tangle's step `from`: where each ends,
|
|
754
|
+
* and how many of the range's own steps it keeps. Each keeps one at least, and
|
|
755
|
+
* a bare range's block is its one step.
|
|
756
|
+
*/
|
|
757
|
+
function rangeBlocks(range, counts, from) {
|
|
758
|
+
const last = Math.min(range.bare ? from + 1 : Infinity, counts.length - 1);
|
|
759
|
+
const blocks = [];
|
|
760
|
+
for (let to = from + 1; to <= last; to += 1) {
|
|
761
|
+
const kept = (counts[to] ?? 0) - (counts[from] ?? 0);
|
|
762
|
+
if (kept > 0) {
|
|
763
|
+
blocks.push({ to, kept });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return blocks;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Record a block in the re-cut's search, in place: the cell where it ends keeps
|
|
770
|
+
* the most steps any way there does, and a second way to as many is a tie.
|
|
771
|
+
*/
|
|
772
|
+
function keep(row, to, prior, kept, from) {
|
|
773
|
+
const total = prior.kept + kept;
|
|
774
|
+
const at = row[to];
|
|
775
|
+
if (at === undefined || total > at.kept) {
|
|
776
|
+
row[to] = { kept: total, ways: prior.ways, from };
|
|
777
|
+
}
|
|
778
|
+
else if (total === at.kept) {
|
|
779
|
+
row[to] = { ...at, ways: Math.min(2, at.ways + prior.ways) };
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
/** Each step's range along the cut the search kept, read back from the last range's block. */
|
|
783
|
+
function walkBack(rows, order, steps) {
|
|
784
|
+
const placed = new Map();
|
|
785
|
+
let to = steps.length;
|
|
786
|
+
order.forEach((_, i) => {
|
|
787
|
+
const j = order.length - i;
|
|
788
|
+
const from = rows[j]?.[to]?.from ?? 0;
|
|
789
|
+
const range = order[j - 1];
|
|
790
|
+
if (range !== undefined) {
|
|
791
|
+
steps.slice(from, to).forEach((n) => placed.set(n, range));
|
|
792
|
+
}
|
|
793
|
+
to = from;
|
|
794
|
+
});
|
|
795
|
+
return placed;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Give each run of steps no range names the range the order puts it in, in
|
|
799
|
+
* place in `placed`, read from the ranges on either side of the run. A run no
|
|
800
|
+
* range can take stays in none, and the dashboard flags it as before.
|
|
801
|
+
*/
|
|
802
|
+
function placeUnnamed(undone, placed, unsettled) {
|
|
803
|
+
const named = undone.map((n) => placed.get(n));
|
|
804
|
+
let start = 0;
|
|
805
|
+
while (start < undone.length) {
|
|
806
|
+
let end = start;
|
|
807
|
+
while (end < undone.length && named[end] === undefined) {
|
|
808
|
+
end += 1;
|
|
809
|
+
}
|
|
810
|
+
const range = end === start ? null : joinRange(named.slice(0, start).findLast((r) => r !== undefined), named[end], unsettled);
|
|
811
|
+
if (range !== null) {
|
|
812
|
+
undone.slice(start, end).forEach((n) => placed.set(n, range));
|
|
813
|
+
}
|
|
814
|
+
start = Math.max(end, start + 1);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* The range a run of steps no range names joins, from the ranges on either
|
|
819
|
+
* side of it in build order: the range around it, else the one it builds ahead
|
|
820
|
+
* of, else the one it builds after. A bare range takes no other step, and a
|
|
821
|
+
* range the order left tangled takes none either, since where its stretch runs
|
|
822
|
+
* is still the human's call.
|
|
823
|
+
*/
|
|
824
|
+
function joinRange(before, after, unsettled) {
|
|
825
|
+
if ((before !== undefined && unsettled.has(before)) || (after !== undefined && unsettled.has(after))) {
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
if (before !== undefined && before === after) {
|
|
829
|
+
return before;
|
|
830
|
+
}
|
|
831
|
+
return [after, before].find((r) => r !== undefined && !r.bare) ?? null;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Write each re-cut range's command into its item's opener: its steps in build
|
|
835
|
+
* order, a landing range down to one step written `9-9` so it still lands. The
|
|
836
|
+
* stop, the model, and every other line of the item stay as written. Null when
|
|
837
|
+
* an opener does not hold its command in its first backticks, so the caller
|
|
838
|
+
* writes nothing rather than half a re-cut.
|
|
839
|
+
*/
|
|
840
|
+
function writeRecut(intent, ranges, recut) {
|
|
841
|
+
const lines = intent.split('\n');
|
|
842
|
+
for (const cut of recut) {
|
|
843
|
+
const range = ranges.find((r) => r.item === cut.item);
|
|
844
|
+
const at = (range?.line ?? 0) - 1;
|
|
845
|
+
const opener = lines[at] ?? '';
|
|
846
|
+
const quoted = /`([^`]*)`/.exec(opener);
|
|
847
|
+
if (range === undefined || quoted === null || (quoted[1] ?? '').trim() !== range.command) {
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
const command = `\`/plumbbob:build ${leftExpression(range, cut.steps)}\``;
|
|
851
|
+
lines[at] = `${opener.slice(0, quoted.index)}${command}${opener.slice(quoted.index + quoted[0].length)}`;
|
|
852
|
+
}
|
|
853
|
+
return lines.join('\n');
|
|
854
|
+
}
|
|
518
855
|
// --- The run: what `/plumbbob:run` names. The turn hook reads it to mint the
|
|
519
856
|
// run's grant, `status --run` to print the run header, and the card to name the
|
|
520
857
|
// run beneath Next Up, so all three read one answer from one plan. ---
|
package/dist/verbs/order.js
CHANGED
|
@@ -7,15 +7,18 @@
|
|
|
7
7
|
// `checkpoint`, `handoff`), so the dashboard's `← next`, a bare
|
|
8
8
|
// `plumbbob build`, and the card's Next Up agree. The verb validates what a
|
|
9
9
|
// hand edit cannot: every number names an undone step, none twice. `--reset`
|
|
10
|
-
// drops the line, and the plan reads in document order again.
|
|
11
|
-
//
|
|
12
|
-
//
|
|
10
|
+
// drops the line, and the plan reads in document order again. Either way the
|
|
11
|
+
// build ranges are re-cut to follow the order written, wherever the order
|
|
12
|
+
// settles which range a step is in, so a step placed next runs in the range
|
|
13
|
+
// it now builds with. It runs in any state, since re-sequencing what comes
|
|
14
|
+
// after the step in flight is the mid-step use, and the sidecar sits outside
|
|
15
|
+
// every seam.
|
|
13
16
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
14
17
|
import { findRepoRoot } from "../lib/git.js";
|
|
15
18
|
import { activeBuild, hasSession, intentPath } from "../lib/sidecar.js";
|
|
16
|
-
import { clearBuildOrder, orderSteps, parseSteps, setBuildOrder } from "../lib/orient.js";
|
|
17
|
-
import { expandStepList, parseStepList } from "../lib/steplist.js";
|
|
18
|
-
import { ending, notice, printEnding, transition } from "../lib/notice.js";
|
|
19
|
+
import { clearBuildOrder, orderSteps, parseBuildRanges, parseOrderedSteps, parseSteps, rangeStanding, recutPlan, setBuildOrder, } from "../lib/orient.js";
|
|
20
|
+
import { expandStepList, formatStepList, parseStepList } from "../lib/steplist.js";
|
|
21
|
+
import { advisory, blocks, ending, notice, printEnding, transition } from "../lib/notice.js";
|
|
19
22
|
import { driverPointer } from "./handoff.js";
|
|
20
23
|
/**
|
|
21
24
|
* Set the build order to the given step numbers and spans, the rest of the
|
|
@@ -36,15 +39,7 @@ export function order(cwd, args) {
|
|
|
36
39
|
const path = intentPath(root);
|
|
37
40
|
const intent = readFileSync(path, 'utf8');
|
|
38
41
|
if (args.includes('--reset')) {
|
|
39
|
-
|
|
40
|
-
printEnding(root, {
|
|
41
|
-
tier: 'driver',
|
|
42
|
-
render: 'order',
|
|
43
|
-
text: ending({
|
|
44
|
-
lead: transition({ label: 'Build order', fact: 'document order' }),
|
|
45
|
-
pointer: driverPointer(root, activeBuild(root)),
|
|
46
|
-
}),
|
|
47
|
-
});
|
|
42
|
+
writeOrder(root, path, clearBuildOrder(intent), 'document order');
|
|
48
43
|
return 0;
|
|
49
44
|
}
|
|
50
45
|
// Numbers and spans arrive space- or comma-separated, with or without a space
|
|
@@ -98,17 +93,66 @@ export function order(cwd, args) {
|
|
|
98
93
|
const full = orderSteps(steps, given)
|
|
99
94
|
.filter((s) => !s.done)
|
|
100
95
|
.map((s) => s.n);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
96
|
+
writeOrder(root, path, setBuildOrder(intent, full), full.join(', '));
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Write the plan under its new build order, its ranges re-cut to follow it,
|
|
101
|
+
* and print the ending: the order, the ranges the re-cut changed, any range it
|
|
102
|
+
* left for the human to re-cut, and the pointer.
|
|
103
|
+
*
|
|
104
|
+
* The pointer reads the plan back from disk, so it already follows the order
|
|
105
|
+
* just written: back at the step in flight, or forward from the boundary to
|
|
106
|
+
* the first step in the new sequence.
|
|
107
|
+
*/
|
|
108
|
+
function writeOrder(root, path, ordered, sequence) {
|
|
109
|
+
const { intent, recut } = recutPlan(ordered);
|
|
110
|
+
writeFileSync(path, intent);
|
|
111
|
+
const standing = rangeStanding(parseBuildRanges(intent), parseOrderedSteps(intent), null);
|
|
105
112
|
printEnding(root, {
|
|
106
113
|
tier: 'driver',
|
|
107
114
|
render: 'order',
|
|
108
115
|
text: ending({
|
|
109
|
-
lead: transition({ label: 'Build order', fact:
|
|
116
|
+
lead: blocks([transition({ label: 'Build order', fact: sequence }), recutLead(recut, standing.count)]),
|
|
117
|
+
advisories: rangeAdvisories(standing),
|
|
110
118
|
pointer: driverPointer(root, activeBuild(root)),
|
|
111
119
|
}),
|
|
112
120
|
});
|
|
113
|
-
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The lead naming each range the re-cut changed and the steps it names now,
|
|
124
|
+
* `range 2 of 3 re-cut (steps 10,4-6)`; null when it changed none.
|
|
125
|
+
*/
|
|
126
|
+
function recutLead(recut, count) {
|
|
127
|
+
const [only] = recut;
|
|
128
|
+
if (only === undefined) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const steps = (cut) => `${cut.steps.length === 1 ? 'step' : 'steps'} ${formatStepList(cut.steps)}`;
|
|
132
|
+
const items = recut.map((cut) => cut.item).join(', ');
|
|
133
|
+
return transition({
|
|
134
|
+
label: 'Build ranges',
|
|
135
|
+
fact: `${recut.length === 1 ? 'range' : 'ranges'} ${items} of ${count} re-cut`,
|
|
136
|
+
detail: recut.length === 1 ? [steps(only)] : recut.map((cut) => `range ${cut.item}: ${steps(cut)}`),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Every range still flagged once the re-cut is written, in the words the
|
|
141
|
+
* dashboard flags it with, and beneath the last the move that re-cuts it by
|
|
142
|
+
* hand: the ties the order did not settle, a step no range can take, and the
|
|
143
|
+
* plan's other drift. Empty when every undone step sits in one stretch of one
|
|
144
|
+
* range.
|
|
145
|
+
*/
|
|
146
|
+
function rangeAdvisories(standing) {
|
|
147
|
+
const { count, drift } = standing;
|
|
148
|
+
const own = drift.filter((d) => d.item !== null && d.kind !== 'malformed');
|
|
149
|
+
const flagged = [...new Set(own.map((d) => d.item))].map((item) => ({
|
|
150
|
+
fact: `range ${item} of ${count} needs a re-cut`,
|
|
151
|
+
detail: own.filter((d) => d.item === item).map((d) => d.words),
|
|
152
|
+
}));
|
|
153
|
+
const lines = [...flagged, ...drift.filter((d) => d.item === null || d.kind === 'malformed').map((d) => ({ fact: d.words }))];
|
|
154
|
+
if (lines.length === 0) {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
return [lines.map((l, i) => advisory(i === lines.length - 1 ? { ...l, remedy: '/plumbbob:refine ranges' } : l)).join('')];
|
|
114
158
|
}
|
package/package.json
CHANGED
package/skills/harvest/SKILL.md
CHANGED
|
@@ -77,18 +77,23 @@ the call on each one theirs. The detail file you write first is the proposal; re
|
|
|
77
77
|
- A confirmed **blocker** also folds its decision into `intent.md`, slugged at birth
|
|
78
78
|
like any Decision, anchored the same way
|
|
79
79
|
(`- <a id="d5"></a>**D5 (retry-cap)**: <the call>, *because* <why>`), never a bare `D5`.
|
|
80
|
-
A blocker that becomes a new step takes the next free number under `## Steps` and
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
carries `## Build ranges`, the
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
80
|
+
A blocker that becomes a new step takes the next free number under `## Steps` and its
|
|
81
|
+
place in the sequence through `plumbbob order 7 8 5` (the `## Build order` line, every
|
|
82
|
+
undone step in the sequence to build them), never by renumbering, even when it builds
|
|
83
|
+
last. When the plan carries `## Build ranges`, the verb files the new step for you: it
|
|
84
|
+
re-cuts the ranges to follow the sequence it writes, so the step joins the range still
|
|
85
|
+
to come that the order puts it in, the one it builds ahead of when it lands first, and
|
|
86
|
+
never a range that has landed, since that range's call was made and the step would
|
|
87
|
+
reopen it
|
|
88
|
+
([D109 (ranges-follow-the-order)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d109)).
|
|
89
|
+
Give the step a bare range of its own instead, written before you run the verb, when it
|
|
90
|
+
brings a call the human has to make before it lands. A new range takes the next free item
|
|
91
|
+
number, and no item is renumbered, because the stop card's stamp, the detail file's
|
|
92
|
+
header, and `plumbbob harvest` find a landed range by its number
|
|
93
|
+
([D92 (build-ranges)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d92)).
|
|
94
|
+
The verb's ending names each range it re-cut, and an advisory names any it left for
|
|
95
|
+
you; re-cut that one in the same confirmed edit, and `plumbbob status` afterward carries
|
|
96
|
+
a `⚠` row for any range still broken.
|
|
92
97
|
- Fold a confirmed blocker into `intent.md` before you run `plumbbob harvest`, so the
|
|
93
98
|
record and the plan agree when it lands.
|
|
94
99
|
- Never reclassify or resolve an item the human hasn't confirmed, and default every
|
package/skills/order/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: order
|
|
3
|
-
description: "Set the build order: turn the human's sequence (or a relative one, such as 7 and 8 before 5) into the full list of undone steps and write it by shelling `plumbbob order`, never by editing intent.md. A step keeps its number; the sequence moves."
|
|
3
|
+
description: "Set the build order: turn the human's sequence (or a relative one, such as 7 and 8 before 5) into the full list of undone steps and write it by shelling `plumbbob order`, never by editing intent.md. A step keeps its number; the sequence moves, and the CLI re-cuts the build ranges to follow it."
|
|
4
4
|
argument-hint: "[steps in the sequence to build them, such as 1, 6, 2-4, 7, 5]"
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
model: haiku
|
|
@@ -15,7 +15,10 @@ A step keeps its number for life, so once `/plumbbob:step` or `/plumbbob:refine`
|
|
|
15
15
|
that has to land before an existing one, the sequence to build them stops being the
|
|
16
16
|
numbering (`4, 7, 8, 5, 10, 6`). This skill writes that sequence down where every other move
|
|
17
17
|
reads it: the `## Build order` line in `intent.md`, which `plumbbob status`, a bare
|
|
18
|
-
`/plumbbob:build`, `plumbbob checkpoint`, and the card's Next Up all follow.
|
|
18
|
+
`/plumbbob:build`, `plumbbob checkpoint`, and the card's Next Up all follow. When the plan
|
|
19
|
+
carries `## Build ranges`, the same verb re-cuts them to follow the sequence, so
|
|
20
|
+
`/plumbbob:run` walks the steps the new order put in its range
|
|
21
|
+
([D109 (ranges-follow-the-order)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d109)).
|
|
19
22
|
|
|
20
23
|
## Wrong-state refusal
|
|
21
24
|
|
|
@@ -39,22 +42,22 @@ after the step in flight is the usual reason to run it.
|
|
|
39
42
|
as typed, and the line it writes spells the span out. `plumbbob order --reset` drops the
|
|
40
43
|
line, and the plan reads in document order again.
|
|
41
44
|
4. **Relay the ending verbatim, then stop.** The verb prints its whole ending: the
|
|
42
|
-
`**Build order**: 7, 8, 5, 10, 6` line
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
turn's last text. With no new `⚠` row, or no ranges block, relay the ending alone.
|
|
45
|
+
`**Build order**: 7, 8, 5, 10, 6` line; a
|
|
46
|
+
`**Build ranges**: range 2 of 3 re-cut (steps 10,4-6)` line when the new sequence moved a
|
|
47
|
+
step into or out of a range, naming each range it re-cut; an advisory for each range the
|
|
48
|
+
order left for the human, in the words its dashboard row carries
|
|
49
|
+
(`Range 2 of 3 needs a re-cut ⚠ (...)`), with
|
|
50
|
+
`→ /plumbbob:refine ranges` beneath the last; and the `**Next Up**` pointer (back at the
|
|
51
|
+
step in flight, or forward to the first step in the new sequence). Relay it whole as the
|
|
52
|
+
turn's last text and run no second command: the advisory already names the range and the
|
|
53
|
+
move, so there is no dashboard to read again. A refusal (a number the plan lacks, a
|
|
54
|
+
repeat, a step already checkpointed) is the CLI's call: report it verbatim, never retry
|
|
55
|
+
or work around it, and ask the human which number they meant.
|
|
54
56
|
|
|
55
57
|
## The one hard contract
|
|
56
58
|
|
|
57
59
|
The line is written by the **dumb CLI**, never by an edit. This skill carries **no Edit and
|
|
58
60
|
no Write tool** on purpose: you may not touch `intent.md` yourself. Compose, get approval,
|
|
59
|
-
then shell `plumbbob order`; that is the only write path,
|
|
60
|
-
|
|
61
|
+
then shell `plumbbob order`; that is the only write path, it validates every number against
|
|
62
|
+
the plan, and it is the one that re-cuts the ranges, so a range the order leaves for the
|
|
63
|
+
human is `/plumbbob:refine`'s to re-cut, never yours to edit here.
|
package/skills/refine/SKILL.md
CHANGED
|
@@ -54,18 +54,25 @@ is fine; refining is always available.
|
|
|
54
54
|
Show the before/after for each, and **write only what the human approves**. A repair
|
|
55
55
|
that inserts a step, or moves one ahead of another, keeps every number where it is and
|
|
56
56
|
writes the sequence instead: shell `plumbbob order 7 8 5` with the undone steps in the
|
|
57
|
-
sequence to build them,
|
|
58
|
-
|
|
57
|
+
sequence to build them, an inserted step included even when it builds last, and the
|
|
58
|
+
`## Build order` line it writes is what every next-step pick follows. Never renumber,
|
|
59
|
+
and never edit that line by hand.
|
|
59
60
|
|
|
60
|
-
**A repair keeps the build ranges true.** When the plan carries `## Build ranges`,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
**A repair keeps the build ranges true.** When the plan carries `## Build ranges`,
|
|
62
|
+
`plumbbob order` re-cuts them to follow the sequence it writes
|
|
63
|
+
([D109 (ranges-follow-the-order)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d109)):
|
|
64
|
+
a step it places joins the range whose steps sit on both sides of it, else the one it
|
|
65
|
+
builds ahead of, else the one it builds after, never a bare range, and never a range that
|
|
66
|
+
has landed, since that range's call was made and the step would reopen it; its ending
|
|
67
|
+
names each range it re-cut. Edit a range yourself only where the
|
|
68
|
+
order cannot say it, and before you run the verb: a bare range of its own for a step
|
|
69
|
+
that brings a call the human has to make before it lands, or a re-cut that moves where a
|
|
70
|
+
call falls, shown in the same before/after
|
|
67
71
|
([D92 (build-ranges)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d92)).
|
|
68
|
-
|
|
72
|
+
A new range takes the next free item number, and no item is renumbered, because the stop
|
|
73
|
+
card's stamp, the detail file's header, and `plumbbob harvest` find a landed range by its
|
|
74
|
+
number. A range the verb's ending names as still needing a re-cut is yours to re-cut in
|
|
75
|
+
the same approved edit. Read `plumbbob status` once the edits are written: its ranges block carries a `⚠` row
|
|
69
76
|
for a range naming a step the plan lacks, a step two ranges name, a step the build order
|
|
70
77
|
puts inside a range that does not name it, and an undone step no range names, and none
|
|
71
78
|
once the section is sound.
|
package/skills/step/SKILL.md
CHANGED
|
@@ -48,29 +48,33 @@ grew, but its everyday job is to sharpen, not to invent.
|
|
|
48
48
|
`**Scope:**` header; drop the scope and it falls back to that default → build slug →
|
|
49
49
|
bare, and the type to `feat` ([D68 (conventional-subjects)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d68)). Aim for a soft
|
|
50
50
|
≤72 chars, no gate. Revise the existing step in place; only append when you are
|
|
51
|
-
genuinely adding an increment. An appended step keeps the next free number for life
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
genuinely adding an increment. An appended step keeps the next free number for life,
|
|
52
|
+
and it takes its place by sequence, never by renumbering: shell `plumbbob order 7 8 5`
|
|
53
|
+
with every undone step in the sequence to build them, the new step included even when
|
|
54
|
+
it builds last, and the `## Build order` line it writes is what `status`, a bare
|
|
55
55
|
`/plumbbob:build`, and the card's Next Up then follow.
|
|
56
56
|
|
|
57
|
-
**Keep the build ranges true.** When the plan carries `## Build ranges`,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
**Keep the build ranges true.** When the plan carries `## Build ranges`,
|
|
58
|
+
`plumbbob order` files the step for you: it re-cuts the ranges to follow the sequence it
|
|
59
|
+
writes, so a step joins the range whose steps sit on both sides of it, else the range it
|
|
60
|
+
builds ahead of, else the one it builds after
|
|
61
|
+
([D109 (ranges-follow-the-order)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d109)):
|
|
62
|
+
never a bare range, and never a range that has landed, since that range's call was made
|
|
63
|
+
and the step would reopen it. Edit `## Build ranges` yourself only where the order
|
|
64
|
+
cannot say what you mean, and before you run the verb. A step that brings a call the
|
|
65
|
+
human has to make before it lands gets a bare range of its own
|
|
61
66
|
([D92 (build-ranges)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d92)).
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
range the
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
keeps the grant it minted
|
|
67
|
+
A new range takes the next free item number, and no item is renumbered, because the
|
|
68
|
+
stop card's stamp, the detail file's header, and `plumbbob harvest` find a landed range
|
|
69
|
+
by its number. A split or re-cut that moves where a call falls moves the stop with it. Show
|
|
70
|
+
any range edit in the same before/after, since it changes where the human will be
|
|
71
|
+
asked. The verb's ending names each range it re-cut
|
|
72
|
+
(`**Build ranges**: range 3 of 5 re-cut (steps 14,6-9)`), and an advisory beneath it
|
|
73
|
+
names any range the order left for you: re-cut that one by hand, in the same approved
|
|
74
|
+
edit. The edit lands on the item's command, the range's definition, not on the command
|
|
75
|
+
the card names: the card names `/plumbbob:run` for the range, and the turn hook mints a
|
|
76
|
+
run's grant from the plan as the human types the run, so the next run lands the range
|
|
77
|
+
as re-cut, while a run already typed keeps the grant it minted
|
|
74
78
|
([D96 (plan-names-the-grant)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d96)).
|
|
75
79
|
Then read `plumbbob status`: its ranges block carries a `⚠` row for any range still
|
|
76
80
|
broken, and none once every undone step sits in exactly one range.
|
|
@@ -88,9 +92,10 @@ grew, but its everyday job is to sharpen, not to invent.
|
|
|
88
92
|
- **One verifiable increment.** Each step carries a done-when `/plumbbob:verify` can check
|
|
89
93
|
and a seam small enough to review in one pass.
|
|
90
94
|
- **Edit `## Steps` only**, in the standard format `status` and `build` parse, plus the
|
|
91
|
-
`## Build ranges` items
|
|
92
|
-
step (`[x]`) is history; do not
|
|
93
|
-
`plumbbob order`'s to write, never yours to
|
|
95
|
+
`## Build ranges` items the order cannot settle (a bare range of its own, a stop that
|
|
96
|
+
moves): never the Roadmap, never loose prose. A done step (`[x]`) is history; do not
|
|
97
|
+
rewrite it. The `## Build order` line is `plumbbob order`'s to write, never yours to
|
|
98
|
+
edit, and so is a range the order re-cuts.
|
|
94
99
|
- **Cite the plan's own numbers in the linked form.** A `D`/`C`/`Q` you write into a
|
|
95
100
|
step's done-when, seam, or notes reads `[D4 (default-waves)](#d4)`; the slug travels
|
|
96
101
|
with the number, and the link lands on the `<a id="d4">` anchor minted where the item
|