pi-gauntlet 8.0.0 → 8.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v8.0.1 - 2026-10-31
|
|
4
|
+
|
|
5
|
+
- Single-repo loops omit coordinated instructions; coordinated entry and resume load the extracted reference before admission. (#107)
|
|
6
|
+
|
|
3
7
|
## v8.0.0 - 2026-10-31
|
|
4
8
|
|
|
5
9
|
- Replace `spec-reviewer` and `code-reviewer` with `wave-reviewer`: one integrated spec/code review per wave, bounded direct report reads and repairs on the current tree. Move model/thinking pins from both removed `subagents.agentOverrides` entries to `wave-reviewer` and remove the old keys. No pi-cohort dispatch-shape change; sibling docs need no update or paired release.
|
package/package.json
CHANGED
|
@@ -18,7 +18,7 @@ Run this driver in the main session. Carry the absolute implementation worktree
|
|
|
18
18
|
| Otherwise overrides has `## verification-before-completion` | Use its exactly one backticked command; zero or several is a configuration error. |
|
|
19
19
|
| Neither section supplies a command | Discover the project's test entrypoint from its actual manifest/build instructions; print discovery as source. Stop on an unresolved or ambiguous entrypoint. |
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
On fresh entry and on every ledger restore, for a spec whose `## Legs` section has more than one `### Leg:` block, read `reference/coordinated-legs.md` IN FULL now with read; stop on an unreadable reference naming its path, never reconstruct it. On fresh entry apply its `## Entry` section instead of the home-only skeleton and cap calculation below. On a restore apply its `## Resume` section before the dirty-tree stop and before any pending recovery. Keep legacy and single-member ledger bytes unchanged, with no coordinated metadata.
|
|
22
22
|
|
|
23
23
|
3. Call `phase_tracker({ action: "skip", phase: "plan", reason: "loop engine" })`.
|
|
24
24
|
4. Require a clean named-branch implementation worktree. Write `<resolved plan dir>/<spec stem>-ledger.md`. Use this grammar with literal values, not placeholders:
|
|
@@ -43,20 +43,10 @@ Use the actual repo-relative spec path alone in the Spec header. Read base with
|
|
|
43
43
|
|
|
44
44
|
For resume, use the admitted ledger, its base and verification command, and durable iterations and cap instead of overwriting it. Skip Entry step 1 for an admitted ledger; settings apply only to fresh seeding. Render rows, cap and decision history; stop on a dirty tree before dispatch.
|
|
45
45
|
|
|
46
|
-
## Coordinated Entry and resume
|
|
47
|
-
|
|
48
|
-
Read `## Legs` with [the scope grammar](../brainstorming/reference/legs.md#scope-grammar-and-validation) before skeleton commit or dispatch. Require exactly one `scope:` per block: `scope: AC 1, AC 3` or `scope: none`. Reject missing/duplicate scope lines, duplicate indices within a list, malformed tokens and out-of-range original indices through the amendment stop. Include cut original AC indices; never infer ownership from a `design:` mention or path. Default only indices absent from every valid list to home; repeat shared indices in every owner's list.
|
|
49
|
-
|
|
50
|
-
Retain the home skeleton's `**Spec:**`, `verification:`, `iterations:`, `cap:` and `## Decisions` fields and its `plan_check`/commit admission. Read each member's verification from the home spec's `## Legs` `contract:` `- verification:` line before dispatch; keep the existing home verification header.
|
|
51
|
-
|
|
52
|
-
Assign numeric entries in original AC order, then membership order for shared ACs, independent of scope-list order. Leave spec rows untouched and copy each cut disposition to every expanded entry. Keep the four-column table and add `## Leg mapping` with `row | ac | leg`, one mapping per entry exactly once. Reject duplicate/missing mappings. Keep `base:` as home base; record one actual `base repo-a: <sha>` / `seeded repo-a: no` pair per member, including home. Resolve bases with each member's `git -C`, not home HEAD.
|
|
53
|
-
|
|
54
|
-
Compute initial cap as non-cut entry count times `maxIterationsPerRow` plus one seed dispatch per leg with non-cut entries. One shared home/companion AC and one home-only AC produce three entries, two seeds and cap 14 at default 4; a wholly cut third leg adds no seed. All-cut cap stays 1 and exits without seed. Preserve cap after cuts, reopens, amendments, funding and recovery; only an explicit cap grant raises it.
|
|
55
|
-
|
|
56
|
-
Recover pending companion work before generic dirty checks and before any new dispatch. Read the durable home ledger's `pending:` tuple and consumed iteration, selected companion HEAD and saved pre-dispatch state. Reconstruct the pre-dispatch ledger from home `HEAD:<ledger path>` on resume and admit only the expected on-disk iterations bump and tuple against it. Admit only the known iterations bump and exact pending tuple as pre-dispatch dirt; stop on other dirt. Reapply companion return validation below only when exactly one new commit after the recorded HEAD matches attribution and result. Apply the home-ledger commit without another attempt or iteration bump, clear pending and retain prior decisions. Stop preserving state on zero commits, unrelated movement, multiple commits or attribution/result mismatch. Never rerun that row or seed. After a successful parent commit no pending tuple remains, so a subsequent resume cannot duplicate the update.
|
|
57
|
-
|
|
58
46
|
## Before each dispatch
|
|
59
47
|
|
|
48
|
+
For a spec whose `## Legs` section has more than one `### Leg:` block, read `reference/coordinated-legs.md` IN FULL now with read; stop on an unreadable reference naming its path, never reconstruct it. Apply its `## Resume`, `## Targeting and dispatch`, `## Companion return validation`, `### Companion commit admission`, `## Exit verification` and `## Amendments` sections together with this driver; never apply `## Entry` here.
|
|
49
|
+
|
|
60
50
|
Read the ledger and require its `cap:` header before the ordered table, including before rule 1. On a missing header stop with `ledger has no cap: line - seeded under the old loopEngine contract`, even for a completed ledger; use the legacy grant below, never derive cap from settings. Count empty child commits and validation misses only after the latest `loop: retry row <n>` commit. Funding alone resets neither window. Preserve every row's attempts and the total iteration cap. Apply this ordered table, starting here after each return or human decision:
|
|
61
51
|
|
|
62
52
|
| Order | Observed state | Action |
|
|
@@ -77,12 +67,8 @@ For attempts 5 with fund 2, append `row 2 escalated | implModel round | allowed
|
|
|
77
67
|
|
|
78
68
|
Select `seed` until a validated `loop: seed evidence` commit with its `row 0 done` decision and evidence on every non-cut row survives on the branch, independently of `iterations`; otherwise select the still-unresolved human retry target, then the still-failed row named by the previous decision, then first failed row, then first open row. Use the latest operational `row <n> ...` decision for targeting and the child's `previous decision:` input, skipping cap grant history while retaining the entire Decisions section. Durable status beats a decision's suggestion. Keep the retry hint only for its target.
|
|
79
69
|
|
|
80
|
-
For coordinated targeting, select the next unseeded leg with non-cut entries in membership order before ordinary targeting. Use the existing retry/failed/open priority only after those seeds validate. Skip wholly cut legs; never mark foreign legs seeded. Map each numeric target to its original AC and leg via `## Leg mapping`.
|
|
81
|
-
|
|
82
70
|
## Dispatch
|
|
83
71
|
|
|
84
|
-
For coordinated dispatch, capture home and selected leg heads and save a pre-dispatch ledger snapshot for the expected-dirt comparison. Select the leg worktree as `cwd` and git root, its own base, mapped original AC, `Leg:` and combined spec/task `Rules:` as additive task inputs. Read spec and ledger by absolute home paths. Bump iterations exactly once. Before every companion dispatch, including seed, persist the exact tuple such as `pending: 2 | repo-b | abc1234` (or `pending: seed | repo-b | abc1234`) with actual target, repo and pre-dispatch HEAD in the home ledger. Keep that expected bump/tuple durable on disk before generic dirty checks; admit no other dirt. After persisting the bump and pending tuple, bind `SAVED_LEDGER` to the durable home ledger for companion admission, not the pre-dispatch snapshot. Pass consumed `iteration:` and the exact `pending:` as attribution. Leave home iterations on the existing single child commit path, validating only home entries and home seeded state.
|
|
85
|
-
|
|
86
72
|
For home-only dispatch, record `HEAD_BEFORE`, copy the ledger text, and take i from the current iterations count. Increment `iterations:` before dispatch in the working tree so the child's commit persists it. Create a unique absolute output path with `mktemp` outside the repository. Dispatch exactly one foreground child in the main implementation worktree, not a nested worktree:
|
|
87
73
|
|
|
88
74
|
```text
|
|
@@ -99,11 +85,7 @@ Omit the model override except on an escalated round (`model: <implModel>`); let
|
|
|
99
85
|
|
|
100
86
|
## Validate the return
|
|
101
87
|
|
|
102
|
-
For
|
|
103
|
-
|
|
104
|
-
After validation update only that leg's target or seed entries, append the validated decision, record companion SHA in evidence, and remove `pending:` in one home-ledger commit. Preserve every other row, header and prior decision. That home-ledger commit (`loop: record <repo> row <n>`) is a parent commit, not a child `loop: row ...` commit: it never counts toward child-only empty-commit stagnation, and a rejected companion return records the rejection in the same parent commit while preserving the pending tuple and every row. Recover interrupted returns by this same validation, never trusted reply text.
|
|
105
|
-
|
|
106
|
-
For home-only returns, apply the following boundary and table. For coordinated home returns apply it only to home entries/seeded state, preserving foreign entries, mappings and bases.
|
|
88
|
+
For home-only returns, apply the following boundary and table.
|
|
107
89
|
|
|
108
90
|
1. Require empty `git -C "<worktree>" status --porcelain` and exactly 1 from `git -C "<worktree>" rev-list HEAD_BEFORE..HEAD --count`. On dirty/zero/multiple commits stop, preserving changes and commits. Use `Stopped: iteration <i> left the tree dirty` or `Stopped: iteration <i> added <k> commits`. Read the ledger from HEAD only after this boundary passes.
|
|
109
91
|
2. Validate by dispatched `target`, not iteration number, against the saved copy using this table. Preserve headers including cap and the bumped iterations, row identifiers and prior decisions.
|
|
@@ -117,72 +99,13 @@ Require ordinary decision grammar `^row <target> (done|failed) \| .{1,200} \| .{
|
|
|
117
99
|
|
|
118
100
|
On an ordinary miss restore every non-target row, headers (including the saved cap) and prior decisions from the saved copy, retaining the incremented iterations. Set the target `failed`, attempts = saved attempts +1, evidence = `iteration <i>: <concrete miss>`. Remove malformed appended decisions and commit `loop: repair row <n>`. Set previous decision to `none`; otherwise use the new conforming line. Two consecutive validation misses stop because the child is not following the contract. A conforming return resets the consecutive-miss count; parent repair commits do not count toward child-only empty stagnation.
|
|
119
101
|
|
|
120
|
-
### Companion commit admission
|
|
121
|
-
|
|
122
|
-
Require the unchanged home HEAD and expected-only home dirt boundary above, then run this read-only admission recipe on both immediate returns and pending recovery. Set `CHECKOUT` to the companion root, `PENDING` to the exact persisted tuple including `pending:`, `ITERATION` to the consumed count and `SAVED_LEDGER` to the durable home ledger after the iterations bump and pending write, including on recovery. Use the pre-dispatch snapshot only for the expected-dirt comparison. On failure preserve pending and stop. On success use the emitted object for the single parent ledger commit described above; never treat it as permission for another dispatch.
|
|
123
|
-
|
|
124
|
-
<!-- companion-return-shell -->
|
|
125
|
-
```bash
|
|
126
|
-
node - "$CHECKOUT" "$PENDING" "$ITERATION" "$SAVED_LEDGER" <<'NODE'
|
|
127
|
-
const fs = require('node:fs');
|
|
128
|
-
const { execFileSync } = require('node:child_process');
|
|
129
|
-
const [root, pending, iteration, savedPath] = process.argv.slice(2);
|
|
130
|
-
const requireThat = (ok, why) => { if (!ok) throw new Error(why); };
|
|
131
|
-
const git = (...args) => execFileSync('git', ['-C', root, ...args], { encoding: 'utf8' }).trim();
|
|
132
|
-
const tuple = /^pending: (seed|\d+) \| (\S+) \| ([a-f0-9]{7,40})$/.exec(pending);
|
|
133
|
-
requireThat(tuple, 'invalid pending');
|
|
134
|
-
const saved = fs.readFileSync(savedPath, 'utf8');
|
|
135
|
-
requireThat(saved.split('\n').filter(line => line.startsWith('pending:')).length === 1 && saved.split('\n').includes(pending) && saved.split('\n').includes(`iterations: ${iteration}`), 'pending already applied or iteration mismatch');
|
|
136
|
-
const [, target, leg, before] = tuple;
|
|
137
|
-
requireThat(git('status', '--porcelain') === '', 'dirty companion');
|
|
138
|
-
requireThat(git('merge-base', '--is-ancestor', before, 'HEAD') === '', 'unrelated HEAD');
|
|
139
|
-
requireThat(git('rev-list', '--count', `${before}..HEAD`) === '1', 'expected one companion commit');
|
|
140
|
-
const commit = git('rev-parse', 'HEAD');
|
|
141
|
-
const message = git('show', '-s', '--format=%B', 'HEAD').split('\n');
|
|
142
|
-
const subject = message.shift();
|
|
143
|
-
requireThat(message.shift() === '', 'missing body separator');
|
|
144
|
-
requireThat(message.shift() === pending, 'pending mismatch');
|
|
145
|
-
requireThat(message.shift() === `iteration: ${iteration}`, 'iteration mismatch');
|
|
146
|
-
const resultLines = message.filter(line => line.trim() !== '');
|
|
147
|
-
const cells = line => line.split(/(?<!\\)\|/).slice(1, -1).map(s => s.trim());
|
|
148
|
-
const table = text => text.split('\n').filter(line => /^\|\s*\d+\s*\|/.test(line)).map(cells);
|
|
149
|
-
const [ledger, mapping] = saved.split('## Leg mapping\n');
|
|
150
|
-
requireThat(mapping, 'missing mapping');
|
|
151
|
-
const map = table(mapping.split('## Decisions')[0]);
|
|
152
|
-
const oldRows = table(ledger);
|
|
153
|
-
requireThat(new Set(map.map(r => r[0])).size === map.length && map.length === oldRows.length && oldRows.every(r => map.some(m => m[0] === r[0])), 'invalid mapping');
|
|
154
|
-
const expected = oldRows.filter(r => map.some(m => m[0] === r[0] && m[2] === leg) && (target === 'seed' ? r[1] !== 'cut' : r[0] === target));
|
|
155
|
-
requireThat(expected.length && expected.every(r => r[1] !== 'cut'), 'foreign or cut target');
|
|
156
|
-
const decision = resultLines.pop();
|
|
157
|
-
const rows = resultLines.map(cells);
|
|
158
|
-
requireThat(rows.length === expected.length && resultLines.every(l => l.startsWith('|') && l.endsWith('|')), 'extra result');
|
|
159
|
-
for (const [i, row] of rows.entries()) {
|
|
160
|
-
const old = expected[i];
|
|
161
|
-
requireThat(row.length === 4 && row[0] === old[0], 'foreign row');
|
|
162
|
-
requireThat((target === 'seed' ? /^(open|done)$/ : /^(done|failed)$/).test(row[1]), 'invalid status');
|
|
163
|
-
requireThat(/^\d+$/.test(row[2]) && Number(row[2]) === Number(old[2]) + (target === 'seed' ? 0 : 1), 'attempt mismatch');
|
|
164
|
-
requireThat(row[3].length > 0 && row[3].split(';').length <= 4 && !/[<>]/.test(row[3]), 'unbounded evidence');
|
|
165
|
-
requireThat(target !== 'seed' || old[1] !== 'done' || row[1] === 'done', 'lost baseline closure');
|
|
166
|
-
}
|
|
167
|
-
if (target === 'seed') {
|
|
168
|
-
requireThat(subject === 'loop: seed evidence', 'seed subject mismatch');
|
|
169
|
-
const d = /^row 0 done \| seeded (\d+) rows, closed (\d+) on baseline \| .{1,200}$/.exec(decision);
|
|
170
|
-
requireThat(d && Number(d[1]) === expected.length && Number(d[2]) === rows.filter(r => r[1] === 'done').length, 'seed counts mismatch');
|
|
171
|
-
} else {
|
|
172
|
-
requireThat(subject === `loop: row ${target} ${rows[0][1]}`, 'row subject mismatch');
|
|
173
|
-
requireThat(new RegExp(`^row ${target} ${rows[0][1]} \\| .{1,200} \\| .{1,200}$`).test(decision), 'invalid decision');
|
|
174
|
-
}
|
|
175
|
-
console.log(JSON.stringify({ sha: commit, rows: resultLines, decision }));
|
|
176
|
-
NODE
|
|
177
|
-
```
|
|
178
|
-
|
|
179
102
|
## Exit verification
|
|
180
103
|
|
|
181
|
-
For
|
|
104
|
+
For legacy/single-member exit, run the ledger's `verification:` command once foreground in the worktree. Failure uses `Stopped: verification failed with every row closed`, Problem quoting failing output.
|
|
182
105
|
|
|
183
|
-
Apply hard boundaries first; at cap use the cap stop without repair. With amend permission, `iterations < cap`, resolved `escalationLoop.implModel` and no spent `loop: repair verification` identity, bump
|
|
106
|
+
Apply hard boundaries first; at cap use the cap stop without repair. With amend permission, `iterations < cap`, resolved `escalationLoop.implModel` and no spent `loop: repair verification` identity, bump iterations once and record `HEAD_BEFORE`. Dispatch exactly one `implementer` with `model: <implModel>`, `context: "fresh"`, `async: false`, the implementation worktree as cwd and its Rules, failing output, absolute spec path and the exact verification command. Require a clean tree and exactly one commit after HEAD_BEFORE; dirty/zero/multiple commits use existing boundary stops preserving count and state. Require all ledger rows unchanged, preserving cap and prior decisions. The child's sole commit includes the already-bumped ledger; after validation parent commits `loop: repair verification`, using an empty parent marker when no dirt remains. This marker is once per flow.
|
|
184
107
|
|
|
185
|
-
Rerun that command once
|
|
108
|
+
Rerun that command once. Never spend verify-phase credits. A second failure retains the original header plus `escalated round spent`, then goes through Human stop. Dispatch error or non-DONE consumes the round before a decision; preserve the consumed count and stop. Recover spent identity from the parent commit on resume; interrupted or unrecoverable identity is spent, never add a carrier or fabricate success. With no permission or spent identity: no reopening and no automatic rerun. Missing settings/model stops with the capability in Problem. On success record verified HEAD and clean status; call `phase_tracker({ action: "complete", phase: "implement" })`, then continue immediately below.
|
|
186
109
|
|
|
187
110
|
## Human stop
|
|
188
111
|
|
|
@@ -203,8 +126,6 @@ Serialize the bare decision as `cap +50 |` with an empty words cell. A cap grant
|
|
|
203
126
|
|
|
204
127
|
For dirty/commit-boundary retry, ask the human to authorize discarding uncommitted changes/resetting to last valid commit. Preserve consumed dispatch count: set iterations to restored HEAD's value +1 before the next normal bump. An interrupted dispatch consumes the same count; recover it from disk/branch rather than silently repeating iteration 0.
|
|
205
128
|
|
|
206
|
-
For coordinated amendments, reconcile by original AC/leg mapping without resetting attempts, iterations or cap. Re-read explicit scope lists after approved ownership amendments and reconcile expanded entries; a disposition-only cut changes neither original indices nor ownership. Preserve unchanged mapped entries and their histories.
|
|
207
|
-
|
|
208
129
|
For ledger amendments use its line-1 discriminant: no plan writer, plan_check or task-tracker lifecycle. Approved deviates/deferred/elsewhere becomes `cut` with disposition evidence; requirement-preserving amendments leave rows unchanged; a re-bounded done row becomes `open` with `reopened by amendment`. Preserve attempts, iterations and cap. Commit spec + ledger as `loop: cut row <n>` or `loop: amend`. Option 4 stops for amendments, option 5 auto-applies amends, redraw always stops and deletes the ledger with the plan. Cut only through the amendment surface, including its standing-grant section 5.
|
|
209
130
|
|
|
210
131
|
End every stop with `Recommendation: <option> - <why>` or `Recommendation: none - <why>`. An unnamed repair is none. Under amend permission take only a named authorized amend: print `took recommendation: amend for row <n>`, apply amendment-surface section 5 (including its audit), commit `loop: amend` / `loop: cut row <n>`, then return to the ordered table. Whitespace-normalize the row evidence cell and compare with evidence already auto-resolved on that row; repetition renders `Recommendation: none - evidence repeats row <n>`. Recover this evidence from amendment/ledger history; missing history never authorizes a repeated action. A cut leaves the stop; an unresolved row at allowance stops again, not a free retry. Retry, more attempts, cap +N and redraw wait for the human. Fix-first enters `/skill:shape-ticket`'s own gate, never an automatic separate repair. Hard boundaries never auto-take recommendations.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Read this reference with the loop driver for a spec whose `## Legs` section has more than one `### Leg:` block.
|
|
2
|
+
|
|
3
|
+
## Entry
|
|
4
|
+
|
|
5
|
+
Read `## Legs` with [the scope grammar](../../brainstorming/reference/legs.md#scope-grammar-and-validation) before skeleton commit or dispatch. Require exactly one `scope:` per block: `scope: AC 1, AC 3` or `scope: none`. Reject missing/duplicate scope lines, duplicate indices within a list, malformed tokens and out-of-range original indices through the amendment stop. Include cut original AC indices; never infer ownership from a `design:` mention or path. Default only indices absent from every valid list to home; repeat shared indices in every owner's list.
|
|
6
|
+
|
|
7
|
+
Retain the home skeleton's `**Spec:**`, `verification:`, `iterations:`, `cap:` and `## Decisions` fields and its `plan_check`/commit admission. Read each member's verification from the home spec's `## Legs` `contract:` `- verification:` line before dispatch; keep the existing home verification header.
|
|
8
|
+
|
|
9
|
+
Assign numeric entries in original AC order, then membership order for shared ACs, independent of scope-list order. Leave spec rows untouched and copy each cut disposition to every expanded entry. Keep the four-column table and add `## Leg mapping` with `row | ac | leg`, one mapping per entry exactly once. Reject duplicate/missing mappings. Keep `base:` as home base; record one actual `base repo-a: <sha>` / `seeded repo-a: no` pair per member, including home. Resolve bases with each member's `git -C`, not home HEAD.
|
|
10
|
+
|
|
11
|
+
Compute initial cap as non-cut entry count times `maxIterationsPerRow` plus one seed dispatch per leg with non-cut entries. One shared home/companion AC and one home-only AC produce three entries, two seeds and cap 14 at default 4; a wholly cut third leg adds no seed. All-cut cap stays 1 and exits without seed. Preserve cap after cuts, reopens, amendments, funding and recovery; only an explicit cap grant raises it.
|
|
12
|
+
|
|
13
|
+
## Resume
|
|
14
|
+
|
|
15
|
+
Recover pending companion work before generic dirty checks and before any new dispatch. Read the durable home ledger's `pending:` tuple and consumed iteration, selected companion HEAD and saved pre-dispatch state. Reconstruct the pre-dispatch ledger from home `HEAD:<ledger path>` on resume and admit only the expected on-disk iterations bump and tuple against it. Admit only the known iterations bump and exact pending tuple as pre-dispatch dirt; stop on other dirt. Reapply companion return validation below only when exactly one new commit after the recorded HEAD matches attribution and result. Apply the home-ledger commit without another attempt or iteration bump, clear pending and retain prior decisions. Stop preserving state on zero commits, unrelated movement, multiple commits or attribution/result mismatch. Never rerun that row or seed. After a successful parent commit no pending tuple remains, so a subsequent resume cannot duplicate the update.
|
|
16
|
+
|
|
17
|
+
## Targeting and dispatch
|
|
18
|
+
|
|
19
|
+
For coordinated targeting, select the next unseeded leg with non-cut entries in membership order before ordinary targeting. Use the existing retry/failed/open priority only after those seeds validate. Skip wholly cut legs; never mark foreign legs seeded. Map each numeric target to its original AC and leg via `## Leg mapping`.
|
|
20
|
+
|
|
21
|
+
For coordinated dispatch, capture home and selected leg heads and save a pre-dispatch ledger snapshot for the expected-dirt comparison. Select the leg worktree as `cwd` and git root, its own base, mapped original AC, `Leg:` and combined spec/task `Rules:` as additive task inputs. Read spec and ledger by absolute home paths. Bump iterations exactly once. Before every companion dispatch, including seed, persist the exact tuple such as `pending: 2 | repo-b | abc1234` (or `pending: seed | repo-b | abc1234`) with actual target, repo and pre-dispatch HEAD in the home ledger. Keep that expected bump/tuple durable on disk before generic dirty checks; admit no other dirt. After persisting the bump and pending tuple, bind `SAVED_LEDGER` to the durable home ledger for companion admission, not the pre-dispatch snapshot. Pass consumed `iteration:` and the exact `pending:` as attribution. Leave home iterations on the existing single child commit path, validating only home entries and home seeded state.
|
|
22
|
+
|
|
23
|
+
## Companion return validation
|
|
24
|
+
|
|
25
|
+
For a companion, validate from its commit, not the reply. Require a clean companion and exactly one commit after pending HEAD; require unchanged home HEAD and no home edits except the expected persisted bump/tuple. Match subject `loop: row <n> <status>` or `loop: seed evidence`, exact pending tuple and consumed `iteration:` in the body. Parse only the resulting four-column target row (all and only that leg's non-cut rows for seed), followed by one bounded decision. Apply the evidence and decision bounds in the driver's `## Validate the return` table. Reject a wrong target, foreign entry, header changes, fabricated counts, malformed evidence or extra results. Validate seed counts against actual rows and baseline closures; preserve attempts and set only this leg's seeded value to `yes` after every non-cut entry validates. Keep seed misses a human stop. Apply ordinary repair only to the target, retaining consumed iterations and saved cap.
|
|
26
|
+
|
|
27
|
+
After validation update only that leg's target or seed entries, append the validated decision, record companion SHA in evidence, and remove `pending:` in one home-ledger commit. Preserve every other row, header and prior decision. That home-ledger commit (`loop: record <repo> row <n>`) is a parent commit, not a child `loop: row ...` commit: it never counts toward child-only empty-commit stagnation, and a rejected companion return records the rejection in the same parent commit while preserving the pending tuple and every row. Recover interrupted returns by this same validation, never trusted reply text.
|
|
28
|
+
|
|
29
|
+
For coordinated home returns apply it only to home entries/seeded state, preserving foreign entries, mappings and bases.
|
|
30
|
+
|
|
31
|
+
### Companion commit admission
|
|
32
|
+
|
|
33
|
+
Require the unchanged home HEAD and expected-only home dirt boundary above, then run this read-only admission recipe on both immediate returns and pending recovery. Set `CHECKOUT` to the companion root, `PENDING` to the exact persisted tuple including `pending:`, `ITERATION` to the consumed count and `SAVED_LEDGER` to the durable home ledger after the iterations bump and pending write, including on recovery. Use the pre-dispatch snapshot only for the expected-dirt comparison. On failure preserve pending and stop. On success use the emitted object for the single parent ledger commit described above; never treat it as permission for another dispatch.
|
|
34
|
+
|
|
35
|
+
<!-- companion-return-shell -->
|
|
36
|
+
```bash
|
|
37
|
+
node - "$CHECKOUT" "$PENDING" "$ITERATION" "$SAVED_LEDGER" <<'NODE'
|
|
38
|
+
const fs = require('node:fs');
|
|
39
|
+
const { execFileSync } = require('node:child_process');
|
|
40
|
+
const [root, pending, iteration, savedPath] = process.argv.slice(2);
|
|
41
|
+
const requireThat = (ok, why) => { if (!ok) throw new Error(why); };
|
|
42
|
+
const git = (...args) => execFileSync('git', ['-C', root, ...args], { encoding: 'utf8' }).trim();
|
|
43
|
+
const tuple = /^pending: (seed|\d+) \| (\S+) \| ([a-f0-9]{7,40})$/.exec(pending);
|
|
44
|
+
requireThat(tuple, 'invalid pending');
|
|
45
|
+
const saved = fs.readFileSync(savedPath, 'utf8');
|
|
46
|
+
requireThat(saved.split('\n').filter(line => line.startsWith('pending:')).length === 1 && saved.split('\n').includes(pending) && saved.split('\n').includes(`iterations: ${iteration}`), 'pending already applied or iteration mismatch');
|
|
47
|
+
const [, target, leg, before] = tuple;
|
|
48
|
+
requireThat(git('status', '--porcelain') === '', 'dirty companion');
|
|
49
|
+
requireThat(git('merge-base', '--is-ancestor', before, 'HEAD') === '', 'unrelated HEAD');
|
|
50
|
+
requireThat(git('rev-list', '--count', `${before}..HEAD`) === '1', 'expected one companion commit');
|
|
51
|
+
const commit = git('rev-parse', 'HEAD');
|
|
52
|
+
const message = git('show', '-s', '--format=%B', 'HEAD').split('\n');
|
|
53
|
+
const subject = message.shift();
|
|
54
|
+
requireThat(message.shift() === '', 'missing body separator');
|
|
55
|
+
requireThat(message.shift() === pending, 'pending mismatch');
|
|
56
|
+
requireThat(message.shift() === `iteration: ${iteration}`, 'iteration mismatch');
|
|
57
|
+
const resultLines = message.filter(line => line.trim() !== '');
|
|
58
|
+
const cells = line => line.split(/(?<!\\)\|/).slice(1, -1).map(s => s.trim());
|
|
59
|
+
const table = text => text.split('\n').filter(line => /^\|\s*\d+\s*\|/.test(line)).map(cells);
|
|
60
|
+
const [ledger, mapping] = saved.split('## Leg mapping\n');
|
|
61
|
+
requireThat(mapping, 'missing mapping');
|
|
62
|
+
const map = table(mapping.split('## Decisions')[0]);
|
|
63
|
+
const oldRows = table(ledger);
|
|
64
|
+
requireThat(new Set(map.map(r => r[0])).size === map.length && map.length === oldRows.length && oldRows.every(r => map.some(m => m[0] === r[0])), 'invalid mapping');
|
|
65
|
+
const expected = oldRows.filter(r => map.some(m => m[0] === r[0] && m[2] === leg) && (target === 'seed' ? r[1] !== 'cut' : r[0] === target));
|
|
66
|
+
requireThat(expected.length && expected.every(r => r[1] !== 'cut'), 'foreign or cut target');
|
|
67
|
+
const decision = resultLines.pop();
|
|
68
|
+
const rows = resultLines.map(cells);
|
|
69
|
+
requireThat(rows.length === expected.length && resultLines.every(l => l.startsWith('|') && l.endsWith('|')), 'extra result');
|
|
70
|
+
for (const [i, row] of rows.entries()) {
|
|
71
|
+
const old = expected[i];
|
|
72
|
+
requireThat(row.length === 4 && row[0] === old[0], 'foreign row');
|
|
73
|
+
requireThat((target === 'seed' ? /^(open|done)$/ : /^(done|failed)$/).test(row[1]), 'invalid status');
|
|
74
|
+
requireThat(/^\d+$/.test(row[2]) && Number(row[2]) === Number(old[2]) + (target === 'seed' ? 0 : 1), 'attempt mismatch');
|
|
75
|
+
requireThat(row[3].length > 0 && row[3].split(';').length <= 4 && !/[<>]/.test(row[3]), 'unbounded evidence');
|
|
76
|
+
requireThat(target !== 'seed' || old[1] !== 'done' || row[1] === 'done', 'lost baseline closure');
|
|
77
|
+
}
|
|
78
|
+
if (target === 'seed') {
|
|
79
|
+
requireThat(subject === 'loop: seed evidence', 'seed subject mismatch');
|
|
80
|
+
const d = /^row 0 done \| seeded (\d+) rows, closed (\d+) on baseline \| .{1,200}$/.exec(decision);
|
|
81
|
+
requireThat(d && Number(d[1]) === expected.length && Number(d[2]) === rows.filter(r => r[1] === 'done').length, 'seed counts mismatch');
|
|
82
|
+
} else {
|
|
83
|
+
requireThat(subject === `loop: row ${target} ${rows[0][1]}`, 'row subject mismatch');
|
|
84
|
+
requireThat(new RegExp(`^row ${target} ${rows[0][1]} \\| .{1,200} \\| .{1,200}$`).test(decision), 'invalid decision');
|
|
85
|
+
}
|
|
86
|
+
console.log(JSON.stringify({ sha: commit, rows: resultLines, decision }));
|
|
87
|
+
NODE
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Exit verification
|
|
91
|
+
|
|
92
|
+
For coordinated exit, read each leg's verification from the home spec's `## Legs` `contract:` `- verification:` line and run it once foreground in its owning checkout. Require every leg to pass before completing implement.
|
|
93
|
+
|
|
94
|
+
With amend permission, `iterations < cap`, resolved `escalationLoop.implModel` and no spent `loop: repair verification` identity, bump home iterations once and record `HEAD_BEFORE` in the failing leg. Dispatch exactly one `implementer` with `model: <implModel>`, `context: "fresh"`, `async: false`, that leg's cwd/Rules, failing output, absolute home spec path and exact leg verification command.
|
|
95
|
+
|
|
96
|
+
Require all ledger rows unchanged, preserving mapping, cap and prior decisions. The home child's sole commit includes the already-bumped home ledger; after validation parent commits `loop: repair verification`, using an empty parent marker when no dirt remains. A companion child commits its leg only; parent commits the bumped home ledger as `loop: repair verification`, preserving rows and mapping. This marker is once per flow, not per failing leg.
|
|
97
|
+
|
|
98
|
+
Rerun that command once in the failing leg only. Never spend another leg's verification or verify-phase credits.
|
|
99
|
+
|
|
100
|
+
Success only after every leg passes: record verified HEAD and clean status.
|
|
101
|
+
|
|
102
|
+
## Amendments
|
|
103
|
+
|
|
104
|
+
For coordinated amendments, reconcile by original AC/leg mapping without resetting attempts, iterations or cap. Re-read explicit scope lists after approved ownership amendments and reconcile expanded entries; a disposition-only cut changes neither original indices nor ownership. Preserve unchanged mapped entries and their histories.
|