pi-gauntlet 8.1.5 → 8.1.7
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 +10 -0
- package/README.md +2 -1
- package/agents/loop-implementer.md +18 -18
- package/extensions/lib/gauntlet-settings.test.ts +37 -17
- package/extensions/lib/gauntlet-settings.ts +9 -3
- package/extensions/phase-tracker.test.ts +8 -6
- package/extensions/plan-tracker.test.ts +10 -0
- package/package.json +1 -1
- package/skills/gauntlet-resume/reference/reconstruction.md +5 -5
- package/skills/loop-implementation/SKILL.md +17 -15
- package/skills/loop-implementation/reference/coordinated-legs.md +30 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v8.1.7 - 2026-11-02
|
|
4
|
+
|
|
5
|
+
- The loop engine escalates a row to `escalationLoop.implModel` once its attempts reach `min(loopEngine.escalateAfter, effective allowance - 1)` instead of only on the last allowed attempt; `piGauntlet.loopEngine.escalateAfter` defaults to 3 (floor 1, non-integer values warn and fall back), so the default configuration still escalates attempt 4 while an allowance of 10 runs attempt 4 on `implModel` and attempts 5-10 on the worker. The threshold is held in driver memory from Entry or restore and never written to the ledger. `loop-implementer` reads its target row's `## Decisions` lines oldest first and honors failed-line guidance, with the dispatch payload unchanged. (#119)
|
|
6
|
+
|
|
7
|
+
## v8.1.6 - 2026-11-01
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **Loop returns close related rows and seed incrementally** (#110) - an ordinary worker can close same-leg open rows alongside its target, with evidence and a decision line for each; extras consume no attempts. Seed can leave unreached rows without evidence, and one seed validation miss gets a repair-and-retry before the human stop. A validated partial seed continues without re-seeding.
|
|
12
|
+
|
|
3
13
|
## v8.1.5 - 2026-11-01
|
|
4
14
|
|
|
5
15
|
- `chase-bug` hotfix uses `loop-implementer` with a one-row ledger and a shared repair budget, escalating the last allowed attempt. Mandatory conformance and code review must pass on the final tested commit before landing; failed audits no longer continue to ship.
|
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ Concretely, one change through the gauntlet:
|
|
|
39
39
|
1. Opt in with `/skill:brainstorming` and describe the change. The command is the reliable discovery-and-entry path; explicit prose requesting brainstorming/the gauntlet also supplies consent when the skill is already loaded or an available instruction or route identifies it. An ordinary implementation request or an agent recommendation is not consent. A leading run of GitHub references (`#N`, `owner/repo#N`, or a `/pull/N` or `/issues/N` URL), separated by whitespace or commas, resolves each reference as a PR first: related open, same-repository, non-fork PRs on one base form one rework subject, including drafts. Admission runs in listed order and relatedness is checked before provisioning; the first retained PR is home, whose existing branch rebases onto the fetched PR base by default, then member commits replay onto it in listed order. The first intent word `merge-forward` opts out and keeps the merge path. An unresolved conflict after its scoped helper aborts that step and continues to the spec with the repair as Task 1. A single non-PR reference follows the ticket path. The first non-reference token starts intent text; mid-sentence references and other ticket/free-text subjects keep their existing paths. The combined code, PR-carried specs, claims, and review threads are the brainstorm input, with each PR's provenance retained; only home's bound ticket supplies acceptance criteria, while member tickets supply context. A single PR retains the same rework path without member replays or a close offer. The combined state is judged against constraints from the fetched current base HEAD, not the fork point or the PRs' proposed policy edits; see the [PR-subject contract](skills/brainstorming/reference/pr-subject.md) and [PR-head worktree owner](skills/using-git-worktrees/reference/pr-head.md). **`brainstorming`** sets up an isolated worktree (on the existing branch for PR rework), explores the codebase, and turns the subject into a written spec - when its recon finds a cited case that the ask is the wrong change (misdiagnosed, wrong-sized, already solved, a band-aid, or cutting across a sound pattern - including a repo convention that is itself debt) and can name the alternative, question one is a framing question (`as framed`, `pivot: <alternative>`, or `smaller or none`); otherwise it opens with one sentence naming what it checked, and every approach it proposes ends with a `Pattern:` line naming what it follows or cuts across; when the run starts from a ticket, the spec carries the ticket's acceptance criteria verbatim, each with a disposition (`in-scope`, `deviates:`, `deferred:`, `venue:`, `elsewhere:` for a row another repo's part delivers), scope cuts come only from your answer to a cut question and are listed at the gate, the conformance gate checks the in-scope ones, and `/skill:check-delivery` verifies `venue:` rows before the prod deploy. A multi-model critique runs on it automatically. If the spec replaces a known prior spec, brainstorming marks the predecessor with a `> **Superseded by:**` banner under its title (default format, syntax overridable via `.pi/gauntlet-overrides.md`; candidates come from brainstorming's scout recon, never a mechanical sweep). **You read and approve the spec - human gate 1.** Options 1/2/4/5 approve and pick an engine; option 6 approves the content only: it writes `**Engine:** deferred` under `**Amend-grant:** none`, pushes the branch, opens a draft PR carrying just the spec, prints the URL, and ends the flow - no plan, no loop, no handoff brief. For a coordinated spec, it first pushes every companion branch to origin, then opens one home draft PR; it creates no companion PRs. A later `brainstorming #N` reconstructs the recorded companions from their origin branches before re-presenting the implementation gate; the [publication and re-gate owner](skills/brainstorming/reference/spec-pr.md) defines all-member preflight, recovery, and cleanup. The branch stays; a gauntlet-owned worktree is removed, a host-owned checkout is reported and left. Option 6 is withheld on a PR subject (`Withheld: draft PR - #N already exists`) and when the branch's diff against the default branch is not spec-only. A later `brainstorming #N` on that PR classifies it from the diff alone: one added spec carrying the marker plus banner-only predecessor edits re-presents the gate with 1/2/4/5 (3 only when the spec commit carries a council audit) and skips gather, questionary, council, and scout; a spec-only diff without the marker prints `spec-only PR without the gate marker - running lint and council` and takes the normal brainstorm path with the carried spec as the draft; any other diff takes the rework path. Approval at that re-gate writes `**Engine:**`, `**Amend-grant:**`, and `**PR:** <owner/repo#N>` as a new commit on the PR branch and chains into planning or the loop as usual. Before approval, PR rework's sole write/run exception outside the spec directories permits only the base sync (rebase or merge), each member replay or merge, their conflict helpers' edits to the conflicted files, and their scoped conflict tests; other subjects have no implementation code yet.
|
|
40
40
|
2. By default, **`writing-plans`** decomposes the approved spec into atomic, independently-verifiable tasks, grouped into parallel waves where they don't touch the same files. Experimental approval options 4/5 instead select **`loop-implementation`**: the plan phase is skipped and a stamped acceptance-criteria ledger replaces the task plan. Option 4 retains amendment review; option 5 grants auto-apply amends, with redraws still stopping. See [Loop engine operations](doc/configuration.md#loop-engine-operations).
|
|
41
41
|
3. **`subagent-driven-development`** executes the plan in waves, with task implementers followed by one integrated `wave-reviewer` after the scoped test gate passes. The reviewer checks anchored spec clauses and code quality together; repairs stay on the current tree and pass the same gate before re-review. TDD-locked: red, green, refactor. Independent implementation, review, and council batches remain parallel, but their dispatches are foreground: the orchestrator waits for terminal results before accepting work or advancing a phase.
|
|
42
|
-
The loop alternative to step 3 runs one fresh `loop-implementer` per seed or row iteration, without implement-phase reviewers. Each row starts with the shared allowance (default 4); approval replies such as `4; allowance 6` override it for one run. Under either loop approval option, the last allowed attempt escalates once per row on `escalationLoop.implModel` (the main-loop model when unset); one exit-verification repair per flow uses the same model. Exhaustion and stagnation stop for a human decision: `retry <n> | <hint>` adds one attempt to that row, while `rows +N` raises every row's allowance. Attempts never reset and there is no total iteration cap. Ledger refresh completes implement when every row is closed; verify step 1 runs the gate set: deduplicated (owning checkout, command) pairs from done rows' recorded checks, with ownership from the ledger. Gate output goes to logs; the driver keeps exit codes and paths, not test output. CI on push owns the full run.
|
|
42
|
+
The loop alternative to step 3 runs one fresh `loop-implementer` per seed or row iteration, without implement-phase reviewers. Each row starts with the shared allowance (default 4); approval replies such as `4; allowance 6` override it for one run. Under either loop approval option, attempt `escalateAfter + 1` (default 4), clamped to the last allowed attempt, escalates once per row on `escalationLoop.implModel` (the main-loop model when unset); later attempts use the ordinary worker model, and one exit-verification repair per flow uses the same escalation model. Exhaustion and stagnation stop for a human decision: `retry <n> | <hint>` adds one attempt to that row, while `rows +N` raises every row's allowance. Attempts never reset and there is no total iteration cap. Ledger refresh completes implement when every row is closed; verify step 1 runs the gate set: deduplicated (owning checkout, command) pairs from done rows' recorded checks, with ownership from the ledger. Gate output goes to logs; the driver keeps exit codes and paths, not test output. CI on push owns the full run.
|
|
43
43
|
|
|
44
44
|
4. **verify**: the parent first runs the plan's full verification command set for waves, or the ledger gate set for loop; only a passing result permits whole-diff code review, then the **conformance gate**. A review fix invalidates that result, so the parent reruns the same set before the next review or conformance gate. Loop conformance repairs and finish also use the gate set, never the ledger's full `verification:` command. When the overrides file declares a happy path for the touched paths (see [Project-specific overrides](#project-specific-overrides)), the parent runs it once here, between code review and the conformance gate, and the reviewer reads its transcript as runtime evidence - a hang or a failed request shows up in the audit with the transcript quoted - as a gap when an origin clause ties to it, otherwise as a `happy-path: failed - unattributable` line - never as a silent green. The conformance subagent reads the finished code and docs against your *original words* from step 1, not the plan, and reports per-requirement: delivered, partial, missing, drifted, or unauthorized. Unauthorized rows - shipped surface no human input asked for, whether it crept in or was laundered through the spec - follow their recommendation like every other row: a contained removal auto-runs, anything another requirement leans on is deferred with a plain-language "I'd cut it / I'd keep it" recommendation. Inside a brainstorming-entered flow this gate is machine-blocked from being skipped. Compatible executable recommendations auto-run through an isolated fix-and-re-audit loop with no prompt; anything still open surfaces as a numbered decision brief - one item per decision, the problem in plain words first, lettered options each with a one-line consequence, and `Recommended: <letter> - <reason>` or `No recommendation - pick one`. Reply with one line per item (`1. B`), or bare `1` to take every recommendation (no-recommendation items are re-asked by number); a current `CONFORMS` / no-concerns result goes straight to the branch options with no extra conformance sign-off.
|
|
45
45
|
5. **`finishing-a-development-branch`**: PR, draft PR, squash, keep, or discard; PR rework's row 1 offers a lease push of the rebased home branch to the existing PR (`Base:` records in `merge:` form push non-force). A spec whose header carries `**PR:**` (approved at a spec-PR re-gate) and no `## Rework record` replaces the two create rows with `Push to PR #N and mark it ready for review` and `Push to PR #N, keep it a draft` (`Push to PR #N (already ready for review)` when the PR is already ready): both push without force after strip-and-seal, the first then runs `gh pr ready`, neither rewrites the PR body, and both are withheld when origin's branch head is no longer an ancestor of local HEAD. For coordinated specs, these [header-linked rows 1/2](skills/finishing-a-development-branch/reference/spec-pr.md#push-rows-12) deliver companions in recorded order before one home seal carrying every companion PR URL, then push home and apply its readiness choice. Row 1 makes companion drafts ready too; row 2 creates drafts but never converts an already-ready PR back to draft. Companion URLs appear in the shipped summary, companions first and home last, not in the unchanged home PR body; keep that summary for cross-repo review because successful home delivery removes receipts. A spec still carrying `**Engine:** deferred` stops finishing (and `gauntlet-resume`) with `spec approved for handoff only - run brainstorming #N` before any phase change, verification, or push. The PR-rework push appends a `## Rework` block naming absorbed PRs while retaining the original description bytes; the header-linked `**PR:**` push leaves the body untouched. Only after a successful rework push and home-description edit does one gated confirmation show the exact close comments and eligible member targets, linking home; declining or any other finish option leaves members open. Closing never deletes member branches, and finish posts no review-thread replies. Its `Before:` handle includes the full portable pre-rework SHA and the local `refs/gauntlet/pre-rework/pr-<N>` ref in the rework checkout. The [finishing owner](skills/finishing-a-development-branch/SKILL.md#pr-subject-row-1-push-to-the-pr-branch) defines the landing and body-update contract. Once a PR exists, run `/skill:gatekeep-pr <pr>` to verify it before merging - it judges the diff against the spec at the PR's head (the acceptance-criteria dispositions and Design you approved; the PR description when no spec applies) - rows the spec defers or deviates are settled, every other row needs its mechanism (code, tests, docs) - and against the rubric; whatever can be seen only after merge is left to step 6. **Human gate 2** - the only other decision you make.
|
|
@@ -617,6 +617,7 @@ The conformance gate's model, the spec council's roster, and the phase-tracker's
|
|
|
617
617
|
| `closureReview.maxFixRounds` | phase-tracker | `5` | fix rounds before escalation - [detail](./doc/configuration.md#conformance-gate-model) |
|
|
618
618
|
| `escalationLoop.implModel` | phase-tracker | unset (main loop) | execution escalation under either loop option; review escalation remains separate - [detail](./doc/configuration.md#escalation-loop) |
|
|
619
619
|
| `loopEngine.maxIterationsPerRow` | phase-tracker | `4` (floor `1`) | fresh-ledger per-row allowance; run override on approval 4/5 - [detail](./doc/configuration.md#setting-and-command-resolution) |
|
|
620
|
+
| `loopEngine.escalateAfter` | phase-tracker | `3` (floor `1`) | pre-dispatch threshold; escalated attempt index is value + 1, clamped to the last allowed attempt - [detail](./doc/configuration.md#setting-and-command-resolution) |
|
|
620
621
|
| `phaseModels.plan` / `.implement` / `.verify` / `.ship` | phase-tracker | unset (no switch) | `provider/id[:level]` applied persistently at that phase's start - [detail](./doc/configuration.md#phase-models) |
|
|
621
622
|
| `flowGuards.enforce` | phase-tracker | `true` | phase gates on/off - [detail](./doc/configuration.md#phase-tracker) |
|
|
622
623
|
| `flowGuards.specDirs` | phase-tracker, plan-tracker; telemetry and spec-index consumers | `["doc/specs", "docs/specs"]` | where specs and sibling plans live - [detail](./doc/configuration.md#phase-tracker) |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: loop-implementer
|
|
3
|
-
description: '
|
|
3
|
+
description: 'Dispatch for a loop-engine iteration on the target acceptance-criteria row plus proved same-leg extras, with evidence and one decision line each.'
|
|
4
4
|
tools: read, write, edit, bash
|
|
5
5
|
defaultContext: fresh
|
|
6
6
|
inheritProjectContext: true
|
|
@@ -13,11 +13,11 @@ You are the loop implementer. Complete one acceptance-criteria ledger iteration
|
|
|
13
13
|
|
|
14
14
|
## Output contract
|
|
15
15
|
|
|
16
|
-
Append
|
|
16
|
+
Append one decision line per changed row to the ledger's `## Decisions` section - the target (`done` or `failed`) and each extra closed `done` - target first. Reply with those lines only, with nothing before or after them:
|
|
17
17
|
|
|
18
|
-
`row <n> <done|failed> | <one sentence: what was done> | <one sentence:
|
|
18
|
+
`row <n> <done|failed> | <one sentence: what was done> | <one sentence: reusable artifacts for done, retry guidance for failed>`
|
|
19
19
|
|
|
20
|
-
Limit each sentence to 200 characters. Use the seed contract below for `target: seed`.
|
|
20
|
+
For `done`, name what later rows can build on (a file, helper, interface or fixture) and how to reuse it; write `nothing new to build on` when the row added no reusable artifact, including an extra closed by a check without further edits. For `failed`, state what the next iteration should not redo or should try differently. Limit each sentence to 200 characters. Use the seed contract below for `target: seed`.
|
|
21
21
|
|
|
22
22
|
## Inputs
|
|
23
23
|
|
|
@@ -28,15 +28,15 @@ Read additive inputs `Leg:`, `Rules:`, mapped original AC and, for companions, e
|
|
|
28
28
|
## Procedure
|
|
29
29
|
|
|
30
30
|
1. Read the given spec in full before reading the ledger or code. Read the given ledger next. Save the exact `pending:` tuple and bumped `iterations:` count before editing.
|
|
31
|
-
2.
|
|
32
|
-
3. Run `git diff --stat <base>`. Open full diff hunks only for files the
|
|
33
|
-
4. Implement the target row using Three-scenario TDD and Test economy below. Run only its targeted check under Evidence command, never the
|
|
34
|
-
5. Update
|
|
35
|
-
6. Append the decision line defined in Output contract.
|
|
36
|
-
7. Remove the `pending:` line from the ledger in your commit. Stage your
|
|
37
|
-
8. Reply with the appended decision
|
|
31
|
+
2. Close the `target` row first. Read every `row <n> ...` line for the target row from `## Decisions`, oldest first, before choosing an approach. Treat them as ordered guidance: avoid what a `failed` line's third slot says not to redo, follow what it says to try differently, and never repeat an approach a prior line of this row rules out. Exclude other rows' lines from guidance for this target. Read `retry` and `escalated` lines in the same pass: use the retry hint as guidance; treat the escalation marker as historical evidence that escalation is spent, and never infer the current attempt's model from it. Keep `previous decision` as the latest admitted target line and the tie-break for most recent. Use `previous decision` and `hint` to avoid repeating work and to try a different approach; keep the target unchanged.
|
|
32
|
+
3. Run `git diff --stat <base>`. Open full diff hunks only for files the row being worked on touches, using `git diff <base> -- <row files>`; read the relevant code before editing it.
|
|
33
|
+
4. Implement the target row using Three-scenario TDD and Test economy below. Treat empty evidence on an unreached row as a normal state after partial seeding, not a seed failure; create its first check before implementing it, then close or fail the row with command-first evidence under step 5. Run only its targeted check under Evidence command, never the owning checkout's full verification command or the full suite.
|
|
34
|
+
5. Update the target row's mutable cells: set `status` to `done` on a passing check or `failed` on a failing check, increment `attempts` by 1, and set `evidence` to one physical Markdown table cell opening with the command you ran under Evidence command, up to the first unescaped semicolon, plus at most three result lines separated by semicolons. Escape pipes as `\|` and use plain text without angle-bracket markup. On failure, record the concrete cause in plain words: the spec-vs-code contradiction, failing assertion, or missing credential, tool, or fixture. Keep reasoning and transcripts out of evidence.
|
|
35
|
+
6. Append the target's decision line defined in Output contract. Only after the target is `done`, consider further open rows of your leg whose work sits in the files or helper just changed: a check already passing, or a small step on the same code. Take extras one at a time; write or name each row's selective check before implementing it, apply Three-scenario TDD, make it pass, record command-first evidence bounded as in step 5, and append its decision line. Set each extra `done` with attempts unchanged. Do not look elsewhere to pad the return or start a row you cannot finish in this session. Revert all code, test, cell and decision changes for an unfinished extra before committing; preserve the completed rows' changes.
|
|
36
|
+
7. Remove the `pending:` line from the ledger in your commit. Stage your row changes and the ledger explicitly. Commit exactly once with subject `loop: row <n> <status>`, naming the target only. Check `git status --porcelain` and leave the working tree clean.
|
|
37
|
+
8. Reply with the appended decision lines only, target first.
|
|
38
38
|
|
|
39
|
-
For every home row or seed commit, put the saved `pending:` tuple and consumed `iteration:` on separate body lines immediately after the subject's blank separator, matching the companion attribution prefix below. Read that count from the ledger's bumped `iterations:` header, not the task's pre-bump `iteration`. Keep result rows and
|
|
39
|
+
For every home row or seed commit, put the saved `pending:` tuple and consumed `iteration:` on separate body lines immediately after the subject's blank separator, matching the companion attribution prefix below. Read that count from the ledger's bumped `iterations:` header, not the task's pre-bump `iteration`. Keep result rows and decisions in the ledger; write no sidecar or extra commit.
|
|
40
40
|
|
|
41
41
|
## Three-scenario TDD
|
|
42
42
|
|
|
@@ -64,22 +64,22 @@ Require the recorded command to exit nonzero when it selects no test. Before rec
|
|
|
64
64
|
|
|
65
65
|
Use this contract for `target: seed`, normally iteration 0. After a human-authorized recovery discards an interrupted seed, repeat seeding at the consumed iteration count; keep the ledger's `iterations` unchanged.
|
|
66
66
|
|
|
67
|
-
Replace steps 2-6 with evidence seeding
|
|
67
|
+
Replace steps 2-6 with evidence seeding for at least one non-cut row; seed a subset or all open rows. Keep the spec-first read order and scope diff hunks to the seeded rows' files. Leave unreached rows unchanged.
|
|
68
68
|
|
|
69
|
-
For
|
|
69
|
+
For each selected `open` row, write or identify its failing test (use an asserting command for a trivial or assertion-only row) and record the command you ran in `evidence` under Evidence command. Close a row already satisfied by the codebase as `done` only with passing baseline evidence; record the command of the passing check for every baseline closure. Keep other open rows `open`. Keep every row's `attempts` at 0; never set `failed`. Preserve `cut` rows and their disposition evidence. Bound evidence as in step 5.
|
|
70
70
|
|
|
71
|
-
Append exactly one decision line: `row 0 done | seeded <k> rows, closed <m> on baseline | <first row to take>`.
|
|
71
|
+
Append exactly one decision line: `row 0 done | seeded <k> rows, closed <m> on baseline | <first row to take>`. Count only rows given evidence in this return in k and their proven baseline closures in m, not unreached rows.
|
|
72
72
|
|
|
73
73
|
Stage the seeded evidence changes and ledger, remove `pending:` in that same commit, commit exactly once with subject `loop: seed evidence`, check for a clean tree, and reply with the appended seed decision only.
|
|
74
74
|
|
|
75
75
|
## Companion result commit
|
|
76
76
|
|
|
77
|
-
For a companion `pending:` input, replace ledger mutation/staging and decision appending in the procedure and output contract with the commit-body path below; leave the home ledger untouched. Limit work and seed checks to mapped entries for this leg. Apply the same one-commit and bounded evidence contracts. Prepare the resulting four-column `row | status | attempts | evidence`
|
|
77
|
+
For a companion `pending:` input, replace ledger mutation/staging and decision appending in the procedure and output contract with the commit-body path below; leave the home ledger untouched. Limit work and seed checks to mapped entries for this leg. Apply the same one-commit and bounded evidence contracts. Prepare the resulting four-column `row | status | attempts | evidence` rows in memory, not a sidecar. Serialize them in the companion commit body instead of editing the home ledger; for seed serialize all and only this leg's non-cut rows, including unreached open rows with empty evidence, preserving zero attempts and proven baseline closures. For ordinary work serialize the target, attempts +1 and status done/failed, plus each same-leg `open` -> `done` extra with attempts unchanged under step 6.
|
|
78
78
|
|
|
79
|
-
Use the existing subject `loop: row <n> <status>` or `loop: seed evidence`. Put the exact dispatched `pending:` tuple and consumed `iteration:` on separate body lines, then a blank line, then the resulting pipe-delimited table rows without a header, then a blank line and
|
|
79
|
+
Use the existing subject `loop: row <n> <status>` or `loop: seed evidence`. Put the exact dispatched `pending:` tuple and consumed `iteration:` on separate body lines, then a blank line, then the resulting pipe-delimited table rows without a header, then a blank line and one bounded decision line per changed row, target first (one seed decision for seed). Include the evidence command from Evidence command plus at most three semicolon-separated result lines and escaped pipes in each evidence cell. Compute seed decision counts from this leg only. Stage code/tests explicitly and commit exactly once in the companion; check clean status. Reply with those decision lines only. Let the parent validate the commit and write the home ledger.
|
|
80
80
|
|
|
81
81
|
For coordinated home work, edit only mapped home entries and its seeded state under the ordinary ledger contract; preserve foreign entries, mappings, bases and all other headers except removal of `pending:`.
|
|
82
82
|
|
|
83
83
|
## Boundaries
|
|
84
84
|
|
|
85
|
-
Edit code and tests only for the target row, or the open rows during seed.
|
|
85
|
+
Edit code and tests only for the target row and same-leg `open` extras under step 6, or the open rows during seed. Preserve every other row, including foreign-leg, `cut` and already `done` rows. Preserve row identifiers, row text, ledger headers (except removal of `pending:`), and existing decisions. Never edit the spec or the plan directory outside the supplied ledger. Never touch `main`. Leave whole-project verification and reviews to the parent.
|
|
@@ -21,35 +21,44 @@ import {
|
|
|
21
21
|
} from "./gauntlet-settings.ts";
|
|
22
22
|
|
|
23
23
|
test("loopEngine resolves defaults, integers, clamps, and malformed values", () => {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
for (const [key, fallback] of [["maxIterationsPerRow", 4], ["escalateAfter", 3]] as const) {
|
|
25
|
+
const warning = `loopEngine.${key} must be an integer; using default ${fallback}`;
|
|
26
|
+
for (const [raw, expected, expectedWarning] of [
|
|
27
|
+
[undefined, fallback, undefined], [7, 7, undefined], [0, 1, undefined], [-3, 1, undefined],
|
|
28
|
+
[String(fallback), fallback, warning], [2.5, fallback, warning], [true, fallback, warning], [null, fallback, warning],
|
|
29
|
+
] as const) {
|
|
30
|
+
assert.deepEqual(resolveLoopEngine({ loopEngine: { [key]: raw } }), {
|
|
31
|
+
maxIterationsPerRow: 4, escalateAfter: 3, [key]: expected, warning: expectedWarning,
|
|
32
|
+
}, `${key}: ${String(raw)}`);
|
|
33
|
+
}
|
|
32
34
|
}
|
|
33
|
-
assert.deepEqual(resolveLoopEngine({}), { maxIterationsPerRow: 4, warning: undefined });
|
|
35
|
+
assert.deepEqual(resolveLoopEngine({}), { maxIterationsPerRow: 4, escalateAfter: 3, warning: undefined });
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
test("loopEngine ignores the removed key by own-property presence and orders warnings", () => {
|
|
37
39
|
const removed = "loopEngine.maxIterations is removed; set loopEngine.maxIterationsPerRow";
|
|
38
40
|
for (const raw of [30, undefined, null, false]) {
|
|
39
41
|
assert.deepEqual(resolveLoopEngine({ loopEngine: { maxIterations: raw } }), {
|
|
40
|
-
maxIterationsPerRow: 4, warning: removed,
|
|
42
|
+
maxIterationsPerRow: 4, escalateAfter: 3, warning: removed,
|
|
41
43
|
});
|
|
42
44
|
}
|
|
43
|
-
for (const [raw, expected,
|
|
44
|
-
[7, 7,
|
|
45
|
-
["4", 4,
|
|
45
|
+
for (const [raw, expected, allowanceWarning] of [
|
|
46
|
+
[7, 7, undefined],
|
|
47
|
+
["4", 4, "loopEngine.maxIterationsPerRow must be an integer; using default 4"],
|
|
46
48
|
] as const) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
for (const [escalateAfter, threshold, thresholdWarning] of [
|
|
50
|
+
[7, 7, undefined],
|
|
51
|
+
["3", 3, "loopEngine.escalateAfter must be an integer; using default 3"],
|
|
52
|
+
] as const) {
|
|
53
|
+
const loopEngine = { maxIterations: 99, maxIterationsPerRow: raw, escalateAfter };
|
|
54
|
+
assert.deepEqual(resolveLoopEngine({ loopEngine }), {
|
|
55
|
+
maxIterationsPerRow: expected, escalateAfter: threshold,
|
|
56
|
+
warning: [removed, allowanceWarning, thresholdWarning].filter(Boolean).join("; "),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
50
59
|
}
|
|
51
60
|
assert.deepEqual(resolveLoopEngine({ loopEngine: Object.create({ maxIterations: 99 }) }), {
|
|
52
|
-
maxIterationsPerRow: 4, warning: undefined,
|
|
61
|
+
maxIterationsPerRow: 4, escalateAfter: 3, warning: undefined,
|
|
53
62
|
});
|
|
54
63
|
});
|
|
55
64
|
|
|
@@ -61,6 +70,17 @@ test("mergeGauntlet: repo key replaces preset key whole-object", () => {
|
|
|
61
70
|
const merged = mergeGauntlet(preset, repo);
|
|
62
71
|
assert.deepEqual(merged.specCouncil, { members: ["b"] });
|
|
63
72
|
assert.deepEqual(merged.closureReview, { model: "m" });
|
|
73
|
+
for (const [repoLoop, expected] of [
|
|
74
|
+
[undefined, { maxIterationsPerRow: 8, escalateAfter: 6 }],
|
|
75
|
+
[{ maxIterationsPerRow: 10 }, { maxIterationsPerRow: 10, escalateAfter: 3 }],
|
|
76
|
+
[{ escalateAfter: 2 }, { maxIterationsPerRow: 4, escalateAfter: 2 }],
|
|
77
|
+
] as const) {
|
|
78
|
+
const loop = mergeGauntlet(
|
|
79
|
+
{ loopEngine: { maxIterationsPerRow: 8, escalateAfter: 6 } },
|
|
80
|
+
repoLoop === undefined ? {} : { loopEngine: repoLoop },
|
|
81
|
+
);
|
|
82
|
+
assert.deepEqual(resolveLoopEngine(loop), { ...expected, warning: undefined });
|
|
83
|
+
}
|
|
64
84
|
});
|
|
65
85
|
|
|
66
86
|
test("mergeGauntlet: undefined layers -> {}", () => {
|
|
@@ -11,7 +11,7 @@ export interface PiGauntlet {
|
|
|
11
11
|
flowGuards?: { enforce?: unknown; specDirs?: unknown };
|
|
12
12
|
verifyBeforeShip?: { testCommands?: unknown; warningReference?: unknown };
|
|
13
13
|
escalationLoop?: { implModel?: unknown };
|
|
14
|
-
loopEngine?: { maxIterationsPerRow?: unknown; maxIterations?: unknown };
|
|
14
|
+
loopEngine?: { maxIterationsPerRow?: unknown; escalateAfter?: unknown; maxIterations?: unknown };
|
|
15
15
|
phaseModels?: { plan?: unknown; implement?: unknown; verify?: unknown; ship?: unknown };
|
|
16
16
|
telemetry?: { enabled?: unknown; dir?: unknown; buckets?: unknown };
|
|
17
17
|
}
|
|
@@ -93,11 +93,14 @@ export function resolveClosureReview(g: PiGauntlet): ClosureReviewResolved {
|
|
|
93
93
|
return { model, enforce, maxFixRounds };
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
export function resolveLoopEngine(g: PiGauntlet): { maxIterationsPerRow: number; warning: string | undefined } {
|
|
96
|
+
export function resolveLoopEngine(g: PiGauntlet): { maxIterationsPerRow: number; escalateAfter: number; warning: string | undefined } {
|
|
97
97
|
const loop = g.loopEngine;
|
|
98
98
|
const raw = loop?.maxIterationsPerRow;
|
|
99
99
|
const valid = typeof raw === "number" && Number.isInteger(raw);
|
|
100
100
|
const maxIterationsPerRow = valid ? Math.max(1, raw) : 4;
|
|
101
|
+
const rawEscalateAfter = loop?.escalateAfter;
|
|
102
|
+
const validEscalateAfter = typeof rawEscalateAfter === "number" && Number.isInteger(rawEscalateAfter);
|
|
103
|
+
const escalateAfter = validEscalateAfter ? Math.max(1, rawEscalateAfter) : 3;
|
|
101
104
|
const warnings: string[] = [];
|
|
102
105
|
if (loop && Object.hasOwn(loop, "maxIterations")) {
|
|
103
106
|
warnings.push("loopEngine.maxIterations is removed; set loopEngine.maxIterationsPerRow");
|
|
@@ -105,7 +108,10 @@ export function resolveLoopEngine(g: PiGauntlet): { maxIterationsPerRow: number;
|
|
|
105
108
|
if (raw !== undefined && !valid) {
|
|
106
109
|
warnings.push("loopEngine.maxIterationsPerRow must be an integer; using default 4");
|
|
107
110
|
}
|
|
108
|
-
|
|
111
|
+
if (rawEscalateAfter !== undefined && !validEscalateAfter) {
|
|
112
|
+
warnings.push("loopEngine.escalateAfter must be an integer; using default 3");
|
|
113
|
+
}
|
|
114
|
+
return { maxIterationsPerRow, escalateAfter, warning: joinWarn(warnings) };
|
|
109
115
|
}
|
|
110
116
|
|
|
111
117
|
export interface EscalationLoopResolved {
|
|
@@ -1468,16 +1468,17 @@ test("gauntlet_setting escalationLoop: setting absent -> ctx-derived main-loop m
|
|
|
1468
1468
|
});
|
|
1469
1469
|
|
|
1470
1470
|
test("gauntlet_setting loopEngine resolves the key", async () => {
|
|
1471
|
-
for (const [loopEngine, expected, warning] of [
|
|
1472
|
-
[undefined, 4, undefined],
|
|
1473
|
-
[{ maxIterationsPerRow: 8 }, 8, undefined],
|
|
1474
|
-
[{
|
|
1475
|
-
|
|
1471
|
+
for (const [loopEngine, expected, escalateAfter, warning] of [
|
|
1472
|
+
[undefined, 4, 3, undefined],
|
|
1473
|
+
[{ maxIterationsPerRow: 8, escalateAfter: 2 }, 8, 2, undefined],
|
|
1474
|
+
[{ escalateAfter: 0 }, 4, 1, undefined],
|
|
1475
|
+
[{ maxIterations: 99, maxIterationsPerRow: "4", escalateAfter: "3" }, 4, 3,
|
|
1476
|
+
"loopEngine.maxIterations is removed; set loopEngine.maxIterationsPerRow; loopEngine.maxIterationsPerRow must be an integer; using default 4; loopEngine.escalateAfter must be an integer; using default 3"],
|
|
1476
1477
|
] as const) {
|
|
1477
1478
|
const h = harness({ cwd: tempCwd({ piGauntlet: { loopEngine } }) });
|
|
1478
1479
|
const tool = h.tools.find(t => t.name === "gauntlet_setting")!;
|
|
1479
1480
|
const result = await tool.execute("loop", { key: "loopEngine" }, undefined, undefined, h.ctx) as any;
|
|
1480
|
-
assert.deepEqual(result.details, { key: "loopEngine", maxIterationsPerRow: expected, warning, errors: [] });
|
|
1481
|
+
assert.deepEqual(result.details, { key: "loopEngine", maxIterationsPerRow: expected, escalateAfter, warning, errors: [] });
|
|
1481
1482
|
assert.equal(Object.hasOwn(result.details, "maxIterations"), false);
|
|
1482
1483
|
}
|
|
1483
1484
|
});
|
|
@@ -1489,6 +1490,7 @@ test("gauntlet_setting loopEngine composes settings load warnings", async () =>
|
|
|
1489
1490
|
const result = await h.tools.find(t => t.name === "gauntlet_setting")!.execute("loop", { key: "loopEngine" }, undefined, undefined, h.ctx) as any;
|
|
1490
1491
|
assert.equal(result.details.key, "loopEngine");
|
|
1491
1492
|
assert.equal(result.details.maxIterationsPerRow, 4);
|
|
1493
|
+
assert.equal(result.details.escalateAfter, 3);
|
|
1492
1494
|
assert.equal(Object.hasOwn(result.details, "maxIterations"), false);
|
|
1493
1495
|
assert.ok(result.details.errors.length);
|
|
1494
1496
|
assert.equal(result.details.warning, `⚠️ gauntlet settings load error (using defaults): ${result.details.errors.join("; ")}`);
|
|
@@ -411,6 +411,15 @@ test("refresh projects ledger states, numeric pending targets and live leg mappi
|
|
|
411
411
|
text: ledger(["1 | cut | 0 | deferred: #1", "2 | done | 0 | baseline", "3 | done | 1 | check \\| passed", "4 | failed | 1 | check failed", "5 | open | 0 |", "6 | open | 0 |"], "row 3 done | passed | row 4\n## Notes\nrow 2 done | not a decision | none", "pending: 5 | home | abc1234"),
|
|
412
412
|
expected: [["row 1", "skipped"], ["row 2", "skipped"], ["row 3", "complete"], ["row 4", "failed"], ["row 5", "in_progress"], ["row 6", "pending"]],
|
|
413
413
|
},
|
|
414
|
+
{
|
|
415
|
+
name: "child-closed extra is complete without a budget prefix while baseline stays skipped",
|
|
416
|
+
text: ledger([
|
|
417
|
+
"1 | done | 1 | node --test primary.test.mjs; 1 passed", "2 | open | 0 |",
|
|
418
|
+
"3 | done | 0 | node --test extra.test.mjs; 1 passed", "4 | done | 0 | baseline",
|
|
419
|
+
], "row 1 done | Added parser | lib/parser.mjs supplies the parser\nrow 3 done | Reused parser | extra.test.mjs covers the shared path", "allowance: 4"),
|
|
420
|
+
expected: [["row 1", "complete"], ["row 3", "complete"], ["row 4", "skipped"], ["row 2", "pending"]],
|
|
421
|
+
labels: [`1/4: ${descriptions[0]}`, descriptions[2], descriptions[3], `0/4: ${descriptions[1]}`],
|
|
422
|
+
},
|
|
414
423
|
{ name: "retry overrides failed", text: ledger(["1 | failed | 1 | failed"], "", "pending: 1 | home | abc1234"), expected: [["row 1", "in_progress"]] },
|
|
415
424
|
{ name: "leading-zero target names its numeric row", text: ledger(["1 | open | 0 |"], "", "pending: 01 | home | abc1234"), expected: [["row 1", "in_progress"]] },
|
|
416
425
|
{ name: "done beats stale pending", text: ledger(["1 | done | 1 | passed"], "row 1 done | passed | none", "pending: 1 | home | abc1234"), expected: [["row 1", "complete"]] },
|
|
@@ -429,6 +438,7 @@ test("refresh projects ledger states, numeric pending targets and live leg mappi
|
|
|
429
438
|
assert.equal(result.details.error, undefined, row.name);
|
|
430
439
|
assert.equal(result.details.action, "refresh", row.name);
|
|
431
440
|
assert.deepEqual(result.details.tasks.map(t => [t.name, t.status]), row.expected, row.name);
|
|
441
|
+
if (row.labels) assert.deepEqual(result.details.tasks.map(t => t.label), row.labels, row.name);
|
|
432
442
|
assert.deepEqual(validateSnapshot(result.details.tasks as any), [], row.name);
|
|
433
443
|
assert.deepEqual((await h.call({ action: "status" })).details.tasks, result.details.tasks, row.name);
|
|
434
444
|
const done = result.details.tasks.filter(t => ["complete", "skipped"].includes(t.status)).length;
|
package/package.json
CHANGED
|
@@ -93,9 +93,9 @@ After confirmation:
|
|
|
93
93
|
1. Read ledger line 1: require `# Ledger - loop engine`. Resolve its path-only `**Spec:**` header to an absolute spec path under the worktree's configured spec directories, not by basename. Run SKILL.md entry checks 1-6 in order, including check 6, **Grant field**, on that spec, before any tracker mutation. Run Pending recovery's read-only admission-evidence check below.
|
|
94
94
|
2. Call `phase_tracker({ action: "start", phase: "brainstorm" })`, then `phase_tracker({ action: "skip", phase: "brainstorm", reason: "resume: <spec path>" })`, then `phase_tracker({ action: "start", phase: "plan" })`.
|
|
95
95
|
3. Admit exactly one positive safe-integer `cap:` or `allowance:` header; both or neither is `ledger header malformed: <observed header lines>`, requiring human repair by hand. Call `plan_check({ planPath: <absolute ledger path> })` with legacy-compatible admission. On FAIL, stop with findings, retaining plan in_progress. Start the message with `gauntlet: stuck` followed by the findings reason on that line; never enter `## Plan repair` or writing-plans. On PASS, call `phase_tracker({ action: "skip", phase: "plan", reason: "resume: <spec> (loop engine)" })`, then `phase_tracker({ action: "start", phase: "implement" })`. Use the resolved absolute spec path as `<spec>` in the reason. Restore implement through refresh, not `init`.
|
|
96
|
-
4. **Shared allowance boundary (every ledger restore, before phase continuation).** Read `/skill:loop-implementation`. Skip Entry step 1
|
|
96
|
+
4. **Shared allowance boundary (every ledger restore, before phase continuation).** Read `/skill:loop-implementation`. Skip Entry step 1's allowance seeding; restore base, verification, iterations, row attempts and decisions from the ledger without reseeding. Resolve `gauntlet_setting({ key: "loopEngine" })` exactly once at the driver's authorized-restore grant boundary and retain `escalateAfter` in driver memory for this run; share this read with step 4.2, never refresh it inside the loop. On an unavailable tool, stop before any dispatch or write with `cannot resolve escalation threshold: settings tool unavailable (loopEngine.escalateAfter)`. Apply the following order on bare-worktree and brief routes alike:
|
|
97
97
|
1. Apply pending recovery below against the unmigrated ledger, including home returns whose child commit removed the tuple. Validate the original one-commit `<pre-dispatch HEAD>..HEAD` range against its unchanged before-image. For companions, retain unchanged home HEAD and admit the expected iterations-bump-plus-tuple dirt. On unrecoverable attribution or before-image, stop before refresh. Resolve ambiguous-history admission before tracker calls as required below. Retain every discard/reset and interrupted-dispatch gate; migration changes neither admission ranges nor the home HEAD used for recovery.
|
|
98
|
-
2. Migrate only an admitted, clean, tuple-free ledger with no retained recovery stop. With `cap:` and no `allowance:`,
|
|
98
|
+
2. Migrate only an admitted, clean, tuple-free ledger with no retained recovery stop. With `cap:` and no `allowance:`, reuse the single restore-boundary settings read and replace the cap line with `allowance: <maxIterationsPerRow>`. Commit only that header replacement as parent `loop: migrate allowance`; preserve iterations, every attempts cell and all decisions. With an existing allowance, use it without allowance resolution or a migration commit; the restore read supplies `escalateAfter` only. Defer migration through any retained recovery stop or dirty tree; after authorized recovery reaches a clean tuple-free tree, migrate before the next dispatch or verification.
|
|
99
99
|
3. Require `allowance:` and no `cap:` before continuation, including for all-closed ledgers; otherwise stop with `ledger has no allowance: line - resume through /skill:gauntlet-resume`. For a retained recovery stop or unresolved dirty tree, defer this final validation with migration and project the unmigrated ledger solely to report the stop.
|
|
100
100
|
4. Call `plan_tracker({ action: "refresh", path: <absolute ledger path> })` exactly once after phase restoration, recovery and eligible migration/final validation, before any unresolved dirty-tree stop or dispatch. On verify/ship, restore the saved mixed task list first as `brief-contract.md` specifies; on implement, use no `init`. On refresh failure, stop with its findings and leave the existing task list untouched. Read `plan_tracker status` and report its exact observed state, never an inferred list.
|
|
101
101
|
Render the allowance header, rows, iterations, attempts, effective allowances and all decisions; distinguish the latest operational `row <n> ...` decision from the last physical history line. Compute each effective allowance as the header plus one per matching retry decision; ignore the retry's audit ceiling. Keep `cap +N` history as inert text and restore no funded rounds. Report any retained recovery stop now with no redispatch, using the driver's unresolved-row counts and discard-authorization menu for dirty trees. Resume the original phase only after this boundary: implement at `Before each dispatch`; verify/ship at `After the loop` step 1.
|
|
@@ -116,13 +116,13 @@ Revalidate a clean, exactly-one-commit return through the home row below. For la
|
|
|
116
116
|
| Confirmed admission state | Action |
|
|
117
117
|
|---|---|
|
|
118
118
|
| Confirmed settled | Preserve the current ledger and continue normal restore; require fresh verification/conformance, not historical gate credit. |
|
|
119
|
-
| Confirmed interrupted | Validate the entire `<pre-dispatch HEAD>..HEAD` range, never just `HEAD^..HEAD`, through the home row below. Render the interrupted/ambiguous-history answer in one sentence: validate the full `<HEAD_BEFORE>..HEAD` range (using the recovered pre-dispatch SHA, including later commits), restore row
|
|
119
|
+
| Confirmed interrupted | Validate the entire `<pre-dispatch HEAD>..HEAD` range, never just `HEAD^..HEAD`, through the home row below. Render the interrupted/ambiguous-history answer in one sentence: validate the full `<HEAD_BEFORE>..HEAD` range (using the recovered pre-dispatch SHA, including later commits), restore every row the child changed (target and extras) and remove their unadmitted decisions from the pre-dispatch copy with the tuple before refresh on boundary rejection, state that iterations stay at the recovered count and child commits/changes are kept; use the saved cells for any other pre-dispatch status/attempts, or all dispatched non-cut rows for seed. Use the recovered tuple/count without another bump or a new commit. |
|
|
120
120
|
| Still unknown | Stop without mutation; ask for the missing admission evidence, never turn uncertainty into a discard/reset decision. |
|
|
121
121
|
|
|
122
122
|
| Tuple target | Recovery |
|
|
123
123
|
|---|---|
|
|
124
|
-
| Home leg or single-member `home` | Reconstruct the pre-dispatch ledger from `HEAD:<ledger path>` when HEAD is the tuple SHA, otherwise from `<pre-dispatch HEAD>:<ledger path>`. Compare live HEAD with that SHA. Admit exactly one new home commit with subject `loop: row <n> <status>` or `loop: seed evidence` only through the driver's `## Validate the return`: require a clean tree and the bounded row/decision result, allowing only the consumed iteration and pending removal header deltas. Clear the tuple through the admitted close or ordinary-miss repair. With zero new commits, keep the tuple and consumed iteration and take the interrupted-dispatch stop. Unrelated movement or multiple commits stop without redispatch or double counting. For a rejected terminal child result, restore the target
|
|
125
|
-
| Actual multi-member companion | Apply `/skill:loop-implementation` `reference/coordinated-legs.md` `## Resume`: require unchanged home HEAD and exactly one new companion commit with tuple attribution
|
|
124
|
+
| Home leg or single-member `home` | Reconstruct the pre-dispatch ledger from `HEAD:<ledger path>` when HEAD is the tuple SHA, otherwise from `<pre-dispatch HEAD>:<ledger path>`. Compare live HEAD with that SHA. Admit exactly one new home commit with subject `loop: row <n> <status>` or `loop: seed evidence` only through the driver's `## Validate the return`: require a clean tree and the bounded row/decision result, allowing only the consumed iteration and pending removal header deltas. Clear the tuple through the admitted close or ordinary-miss repair. With zero new commits, keep the tuple and consumed iteration and take the interrupted-dispatch stop. Unrelated movement or multiple commits stop without redispatch or double counting. For a rejected terminal child result, restore every row the child changed (target and extras) and remove their unadmitted decisions in the working-tree ledger from the pre-dispatch copy with the tuple before refresh (all dispatched non-cut rows for seed); preserve other child changes, child commits and consumed iterations for the human's discard/reset decision. |
|
|
125
|
+
| Actual multi-member companion | Apply `/skill:loop-implementation` `reference/coordinated-legs.md` `## Resume`: require unchanged home HEAD and exactly one new companion commit with tuple attribution, then validate its result. Record that leg's admitted rows, decisions or seeded state and SHA, removing the tuple in the home-ledger commit; never rerun the recovered row or its admitted extras. Stop preserving state on boundary/attribution failures (zero, unrelated or multiple commits or attribution mismatch); route post-boundary result-validation misses through ordinary `loop: repair row <n>` or seed `loop: repair seed` repair in that protocol, clear `pending:` and preserve the consumed iteration. |
|
|
126
126
|
|
|
127
127
|
## Spec with plan
|
|
128
128
|
|
|
@@ -9,7 +9,7 @@ Run this driver in the main session. Carry the absolute implementation worktree
|
|
|
9
9
|
|
|
10
10
|
## Entry
|
|
11
11
|
|
|
12
|
-
1. Read project overrides below. Resolve `gauntlet_setting({ key: "loopEngine" })` and retain `maxIterationsPerRow` (default 4). Stop on an unavailable settings tool. Select this run's seed value in step 4 and persist it only in the ledger, not in the spec or settings.
|
|
12
|
+
1. Read project overrides below. Resolve `gauntlet_setting({ key: "loopEngine" })` and retain `maxIterationsPerRow` (default 4) and `escalateAfter` (default 3). Hold `escalateAfter` in driver memory for this run; never print it or write it to the ledger, and never re-resolve it inside the loop, including after amendment grant re-decodes. Stop on an unavailable settings tool. Select this run's seed value in step 4 and persist it only in the ledger, not in the spec or settings.
|
|
13
13
|
2. Resolve verification once using this ordered table. Print `verification command: <command> (<source>)`.
|
|
14
14
|
|
|
15
15
|
| Observed configuration | Action |
|
|
@@ -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
|
-
At seed and at an authorized restore, before entering the dispatch loop, read the spec in full. Decode `**Amend-grant:**` through [amendment-surface Value decoding](../brainstorming/reference/amendment-surface.md). Capture the decoded grant, ordered leg membership, each leg's absolute checkout paths and combined spec/task Rules for dispatch and handoff; use home for zero or one leg.
|
|
21
|
+
At seed and at an authorized restore, before entering the dispatch loop, read the spec in full. Decode `**Amend-grant:**` through [amendment-surface Value decoding](../brainstorming/reference/amendment-surface.md). Capture the decoded grant, ordered leg membership, each leg's absolute checkout paths and combined spec/task Rules for dispatch and handoff; use home for zero or one leg. At an authorized fresh-session restore, resolve `gauntlet_setting({ key: "loopEngine" })` exactly once at this boundary and hold `escalateAfter` for the run; share that read with reconstruction's Shared allowance boundary, including legacy allowance migration. With an unavailable tool, stop before dispatch with `cannot resolve escalation threshold: settings tool unavailable (loopEngine.escalateAfter)`; never supply an in-loop default.
|
|
22
22
|
|
|
23
23
|
At seed and at an authorized restore, when the held membership has more than one leg, read `reference/coordinated-legs.md` IN FULL now with read at its absolute path; stop on an unreadable reference naming its path, never reconstruct it. On fresh entry apply its `## Entry` section at step 4 instead of the home-only skeleton below, using step 4's resolved allowance seed value. 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.
|
|
24
24
|
|
|
@@ -44,7 +44,7 @@ allowance: <n>
|
|
|
44
44
|
Use the actual repo-relative spec path alone in the Spec header. Read base with `git -C "<worktree>" rev-parse HEAD`. Index every spec AC in order, including nested disposition lines without treating them as extra ACs; keep row text only in the spec. Initialize explicit/absent `in-scope` and `venue:` rows `open`, attempts 0, evidence empty. Initialize only `deviates:`, `deferred:`, `elsewhere:` rows `cut`, attempts 0, disposition text as evidence. Write `allowance:` immediately after `iterations: 0` using step 4's seed value; never scale it by row count or seed dispatches: 24 non-cut rows at default 4 yield allowance 4. All-cut ledgers keep the same allowance and exit through rule 1 without a seed dispatch. Later cuts, reopens and requirement-preserving amendments leave allowance unchanged; redraw deletes the ledger and reseeds it. Serialize evidence in one physical table cell, result lines separated by semicolons, pipes escaped, plain text without angle markup.
|
|
45
45
|
5. Call `plan_check({ planPath: <ledger path> })`. Stop with findings as a configuration error. Commit the stamped skeleton as `loop: seed ledger`; call `phase_tracker({ action: "start", phase: "implement" })`, then `plan_tracker({ action: "refresh", path: <absolute ledger path> })`, including all-cut ledgers with no seed dispatch.
|
|
46
46
|
|
|
47
|
-
For resume, use the admitted ledger, its base and verification command, and durable iterations and allowance instead of overwriting it. Skip Entry step 1 for an admitted ledger;
|
|
47
|
+
For resume, use the admitted ledger, its base and verification command, and durable iterations and allowance instead of overwriting it. Skip Entry step 1's allowance seeding for an admitted ledger; the allowance setting applies only to fresh seeding, and the restore boundary resolves `escalateAfter` once. Apply `/skill:gauntlet-resume` pending recovery and its `loop: migrate allowance` step before refresh and any unresolved dirty-tree stop. Render rows, allowance and decision history; stop on a dirty tree before dispatch.
|
|
48
48
|
|
|
49
49
|
## Driver holds
|
|
50
50
|
|
|
@@ -65,17 +65,17 @@ Read the ledger and require `allowance:` and no `cap:` before the ordered table,
|
|
|
65
65
|
| 1 | No `open` or `failed` row | Refresh; require implement complete; continue at ## After the loop step 0. All-cut and baseline-complete ledgers exit here. |
|
|
66
66
|
| 2 | Unresolved row attempts >= effective allowance | Human stop under approval option 4 and option 5 alike. Exempt `done` and `cut`. |
|
|
67
67
|
| 3 | Last two child row commits both changed no file outside the ledger and no row status, or two consecutive validation misses | Human stop. Count child `loop: row ...` commits only for empty-child stagnation, excluding seed and parent repair/retry/cut commits. A decision alone is not progress. |
|
|
68
|
-
| 4 | Dispatch allowed | Select target by the priority below, then apply the
|
|
68
|
+
| 4 | Dispatch allowed | Select target by the priority below, then apply the escalation threshold check. |
|
|
69
69
|
|
|
70
70
|
When a refresh leaves implement active at rule 1, take the matching human stop; never start verify. Before evaluating rule 2, compute each row's effective allowance as the ledger's `allowance:` plus the count of that row's `row <n> retry | ... | allowed attempts <m>` lines in `## Decisions`. Count each retry once; the stored `<m>` is historical, not an absolute override. Ignore every other decision and grant in this arithmetic. Use the same rule for every coordinated entry, regardless of leg. With allowance 4 and two retries, a row has effective allowance 6 even if the historical `<m>` values are 5 and 99; raising the shared header to 7 makes it 9, while an unretried row gets 7. After migration, a row already at or above its effective allowance is exhausted. Never reset or reduce attempts.
|
|
71
71
|
|
|
72
72
|
Use the held decoded amendment grant for amendment choices only; ignore its waves-only budget token, retaining no amend permission for a budget-only value. Never consult this value for row escalation. Apply hard boundaries and rule 3 before escalation: stagnation stops without dispatch. Preserve `STAGNANT`, `REJECT`, closureReview and grant_fix_rounds boundaries; this is implement permission, not verify credit.
|
|
73
73
|
|
|
74
|
-
Select `seed` until a validated `loop: seed evidence` commit with its `row 0 done` decision
|
|
74
|
+
Select `seed` until a validated `loop: seed evidence` commit with its `row 0 done` decision 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. Read seed survival from the ledger at HEAD and the matching validated commit, not from evidence completeness. Never re-seed a validated partial seed to fill empty evidence cells. Use the latest admitted return's target decision for targeting and the child's `previous decision:` input, never an extra's line; skip allowance-grant and legacy grant history while retaining the entire Decisions section. Durable status beats a decision's suggestion. Keep the retry hint only for its target.
|
|
75
75
|
|
|
76
|
-
For a numbered target
|
|
76
|
+
For a numbered target with no durable `row <n> escalated` decision, compute `threshold = min(escalateAfter, effective allowance - 1)` from the held `escalateAfter` and the row's effective allowance. When the row's pre-dispatch attempts >= threshold, make this dispatch the escalated round: resolve `gauntlet_setting({ key: "escalationLoop" })` under approval option 4 and option 5 alike. Use the returned non-empty `implModel` string, including the resolver's main-loop model fallback when the setting is unset or malformed. With the settings tool unavailable or `implModel` undefined (neither a configured model nor a main-loop model), stop before dispatch with `Problem: escalationLoop.implModel unavailable`; write no escalation decision and leave attempts unchanged. Never substitute a default-model attempt for that stop.
|
|
77
77
|
|
|
78
|
-
With the resolved model, append `row <n> escalated | implModel round | allowed attempts <effective allowance>`, commit parent `loop: escalate row <n>`, print `escalated round for row <n>`, and dispatch that
|
|
78
|
+
With the resolved model, append `row <n> escalated | implModel round | allowed attempts <effective allowance>`, commit parent `loop: escalate row <n>`, print `escalated round for row <n>`, and dispatch that attempt with `model: <implModel>` through the normal loop-implementer target, bump and validation path. Exclude this parent commit from child-only stagnation; it starts no new empty-child or validation-miss window. With defaults (allowance 4, `escalateAfter` 3), escalate attempt 4; clamp larger thresholds to the last allowed attempt. At allowance 1, escalate the first and only allowed attempt. A row found past the threshold unspent escalates on its next dispatch. Treat the escalated decision as exactly one round within the effective allowance, never an extra attempt. Recover the row's escalation identity from `## Decisions` on every dispatch and resume; unrecoverable means spent. Preserve `row <n> escalated` for the whole flow across retry, `rows +N`, amendments and resume. Once spent, omit the model override on every later authorized attempt, including every attempt up to a raised effective allowance; never resolve another row escalation or append a second marker. With no marker and recoverable history, apply the threshold predicate after retry or `rows +N`; a previously missing model or migrated row has not spent escalation. Dispatch errors, non-DONE and validation failures consume it before any recommendation decision.
|
|
79
79
|
|
|
80
80
|
## Dispatch
|
|
81
81
|
|
|
@@ -96,24 +96,26 @@ Omit the model override except on an escalated round (`model: <implModel>`); let
|
|
|
96
96
|
## Validate the return
|
|
97
97
|
|
|
98
98
|
For home returns, apply the following boundary and table. Never project terminal child results before clean/exactly-one-commit admission.
|
|
99
|
-
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 restore the
|
|
99
|
+
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 restore every row the child changed (target and extras) from the saved copy and use the separately recorded dispatch identity to restore the `pending:` tuple in the working ledger; for seed restore all dispatched non-cut rows. Preserve consumed iterations and all other child changes and commits. Refresh before the discard/reset stop. 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.
|
|
100
100
|
2. Validate by dispatched `target`, not iteration number, against the saved copy using this table. Require the commit body's first two lines to match the exact dispatched `pending:` tuple and consumed `iteration:` (saved iterations +1), using the persona's home attribution contract for row and seed alike. Admit only `iterations` +1 and removal of the dispatched `pending:` line; reject every other header change, including allowance. Preserve row identifiers and prior decisions.
|
|
101
101
|
|
|
102
102
|
| Target | Required changes | Miss |
|
|
103
103
|
|---|---|---|
|
|
104
|
-
| Seed | All attempts stay 0; only `open` -> `done` status transitions;
|
|
105
|
-
| Ordinary |
|
|
106
|
-
| Seed or ordinary | No committed tuple remains. | A retained committed tuple is
|
|
104
|
+
| Seed | Require bounded nonempty evidence on at least one non-cut row; admit a subset or all rows. All attempts stay 0; only `open` -> `done` status transitions; keep unreached rows unchanged, including open rows with empty evidence, and preserve cut rows. Require exactly one new `row 0 done | seeded <k> rows, closed <m> on baseline | <first row to take>` decision, counting only rows given evidence in this return and their proven baseline closures. Partial and complete baseline closure both pass. Never `failed`. | Seed repair below; retry seed once with the miss in `hint:`. A second consecutive miss stops for a human. |
|
|
105
|
+
| Ordinary | Require the target row's mutable cells to change: status `done` or `failed`, attempts +1, nonempty bounded evidence. Admit zero or more other rows in the same leg changing `open` -> `done`, each with nonempty bounded evidence and attempts unchanged. Require set equality between changed rows and rows with exactly one new decision line, target first, each matching its row and status. Keep every other row, header and prior decision unchanged apart from the dispatch header changes in step 2. | Parent repair below. |
|
|
106
|
+
| Seed or ordinary | No committed tuple remains. | A retained committed tuple is a validation miss for the dispatched target after boundary admission, not a boundary rejection. |
|
|
107
107
|
|
|
108
|
-
Require ordinary decision
|
|
108
|
+
Require each ordinary decision to match `^row <that row> <that status> \| .{1,200} \| .{1,200}$`; an old conforming line is not a new line. Require check name plus at most three result lines in evidence, serialized with semicolons and escaped pipes as in the persona; add no command-format check. Reject a `failed` extra, a foreign-leg extra, a changed `cut` or `done` row, extra attempts changes, empty evidence on a changed row, or a missing, duplicate, malformed or unmatched decision. Admit an evidenced `done` extra beside a `failed` target. Never police worker ordering or execute a check after a return; leave regressions to verify step 1's gate set. Keep the child subject `loop: row <target n> <status>`; declare extras only in the ledger diff and their decision lines.
|
|
109
109
|
|
|
110
|
-
On
|
|
110
|
+
On a seed miss after clean/exactly-one-commit admission, restore every non-cut row, headers (including the saved allowance) and decisions from the saved copy; preserve cut rows and retain the incremented iterations. Clear `pending:`, commit parent `loop: repair seed`, then refresh. Count the miss in the existing consecutive-validation-miss window; after the first miss re-dispatch `seed` once with the existing seed-contract instruction followed by `seed miss: <concrete miss>` in `hint:` and `previous decision: none`. After the second consecutive miss, repair and take the existing human stop without another dispatch. A validated seed resets the window, so ordinary dispatch starts clean. Exclude `loop: repair seed` from child-only stagnation. Recover the miss window from branch history on resume; between repair and retry select seed from the restored ledger with the seed-contract instruction only, since the miss hint is transient. Keep dirty/zero/multiple-commit seed returns at the human-gated boundary stop, outside this repair.
|
|
111
|
+
|
|
112
|
+
On an ordinary miss restore every non-target row, headers (including the saved allowance) and prior decisions from the saved copy, retaining the incremented iterations. Set the target `failed`, attempts = saved attempts +1, evidence = `iteration <i>: <concrete miss>`. Clear `pending:`, remove malformed appended decisions and commit `loop: repair row <n>`, then refresh. Set previous decision to `none`; on an admitted ordinary return use the target's new conforming line, never the last extra's. 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.
|
|
111
113
|
|
|
112
114
|
## Human stop
|
|
113
115
|
|
|
114
116
|
Read and apply [menu-layout](../brainstorming/reference/menu-layout.md). Start the message with `gauntlet: question` at every human stop, including the discard/reset authorization ask for a dirty or commit-boundary return. Put it alone on the first output line, followed by a newline before the stop text, without a driver-state preamble, wrapper or code fence. These decisions are neither done nor stuck outcomes.
|
|
115
117
|
|
|
116
|
-
Implement-phase stops only: leave implement active and use the row offers below. For verify stops use the menus in After the loop, retaining verify active. Use one screen of plain words: `Stopped on row <n>` for row failures, or the concrete boundary/verification header. `Problem:` quotes the evidence cell and last decision line verbatim; attribute any spec/code contradiction, failing assertion/output or missing capability to that evidence, without investigating it. At every human stop, including allowance, stagnation, missing-model, dirty-tree, commit-boundary, interrupted-dispatch and exit-verification stops, list every `open` or `failed` row after the header as `row <n>: <attempts> of <effective allowance>`, then print `Preserve every row's attempts.` Compute each denominator from the ledger using Before each dispatch; keep retry increments in `## Decisions`, never a new table column. Use restored pre-dispatch
|
|
118
|
+
Implement-phase stops only: leave implement active and use the row offers below. For verify stops use the menus in After the loop, retaining verify active. Use one screen of plain words: `Stopped on row <n>` for row failures, or the concrete boundary/verification header. `Problem:` quotes the evidence cell and last decision line verbatim; attribute any spec/code contradiction, failing assertion/output or missing capability to that evidence, without investigating it. At every human stop, including allowance, stagnation, missing-model, dirty-tree, commit-boundary, interrupted-dispatch and exit-verification stops, list every `open` or `failed` row after the header as `row <n>: <attempts> of <effective allowance>`, then print `Preserve every row's attempts.` Compute each denominator from the ledger using Before each dispatch; keep retry increments in `## Decisions`, never a new table column. Use restored pre-dispatch row cells at rejected-return stops, not unadmitted child results. With no open or failed rows, omit row lines but retain the preservation sentence. Name `escalated round spent` when applicable; omit round history and reasoning traces.
|
|
117
119
|
|
|
118
120
|
Offer retry and `rows +N` only at row stops (allowance, stagnation or missing-model), alongside Amend and Stop. Dirty-tree and commit-boundary stops keep discard authorization only; interrupted-dispatch stops keep their recovery authorization, never budget offers; the verify-step-1 verification-failure stop keeps amend/stop only. A budget increase never bypasses those gates.
|
|
119
121
|
|
|
@@ -126,7 +128,7 @@ Offer retry and `rows +N` only at row stops (allowance, stagnation or missing-mo
|
|
|
126
128
|
|
|
127
129
|
Serialize the bare decision as `rows +50 |` with an empty words cell. Raising allowance resets neither window (empty-child or validation-miss); exclude `loop: raise allowance` from child-commit counting. Only retry starts new windows.
|
|
128
130
|
|
|
129
|
-
For dirty/commit-boundary retry, ask the human to authorize discarding uncommitted changes/resetting to last valid commit. Preserve the pre-dispatch escalation decision through authorized discard/reset; it stays spent. Boundary rejection never increments attempts: an escalated
|
|
131
|
+
For dirty/commit-boundary retry, ask the human to authorize discarding uncommitted changes/resetting to last valid commit. Preserve the pre-dispatch escalation decision through authorized discard/reset; it stays spent. Boundary rejection never increments attempts: an escalated rejection leaves attempts at the pre-dispatch count, so after authorized recovery dispatch on the worker model without requiring retry or `rows +N`, subject to the normal boundaries. An admitted validation miss instead increments attempts by exactly one through `loop: repair row <n>`, retains the spent decision and returns to the ordered table: a row at its effective allowance stops under rule 2; a row below it gets its next attempt on the worker model. Exhaustion and stagnation keep precedence over dispatch; neither window resets. 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.
|
|
130
132
|
|
|
131
133
|
Outside the dispatch loop, perform authorized amendment actions under the amendment surface's own reads. After the amendment commit, re-decode the grant and refresh the held membership, checkout paths and Rules from the updated spec before returning to the loop; apply this to funding and standing-grant amendments too. Leave the `**Amend-grant:**` header grammar unchanged.
|
|
132
134
|
|
|
@@ -14,7 +14,7 @@ Use the same allowance seed value and source echo as the home driver's Entry ste
|
|
|
14
14
|
|
|
15
15
|
## Resume
|
|
16
16
|
|
|
17
|
-
Use the held membership and checkout paths captured by the driver's Entry at seed or authorized restore. 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
|
|
17
|
+
Use the held membership and checkout paths captured by the driver's Entry at seed or authorized restore. 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. Apply the home-ledger commit without another attempt or iteration bump, clear pending and retain prior decisions. Stop preserving state on boundary/attribution failures (zero commits, unrelated movement, multiple commits or attribution mismatch); route post-boundary result-validation misses through ordinary `loop: repair row <n>` or seed `loop: repair seed` repair below, clear `pending:` and preserve the consumed iteration. After a successful parent commit no pending tuple remains, so a subsequent resume cannot duplicate the update.
|
|
18
18
|
|
|
19
19
|
## Targeting and dispatch
|
|
20
20
|
|
|
@@ -24,15 +24,15 @@ For coordinated dispatch, capture home and selected leg heads and save a pre-dis
|
|
|
24
24
|
|
|
25
25
|
## Companion return validation
|
|
26
26
|
|
|
27
|
-
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
|
|
27
|
+
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 four-column row lines followed by decision lines. For ordinary returns require the target (`done` or `failed`, attempts +1) plus zero or more same-leg `open` -> `done` extras with attempts unchanged. Require nonempty bounded evidence and exactly one matching bounded decision for each changed row, target decision first, under the driver's `## Validate the return` table; admit extras beside a failed target without policing worker order or adding a command-format check. For seed require all and only that leg's non-cut rows, at least one with evidence, and one bounded seed decision; admit unchanged open rows with empty evidence. Reject a wrong target, foreign entry, changed cut/done extra, header changes, fabricated counts, malformed evidence or unmatched/duplicate rows or decisions. Validate seed counts against nonempty evidence cells and baseline closures; require zero attempts and set only this leg's seeded value to `yes` after the complete row set validates. Route seed validation misses after boundary admission through the driver's seed repair and two-miss window: commit `loop: repair seed` in home to record the rejection, write no returned rows or decisions, clear `pending:`, leave this leg's seeded value `no` and preserve foreign state. Retain consumed iterations and saved allowance; retry this leg's seed once with the seed-contract instruction plus `seed miss: <concrete miss>` in `hint:`, then stop on a second consecutive miss. A validated seed resets the window. Route ordinary validation misses after boundary admission through the driver's ordinary repair: set only the target `failed` with attempts +1 and `iteration <i>: <miss>` evidence, append its repair decision, clear `pending:` and commit `loop: repair row <n>` in home. Preserve every non-target row and prior decision from the saved copy, consumed iterations and saved allowance; count the miss in the existing two-miss window.
|
|
28
28
|
|
|
29
|
-
After validation
|
|
29
|
+
After validation write every admitted row and append every validated decision to the home ledger, record companion SHA in evidence, and remove `pending:` in one home-ledger commit. Use the target's decision as the next dispatch's `previous decision:`, never an extra's. 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; only the companion's own commits are counted at the boundary. For rejected returns apply the post-boundary repair above; preserve the pending tuple and every row only on boundary rejection. Recover interrupted returns by this same validation, never trusted reply text.
|
|
30
30
|
|
|
31
31
|
For coordinated home returns apply it only to home entries/seeded state, preserving foreign entries, mappings and bases.
|
|
32
32
|
|
|
33
33
|
### Companion commit admission
|
|
34
34
|
|
|
35
|
-
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.
|
|
35
|
+
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 boundary failure preserve pending and stop; on a seed validation miss after the boundary passes apply the seed repair above. On an ordinary validation miss after the boundary passes apply ordinary repair above, not the boundary stop. On success use the emitted object for the single parent ledger commit described above; never treat it as permission for another dispatch.
|
|
36
36
|
|
|
37
37
|
<!-- companion-return-shell -->
|
|
38
38
|
```bash
|
|
@@ -66,26 +66,38 @@ const oldRows = table(ledger);
|
|
|
66
66
|
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');
|
|
67
67
|
const expected = oldRows.filter(r => map.some(m => m[0] === r[0] && m[2] === leg) && (target === 'seed' ? r[1] !== 'cut' : r[0] === target));
|
|
68
68
|
requireThat(expected.length && expected.every(r => r[1] !== 'cut'), 'foreign or cut target');
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
const decisionStart = resultLines.findIndex(line => /^row \d+ /.test(line));
|
|
70
|
+
requireThat(decisionStart > 0, 'missing rows or decisions');
|
|
71
|
+
const rowLines = resultLines.slice(0, decisionStart);
|
|
72
|
+
const decisions = resultLines.slice(decisionStart);
|
|
73
|
+
const decision = decisions[0];
|
|
74
|
+
const rows = rowLines.map(cells);
|
|
75
|
+
requireThat(rowLines.every(l => l.startsWith('|') && l.endsWith('|')) && rows.every(r => r.length === 4), 'malformed row');
|
|
76
|
+
requireThat(new Set(rows.map(r => r[0])).size === rows.length, 'duplicate row');
|
|
77
|
+
requireThat(target === 'seed' ? rows.length === expected.length : rows.some(r => r[0] === target), 'missing target or seed row');
|
|
78
|
+
for (const row of rows) {
|
|
79
|
+
const old = (target === 'seed' ? expected : oldRows).find(r => r[0] === row[0]);
|
|
80
|
+
requireThat(old && row[0] === old[0] && map.some(m => m[0] === row[0] && m[2] === leg), 'foreign row');
|
|
81
|
+
const isTarget = row[0] === target;
|
|
82
|
+
requireThat((target === 'seed' ? /^(open|done)$/ : isTarget ? /^(done|failed)$/ : /^done$/).test(row[1]), 'invalid status');
|
|
83
|
+
requireThat(target === 'seed' ? /^(open|done)$/.test(old[1]) : (isTarget ? /^(open|failed)$/.test(old[1]) : old[1] === 'open'), 'changed cut or closed row');
|
|
84
|
+
requireThat(/^\d+$/.test(row[2]) && Number(row[2]) === Number(old[2]) + (isTarget ? 1 : 0) && (target !== 'seed' || Number(row[2]) === 0), 'attempt mismatch');
|
|
85
|
+
requireThat((row[3].length > 0 || (target === 'seed' && row[1] === 'open' && row[3] === old[3])) && row[3].split(';').length <= 4 && !/[<>]/.test(row[3]), 'unbounded evidence');
|
|
78
86
|
requireThat(target !== 'seed' || old[1] !== 'done' || row[1] === 'done', 'lost baseline closure');
|
|
79
87
|
}
|
|
80
88
|
if (target === 'seed') {
|
|
81
|
-
requireThat(subject === 'loop: seed evidence', 'seed subject mismatch');
|
|
89
|
+
requireThat(subject === 'loop: seed evidence' && decisions.length === 1, 'seed subject or decision mismatch');
|
|
82
90
|
const d = /^row 0 done \| seeded (\d+) rows, closed (\d+) on baseline \| .{1,200}$/.exec(decision);
|
|
83
|
-
requireThat(d && Number(d[1]) ===
|
|
91
|
+
requireThat(d && Number(d[1]) >= 1 && Number(d[1]) === rows.filter(r => r[3].length > 0).length && Number(d[2]) === rows.filter(r => r[1] === 'done').length, 'seed counts mismatch');
|
|
84
92
|
} else {
|
|
85
|
-
|
|
86
|
-
requireThat(
|
|
93
|
+
const targetRow = rows.find(r => r[0] === target);
|
|
94
|
+
requireThat(subject === `loop: row ${target} ${targetRow[1]}`, 'row subject mismatch');
|
|
95
|
+
const parsed = decisions.map(line => /^row (\d+) (done|failed) \| .{1,200} \| .{1,200}$/.exec(line));
|
|
96
|
+
requireThat(parsed.length === rows.length && parsed.every(Boolean), 'invalid decision set');
|
|
97
|
+
requireThat(parsed[0][1] === target && new Set(parsed.map(d => d[1])).size === rows.length, 'decision order or duplicate');
|
|
98
|
+
requireThat(parsed.every(d => rows.some(r => r[0] === d[1] && r[1] === d[2])), 'unmatched decision');
|
|
87
99
|
}
|
|
88
|
-
console.log(JSON.stringify({ sha: commit, rows:
|
|
100
|
+
console.log(JSON.stringify({ sha: commit, rows: rowLines, decisions, decision }));
|
|
89
101
|
NODE
|
|
90
102
|
```
|
|
91
103
|
|