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.
@@ -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 (planText.split("\n", 1)[0] === "# Ledger - loop engine") return checkLedger(planText, specText);
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 findings = checkPlan(planText, specText, port);
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: "PASS \u2014 plan verified and stamped for implement-start" }],
1078
+ content: [{ type: "text" as const, text: resultText }],
1073
1079
  details,
1074
1080
  };
1075
1081
  }
1076
- return { content: [{ type: "text" as const, text: "PASS (no flow to stamp)" }], details };
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
  {
2
2
  "name": "pi-gauntlet",
3
- "version": "7.41.3",
3
+ "version": "7.42.0",
4
4
  "description": "Opinionated, gated workflow skills, subagent personas, and runtime extensions for the pi coding agent.",
5
5
  "author": "Jacek Juraszek",
6
6
  "type": "module",
@@ -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. Only the main loop holding `edit`/`write` authors amendments and human-facing amendment text. Use the human last: a fresh reviewer clears evidence-backed factual corrections; present the rest to the human once per batch plainly.
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