pi-gauntlet 7.41.3 → 7.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/agents/code-reviewer.md +16 -0
- package/agents/implementer.md +16 -0
- package/agents/plan-writer.md +18 -4
- package/agents/spec-reviewer.md +18 -0
- package/extensions/lib/plan-check.test.ts +132 -85
- package/extensions/lib/plan-check.ts +28 -8
- package/extensions/phase-tracker.test.ts +60 -2
- package/extensions/phase-tracker.ts +11 -5
- package/package.json +1 -1
- package/skills/brainstorming/reference/amendment-surface.md +5 -1
- package/skills/subagent-driven-development/SKILL.md +74 -112
- package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +17 -11
- package/skills/subagent-driven-development/implementer-prompt.md +16 -11
- package/skills/subagent-driven-development/reference/dispatch-contract.md +39 -0
- package/skills/subagent-driven-development/reference/fix-loop.md +6 -4
- package/skills/subagent-driven-development/reference/implement-amendments.md +9 -0
- package/skills/subagent-driven-development/spec-reviewer-prompt.md +13 -10
- package/skills/subagent-driven-development/stop-note.md +3 -1
- package/skills/writing-plans/reference/plan-contract.md +1 -0
|
@@ -43,6 +43,7 @@ interface TestsBullet {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
interface Task {
|
|
46
|
+
orchestrator?: string;
|
|
46
47
|
leg?: string;
|
|
47
48
|
number: number;
|
|
48
49
|
line: number;
|
|
@@ -96,7 +97,7 @@ interface Header {
|
|
|
96
97
|
separatorLine: number | undefined;
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
interface ParsedPlan {
|
|
100
|
+
export interface ParsedPlan {
|
|
100
101
|
lines: string[];
|
|
101
102
|
header: Header;
|
|
102
103
|
waves: Wave[];
|
|
@@ -303,6 +304,7 @@ function parsePlan(planText: string): ParsedPlan {
|
|
|
303
304
|
bodyStartLine: i + 1,
|
|
304
305
|
bodyEndLine: bodyEndIdx + 1,
|
|
305
306
|
waveNumber,
|
|
307
|
+
orchestrator: lines.slice(i + 1, bodyEndIdx + 1).map(line => /^Orchestrator: (.+)$/.exec(line)).find(match => match)?.[1],
|
|
306
308
|
filesBlockMissing: filesLine === undefined,
|
|
307
309
|
files,
|
|
308
310
|
specAnchorLine,
|
|
@@ -1278,11 +1280,12 @@ function checkLedger(planText: string, specText: string): PlanCheckFinding[] {
|
|
|
1278
1280
|
return findings;
|
|
1279
1281
|
}
|
|
1280
1282
|
|
|
1281
|
-
export function checkPlan(planText: string, specText: string, fs: FsPort): PlanCheckFinding[] {
|
|
1283
|
+
export function checkPlan(planText: string, specText: string, fs: FsPort): { findings: PlanCheckFinding[]; parsed?: ParsedPlan; ledger: boolean } {
|
|
1284
|
+
const ledger = planText.split("\n", 1)[0] === "# Ledger - loop engine";
|
|
1282
1285
|
try {
|
|
1283
1286
|
const ownership = legOwnership(specText);
|
|
1284
|
-
if (ownership.findings.length) return ownership.findings;
|
|
1285
|
-
if (
|
|
1287
|
+
if (ownership.findings.length) return { findings: ownership.findings, ledger };
|
|
1288
|
+
if (ledger) return { findings: checkLedger(planText, specText), ledger };
|
|
1286
1289
|
const parsed = parsePlan(planText);
|
|
1287
1290
|
const findings: PlanCheckFinding[] = [];
|
|
1288
1291
|
if (ownership.legs.length) {
|
|
@@ -1293,7 +1296,7 @@ export function checkPlan(planText: string, specText: string, fs: FsPort): PlanC
|
|
|
1293
1296
|
if (!ownership.legs.some(l => l.repo === task.leg)) findings.push({ check: "leg-unknown", line: task.line + 1, text: first, reason: `unknown task leg ${task.leg}` });
|
|
1294
1297
|
if (parsed.lines.slice(task.line + (explicit ? 1 : 0), task.bodyEndLine).some(l => /^Leg:/.test(l))) findings.push({ check: "leg-unknown", line: task.line, text: task.text, reason: "Leg: must be the first line after the task heading" });
|
|
1295
1298
|
}
|
|
1296
|
-
if (findings.length) return findings;
|
|
1299
|
+
if (findings.length) return { findings, parsed, ledger };
|
|
1297
1300
|
for (const wave of parsed.waves) if (new Set(parsed.tasks.filter(t => t.waveNumber === wave.number).map(t => t.leg)).size > 1) findings.push({ check: "wave-leg", line: wave.line, text: wave.text, reason: "wave mixes task legs" });
|
|
1298
1301
|
}
|
|
1299
1302
|
|
|
@@ -1343,16 +1346,33 @@ export function checkPlan(planText: string, specText: string, fs: FsPort): PlanC
|
|
|
1343
1346
|
findings.push(...checkHeaderEntrypoint(parsed));
|
|
1344
1347
|
findings.push(...checkHappyPathLine(parsed));
|
|
1345
1348
|
findings.push(...checkWaiverLiteral(parsed));
|
|
1346
|
-
return findings;
|
|
1349
|
+
return { findings, parsed, ledger };
|
|
1347
1350
|
} catch (err) {
|
|
1348
1351
|
const message = String(err instanceof Error ? err.message : err);
|
|
1349
|
-
return [
|
|
1352
|
+
return { ledger, findings: [
|
|
1350
1353
|
{
|
|
1351
1354
|
check: "internal",
|
|
1352
1355
|
line: 0,
|
|
1353
1356
|
text: "",
|
|
1354
1357
|
reason: message.split("\n")[0],
|
|
1355
1358
|
},
|
|
1356
|
-
];
|
|
1359
|
+
] };
|
|
1357
1360
|
}
|
|
1358
1361
|
}
|
|
1362
|
+
|
|
1363
|
+
export function renderManifest(parsed: ParsedPlan, planAbs: string, specAbs: string): string {
|
|
1364
|
+
const kinds = { create: "Create", modify: "Modify", test: "Test" };
|
|
1365
|
+
return [
|
|
1366
|
+
`manifest: ${planAbs} | spec: ${specAbs} | tasks: ${parsed.tasks.length} | waves: ${parsed.waves.length}`,
|
|
1367
|
+
...parsed.tasks.map(task => JSON.stringify({
|
|
1368
|
+
task: task.number,
|
|
1369
|
+
wave: task.waveNumber,
|
|
1370
|
+
...(/\bin-place\b/.test(parsed.waves.find(wave => wave.number === task.waveNumber)?.label ?? "") ? { mode: "in-place" } : {}),
|
|
1371
|
+
heading: task.text.replace(/^### Task \d+: /, ""),
|
|
1372
|
+
files: task.files.map(file => ({ kind: kinds[file.kind], path: file.path })),
|
|
1373
|
+
tests: task.tests.map(bullet => bullet.value),
|
|
1374
|
+
...(task.leg !== undefined ? { leg: task.leg } : {}),
|
|
1375
|
+
...(task.orchestrator !== undefined ? { orchestrator: task.orchestrator } : {}),
|
|
1376
|
+
})),
|
|
1377
|
+
].join("\n");
|
|
1378
|
+
}
|
|
@@ -990,7 +990,12 @@ test("plan_check pass: text, details, and stamp arming when a flow is entered",
|
|
|
990
990
|
content: { text: string }[];
|
|
991
991
|
details: { status: string; planPath: string; planSha256: string; specPath: string; specSha256: string };
|
|
992
992
|
};
|
|
993
|
-
assert.match(res.content[0].text, /^PASS/);
|
|
993
|
+
assert.match(res.content[0].text, /^PASS[^\n]*\n\nmanifest: /);
|
|
994
|
+
const manifestLines = res.content[0].text.split("\n").slice(2);
|
|
995
|
+
assert.equal(manifestLines[0], `manifest: ${planPath} | spec: ${specPath} | tasks: 2 | waves: 1`);
|
|
996
|
+
assert.deepEqual(manifestLines.slice(1).map(line => JSON.parse(line).task), [1, 2]);
|
|
997
|
+
const repeated = await tool.execute("repeat", { planPath }, undefined, undefined, h.ctx);
|
|
998
|
+
assert.deepEqual(repeated, res);
|
|
994
999
|
assert.equal(res.details.status, "pass");
|
|
995
1000
|
assert.equal(res.details.planPath, planPath);
|
|
996
1001
|
assert.equal(res.details.specPath, specPath);
|
|
@@ -1043,7 +1048,7 @@ test("plan_check pass outside a flow: no stamp, plain-linter text", async () =>
|
|
|
1043
1048
|
await h.emit("session_start");
|
|
1044
1049
|
const tool = h.tools.find((t) => t.name === "plan_check")!;
|
|
1045
1050
|
const res = (await tool.execute("t1", { planPath }, undefined, undefined, h.ctx)) as { content: { text: string }[] };
|
|
1046
|
-
assert.match(res.content[0].text, /^PASS \(no flow to stamp\)/);
|
|
1051
|
+
assert.match(res.content[0].text, /^PASS \(no flow to stamp\)\n\nmanifest: /);
|
|
1047
1052
|
});
|
|
1048
1053
|
|
|
1049
1054
|
test("plan_check fail: mutated plan body, no error key, clears stamp", async () => {
|
|
@@ -1059,6 +1064,7 @@ test("plan_check fail: mutated plan body, no error key, clears stamp", async ()
|
|
|
1059
1064
|
details: { status: string; error?: string };
|
|
1060
1065
|
};
|
|
1061
1066
|
assert.match(res.content[0].text, /^FAIL/);
|
|
1067
|
+
assert.ok(!res.content[0].text.includes("\nmanifest:"));
|
|
1062
1068
|
assert.equal(res.details.status, "fail");
|
|
1063
1069
|
assert.equal("error" in res.details, false);
|
|
1064
1070
|
|
|
@@ -1448,6 +1454,7 @@ test("ledger handler stamps after brainstorm and skipped plan, then admits imple
|
|
|
1448
1454
|
await call("skip", "plan", "loop engine");
|
|
1449
1455
|
const checked = await h.tools.find(t => t.name === "plan_check")!.execute("check", { planPath }, undefined, undefined, h.ctx) as any;
|
|
1450
1456
|
assert.equal(checked.details.status, "pass");
|
|
1457
|
+
assert.equal(checked.content[0].text, "PASS \u2014 plan verified and stamped for implement-start");
|
|
1451
1458
|
assert.equal(checked.details.planSha256, sha256Hex(readFileSync(planPath)));
|
|
1452
1459
|
const started = await call("start", "implement");
|
|
1453
1460
|
assert.equal(started.details.error, undefined);
|
|
@@ -2649,3 +2656,54 @@ for (const pattern of [".worktrees/<name>", "../companion-<name>"]) test(`plan_c
|
|
|
2649
2656
|
const missing = await tool.execute("legs-missing-root", { planPath }, undefined, undefined, h.ctx) as { details: { findings: { check: string }[] } };
|
|
2650
2657
|
assert.ok(missing.details.findings.some(f => f.check === "input"));
|
|
2651
2658
|
});
|
|
2659
|
+
|
|
2660
|
+
for (const length of [32767, 32768, 32769]) {
|
|
2661
|
+
test(`plan_check complete manifest JS length ${length} bounds stamping`, async () => {
|
|
2662
|
+
const dir = gitPrimary().primary;
|
|
2663
|
+
const { planPath, specPath } = writePlanFixture(dir);
|
|
2664
|
+
const passLine = "PASS \u2014 plan verified and stamped for implement-start";
|
|
2665
|
+
const prefix = `${passLine}\n\nmanifest: ${planPath} | spec: ${specPath} | tasks: 1 | waves: 1\n`;
|
|
2666
|
+
const task = { task: 1, wave: 1, heading: "Docs", files: [{ kind: "Modify", path: "file-a.ts" }], tests: [], orchestrator: "é" };
|
|
2667
|
+
task.orchestrator += "a".repeat(length - prefix.length - JSON.stringify(task).length);
|
|
2668
|
+
const expected = prefix + JSON.stringify(task);
|
|
2669
|
+
assert.equal(expected.length, length);
|
|
2670
|
+
assert.ok(Buffer.byteLength(expected, "utf8") > length);
|
|
2671
|
+
writeFileSync(planPath, `# Plan
|
|
2672
|
+
**Spec:** \`doc/specs/spec.md\`
|
|
2673
|
+
**Verification:** all-checks
|
|
2674
|
+
---
|
|
2675
|
+
## Wave 1 - Docs
|
|
2676
|
+
### Task 1: Docs
|
|
2677
|
+
Orchestrator: ${task.orchestrator}
|
|
2678
|
+
**Files:**
|
|
2679
|
+
- Modify: file-a.ts
|
|
2680
|
+
**Tests:**
|
|
2681
|
+
- none: docs
|
|
2682
|
+
## Spec coverage
|
|
2683
|
+
| anchor | requirement | owner |
|
|
2684
|
+
|---|---|---|
|
|
2685
|
+
| - | mechanical: docs | Task 1 |
|
|
2686
|
+
`);
|
|
2687
|
+
const h = harness({ cwd: dir, branch: readyForImplementBranch });
|
|
2688
|
+
await h.emit("session_start");
|
|
2689
|
+
const tool = h.tools.find(t => t.name === "plan_check")!;
|
|
2690
|
+
const result = await tool.execute("size", { planPath }, undefined, undefined, h.ctx) as {
|
|
2691
|
+
content: { text: string }[]; details: { status: string; findings?: { check: string; line: number; reason: string }[] };
|
|
2692
|
+
};
|
|
2693
|
+
if (length < 32768) {
|
|
2694
|
+
assert.equal(result.details.status, "pass");
|
|
2695
|
+
assert.equal(result.content[0].text, expected);
|
|
2696
|
+
} else {
|
|
2697
|
+
assert.equal(result.details.status, "fail");
|
|
2698
|
+
assert.equal(result.details.findings?.length, 1);
|
|
2699
|
+
assert.equal(result.details.findings?.[0].check, "manifest-size");
|
|
2700
|
+
assert.equal(result.details.findings?.[0].line, 0);
|
|
2701
|
+
assert.equal(result.details.findings?.[0].reason, `manifest content is ${length} characters (limit 32,768); the plan is too large for the orchestrator's manifest - stop and ask the user to reduce it`);
|
|
2702
|
+
assert.ok(!result.content[0].text.includes("\nmanifest:"));
|
|
2703
|
+
}
|
|
2704
|
+
const phase = h.tools.find(t => t.name === "phase_tracker")!;
|
|
2705
|
+
const start = await phase.execute("start", { action: "start", phase: "implement" }, undefined, undefined, h.ctx) as { details: { error?: string; phases: { implement: { status: string } } } };
|
|
2706
|
+
if (length < 32768) assert.equal(start.details.phases.implement.status, "in_progress");
|
|
2707
|
+
else assert.match(start.details.error ?? "", /plan_check/);
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from "./lib/gauntlet-settings.ts";
|
|
30
30
|
import { loadGauntletSettings } from "./lib/gauntlet-settings-loader.ts";
|
|
31
31
|
import { checkoutOf, gitSync } from "./lib/checkout.ts";
|
|
32
|
-
import { checkPlan, legOwnership, sha256, type FsPort, type PlanCheckFinding } from "./lib/plan-check.ts";
|
|
32
|
+
import { checkPlan, renderManifest, legOwnership, sha256, type FsPort, type PlanCheckFinding } from "./lib/plan-check.ts";
|
|
33
33
|
import {
|
|
34
34
|
checkSubstep,
|
|
35
35
|
dispatchLabel,
|
|
@@ -1054,8 +1054,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1054
1054
|
exists: (p, leg) => existsSync(resolve(leg ? roots.get(leg)! : root, p)),
|
|
1055
1055
|
glob: (pattern, leg) => globSync(pattern, { cwd: leg ? roots.get(leg)! : root }),
|
|
1056
1056
|
};
|
|
1057
|
-
const
|
|
1058
|
-
if (findings.length > 0) return fail(findings, planAbs);
|
|
1057
|
+
const result = checkPlan(planText, specText, port);
|
|
1058
|
+
if (result.findings.length > 0) return fail(result.findings, planAbs);
|
|
1059
|
+
const passLine = gauntletEntered ? "PASS \u2014 plan verified and stamped for implement-start" : "PASS (no flow to stamp)";
|
|
1060
|
+
const resultText = result.ledger ? passLine : `${passLine}\n\n${renderManifest(result.parsed!, planAbs, specAbs)}`;
|
|
1061
|
+
if (resultText.length >= 32768) return fail([{
|
|
1062
|
+
check: "manifest-size", line: 0, text: "",
|
|
1063
|
+
reason: `manifest content is ${resultText.length} characters (limit 32,768); the plan is too large for the orchestrator's manifest - stop and ask the user to reduce it`,
|
|
1064
|
+
}], planAbs);
|
|
1059
1065
|
|
|
1060
1066
|
const planSha256 = sha256(planBytes);
|
|
1061
1067
|
const specSha256 = sha256(specBytes);
|
|
@@ -1069,11 +1075,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1069
1075
|
if (gauntletEntered) {
|
|
1070
1076
|
planCheckStamp = { planPath: planAbs, planSha256, specPath: specAbs, specSha256 };
|
|
1071
1077
|
return {
|
|
1072
|
-
content: [{ type: "text" as const, text:
|
|
1078
|
+
content: [{ type: "text" as const, text: resultText }],
|
|
1073
1079
|
details,
|
|
1074
1080
|
};
|
|
1075
1081
|
}
|
|
1076
|
-
return { content: [{ type: "text" as const, text:
|
|
1082
|
+
return { content: [{ type: "text" as const, text: resultText }], details };
|
|
1077
1083
|
} catch (err) {
|
|
1078
1084
|
return fail([{ check: "internal", line: 0, text: "", reason: String(err) }], planAbs);
|
|
1079
1085
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Amendment surface
|
|
2
2
|
|
|
3
|
-
`read` this file (the `read` tool at its absolute path, never a shell command) from `skills/brainstorming/SKILL.md` section Amending an approved spec, from any phase, for amend-class changes; at approval, load only [Standing grants](#standing-grants), not the batch procedure. Keep redraws outside this procedure. Classify a text correction to an approved drafted acceptance-criteria row backed by implementer evidence (a quoted test, assertion, or code line in a `BLOCKED`/`NEEDS_CONTEXT` return) as an amend item. Keep ticket rows, row add/remove, reviewer-only or taste objections, problem statement changes, and component boundary changes as redraws at batch preparation, before any standing grant: re-draft from the new design and ratify at the full gate. Keep a disposition-only change to such a row in this grammar so it keeps the cut question. Apply an item the human has answered by letter or custom text as recorded.
|
|
3
|
+
`read` this file (the `read` tool at its absolute path, never a shell command) from `skills/brainstorming/SKILL.md` section Amending an approved spec, from any phase, for amend-class changes; at approval, load only [Standing grants](#standing-grants), not the batch procedure. Keep redraws outside this procedure. Classify a text correction to an approved drafted acceptance-criteria row backed by implementer evidence (a quoted test, assertion, or code line in a `BLOCKED`/`NEEDS_CONTEXT` return) as an amend item. Keep ticket rows, row add/remove, reviewer-only or taste objections, problem statement changes, and component boundary changes as redraws at batch preparation, before any standing grant: re-draft from the new design and ratify at the full gate. Keep a disposition-only change to such a row in this grammar so it keeps the cut question. Apply an item the human has answered by letter or custom text as recorded. Keep human-facing amendment text with the main loop holding `edit`/`write`; delegate implement-phase spec authorship only through the implement-only route below. Keep all other callers' authorship with the main loop. Use the human last: a fresh reviewer clears evidence-backed factual corrections; present the rest to the human once per batch plainly.
|
|
4
4
|
|
|
5
5
|
## Standing grants
|
|
6
6
|
|
|
@@ -40,6 +40,8 @@ When the spec file is mid-batch (uncommitted amend edits), the consent edit ride
|
|
|
40
40
|
|
|
41
41
|
## 1. Prepare - never apply yet
|
|
42
42
|
|
|
43
|
+
**Implement callers only, with a task plan:** delegate preparation to `plan-writer` in a one-step chain with the ordinary five absolute paths, this surface's absolute path, and an implement-only `Amend-prepare:` block carrying evidence already in context (checker FAIL lines, structured `concerns`/`question`, or baseline rows). Consume `{ items: [{ evidence, recommended: "accept" | "alt-<n>", location: "<spec section>", edit: string }], redraws: [{ location: "<spec section>", reason: string (max 300 characters) }] }`; keep supporting context in the saved report. Require both `items` and `redraws` arrays, including when empty; redraw proposals appear only in `redraws`, never in `items`. Fail malformed preparation output. The child reads spec and plan, writes nothing, and bounds each proposed edit to one paragraph. Enforce the redraw gate: a nonempty `redraws` array holds the whole batch before grant evaluation or applying any sibling item. Hold amend siblings for re-batching after the redraw resolves; the primary never opens the preparation report to discover redraws. Run the standing-grant predicate and Value decoding unchanged; without amend permission, take sections 2-4 and stop for the human's answer. Forward only resolved, authorized items to the apply route in section 5. Planning callers keep the ordinary preparation below. For ledger-engine (`loop-implementation`) callers, prepare through the ordinary path below without a `plan-writer` dispatch.
|
|
44
|
+
|
|
43
45
|
Collect every amendment pending at this decision point (same spec-review round, same blocked wave, same conformance inventory) into one batch. Never wait for more; a later finding is a new batch. For each item hold, unapplied:
|
|
44
46
|
|
|
45
47
|
| Field | Content |
|
|
@@ -143,6 +145,8 @@ Reply grammar: one line per item, `<n>. <letter or free text>`; bare `1` applies
|
|
|
143
145
|
|
|
144
146
|
Before the aftermath, `read` this section again; never run it from memory.
|
|
145
147
|
|
|
148
|
+
**Implement callers only, with a task plan:** replace parent spec edits and the ordinary plan aftermath with a one-step chain to `plan-writer` carrying an implement-only `Amend-apply:` block, the granted items verbatim, the five absolute paths, and the parent's explicit `In-flight:` list. Authorize only those named spec sections. Require ordinary `Amend:` preservation of unaffected titles and wave positions, Baseline ownership, dirty-baseline safety, and the child's self-check. Consume `{ amendedSections: string[], tasksAdded: number[], tasksAnchorChanged: number[], tasksRemoved: number[], passed: boolean, failLines?: string[] }`; use pre-apply task numbers, never tracker positions. Require the child to capture the original task numbers, titles, spec anchors, and wave positions in the existing saved apply report before the first apply and retain that original inventory on every return, including failures; add no sidecar. On `passed: false`, repeat once with `failLines` as the rule and `previousApplyReport` set to the first apply report path. Require the fresh retry worker to read that original inventory, apply granted edits idempotently without duplicating already-applied edits, and compute deltas against that original inventory rather than the already-mutated plan. Missing original inventory fails the retry; send a second failure to the human with verbatim findings, including substantive baseline failures. Keep the attribution, protected-path, and dirty-baseline exits below; no delegated restoration permission is granted. On pass, print `amended the spec: <title> - <location>` for each applied item, run the parent's `plan_check` for stamping, and use its fresh manifest with the returned task numbers without reading the plan or spec. Add `tasksAdded`, reopen completed `tasksAnchorChanged` as `in_progress`, and handle `tasksRemoved` through the removal rules below. Require parent PASS before tracker changes. Skip the ordinary parent edit, `Amend:` dispatch, and amended-plan read; preserve companion aftermath, audit, and the single batch commit below. Planning callers keep the ordinary apply and aftermath below; ledger and no-plan routes stay unchanged.
|
|
149
|
+
|
|
146
150
|
Apply accepted items only: reviewer clears, `accept`, `alt-n`, and state-changing `custom`. Print one line per applied item: `amended the spec: <title> - <what>`.
|
|
147
151
|
|
|
148
152
|
Preserve scope indices on disposition-only amendments, including cuts; validate
|