pi-gauntlet 7.41.3 → 7.43.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.43.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
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: gatekeep-pr
3
3
  description: Use only when explicitly asked to gatekeep, verify, or gate a named pull request before it merges - or on pi a set of them - ("gatekeep PR 123", "gatekeep PRs 123 124 125", a loop task naming the PR and the verb); post-merge delivery proof belongs to /skill:check-delivery.
4
- argument-hint: "<pr> [issue-ref] [--rebase [base]] | <pr> <pr>... [--rebase [base]] (e.g. 123, 123 gh-45, 123 --rebase, 123 gh-45 --rebase release/2, 123 124 125 on pi only)"
4
+ argument-hint: "[no argument: my open PRs] | <pr> [issue-ref] [--rebase [base]] | <pr> <pr>... [--rebase [base]] (e.g. 123, 123 gh-45, 123 --rebase, 123 gh-45 --rebase release/2, 123 124 125 on pi only)"
5
5
  ---
6
6
 
7
7
  # gatekeep-pr
@@ -10,8 +10,8 @@ Verify, don't trust: a PR description is a claim, and over-claimed coverage, hal
10
10
 
11
11
  ## Arguments
12
12
 
13
- - Route two or more PR tokens (numbers or URLs, space-separated, before any `--rebase`) through the set-routing row before steps 1-7; reject a positional issue reference in a set with a one-line error. Apply `--rebase [base]` to every PR in the set; keep zero or one PR token on the existing single-PR path below.
14
- - PR number or URL; omitted -> resolved from the current branch per `verification-brief.md` Section A.
13
+ - Route two or more PR tokens (numbers or URLs, space-separated, before any `--rebase`) through the set-routing row before steps 1-7; reject a positional issue reference in a set with a one-line error. Apply `--rebase [base]` to every PR in the set; keep one PR token on the single-PR path below; zero tokens discover per verification-brief.md Section A before this row runs and re-enter it with the discovered roster when the roster holds two or more; a trailing comma on a PR token is stripped before identity normalization, so a pasted discovered, smallest first: list is a valid set.
14
+ - PR number or URL; omitted -> discovered per verification-brief.md Section A during argument resolution (my open PRs in origin: authored or directly requested, no drafts or decided PRs, smallest first); one match gates on this path, two or more re-enter the set-routing row on pi and stop with the discovered list on Claude Code.
15
15
  - Optional issue reference; omitted -> resolved in step 1 (`verification-brief.md` Section A) and fetched in step 2; none found -> select the scope contract in step 2 (spec at the head, else the PR's stated intent), never invent acceptance criteria.
16
16
  - Optional `--rebase [base]`, parsed after `<pr> [issue-ref]`: the token after it, when present, is the base branch name (`123 --rebase gh-45` means base `gh-45`, not an issue ref); absent, the base is the digest's `baseRefName`. Step 2b fetches `origin/<base>` and validates it: a missing ref stops with a one-line error before any rebase or push. Without `--rebase` the run performs no additional fetch, rebase, or push before a menu pick and renders no sync line.
17
17
 
@@ -4,12 +4,14 @@ Use this contract only for two or more PR tokens. Keep the single-PR path and it
4
4
 
5
5
  ## Admit before effects
6
6
 
7
- Reject with a one-line error before any fetch, confirmation, provisioning, or dispatch: positional issue references, duplicate identities (normalize number and URL to the same repository/number), URLs from different repositories or not matching origin, an oversized set, and Claude Code. Multi-PR mode requires pi's nested dispatch. Resolve each issue from its PR, never from a positional issue argument.
7
+ Reject with a one-line error before any fetch, confirmation, provisioning, or dispatch: positional issue references, duplicate identities (normalize number and URL to the same repository/number), URLs from different repositories or not matching origin, an oversized set, and Claude Code. For Claude Code print exactly: `multi-PR mode requires pi's nested dispatch`. Resolve each issue from its PR, never from a positional issue argument.
8
8
 
9
9
  Read the active pi-cohort configuration's `parallel.maxTasks`, default `8`; gauntlet_setting is not its source. Never chunk the set. For an oversized set print exactly:
10
10
 
11
11
  `multi-PR mode takes at most <max> PRs (pi-cohort parallel.maxTasks); got <n>`
12
12
 
13
+ For `source: discovered`, append exactly one `discovered, smallest first: <numbers comma-separated>` line after either cap or harness rejection, in supplied sorted order; for example after cap 4 got 6: `discovered, smallest first: 101, 97, 112, 88, 130, 104`. Print no discovered list for explicit sets. Preserve supplied token order through provisioning, summary rows, and queued menus; never trim.
14
+
13
15
  Gather each PR with `gh pr view --json number,author,headRefOid,headRefName,baseRefName,isCrossRepository,headRepository,state,files`. Resolve trusted worktree configuration from the base merge-base, never from PR-controlled files.
14
16
 
15
17
  ## Provision serially
@@ -169,7 +169,7 @@ After every push or any mutation that can change readiness (fix wave pushed, PR
169
169
 
170
170
  ### Pre-menu refresh
171
171
 
172
- Refresh read-only before step 5, first report included, and at each Re-render. Run one `gh pr view <N> --json headRefOid,state,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup,body,baseRefName,number,title,author,isDraft,headRefName,isCrossRepository,files,additions,deletions,commits,reviews,closingIssuesReferences,headRepository,maintainerCanModify,url,mergeCommit` and one `gh run list -R <owner>/<repo> -c <headRefOid> --json databaseId,status,conclusion,workflowName,url`. Fold `gh pr view <N> --json headRefOid,body,baseRefName` into that read. Apply target/body freshness from Next action. While locally rewritten under a loop checkpoint compare remote to `repair.wave.sync.expected_remote_head`; preserve that expectation on mismatch and recover to `repair.wave.sync.pre_head` through Sync's restore/assert obstacle. An entry rewrite without a loop checkpoint keeps its own pre_head for comparison and recovery. An unmoved remote retains local-head source: pending until lease push; preserve the loop checkpoint through this read and any local-head assessment. On other head moves re-enter step 3. On an unchanged head replace merge state, reviewDecision, status_checks and actions_runs together; reclassify via the brief's binding rule and re-resolve the Evidence resolution table. Pending to CI-sufficient dispatches a fresh helper in claim-check-only mode with a disposition for every material claim.
172
+ Refresh read-only before step 5, first report included, and at each Re-render. Run one `gh pr view <N> --json headRefOid,state,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup,body,baseRefName,number,title,author,isDraft,headRefName,isCrossRepository,files,additions,deletions,commits,reviews,closingIssuesReferences,headRepository,maintainerCanModify,url,mergeCommit,reviewRequests` and one `gh run list -R <owner>/<repo> -c <headRefOid> --json databaseId,status,conclusion,workflowName,url`. Fold `gh pr view <N> --json headRefOid,body,baseRefName` into that read. Apply target/body freshness from Next action. While locally rewritten under a loop checkpoint compare remote to `repair.wave.sync.expected_remote_head`; preserve that expectation on mismatch and recover to `repair.wave.sync.pre_head` through Sync's restore/assert obstacle. An entry rewrite without a loop checkpoint keeps its own pre_head for comparison and recovery. An unmoved remote retains local-head source: pending until lease push; preserve the loop checkpoint through this read and any local-head assessment. On other head moves re-enter step 3. On an unchanged head replace merge state, reviewDecision, status_checks, actions_runs, and pr.review_requests together and re-derive `viewer.role` per Section A; reclassify via the brief's binding rule and re-resolve the Evidence resolution table. Pending to CI-sufficient dispatches a fresh helper in claim-check-only mode with a disposition for every material claim.
173
173
 
174
174
  A failed refresh retains prior values, classifies every pending check as binding, records `merge state not refreshed (<reason>)`, retries once then exits through Exit check. This read never mints `C#` rows and never starts an independent watch. A failing resolved check mints one P# with its failing line from `gh run view <id> -R <repo> --log-failed` and drafted payload; apply the repeated-red/source-repair rule. An action_required run goes through Exit check.
175
175
 
@@ -18,6 +18,8 @@ Delivers: <one sentence: what the PR does. With source: spec, a second sentence
18
18
  order ("3, 5 deferred per spec to <ref>"); deviating rows ("2 deviates per spec: <why>"); elsewhere rows grouped by repo ("6 elsewhere per spec: <repo>");
19
19
  observation halves ("4's observable half (<what>) is checked after merge, not here").>
20
20
 
21
+ Role: <author | requested reviewer | none | unreadable>
22
+
21
23
  PR comments: (replyable review comments, a pending reviewer run, a blocked or unknown merge state, a binding pending check, a failed refresh, or an incomplete source review - see the paragraph above)
22
24
  - <reviewer>'s comment on <topic> <verdict clause>, so the drafted reply <what it says>. (<locator>)
23
25
  - The reviewer run is still in progress, so merge waits. (<run url>)
@@ -39,7 +41,9 @@ Verdict: mergeable - <evidence clause> | fixable - <N> blockers | not ready - <w
39
41
  <menu>
40
42
  ```
41
43
 
42
- Render `Sync:` only when `--rebase` was passed. Open the `Sync:` line with the digest's `sync` field value verbatim (for example `Sync: declined - other author's branch`). Put any explanation after that value as a second clause or sentence. Never paraphrase the status value into prose. Exempt this line from SKILL.md wording rule 5 (passing facts only under `show evidence`): name the resolved files even when the sync succeeded.
44
+ Print exactly one `Role:` directly under `Delivers` on every report from the refreshed `viewer.role` and `pr.review_requests`; append `(also requested: <entries>)` for requested reviewer, `(requested: <entries>)` otherwise, omitted when `<entries>` is empty. `<entries>`: the User and Team entries other than the viewer, API order (Keep API order, exclude the viewer by case-insensitive User login comparison, exclude nobody when the login is unreadable), User as login, Team as `slug`; render missing login/slug as `unknown`. Example: `Role: requested reviewer (also requested: robin, acme/reviewers)`.
45
+
46
+ Render `Sync:` only when `--rebase` was passed. Open the `Sync:` line with the digest's `sync` field value verbatim (for example `Sync: declined - other author's branch`). Put any explanation after that value as a second clause or sentence. Never paraphrase the status value into prose. Exempt `Sync:` and `Role:` from SKILL.md wording rule 5 (passing facts only under `show evidence`): name the resolved files even when the sync succeeded.
43
47
 
44
48
  For obstacle reports, print exactly one `Evidence:` line naming the resolved evidence source and the assessed head, immediately before the verdict (for example `Evidence: CI (run 7001, test_unit and build_image green on 9c1e4f2)`). Use the resolved local verification summary instead when local evidence owns the assessment; do not invent a passing result when evidence is unavailable. Carry the same source summary into the obstacle menu's evidence hint. Exempt this compact source summary and the final template's `Evidence:` line from the evidence-only rules below and SKILL.md wording rule 5; keep the full record under `show evidence`.
45
49
 
@@ -51,7 +55,7 @@ For ordinary assessments, use `mergeable`, `fixable`, or `not ready`. Carry the
51
55
 
52
56
  ## What stays out
53
57
 
54
- Verbatim command output stays in its `log_path` file and never prints; `show evidence` prints its `show evidence: <log_path>` line instead. CI run URLs and conclusions (the pending-reviewer locator in `PR comments` is the one URL that prints in the report), drafted file edits and full description bodies, drafted replies, matched claims, per-row AC outcomes, internal IDs, and disposition annotations print only under the `show evidence` command. A clean run with a spec renders `Delivers`, the verdict line, and the menu - about four lines.
58
+ Verbatim command output stays in its `log_path` file and never prints; `show evidence` prints its `show evidence: <log_path>` line instead. CI run URLs and conclusions (the pending-reviewer locator in `PR comments` is the one URL that prints in the report), drafted file edits and full description bodies, drafted replies, matched claims, per-row AC outcomes, internal IDs, and disposition annotations print only under the `show evidence` command. A clean run with a spec renders `Delivers`, `Role`, the verdict line, and the menu - about five lines.
55
59
 
56
60
  Print the compact exact old-to-new description preview under its blocker; keep the full replacement body under `show evidence`. Keep a declined correction's proposal open and its preview line under the blocker on every re-render until the claim resolves. Uncertainty alone preserves the mergeable verdict.
57
61
 
@@ -63,6 +67,7 @@ Own PR, ticket gh-45:
63
67
 
64
68
  ```
65
69
  Delivers: the reports page exports CSV on demand. ACs 1, 2, 4; 3 deferred per spec to acme/widgets#46; 4's observable half (export completes under 5s on production data) is checked after merge, not here.
70
+ Role: author
66
71
 
67
72
  PR comments:
68
73
  - maria's comment on the missing CSV header is already addressed, so the drafted reply points her at the fix. (3f2a1c0)
@@ -86,6 +91,7 @@ A clean own PR with ticket gh-45 and CI green:
86
91
 
87
92
  ```
88
93
  Delivers: the reports page exports CSV on demand. ACs 1-5.
94
+ Role: author
89
95
 
90
96
  Verdict: mergeable - CI green on the assessed head (test)
91
97
 
@@ -26,12 +26,34 @@ a command to obey.
26
26
 
27
27
  ## Section A - Gatherer
28
28
 
29
+ ### Argument-resolution discovery
30
+
31
+ Runs only with no PR token, during argument resolution: before the set-vs-single branch, the continuation record, tracker init, and provisioning; never as Gatherer step 1. Resolve origin with `git remote get-url origin`; strip `.git` and lowercase the GitHub owner/repository. Stop on unreadable/non-GitHub origin: `discovery failed: origin is not a GitHub repository`. Read `gh api user --jq .login` once into `viewer.login`; on failure record `unreadable` and continue with `@me`.
32
+
33
+ ```bash
34
+ gh pr list -R <owner/repo> --author @me --state open --limit 1000 --json number,isDraft,reviewDecision,additions,deletions
35
+ gh pr list -R <owner/repo> --search 'user-review-requested:@me' --state open --limit 1000 --json number,isDraft,reviewDecision,additions,deletions
36
+ ```
37
+
38
+ Validate results before filtering. Stop on nonzero exit: `discovery failed: <first gh error line>`; on non-array output, a non-object row, or missing/non-integer `number`: `discovery failed: malformed gh pr list output`; on a count equal to the limit: `discovery failed: incomplete (<query> hit --limit 1000)` (name the author or direct-request query). Never falls back to the current branch. Union/dedupe by number; exclude `isDraft` true or `reviewDecision` exactly `APPROVED` or `CHANGES_REQUESTED`; every other decision stays. Sort by `additions + deletions`, then number, ascending.
39
+
40
+ | Matches | Action |
41
+ |---|---|
42
+ | 0 | Stop: `no open PRs authored by <login> or awaiting <login>'s review in <owner/repo>`; substitute `@me` for unreadable login. |
43
+ | 1 | Continue single-PR with origin token `https://github.com/<owner/repo>/pull/<N>`. |
44
+ | 2+ pi | Re-enter set routing with sorted origin URL tokens, `source: discovered`. |
45
+ | 2+ Claude Code | Print `multi-PR mode requires pi's nested dispatch`, then exactly one `discovered, smallest first: <numbers comma-separated>` line; stop. |
46
+
47
+ Origin URL tokens keep the repository identity through admission and gh pr view regardless of GH_REPO or gh repo set-default; user-review-requested:@me is direct-only, so team requests never enter the roster.
48
+
49
+ ### Gather fixed commands
50
+
29
51
  Read-only. Fixed `gh` command set - do not substitute ad hoc queries:
30
52
 
31
53
  ```bash
32
- gh pr view <N> --json number,title,body,author,state,isDraft,headRefName,baseRefName,isCrossRepository,mergeable,headRefOid,statusCheckRollup,files,additions,deletions,commits,reviews,closingIssuesReferences,reviewDecision,headRepository,maintainerCanModify,mergeStateStatus,url,mergeCommit
54
+ gh pr view <N> --json number,title,body,author,state,isDraft,headRefName,baseRefName,isCrossRepository,mergeable,headRefOid,statusCheckRollup,files,additions,deletions,commits,reviews,closingIssuesReferences,reviewDecision,headRepository,maintainerCanModify,mergeStateStatus,url,mergeCommit,reviewRequests
33
55
  gh run list -R <owner>/<repo> -c <headRefOid> --json databaseId,status,conclusion,workflowName,url # Actions runs on the assessed head: held-run detection (action_required with or without a rollup entry)
34
- gh api user --jq .login # viewer_is_author = (login == pr.author.login)
56
+ gh api user --jq .login # viewer.login; a no-token run reuses discovery's value, unreadable included
35
57
  gh api graphql -f query='query($o:String!,$r:String!,$n:Int!){ repository(owner:$o,name:$r){ viewerPermission pullRequest(number:$n){ viewerCanUpdateBranch } } }' -f o=<owner> -f r=<repo> -F n=<N> # viewer_permission, can_update_branch; a null field is recorded as `unreadable`
36
58
  gh repo view <base-repository> --json nameWithOwner # resolved base repository context, never headRepository
37
59
  gh pr diff <N>
@@ -52,7 +74,7 @@ the paginated calls complete. A comment fetch whose pagination fails part-way
52
74
  is a refetch failure (`reference/post-selection-loop.md` `### Re-render`),
53
75
  never a partial digest.
54
76
 
55
- Missing PR number: `gh pr view --json number,url` on the current branch; no PR found -> STOP and report. Issue reference: the explicit argument, else `closingIssuesReferences`, then the branch name, PR title, PR body, and commit subjects; none found -> `issue_ref: null`. The Gatherer only resolves the reference - fetching the ticket is step 2 (`reference/assessment.md` `## Fetch the ticket`), after the issue-fetch command resolves from the merge-base.
77
+ Issue reference: the explicit argument, else `closingIssuesReferences`, then the branch name, PR title, PR body, and commit subjects; none found -> `issue_ref: null`. The Gatherer only resolves the reference - fetching the ticket is step 2 (`reference/assessment.md` `## Fetch the ticket`), after the issue-fetch command resolves from the merge-base.
56
78
 
57
79
  `mergeable` is reported as-is, including `UNKNOWN` - the Gatherer runs before
58
80
  provisioning, so it never re-reads; step 2 re-reads once after provisioning (`reference/assessment.md`). Bot author noted
@@ -75,7 +97,8 @@ provisioning, so it never re-reads; step 2 re-reads once after provisioning (`re
75
97
  - repair.wave: null | { pre_wave_head, pushed_head, selected_ids, selected_payloads, sync: null | { pre_head, expected_remote_head }, action: fix-nits?, affected_claims, round_start_head, counted: false | true, counted_head, checkpoint_history: [], stage: apply | review | push, no_progress: { failed_prerequisite, observed_value, result_path }? }
76
98
  - approved_head: null | <sha> # set by reference/post-selection-loop.md when gh pr review --approve exits 0; selects the approved template
77
99
  - cleanup_done: false | true # set by reference/cleanup.md after the cleanup pick runs
78
- - viewer: { login, is_author }
100
+ - pr.review_requests: [ { __typename: User, login } | { __typename: Team, name, slug } ] # raw reviewRequests entries
101
+ - viewer: { login, is_author, role }
79
102
  - permissions: { viewer_permission, is_cross_repository, maintainer_can_modify, merge_state_status, can_update_branch, head_url, head_ref, head_pushable } # head_pushable is set in step 2 after provisioning (reference/assessment.md); merge_state_status is `BLOCKED`, `BEHIND`, `UNKNOWN`, `unreadable` (null), or any other GitHub value (`CLEAN`, `UNSTABLE`, `HAS_HOOKS`, `DIRTY`) and is written only from a `gh pr view` invocation that names `mergeStateStatus` (this call, the step-2 re-read, the step-2b re-gather (`reference/sync.md`), the pre-menu refresh before and after the single watch, compare-and-swap), never from an ad hoc GraphQL query or `gh pr list`, and never from a reply carrying `errors`; any field that cannot be read is the literal `unreadable`; menu availability derives from this block (reference/decision-menu.md ## Availability)
80
103
  - status_checks: [ { name, kind, status, conclusion, required, url, workflowName } ] # kind: check_run | status_context | unreadable, from the rollup entry's __typename; evidence semantics: Section B Evidence resolution; workflowName from the CheckRun rollup entry (absent on StatusContext)
81
104
  - actions_runs: [ { databaseId, status, conclusion, workflowName, url } ] # from the fixed gh run list call on the assessed head
@@ -97,7 +120,7 @@ and top-level comment ids, so a thread's inline comments are diffed once, as
97
120
  inline comments.
98
121
 
99
122
  `viewer_is_author` lives at `viewer.is_author` in the digest, computed as
100
- `viewer.login == pr.author.login`. Each `status_checks` entry's `url` is the CheckRun `detailsUrl` / StatusContext
123
+ `viewer.login == pr.author.login`. Derive viewer.role: unreadable when viewer.login is unreadable; else author when is_author; else requested reviewer when a pending User reviewRequests entry's login equals viewer.login case-insensitively; else none. Pending reviewRequests only, never reviews history; approve availability keeps is_author. Each `status_checks` entry's `url` is the CheckRun `detailsUrl` / StatusContext
101
124
  `targetUrl` already present in the fetched payload; when the payload omits it,
102
125
  downstream CI claims record `url: unavailable` - absence never disqualifies the
103
126
  check. GraphQL enums are case-folded; a StatusContext's `state` maps per Section B's Conclusion semantics. Missing `required` is