pi-gauntlet 8.4.0 → 8.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## v8.4.2 - 2026-11-07
4
+
5
+ ### Fixed
6
+
7
+ - `gatekeep-pr`: a viewer GitHub reports as a ruleset bypass actor can merge a `BLOCKED` PR on the ordinary `merge` row, with CI evidence and every other merge precondition still binding. False or unreadable eligibility fails closed; live non-admin bypass behavior remains unverified.
8
+
9
+ ## v8.4.1 - 2026-11-07
10
+
11
+ - Make `phase_tracker`, `plan_check`, and `plan_tracker` model-only so codemode scripts cannot make updates that disappear on session reconstruction; requires pi >= 1.5.0.
12
+
3
13
  ## v8.4.0 - 2026-11-07
4
14
 
5
15
  - Telemetry publishes run and per-session skill records on the orphan `gauntlet/telemetry` branch, outside product commits. `telemetry.remote` replaces `telemetry.dir`; readers refresh the branch and cost digests allocate ticket and PR rounds, including unattributed shares. Finish and resume use branch records and full-checkout verification proofs. (#124)
package/README.md CHANGED
@@ -97,9 +97,9 @@ Non-final `stop` removes an eligible tracked-clean, non-primary worktree, create
97
97
 
98
98
  pi-gauntlet ships three kinds of pieces, layered on top of pi-cohort's dispatch:
99
99
 
100
- - **21 skills** - the workflow logic. Sixteen activate automatically when pi sees the matching kind of task, and the workflow skills chain through their gates: `shape-ticket` (creates one tracker issue or repairs a comma/whitespace list of refs against a Context/Problem/Idea/Acceptance-Criteria template, with a grounded repo audit, an AC integrity check, a cheap council roast, and sequential per-ticket exact-diff write confirmations; model-invocable so other skills and commands can route "file a ticket" to it, the gate still needs your yes), `gatekeep-pr` (model-invocable on an explicit ask that names the PR and the verb; the gates still need your selection - is consent-gated pre-merge verification of a PR against the scope contract at its head - the approved spec's acceptance-criteria dispositions and Design when the PR carries one, else the PR's stated intent; the ticket is cross-checked, never judged here (`check-delivery` owns it) - read-only gathering, verification evidence resolved CI-first (green checks on the exact assessed head count as evidence; a pending commit status that GitHub does not report as blocking the gating actor (`mergeStateStatus` not `BLOCKED`) is not waited for - external-CI commit statuses included - while a running Actions check, an unknown merge state, and every other merge precondition still withhold; the project's verification command runs only as fallback), a rubric-based review, a compact bottom-up report (what the PR delivers, including rows deferred or deviating per spec, PR comments, nits, blockers, then a `mergeable` / `fixable` verdict) and course and terminal menus whose rows derive from the actor's permissions on the PR; nothing mutates (repairs, pushes, reviews, merges, tracker comments) until you pick a course or terminal row, except the opt-in `--rebase [base]` sync that rebases, reviews, and lease-pushes the head before verification, or holds the rebased head locally when a conflict stop's scoped tests fail, so a selected delivery course resolves it before the lease push; after your own merge two opt-in rows post the `AC coverage` block to the ticket or the PR; own-merge, approved, and closed-PR menus offer a recommended `cleanup` row that tears down the gate's worktree and branch and runs the overrides file's `## cleanup` steps - run it with `/skill:gatekeep-pr [<pr>] [--rebase [base]]` (no argument: your open PRs)), `check-delivery` (model-invocable on an explicit ask that names the ticket and the verb; the gates still need your selection - is a post-merge detective control: proves an issue actually shipped (default-branch landing, delivery target, per-AC evidence) before its tracker status advances; it never writes a terminal status - run it with `/skill:check-delivery <ref>` or, in pi, `/skill:check-delivery #12 #15` for fresh parallel verification and one gate and write per ticket in turn), `writing-plans`, `roasting-the-spec`, `test-driven-development`, `subagent-driven-development`, `loop-implementation` (the plan-free implement driver that iterates a fresh `loop-implementer` over the spec's acceptance-criteria ledger when approval picks the loop engine), `dispatching-parallel-agents`, `verification-before-completion`, `requesting-code-review`, `receiving-code-review`, `using-git-worktrees`, `finishing-a-development-branch`, `forge-skill`, `linear` (reads/searches/comments on/manages Linear tickets via the `linearis` CLI; owns all linearis mechanics and the `## Issue tracker` overrides schema; tracker-facing skills route to it). Five more require explicit entry (`disable-model-invocation: true`): `brainstorming` starts the gated workflow on explicit consent - use `/skill:brainstorming` for reliable discovery, or opt in through reachable explicit prose or a human-selected handoff; after spec approval, downstream planning and implementation still chain automatically. `chase-bug` is human-only bug triage: read-only root-cause discovery to an evidenced verdict menu (real bug -> ticket/brainstorm/hotfix/respond; five negative verdicts), then a gated response to the reporter for addressable origins (GitHub issue / tracker ticket) and a rendered verdict summary otherwise - it never fixes during triage; the hotfix row hands off to `skills/chase-bug/hotfix.md` after the menu - run it with `/skill:chase-bug`. `gauntlet-performance` reads the committed run telemetry (current repo by default, `--dir <path>` adds others) through the parse-only `gauntlet-performance` CLI and answers with one example-led recommendation, 3-5 cornerstone numbers, a council assessment when available, and a menu of at most three actions (render a report file, open the recommendation as a ticket via `shape-ticket`, drill into one run); it writes nothing unless you pick render - run it with `/skill:gauntlet-performance [--dir <path>]... [--since <version>]`. `gauntlet-handoff` ends a session whose flow a fresh session will continue: it invokes pi-cohort's `handoff` skill (`--out <path>` or `--key <name>`, default key = the run worktree's branch with `/` flattened to `-`, default file `<tmpdir>/pi-handoff/<key>.md`), then appends the gauntlet process-state section (phase/plan tracker status) per the shared contract `skills/gauntlet-resume/reference/brief-contract.md` - run it with `/skill:gauntlet-handoff [--out <path> | --key <name>]`. `gauntlet-resume` is the only way back into an interrupted flow from a fresh session: it takes a `gauntlet-handoff` brief (a file path, pasted text, or - with no arguments - a pick from the briefs under `<tmpdir>/pi-handoff/`), a bare worktree that already holds a spec, or a spec tracked on main under a configured spec dir (a spec seed: it creates or reuses the worktree through `using-git-worktrees` and pins the spec), restores phase/plan tracker state through the legal arming sequence (`start brainstorm`, `skip` with `resume:` reasons, `plan_check` before implement-or-later), and never infers approval from artifacts - run it with `/skill:gauntlet-resume [<brief-file> | <spec>.md | <worktree-name-or-path>]`.
100
+ - **21 skills** - the workflow logic. Sixteen activate automatically when pi sees the matching kind of task, and the workflow skills chain through their gates: `shape-ticket` (creates one tracker issue or repairs a comma/whitespace list of refs against a Context/Problem/Idea/Acceptance-Criteria template, with a grounded repo audit, an AC integrity check, a cheap council roast, and sequential per-ticket exact-diff write confirmations; model-invocable so other skills and commands can route "file a ticket" to it, the gate still needs your yes), `gatekeep-pr` (model-invocable on an explicit ask that names the PR and the verb; the gates still need your selection - is consent-gated pre-merge verification of a PR against the scope contract at its head - the approved spec's acceptance-criteria dispositions and Design when the PR carries one, else the PR's stated intent; the ticket is cross-checked, never judged here (`check-delivery` owns it) - read-only gathering, verification evidence resolved CI-first (green checks on the exact assessed head count as evidence; a pending commit status that GitHub does not report as blocking the gating actor (`mergeStateStatus` not `BLOCKED`) is not waited for - external-CI commit statuses included - while a running Actions check and an unknown merge state still withhold; a viewer GitHub reports as a ruleset bypass actor merges a `BLOCKED` PR on the ordinary `merge` row with CI evidence still binding and all other merge preconditions unchanged; false or unreadable bypass eligibility keeps `BLOCKED` withheld, and live non-admin bypass behavior remains unverified; the project's verification command runs only as fallback), a rubric-based review, a compact bottom-up report (what the PR delivers, including rows deferred or deviating per spec, PR comments, nits, blockers, then a `mergeable` / `fixable` verdict) and course and terminal menus whose rows derive from the actor's permissions on the PR; nothing mutates (repairs, pushes, reviews, merges, tracker comments) until you pick a course or terminal row, except the opt-in `--rebase [base]` sync that rebases, reviews, and lease-pushes the head before verification, or holds the rebased head locally when a conflict stop's scoped tests fail, so a selected delivery course resolves it before the lease push; after your own merge two opt-in rows post the `AC coverage` block to the ticket or the PR; own-merge, approved, and closed-PR menus offer a recommended `cleanup` row that tears down the gate's worktree and branch and runs the overrides file's `## cleanup` steps - run it with `/skill:gatekeep-pr [<pr>] [--rebase [base]]` (no argument: your open PRs)), `check-delivery` (model-invocable on an explicit ask that names the ticket and the verb; the gates still need your selection - is a post-merge detective control: proves an issue actually shipped (default-branch landing, delivery target, per-AC evidence) before its tracker status advances; it never writes a terminal status - run it with `/skill:check-delivery <ref>` or, in pi, `/skill:check-delivery #12 #15` for fresh parallel verification and one gate and write per ticket in turn), `writing-plans`, `roasting-the-spec`, `test-driven-development`, `subagent-driven-development`, `loop-implementation` (the plan-free implement driver that iterates a fresh `loop-implementer` over the spec's acceptance-criteria ledger when approval picks the loop engine), `dispatching-parallel-agents`, `verification-before-completion`, `requesting-code-review`, `receiving-code-review`, `using-git-worktrees`, `finishing-a-development-branch`, `forge-skill`, `linear` (reads/searches/comments on/manages Linear tickets via the `linearis` CLI; owns all linearis mechanics and the `## Issue tracker` overrides schema; tracker-facing skills route to it). Five more require explicit entry (`disable-model-invocation: true`): `brainstorming` starts the gated workflow on explicit consent - use `/skill:brainstorming` for reliable discovery, or opt in through reachable explicit prose or a human-selected handoff; after spec approval, downstream planning and implementation still chain automatically. `chase-bug` is human-only bug triage: read-only root-cause discovery to an evidenced verdict menu (real bug -> ticket/brainstorm/hotfix/respond; five negative verdicts), then a gated response to the reporter for addressable origins (GitHub issue / tracker ticket) and a rendered verdict summary otherwise - it never fixes during triage; the hotfix row hands off to `skills/chase-bug/hotfix.md` after the menu - run it with `/skill:chase-bug`. `gauntlet-performance` reads the committed run telemetry (current repo by default, `--dir <path>` adds others) through the parse-only `gauntlet-performance` CLI and answers with one example-led recommendation, 3-5 cornerstone numbers, a council assessment when available, and a menu of at most three actions (render a report file, open the recommendation as a ticket via `shape-ticket`, drill into one run); it writes nothing unless you pick render - run it with `/skill:gauntlet-performance [--dir <path>]... [--since <version>]`. `gauntlet-handoff` ends a session whose flow a fresh session will continue: it invokes pi-cohort's `handoff` skill (`--out <path>` or `--key <name>`, default key = the run worktree's branch with `/` flattened to `-`, default file `<tmpdir>/pi-handoff/<key>.md`), then appends the gauntlet process-state section (phase/plan tracker status) per the shared contract `skills/gauntlet-resume/reference/brief-contract.md` - run it with `/skill:gauntlet-handoff [--out <path> | --key <name>]`. `gauntlet-resume` is the only way back into an interrupted flow from a fresh session: it takes a `gauntlet-handoff` brief (a file path, pasted text, or - with no arguments - a pick from the briefs under `<tmpdir>/pi-handoff/`), a bare worktree that already holds a spec, or a spec tracked on main under a configured spec dir (a spec seed: it creates or reuses the worktree through `using-git-worktrees` and pins the spec), restores phase/plan tracker state through the legal arming sequence (`start brainstorm`, `skip` with `resume:` reasons, `plan_check` before implement-or-later), and never infers approval from artifacts - run it with `/skill:gauntlet-resume [<brief-file> | <spec>.md | <worktree-name-or-path>]`.
101
101
  - **11 subagent personas** - the specialized child agents the skills dispatch via pi-cohort: `implementer`, `loop-implementer`, `wave-reviewer`, `conformance-reviewer`, `spec-summarizer`, `plan-writer`, `spec-council-member`, `spec-council-synthesizer`, `spec-council-applier`, `pr-gatekeeper`, `delivery-checker`. See [doc/personas.md](./doc/personas.md) for what each one does and why its permissions are scoped the way they are.
102
- - **4 runtime extensions** - the enforcement layer. `plan-tracker` and `phase-tracker` are tools skills call to track progress (with a TUI widget); `verify-before-ship` is a hook that warns if you push or open a PR without a passing test run since your last edit (expected when a loop gate set does not match its patterns; the scoped `Verification:` line answers it, not a full local run); a phase-tracker flow guard reminds on implement-phase commits missing spec/code review. Both engines use the tracker: waves manages plan tasks, while loop calls `plan_tracker refresh` to project ledger rows and preserve conformance tasks. Loop implement completes when every row is closed, then verify step 1 runs the ledger's gate set in the owning checkouts. In a brainstorming-entered flow, phase-tracker rejects `implement` or `verify` completion while tracker tasks remain pending or in progress; see [its configuration reference](./doc/configuration.md#phase-tracker). phase-tracker also registers `plan_check`, which verifies a plan against its spec and against the grammar in [skills/writing-plans/reference/plan-contract.md](./skills/writing-plans/reference/plan-contract.md), including that each task's `Tests:` commands are selective and never the full suite, and that every test command carries a passing base-commit result; a pass stamps the plan and spec content hashes for implementation and returns a bounded task manifest; see the [manifest contract and size limit](./doc/configuration.md#phase-tracker). `telemetry` publishes YAML records on the orphan `gauntlet/telemetry` branch, outside the product diff: one sealed brainstorming run plus one session leg per settled `shape-ticket`, `gatekeep-pr` or `check-delivery` invocation. In-progress runs live in Git common-dir metadata; `gauntlet-telemetry-seal` publishes before landing without changing checkout HEAD or index. Publication failure warns without blocking ship; plain jj keeps local metadata and reports no Git remote. A fetched shipped record blocks a brainstorm `write` into that spec. See [the record layout, settings and migration guide](./doc/configuration.md#telemetry). See [doc/configuration.md](./doc/configuration.md) for the settings each one reads.
102
+ - **4 runtime extensions** - the enforcement layer. `plan-tracker` and `phase-tracker` are tools skills call to track progress (with a TUI widget); `verify-before-ship` is a hook that warns if you push or open a PR without a passing test run since your last edit (expected when a loop gate set does not match its patterns; the scoped `Verification:` line answers it, not a full local run); a phase-tracker flow guard reminds on implement-phase commits missing spec/code review. Both engines use the tracker: waves manages plan tasks, while loop calls `plan_tracker refresh` to project ledger rows and preserve conformance tasks. Loop implement completes when every row is closed, then verify step 1 runs the ledger's gate set in the owning checkouts. In a brainstorming-entered flow, phase-tracker rejects `implement` or `verify` completion while tracker tasks remain pending or in progress; see [its configuration reference](./doc/configuration.md#phase-tracker). phase-tracker also registers `plan_check`, which verifies a plan against its spec and against the grammar in [skills/writing-plans/reference/plan-contract.md](./skills/writing-plans/reference/plan-contract.md), including that each task's `Tests:` commands are selective and never the full suite, and that every test command carries a passing base-commit result; a pass stamps the plan and spec content hashes for implementation and returns a bounded task manifest; see the [manifest contract and size limit](./doc/configuration.md#phase-tracker). `telemetry` publishes YAML records on the orphan `gauntlet/telemetry` branch, outside the product diff: one sealed brainstorming run plus one session leg per settled `shape-ticket`, `gatekeep-pr` or `check-delivery` invocation. In-progress runs live in Git common-dir metadata; `gauntlet-telemetry-seal` publishes before landing without changing checkout HEAD or index. Publication failure warns without blocking ship; plain jj keeps local metadata and reports no Git remote. A fetched shipped record blocks a brainstorm `write` into that spec. See [the record layout, settings and migration guide](./doc/configuration.md#telemetry). See [doc/configuration.md](./doc/configuration.md) for the settings each one reads. `phase_tracker`, `plan_check`, and `plan_tracker` are model-only (not callable from codemode scripts) because their state is rebuilt from transcript tool results, which nested calls do not persist.
103
103
 
104
104
  pi-gauntlet is **opinionated**: it recommends this pipeline for non-trivial changes, but starts `brainstorming` only after explicit user consent. Enforcement is opt-in by entry, not ambient: once brainstorming starts a flow, the phase-tracker extension mechanically blocks a phase from closing before its gate runs, and warns once if the main loop writes code during implement (subagents own implement-phase edits). A change made *without* entering the flow (a typo, a formatting run, a dependency bump - see "When to use / when NOT to use") is not gated; the discipline of routing real work through the pipeline is a convention the tooling supports, not a trap it springs on every edit.
105
105
 
@@ -141,7 +141,7 @@ Run by a human against pi-cohort >= 7.1.0, before a pi-gauntlet release claims t
141
141
 
142
142
  ## Requirements
143
143
 
144
- - [pi-coding-agent](https://github.com/earendil-works/pi) ≥ 0.85.1 - tested minimum for sequential `phase_tracker` tool execution, which persists a tracker update before a later tool call in the same model message.
144
+ - [pi-coding-agent](https://github.com/earendil-works/pi) >= 1.5.0 - supports model-only tracker exposure and sequential tracker execution, so updates persist as transcript tool results rather than nested calls.
145
145
  - [pi-cohort](https://github.com/jjuraszek/pi-cohort) >= 7.6.6 - required peer package, including binary-safe worktree patches; it also lets a persona with `subagent` in `tools:` start with inherited extensions ([pi-cohort #26](https://github.com/jjuraszek/pi-cohort/issues/26)), which multi-PR `gatekeep-pr` needs. Skills that dispatch agents (`requesting-code-review`, `subagent-driven-development`, `dispatching-parallel-agents`, `writing-plans`, `forge-skill`, `shape-ticket`, `roasting-the-spec`) call `subagent({})`, which pi-cohort provides. pi-gauntlet does not vendor the dispatch tool; without pi-cohort those skills have nothing to call. Gauntlet execution dispatches explicitly use `async: false`; do not enable pi-cohort's incompatible `forceTopLevelAsync` setting. See pi-cohort's [dispatch configuration](https://github.com/jjuraszek/pi-cohort/blob/main/doc/configuration.md) for that setting's owner and semantics. A wave of N >= 1 tasks dispatches its implementers and first spec reviews as one chain call. `subagent-driven-development` stops before dispatch on unsupported installs; there is no older-runtime fallback.
146
146
 
147
147
  Both packages must be listed in your `.pi/settings.json#packages` array (pi adds them automatically when you `pi install`). pi-gauntlet and pi-cohort are versioned independently but release together whenever dispatch semantics change - pin compatible versions of both. At each pi-gauntlet release, set the pi-cohort peer minimum and README floor together to the newest released pi-cohort version; `/skill:release` checks the registry before proceeding.
@@ -76,7 +76,7 @@ type TestModel = { provider: string; id: string };
76
76
 
77
77
  function harness(options: { hasUI?: boolean; cwd?: string; branch?: unknown[]; idle?: boolean; beforeSettled?: (setIdle: (idle: boolean) => void) => void; sendThrows?: boolean; model?: TestModel; thinkingLevel?: string; registry?: Record<string, TestModel>; setModelResult?: boolean | Error } = {}) {
78
78
  const handlers = new Map<string, ((event: unknown, ctx: unknown) => unknown)[]>();
79
- const tools: { name: string; executionMode?: string; parameters?: any; execute: (...args: any[]) => unknown }[] = [];
79
+ const tools: { name: string; exposure?: string; executionMode?: string; parameters?: any; execute: (...args: any[]) => unknown }[] = [];
80
80
  const sent: { message: any; options: any }[] = [];
81
81
  const widgets: (string | undefined)[] = [];
82
82
  let idle = options.idle ?? true;
@@ -111,7 +111,7 @@ function harness(options: { hasUI?: boolean; cwd?: string; branch?: unknown[]; i
111
111
  registered.push(handler);
112
112
  handlers.set(event, registered);
113
113
  },
114
- registerTool(tool: { name: string; executionMode?: string; parameters?: any; execute: (...args: any[]) => unknown }) {
114
+ registerTool(tool: { name: string; exposure?: string; executionMode?: string; parameters?: any; execute: (...args: any[]) => unknown }) {
115
115
  tools.push(tool);
116
116
  },
117
117
  sendMessage(message: unknown, sendOptions: unknown) {
@@ -491,9 +491,12 @@ const completePhase = async (h: ReturnType<typeof harness>, phase: "implement" |
491
491
  };
492
492
  };
493
493
 
494
- test("phase_tracker registration requests sequential execution", () => {
494
+ test("tracker registration is model-only and phase_tracker requests sequential execution", () => {
495
495
  const h = harness();
496
496
  assert.equal(h.tools.find((t) => t.name === "phase_tracker")!.executionMode, "sequential");
497
+ for (const name of ["phase_tracker", "plan_check"]) {
498
+ assert.equal(h.tools.find((t) => t.name === name)!.exposure, "model-only", name);
499
+ }
497
500
  });
498
501
 
499
502
  test("all-complete plan activity auto-completes an active implement phase", async () => {
@@ -933,6 +933,7 @@ export default function (pi: ExtensionAPI) {
933
933
 
934
934
  pi.registerTool({
935
935
  name: "plan_check",
936
+ exposure: "model-only",
936
937
  label: "Plan Check",
937
938
  description:
938
939
  "Deterministically verify an implementation plan against its spec (mechanical checks); " +
@@ -1088,6 +1089,7 @@ export default function (pi: ExtensionAPI) {
1088
1089
 
1089
1090
  pi.registerTool({
1090
1091
  name: "phase_tracker",
1092
+ exposure: "model-only",
1091
1093
  label: "Phase Tracker",
1092
1094
  description:
1093
1095
  "Track workflow phase progress (brainstorm → plan → implement → verify → ship); " +
@@ -15,6 +15,7 @@ function harness(branch: unknown[] = [], cwd = process.cwd()) {
15
15
  const tools: {
16
16
  name: string;
17
17
  description: string;
18
+ exposure?: string;
18
19
  executionMode?: string;
19
20
  parameters: any;
20
21
  execute: (...args: any[]) => unknown;
@@ -752,8 +753,9 @@ test("refresh rejects unreadable or malformed ledgers atomically, including abse
752
753
  }
753
754
  });
754
755
 
755
- test("registration: sequential, tasks schema is Array<Union<String, Object>>, description carries the contract", () => {
756
- const { executionMode, parameters, description } = harness();
756
+ test("registration: model-only, sequential, tasks schema is Array<Union<String, Object>>, description carries the contract", () => {
757
+ const { tool, executionMode, parameters, description } = harness();
758
+ assert.equal(tool().exposure, "model-only");
757
759
  assert.equal(executionMode(), "sequential");
758
760
  const tasksSchema = parameters().args[0].tasks.args[0];
759
761
  assert.equal(tasksSchema.kind, "Array");
@@ -237,6 +237,7 @@ export default function (pi: ExtensionAPI) {
237
237
 
238
238
  pi.registerTool({
239
239
  name: "plan_tracker",
240
+ exposure: "model-only",
240
241
  label: "Plan Tracker",
241
242
  description:
242
243
  "Track progress while EXECUTING an implementation plan (the implement phase), a verify-phase conformance fix wave, or another bounded gate checklist (e.g. pre-merge PR verification). Statuses: pending (not yet touched), in_progress (started; several at once is fine), complete, failed (terminal-negative: ran and did not pass; never counted done), skipped (terminal: not applicable in this run; counted done). Rule: pending tasks must trail every started or finished task - an update that would leave a pending task ahead of a non-pending one is rejected with the fix. update never sets pending; init and add do. Actions: init (set task list; elements are task-name strings or { name, status } to recreate a list with known statuses), add (append tasks as pending; existing statuses preserved), update (change task status), status (show current state), clear (remove plan), refresh (derive tasks from an artifact path while retaining other tasks). Do NOT use for brainstorming, research, or planning checklists: those phases are open-ended and a bounded task list misrepresents them as a fixed N-step process.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-gauntlet",
3
- "version": "8.4.0",
3
+ "version": "8.4.2",
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",
@@ -72,7 +72,7 @@ These bind every rendered report, menu, and external payload (review bodies, rep
72
72
  - The orchestrator resolving a merge conflict itself, or editing or committing a tracked file during a fix wave - owner: `reference/fix-wave.md` `## Sync` and `## Wave`.
73
73
  - A helper's duty run in the orchestrator's own context - owner: `## Harness notes`.
74
74
  - Approving your own PR - owner: `reference/decision-menu.md` `## Availability`.
75
- - Merging around an undispositioned failing check or a binding pending check, or a `BLOCKED`/`UNKNOWN`/`unreadable` merge state - owner: `reference/findings.md` `## Dispositions`.
75
+ - Merging around an undispositioned failing check or a binding pending check, or a merge-state withhold under `reference/post-selection-loop.md` `### Merge preconditions` - owner: `reference/findings.md` `## Dispositions`.
76
76
  - A menu rendered without the pre-menu refresh - owner: `reference/post-selection-loop.md` `### Pre-menu refresh`.
77
77
  - Reading `mergeStateStatus` from anything but a `gh pr view` invocation, attributing a `BLOCKED` to a named rule, or treating a non-`BLOCKED` value as a merge verdict - owner: `verification-brief.md` Section A.
78
78
  - A claim verdict recorded behind a pending tracker stage - owner: `## Progress tracking`.
@@ -14,7 +14,7 @@ Read and apply [menu-layout](../../brainstorming/reference/menu-layout.md) befor
14
14
  - `preserve contract` - keep the baseline and independent blockers; resume scope-preserving course work without the rejected payload on that re-run.
15
15
  - `approve` - approve someone else's non-draft PR with zero blockers; persist approved_head only after success.
16
16
  - `approve workflow run` - execute `gh api --method POST repos/<base-owner>/<base-repo>/actions/runs/<id>/approve`, then refresh and watch under the course.
17
- - `merge` - execute `gh pr merge --match-head-commit <assessed-sha>` with the resolved policy flag and all merge preconditions; never infer approval from a setting.
17
+ - `merge` - execute `gh pr merge --match-head-commit <assessed-sha>` with the resolved policy flag and all merge preconditions; append `--admin` only on the bypass path defined in `post-selection-loop.md` `### Merge course`; never infer approval from a setting.
18
18
  - `reply` - post drafted replyable C# replies; exact `reply <n>` selects one.
19
19
  - `post coverage to ticket` - post the rendered AC coverage block through the resolved tracker verb; renders only under the `own merge` template with issue and rows.
20
20
  - `post coverage to PR` - post the same block with `gh pr comment <N> --body-file`; renders only under the `own merge` template.
@@ -45,7 +45,7 @@ Resolve every actual unmet `### Merge preconditions` reason through `post-select
45
45
 
46
46
  | Observation | Render |
47
47
  |---|---|
48
- | BLOCKED with no local repair | Use findings.md `## Dispositions` for the actor-framed PR-comments line and obstacle verdict; render obstacle even with course unset, plus `Not offered: merge (blocked by GitHub)`; never attribute a named rule |
48
+ | BLOCKED with no eligible bypass and no local repair | Use findings.md `## Dispositions` for the actor-framed PR-comments line and obstacle verdict; render obstacle even with course unset, plus `Not offered: merge (blocked by GitHub)`; never attribute a named rule |
49
49
  | UNKNOWN/unreadable after one re-poll | `stuck - merge state unknown`; `Not offered: merge (merge state unknown)` |
50
50
  | binding pending check | name the check under PR comments, `Not offered: merge (<check name> pending)`; watch only at delivery step 4 |
51
51
  | draft | `Not offered: merge (draft)` |
@@ -87,7 +87,7 @@ Any blocking conclusion in the resolved check set (required or not - `../verific
87
87
 
88
88
  Only `flaky` lifts the blocker; an undispositioned check, `real`, and `ci-infrastructure-broken` keep it until the check - or, for `ci-infrastructure-broken`, the fallback local run (brief Section B, Failed CI row) - is green.
89
89
 
90
- Record dispositions only from direct human check commands. A flaky disposition followed by numeric deliver can resume; no helper can disposition a check. With BLOCKED and no dispositionable local repair, render the obstacle template from `menus.md`, even with course unset, and use verdict `GitHub reports BLOCKED, no local repair applies`. Render its PR-comments line as `GitHub reports this actor's merge as blocked, so merge waits. (merge_state_status BLOCKED; reviewDecision <value>)`; keep the locator and never attribute a named rule.
90
+ Record dispositions only from direct human check commands. A flaky disposition followed by numeric deliver can resume; no helper can disposition a check. With BLOCKED, no eligible bypass under `post-selection-loop.md` `### Merge preconditions`, and no dispositionable local repair, render the obstacle template from `menus.md`, even with course unset, and use verdict `GitHub reports BLOCKED, no local repair applies`. Render its PR-comments line as `GitHub reports this actor's merge as blocked, so merge waits. (merge_state_status BLOCKED; reviewDecision <value>)`; keep the locator and never attribute a named rule.
91
91
 
92
92
  ## Drafted payloads
93
93
 
@@ -15,6 +15,8 @@ Read with `decision-menu.md` at step 6. Select the template from this state map;
15
15
  | first report not ready, course unset | initial |
16
16
  | multi-PR parent round | batch summary |
17
17
 
18
+ Resolve BLOCKED through `post-selection-loop.md` `### Merge preconditions` before selecting an obstacle; treat only BLOCKED without eligible bypass as undispositionable.
19
+
18
20
  Render every menu in full from its template: the verdict line, every available row, and the hint line. Never narrate or summarize a menu; do not substitute "the initial template applied; the human picked 2" for its rendering. Quote a numeric pick only after the rendered menu.
19
21
 
20
22
  Append ` recommended=<n>` to each gate line using the actual rendered number of its tagged recommended row. Emit one first line per turn; when combining batch summary and child menus, emit only the parent `gatekeep-pr/batch-summary` line. Keep runtime guard text, including any quoted escalation instruction, inside the obstacle body, never as a second standalone marker.
@@ -46,6 +48,8 @@ Course holds until the final menu; `show evidence` prints the full record.
46
48
 
47
49
  Start the message with `gauntlet: gate=gatekeep-pr/final`.
48
50
 
51
+ For eligible BLOCKED readiness, render `merge - <policy>-merge on ruleset bypass authority; CI evidence stands` with the resolved policy, and carry `merge_state_status BLOCKED (bypass eligible; reviewDecision <value>)` in the `Evidence:` line below. Keep the ordinary `merge` row and the same recommendation defaults; render no separate bypass row.
52
+
49
53
  Offer available `merge`, `approve`, `fix nits` when nits remain and the course excluded them, `reply` when drafted replies exist, then `stop`. Recommend merge for own PR; recommend approve for someone else's when available, else merge; with neither available recommend stop. Omit merge on a draft with `Not offered: merge (draft)` or below WRITE with `Not offered: merge (needs WRITE on <owner>/<repo>)`. Omit approve on drafts and own PRs. Authorize exactly one delivery iteration for fix nits, leaving course unchanged (`post-selection-loop.md` `### Next action`). Hint: `Type a number; show evidence prints the full record.` Print one compact `Evidence:` line above the `Delivers:` line naming the source (`CI (source: ci)` or the local run), the head SHA, each run and its checks with their conclusions, the binding-pending count, and `merge_state_status`; put the same record in the `show evidence` hint's parenthetical.
50
54
 
51
55
  ## assess terminal
@@ -60,7 +60,7 @@ Resolve every unmet merge prerequisite; retain failed prerequisites and append c
60
60
  | Helper open - needs <what>, or pi failed decision's second slot starts needs <what> | `stuck - needs <what>`. |
61
61
  | Helper open - contract exception, or pi failed decision's second slot starts contract exception: | Render contract exception through `### Contract decision`, not obstacle. |
62
62
  | HEAD equals pre_wave_head with all payloads open, or scoped test unchanged-red after restore | render per ### No-progress obstacle |
63
- | Undispositionable BLOCKED | `GitHub reports BLOCKED, no local repair applies`; invent no rule attribution. |
63
+ | Undispositionable BLOCKED without eligible bypass under Merge preconditions | `GitHub reports BLOCKED, no local repair applies`; invent no rule attribution. |
64
64
  | UNKNOWN/unreadable after step 2's single re-poll, regardless of CI color or a pending binding check | Render `stuck - merge state unknown` through `menus.md`'s obstacle template; let merge state own the withhold reason and the PR comments line. Authorize no sync, offer no ordinary wait row, keep stop last and `[recommended]`, report both usage counters unchanged, and print `Not offered: merge (merge state unknown)`. |
65
65
  | Nonzero gh or incomplete pagination during refresh/refetch | One retry, then `stuck - refresh failed: <actual reason>`. |
66
66
  | Incomplete comment source review | One fresh reviewer retry, then `stuck - comment source review incomplete`. |
@@ -112,7 +112,7 @@ All must hold before `merge` executes; a `merge` pick while one fails is refused
112
112
  - `mergeable == MERGEABLE` (`UNKNOWN` after step 2's single re-poll withholds like `CONFLICTING`)
113
113
  - `merge_state_status` is not `BEHIND`; while it is, run `### Loop sync` under delivery
114
114
  - no undispositioned failing check in the resolved set, no **binding** pending check (`../verification-brief.md` Section B, binding classification)
115
- - `merge_state_status` is not `BLOCKED`, `UNKNOWN`, or `unreadable`, and the pre-menu refresh succeeded (`### Pre-menu refresh`); a non-`BLOCKED` value is never a verdict - it only leaves this row satisfied, and a `gh pr merge` refusal is reported verbatim
115
+ - `merge_state_status` is not `UNKNOWN` or `unreadable`, and the pre-menu refresh succeeded (`### Pre-menu refresh`). For `BLOCKED`, require fresh `permissions.bypass_eligible == true`; lift only this merge-state withhold and keep every other precondition binding. Treat false or unreadable eligibility as no bypass. A non-`BLOCKED` value is never a verdict - it only leaves this row satisfied, and a `gh pr merge` refusal is reported verbatim
116
116
  - no retained, unreviewed comment delta (`### Re-render` step 4)
117
117
  - worktree tracked-clean and synced with the remote head (fixes pushed first)
118
118
  - the repo allows the resolved merge policy: read `gh api repos/<owner>/<repo> --jq '{allow_squash_merge,allow_merge_commit,allow_rebase_merge}'` and require the policy's flag (`squash` -> `allow_squash_merge`, `merge-commit` -> `allow_merge_commit`, `rebase` -> `allow_rebase_merge`) to be `true`; a `false` is a blocker on the `merge` pick, named as `merge policy <policy> is disabled on <owner>/<repo>`, never a `gh pr merge` failure
@@ -120,6 +120,8 @@ All must hold before `merge` executes; a `merge` pick while one fails is refused
120
120
 
121
121
  ### Compare-and-swap
122
122
 
123
+ Re-read `permissions.bypass_eligible` with Section A's fixed GraphQL query beside the freshness read below; apply its fail-closed decoding. On a head, body, or base mismatch across reads, discard eligibility and re-enter Pre-menu refresh. A `bypass_eligible` change from the rendered menu invalidates the selected merge even with `BLOCKED` unchanged: run no `gh pr merge` and re-enter Pre-menu refresh for a fresh selection.
124
+
123
125
  Before every external write, re-fetch `headRefOid`, `state`, `mergeable`, and `mergeStateStatus`, plus actual `body` and `baseRefName`; apply Next action's target check. A body change aborts selected merge pending narrow reassessment and fresh numeric selection. Under a loop rewrite compare remote head to `repair.wave.sync.expected_remote_head`, never replace it on mismatch, and recover to `repair.wave.sync.pre_head` through Sync. Without a loop checkpoint, an entry rewrite keeps its own pre_head for both comparison and recovery. Movement restores/asserts and exits `stuck - remote head moved`; closed/merged state renders closed PR. Other head movement re-syncs and re-enters step 3; merge-state change alone invalidates a selected merge and re-enters Pre-menu refresh. Preserve course and accounting, never open another initial menu mid-course. The gate's own successful push updates the assessed SHA before later writes.
124
126
 
125
127
  Before a merge executes, run the full comment refetch and reconciliation (`### Re-render` steps 1-5). A head move re-enters step 3. A new or changed-body delta aborts the selected merge even when source review finds no blocker: render the fresh final or obstacle template for a new pick. A pending reviewer row, queued/in-progress reviewer run, failed refetch or unreviewed delta withholds merge. Resolve failures through Exit check's bounded retries; no override bypasses a new blocker or unreviewed delta.
@@ -142,9 +144,11 @@ After successful `gh pr review --approve`, persist approved_head as the assessed
142
144
 
143
145
  ### Merge course
144
146
 
145
- `merge` executes as `gh pr merge --match-head-commit <assessed-sha>` with the flag of the resolved merge policy: `squash` -> `--squash`, `merge-commit` -> `--merge`, `rebase` -> `--rebase`. Push and merge are never one pick.
147
+ `merge` executes as `gh pr merge --match-head-commit <assessed-sha>` with the flag of the resolved merge policy: `squash` -> `--squash`, `merge-commit` -> `--merge`, `rebase` -> `--rebase`. Push and merge are never one pick. Append `--admin` only on the bypass path: `merge_state_status == BLOCKED` and `permissions.bypass_eligible == true`, with every merge precondition satisfied. Omit `--admin` on every other path. This flag skips the merge queue; the bypass path merges directly, not through the merge queued template. In batch mode the parent reads `permissions.bypass_eligible` from the retained child record and applies this same predicate and refusal branch, without a new handoff field or batch compare-and-swap.
148
+
149
+ On a nonzero exit, print gh's error verbatim, consume the selection without retry (no second attempt with or without `--admin`), and re-run `### Pre-menu refresh`. Render from the refreshed prerequisites: final while all merge preconditions hold; obstacle with `Not offered: merge (blocked by GitHub)` when `BLOCKED` remains and `bypass_eligible` is no longer `true`; otherwise use the refreshed withhold resolver. Do not treat refusal as a queued merge or `stuck` by itself.
146
150
 
147
- After `gh pr merge` returns, read `gh pr view <N> --json state,mergeCommit`. `state: MERGED` -> write the returned `state` into `pr.state` and `mergeCommit.oid` into `pr.merge_commit` (the same two writes when a queued merge's `wait` re-read returns `MERGED`), render the `AC coverage` block once and re-render the menu under the `own merge` overlay (`menus.md`); any other state (a merge queue) -> one `PR comments` line `The merge is queued, so coverage posts wait. (<state>)` and the merge queued template (wait re-runs the read; show evidence stays hint-only). The block, `<sha>` = `mergeCommit.oid`, one row per `scope.rows` entry in row order with its outcome (`covered`, `deferred per spec to <ref>`, `deviates per spec: <why>`, `elsewhere per spec: <repo>`, `venue: <env> - <observation>, checked before prod deploy`); with no rows (`source: pr`, or an empty `scope.rows`) the block has no row lines and `post coverage to ticket` is not offered. With `source: pr` the header line drops `, spec <path>`. An `in-scope` row whose observation half is `not judged here` renders `covered; observable half checked after merge`.
151
+ On a zero exit, read `gh pr view <N> --json state,mergeCommit`. `state: MERGED` -> write the returned `state` into `pr.state` and `mergeCommit.oid` into `pr.merge_commit` (the same two writes when a queued merge's `wait` re-read returns `MERGED`), render the `AC coverage` block once and re-render the menu under the `own merge` overlay (`menus.md`); any other state (a merge queue) -> one `PR comments` line `The merge is queued, so coverage posts wait. (<state>)` and the merge queued template (wait re-runs the read; show evidence stays hint-only). The block, `<sha>` = `mergeCommit.oid`, one row per `scope.rows` entry in row order with its outcome (`covered`, `deferred per spec to <ref>`, `deviates per spec: <why>`, `elsewhere per spec: <repo>`, `venue: <env> - <observation>, checked before prod deploy`); with no rows (`source: pr`, or an empty `scope.rows`) the block has no row lines and `post coverage to ticket` is not offered. With `source: pr` the header line drops `, spec <path>`. An `in-scope` row whose observation half is `not judged here` renders `covered; observable half checked after merge`.
148
152
 
149
153
  Read and apply [menu-layout](../../brainstorming/reference/menu-layout.md) before presenting these options and before acting on a numeric reply.
150
154
 
@@ -179,7 +183,9 @@ After every push or any mutation that can change readiness (fix wave pushed, PR
179
183
 
180
184
  ### Pre-menu refresh
181
185
 
182
- 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.
186
+ Refresh `permissions.bypass_eligible` through Section A's fixed GraphQL query on every pre-menu refresh, using the recorded base repository and PR number; apply Section A's fail-closed decoding instead of retaining a cached true. On a head, body, or base mismatch across the reads, discard eligibility and refresh the matching snapshot before offering merge.
187
+
188
+ 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, bypass_eligible, 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.
183
189
 
184
190
  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.
185
191
 
@@ -6,6 +6,8 @@ Read from SKILL.md step 5, after `findings.md`. Input: the integration output. O
6
6
 
7
7
  Render bottom-up for a terminal: least important first, verdict and menu last. Empty sections are omitted - never "None". A failed comment refresh, an incomplete comment source review (`post-selection-loop.md` `### Re-render`), a blocked or unknown merge state, or a binding pending check renders its one line under `PR comments` as well.
8
8
 
9
+ For an eligible BLOCKED merge, omit the blocked PR-comments line; resolve eligibility through `post-selection-loop.md` `### Merge preconditions` and render the bypass consequence through `menus.md` `## final`. For false or unreadable eligibility, render the actor-framed BLOCKED line below and `Not offered: merge (blocked by GitHub)` unchanged. Keep `bypass_eligible` and its value only in the digest `permissions` block; never name the field in rendered output, including obstacle explanations and `show evidence`.
10
+
9
11
  Section order is fixed:
10
12
 
11
13
  Read and apply [menu-layout](../../brainstorming/reference/menu-layout.md) before presenting these options and before acting on a numeric reply.
@@ -54,7 +54,7 @@ Read-only. Fixed `gh` command set - do not substitute ad hoc queries:
54
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
55
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)
56
56
  gh api user --jq .login # viewer.login; a no-token run reuses discovery's value, unreadable included
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`
57
+ gh api graphql -f query='query($o:String!,$r:String!,$n:Int!){ repository(owner:$o,name:$r){ viewerPermission pullRequest(number:$n){ viewerCanUpdateBranch viewerCanMergeAsAdmin headRefOid body baseRefName } } }' -f o=<owner> -f r=<repo> -F n=<N> # viewer_permission, can_update_branch, bypass_eligible; a null field is recorded as `unreadable`
58
58
  gh repo view <base-repository> --json nameWithOwner # resolved base repository context, never headRepository
59
59
  gh pr diff <N>
60
60
  gh api repos/{owner}/{repo}/pulls/<N>/reviews --paginate # REST review ids join inline comments; prior-review selection source
@@ -101,7 +101,7 @@ provisioning, so it never re-reads; step 2 re-reads once after provisioning (`re
101
101
  - pr.review_requests: [ { __typename: User, login } | { __typename: Team, name, slug } ] # raw reviewRequests entries
102
102
  - viewer: { login, is_author, role }
103
103
  - prior_review: null | { id, state, commit, submitted_at, body, inline[ { id, path, original_line, body } ], delta: ancestor | not ancestor (<reason>) | unset }
104
- - 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)
104
+ - permissions: { viewer_permission, is_cross_repository, maintainer_can_modify, merge_state_status, can_update_branch, bypass_eligible, 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)
105
105
  - 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)
106
106
  - actions_runs: [ { databaseId, status, conclusion, workflowName, url } ] # from the fixed gh run list call on the assessed head
107
107
  - comments: { inline[ { id, updated_at, user_type, body, ... } ], top_level[ { id, updated_at, user_type, body, ... } ], review_threads[]? } # retain REST body for source-review deltas; C# identity diffs on id/updated_at, Section C gates placeholder detection on user_type
@@ -150,7 +150,7 @@ uses (`https://github.com/<nameWithOwner>.git` or `git@github.com:<nameWithOwner
150
150
  a null `headRepository` (deleted fork) gives `unreadable`;
151
151
  `head_ref` is `headRefName`. `viewer_permission` is the GraphQL
152
152
  `viewerPermission` (null when authenticated as an App -> `unreadable`);
153
- `can_update_branch` is `viewerCanUpdateBranch`.
153
+ `can_update_branch` is `viewerCanUpdateBranch`. Set `bypass_eligible` from the viewer-relative GraphQL `viewerCanMergeAsAdmin`: true alone establishes eligibility; false stays false; null, missing, non-boolean, or a failed/error-bearing response becomes `unreadable`. Never infer bypass eligibility from `viewer_permission` or a repository role. Keep this field in the fixed GraphQL query, not `gh pr view --json`; apply eligibility only through `reference/post-selection-loop.md` `### Merge preconditions`.
154
154
 
155
155
  That `mergeStateStatus` reflects the viewer's own standing is an observation (an exempt viewer reads `UNSTABLE` on a review-required PR), not a documented guarantee; GitHub documents `BLOCKED` only as "the merge is blocked" with no per-rule attribution, so the gate never names which rule blocks. A non-`BLOCKED` value never approves a merge, it only leaves the `merge` row available to the preconditions (`reference/post-selection-loop.md`).
156
156