@codyswann/lisa 4.83.2 → 4.84.1

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.
Files changed (160) hide show
  1. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  2. package/dist/core/lisa-owned-hash-ledger.js +6 -0
  3. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  4. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  5. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  6. package/dist/core/upstream-evidence-manifest.js +17 -13
  7. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  8. package/package.json +4 -4
  9. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  10. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  11. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
  12. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/SKILL.md +39 -0
  13. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/references/runbook-template.md +6 -0
  14. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
  15. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
  16. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
  17. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/config.mjs +1 -0
  18. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
  19. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
  20. package/plugins/lisa/.codex-plugin/skills/lisa-setup-session-manager/SKILL.md +8 -1
  21. package/plugins/lisa/rules/reference/config-resolution.md +1 -0
  22. package/plugins/lisa/scripts/ready-queue-order.mjs +109 -6
  23. package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
  24. package/plugins/lisa/skills/lisa-session-manager/SKILL.md +39 -0
  25. package/plugins/lisa/skills/lisa-session-manager/references/runbook-template.md +6 -0
  26. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
  27. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
  28. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
  29. package/plugins/lisa/skills/lisa-session-manager/scripts/config.mjs +1 -0
  30. package/plugins/lisa/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
  31. package/plugins/lisa/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
  32. package/plugins/lisa/skills/lisa-setup-session-manager/SKILL.md +8 -1
  33. package/plugins/lisa-agy/plugin.json +1 -1
  34. package/plugins/lisa-agy/scripts/ready-queue-order.mjs +109 -6
  35. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
  36. package/plugins/lisa-agy/skills/lisa-session-manager/SKILL.md +39 -0
  37. package/plugins/lisa-agy/skills/lisa-session-manager/references/runbook-template.md +6 -0
  38. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
  39. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
  40. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
  41. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/config.mjs +1 -0
  42. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
  43. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
  44. package/plugins/lisa-agy/skills/lisa-setup-session-manager/SKILL.md +8 -1
  45. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  48. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +1 -1
  52. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-copilot/rules/reference/config-resolution.md +1 -0
  54. package/plugins/lisa-copilot/scripts/ready-queue-order.mjs +109 -6
  55. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
  56. package/plugins/lisa-copilot/skills/lisa-session-manager/SKILL.md +39 -0
  57. package/plugins/lisa-copilot/skills/lisa-session-manager/references/runbook-template.md +6 -0
  58. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
  59. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
  60. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
  61. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/config.mjs +1 -0
  62. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
  63. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
  64. package/plugins/lisa-copilot/skills/lisa-setup-session-manager/SKILL.md +8 -1
  65. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +1 -0
  67. package/plugins/lisa-cursor/scripts/ready-queue-order.mjs +109 -6
  68. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
  69. package/plugins/lisa-cursor/skills/lisa-session-manager/SKILL.md +39 -0
  70. package/plugins/lisa-cursor/skills/lisa-session-manager/references/runbook-template.md +6 -0
  71. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
  72. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
  73. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
  74. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/config.mjs +1 -0
  75. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
  76. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
  77. package/plugins/lisa-cursor/skills/lisa-setup-session-manager/SKILL.md +8 -1
  78. package/plugins/lisa-devin/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
  80. package/plugins/lisa-devin/rules/reference/config-resolution.md +1 -0
  81. package/plugins/lisa-devin/scripts/ready-queue-order.mjs +109 -6
  82. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
  83. package/plugins/lisa-devin/skills/lisa-session-manager/SKILL.md +39 -0
  84. package/plugins/lisa-devin/skills/lisa-session-manager/references/runbook-template.md +6 -0
  85. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
  86. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
  87. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
  88. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/config.mjs +1 -0
  89. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
  90. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
  91. package/plugins/lisa-devin/skills/lisa-setup-session-manager/SKILL.md +8 -1
  92. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  93. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  94. package/plugins/lisa-expo-agy/plugin.json +1 -1
  95. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +1 -1
  98. package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +1 -1
  99. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  100. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  101. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  102. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  104. package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
  105. package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +1 -1
  106. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  108. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  109. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  110. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  111. package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +1 -1
  113. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  114. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  115. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  116. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  117. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  118. package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +1 -1
  119. package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +1 -1
  120. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  121. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  122. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  123. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  124. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  125. package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +1 -1
  126. package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +1 -1
  127. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  128. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  129. package/plugins/lisa-rails-agy/plugin.json +1 -1
  130. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  131. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  132. package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +1 -1
  133. package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +1 -1
  134. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  135. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  136. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  137. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  138. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  139. package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +1 -1
  140. package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +1 -1
  141. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  142. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  143. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  144. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  145. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  146. package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
  147. package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
  148. package/plugins/src/base/rules/reference/config-resolution.md +1 -0
  149. package/plugins/src/base/scripts/ready-queue-order.mjs +109 -6
  150. package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
  151. package/plugins/src/base/skills/lisa-session-manager/SKILL.md +39 -0
  152. package/plugins/src/base/skills/lisa-session-manager/references/runbook-template.md +6 -0
  153. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
  154. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
  155. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
  156. package/plugins/src/base/skills/lisa-session-manager/scripts/config.mjs +1 -0
  157. package/plugins/src/base/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
  158. package/plugins/src/base/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
  159. package/plugins/src/base/skills/lisa-setup-session-manager/SKILL.md +8 -1
  160. package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +276 -6
@@ -2129,6 +2129,12 @@ export const REVIEW_GATE_CONDITIONS = Object.freeze({
2129
2129
  pending: "pending",
2130
2130
  unrecognised: "unrecognised",
2131
2131
  undetermined: "undetermined",
2132
+ // A SATISFYING condition, and only ever reached when the reviewer posted NO
2133
+ // status row at all (`absent`, or `undetermined` while still unreported —
2134
+ // never a row still in flight), and a writer posted a local
2135
+ // adversarial review for this exact head in its place. Never from
2136
+ // `objected` — a substitute does not overrule a review that ran and said no.
2137
+ substituted: "substituted",
2132
2138
  });
2133
2139
 
2134
2140
  /**
@@ -2500,6 +2506,17 @@ export function evaluateReviewGate(declaration, checks, options = {}) {
2500
2506
  },
2501
2507
  vocabulary
2502
2508
  );
2509
+ // Only a reviewer that NEVER REPORTED can be stood in for, keyed on the
2510
+ // missing row rather than on the condition: `undetermined` is also what a
2511
+ // reviewer STILL RUNNING reads as once the wait expires, and a substitute
2512
+ // must not race a review in flight whose objection would land after the
2513
+ // merge. One that is pending, said something unrecognised, or objected
2514
+ // has spoken, and a substitute does not get to overrule it.
2515
+ if (found === undefined && options.substitutes?.has(name) === true) {
2516
+ states[name] = REVIEW_GATE_STATES.satisfied;
2517
+ conditions[name] = REVIEW_GATE_CONDITIONS.substituted;
2518
+ continue;
2519
+ }
2503
2520
  states[name] = verdict.state;
2504
2521
  conditions[name] = verdict.condition;
2505
2522
  // Assigned ONLY when the check reported. An absent check with a `""`
@@ -2542,6 +2559,191 @@ export function evaluateReviewGate(declaration, checks, options = {}) {
2542
2559
  */
2543
2560
  export const REVIEW_OBJECTION_STATE = "CHANGES_REQUESTED";
2544
2561
 
2562
+ /**
2563
+ * The marker a local adversarial review carries when it stands in for a
2564
+ * declared reviewer that never reported.
2565
+ *
2566
+ * Byte-identical to `substituteMarker` in
2567
+ * `check-third-party-review-evidence.mjs`, which is the format
2568
+ * `lisa-drive-pr-to-merge` posts. Duplicated rather than imported because the
2569
+ * two scripts ship from different template trees, and a host that has one need
2570
+ * not have the other.
2571
+ *
2572
+ * WHY THIS GATE READS IT AT ALL. A repository that declares a reviewer it has
2573
+ * not installed — or whose reviewer is down — saw this gate red on EVERY pull
2574
+ * request, with no route to green that did not also delete the declaration.
2575
+ * The response arm already accepted a posted substitute; this arm did not, so
2576
+ * the two published opposite verdicts for the same head.
2577
+ */
2578
+ export const REVIEW_SUBSTITUTE_MARKER = "lisa:review-substitute";
2579
+
2580
+ /** Repository permissions whose holder may stand in for a reviewer. */
2581
+ const SUBSTITUTE_WRITER_PERMISSIONS = new Set(["admin", "maintain", "write"]);
2582
+
2583
+ /**
2584
+ * The lead of a satisfied verdict, which says whether a substitute was involved.
2585
+ *
2586
+ * @param {ReadonlyArray<string>} names - The declared reviewers
2587
+ * @param {Record<string, string>} conditions - Each reviewer's condition
2588
+ * @returns {string} `REVIEWED`, or a SUBSTITUTED lead accurate for all or some
2589
+ */
2590
+ function substitutedLead(names, conditions) {
2591
+ const substituted = names.filter(
2592
+ name => conditions[name] === REVIEW_GATE_CONDITIONS.substituted
2593
+ );
2594
+ if (substituted.length === 0) return "REVIEWED";
2595
+ if (substituted.length === names.length) {
2596
+ return "SUBSTITUTED — no third-party review; a local adversarial review stands in";
2597
+ }
2598
+ return `SUBSTITUTED IN PART — a local adversarial review stands in for ${substituted.length} of ${names.length} declared reviewers; the rest reviewed`;
2599
+ }
2600
+
2601
+ /**
2602
+ * The marker line a substitute carries for one reviewer at one commit.
2603
+ *
2604
+ * @param {string} context - The declared evidence-bearing check name
2605
+ * @param {string} sha - The head commit the substitute reviewed
2606
+ * @returns {string} The marker line
2607
+ */
2608
+ export function reviewSubstituteMarker(context, sha) {
2609
+ return `<!-- ${REVIEW_SUBSTITUTE_MARKER} context="${context}" head="${sha}" -->`;
2610
+ }
2611
+
2612
+ /**
2613
+ * Which declared checks a WRITER posted a substitute for at exactly this head.
2614
+ *
2615
+ * Stricter than the response arm, on purpose, because here a match turns a
2616
+ * blocking verdict green:
2617
+ *
2618
+ * - The marker must be the comment's FIRST LINE. A comment that merely quotes
2619
+ * the marker — instructions, a pasted log — is not a review.
2620
+ * - The author must hold write access. Anyone who can comment can type the
2621
+ * marker; only someone who could have merged anyway can vouch for a review.
2622
+ * - Evidence is per reviewer and per head: a substitute for another check, or
2623
+ * for an earlier push, satisfies nothing.
2624
+ *
2625
+ * @param {ReadonlyArray<{body?: string, author?: string}>} comments - Issue comments on the pull request
2626
+ * @param {ReadonlyArray<string>} names - Declared evidence-bearing check names
2627
+ * @param {string|undefined} headSha - The head commit under inspection
2628
+ * @param {(login: string) => boolean} canWrite - Whether a login holds write access
2629
+ * @returns {Set<string>} The names a writer's substitute stands in for
2630
+ */
2631
+ export function substitutesAtHead(comments, names, headSha, canWrite) {
2632
+ const found = new Set();
2633
+ if (headSha === undefined || headSha === "") return found;
2634
+ for (const name of names) {
2635
+ const marker = reviewSubstituteMarker(name, headSha);
2636
+ const posted = (comments ?? []).some(
2637
+ comment =>
2638
+ String(comment?.body ?? "")
2639
+ .split("\n")[0]
2640
+ .replace(/\r$/, "") === marker &&
2641
+ canWrite(String(comment?.author ?? ""))
2642
+ );
2643
+ if (posted) found.add(name);
2644
+ }
2645
+ return found;
2646
+ }
2647
+
2648
+ /**
2649
+ * Reads the issue comments on one pull request.
2650
+ *
2651
+ * @param {string|number} pr - The pull request
2652
+ * @param {string} [repo] - `OWNER/NAME`; defaults to the current repository
2653
+ * @returns {Array<{body: string, author: string}>} The comments
2654
+ */
2655
+ export function fetchIssueComments(pr, repo) {
2656
+ const slug = resolveRepoSlug(repo);
2657
+ if (slug === undefined) {
2658
+ throw new Error(
2659
+ `check-skipped-required-checks: cannot read the comments on PR ${pr} without an OWNER/NAME. Pass \`--repo=OWNER/NAME\`, or set GITHUB_REPOSITORY.`
2660
+ );
2661
+ }
2662
+ return ghApiPaginatedArray(
2663
+ `repos/${slug}/issues/${pr}/comments?per_page=100`,
2664
+ "[.[] | {body: .body, author: .user.login}]"
2665
+ );
2666
+ }
2667
+
2668
+ /**
2669
+ * Whether a login holds write access to the repository.
2670
+ *
2671
+ * probe-direction: fail-closed — any failure to read the permission answers
2672
+ * `false`, which can only leave a substitute uncounted, never count one.
2673
+ *
2674
+ * Two known shapes never count, both in the safe direction: a login the
2675
+ * pattern below rejects (Enterprise Managed User logins carry `_shortcode`),
2676
+ * and a bot author such as `github-actions[bot]`, which is not a collaborator.
2677
+ * A substitute posted from CI therefore needs a human writer's comment.
2678
+ *
2679
+ * @param {string} login - The comment author
2680
+ * @param {string} [repo] - `OWNER/NAME`; defaults to the current repository
2681
+ * @returns {boolean} Whether the login may stand in for a reviewer
2682
+ */
2683
+ export function fetchWritePermission(login, repo) {
2684
+ const slug = resolveRepoSlug(repo);
2685
+ if (slug === undefined || !/^[A-Za-z0-9-]+(\[bot\])?$/.test(login)) {
2686
+ return false;
2687
+ }
2688
+ try {
2689
+ const permission = boundedExecFileSync(
2690
+ "gh",
2691
+ [
2692
+ "api",
2693
+ `repos/${slug}/collaborators/${encodeURIComponent(login)}/permission`,
2694
+ "--jq",
2695
+ ".permission",
2696
+ ],
2697
+ { encoding: "utf8" }
2698
+ ).trim();
2699
+ return SUBSTITUTE_WRITER_PERMISSIONS.has(permission);
2700
+ } catch {
2701
+ return false;
2702
+ }
2703
+ }
2704
+
2705
+ /**
2706
+ * Reads which declared checks a writer's substitute stands in for at a head.
2707
+ *
2708
+ * probe-direction: fail-closed — an unreadable comment list returns an empty
2709
+ * set, which leaves the gate exactly where it would have been without this
2710
+ * read. Nothing here can redden a pull request; it can only fail to green one.
2711
+ *
2712
+ * @param {object} declaration - The per-repo declaration
2713
+ * @param {string|number} pr - The pull request under inspection
2714
+ * @param {string|undefined} repo - `OWNER/NAME`; defaults to the current repository
2715
+ * @param {string|undefined} headSha - The head commit under inspection
2716
+ * @param {{fetchComments?: Function, canWrite?: Function}} [options] - Injection seams
2717
+ * @returns {Set<string>} The names a writer's substitute stands in for
2718
+ */
2719
+ export function readReviewSubstitutes(
2720
+ declaration,
2721
+ pr,
2722
+ repo,
2723
+ headSha,
2724
+ options = {}
2725
+ ) {
2726
+ const names = Object.keys(declaration.evidence_bearing_checks ?? {});
2727
+ if (names.length === 0 || headSha === undefined || headSha === "") {
2728
+ return new Set();
2729
+ }
2730
+ let comments;
2731
+ try {
2732
+ comments = (options.fetchComments ?? fetchIssueComments)(pr, repo);
2733
+ } catch {
2734
+ return new Set();
2735
+ }
2736
+ const permission =
2737
+ options.canWrite ?? (login => fetchWritePermission(login, repo));
2738
+ const seen = new Map();
2739
+ const canWrite = login => {
2740
+ if (login === "") return false;
2741
+ if (!seen.has(login)) seen.set(login, permission(login) === true);
2742
+ return seen.get(login);
2743
+ };
2744
+ return substitutesAtHead(comments, names, headSha, canWrite);
2745
+ }
2746
+
2545
2747
  /**
2546
2748
  * Whether an objection STANDS at this head, from the review objects.
2547
2749
  *
@@ -3001,6 +3203,9 @@ export function reviewGateVerdict(reading = {}) {
3001
3203
  * @returns {string} `<name> reported "<description>"`, or what the wait did
3002
3204
  */
3003
3205
  const quote = name => {
3206
+ if (conditions[name] === REVIEW_GATE_CONDITIONS.substituted) {
3207
+ return `${name} posted NO review status; a writer's ${REVIEW_SUBSTITUTE_MARKER} local adversarial review stands in for it at this head`;
3208
+ }
3004
3209
  if (conditions[name] === REVIEW_GATE_CONDITIONS.undetermined) {
3005
3210
  return `${name} had not settled when the gate's wait EXPIRED — not observed to be unreviewed`;
3006
3211
  }
@@ -3118,7 +3323,14 @@ export function reviewGateVerdict(reading = {}) {
3118
3323
  return {
3119
3324
  verdict: REVIEW_GATE_STATES.satisfied,
3120
3325
  conclusion: REVIEW_VERDICT_CONCLUSIONS.satisfied,
3121
- title: fitTitle(`REVIEWED — ${names.map(quote).join("; ")}${suffix}`),
3326
+ // SUBSTITUTED is not REVIEWED. A self-review standing in for a third-party
3327
+ // one is a weaker claim, and the title is what gets quoted into a merge
3328
+ // disclosure, so it says which one this was.
3329
+ // And it is only "no third-party review" when that is true of EVERY
3330
+ // declared reviewer: a mixed result had a real review alongside the stand-in.
3331
+ title: fitTitle(
3332
+ `${substitutedLead(names, conditions)} — ${names.map(quote).join("; ")}${suffix}`
3333
+ ),
3122
3334
  };
3123
3335
  }
3124
3336
 
@@ -4207,13 +4419,38 @@ export function inspectVacuity(argv, declaration, options = {}) {
4207
4419
  }
4208
4420
 
4209
4421
  const repo = readFlagValue(argv, "--repo");
4210
- let read;
4211
- try {
4212
- read = fetchSettledChecks(declaration, pr, repo, {
4422
+ // Substitutes are read only when the gate is armed, like every other review
4423
+ // read here. Read BEFORE the settle wait as well as after it: when a writer
4424
+ // has already stood in for every declared reviewer, waiting five minutes for
4425
+ // a reviewer that is not coming proves nothing and only delays the green.
4426
+ const readSubstitutes = sha =>
4427
+ requireReviewEvidence
4428
+ ? (options.readSubstitutes ?? readReviewSubstitutes)(
4429
+ declaration,
4430
+ pr,
4431
+ repo,
4432
+ sha,
4433
+ options
4434
+ )
4435
+ : new Set();
4436
+ const declaredReviewers = Object.keys(
4437
+ declaration.evidence_bearing_checks ?? {}
4438
+ );
4439
+ const earlyHead = requireReviewEvidence
4440
+ ? (options.headSha ?? resolveHeadSha)(pr, repo)
4441
+ : undefined;
4442
+ const earlySubstitutes = readSubstitutes(earlyHead);
4443
+ const allStoodIn =
4444
+ declaredReviewers.length > 0 &&
4445
+ declaredReviewers.every(name => earlySubstitutes.has(name));
4446
+ const settle = skipWait =>
4447
+ fetchSettledChecks(declaration, pr, repo, {
4213
4448
  timeoutSeconds: readSecondsFlag(
4214
4449
  argv,
4215
4450
  "--settle-timeout",
4216
- wired || requireReviewEvidence ? SETTLE_TIMEOUT_SECONDS : 0
4451
+ (wired || requireReviewEvidence) && !skipWait
4452
+ ? SETTLE_TIMEOUT_SECONDS
4453
+ : 0
4217
4454
  ),
4218
4455
  intervalSeconds: readSecondsFlag(
4219
4456
  argv,
@@ -4225,6 +4462,22 @@ export function inspectVacuity(argv, declaration, options = {}) {
4225
4462
  sleep: options.sleep,
4226
4463
  headSha: options.headSha,
4227
4464
  });
4465
+ let read;
4466
+ try {
4467
+ read = settle(allStoodIn);
4468
+ // The wait is skipped only for a reviewer that is NOT COMING. A declared
4469
+ // reviewer whose row is already there but still in flight is coming, and
4470
+ // a substitute does not stand in for it — so wait for its answer exactly
4471
+ // as an ordinary run would, rather than going red on one snapshot.
4472
+ if (
4473
+ allStoodIn &&
4474
+ !read.settled &&
4475
+ declaredReviewers.some(name =>
4476
+ read.checks.some(check => check.name === name)
4477
+ )
4478
+ ) {
4479
+ read = settle(false);
4480
+ }
4228
4481
  } catch (error) {
4229
4482
  return refused(vacuityRefusal({ declaration, pr, error }));
4230
4483
  }
@@ -4263,6 +4516,14 @@ export function inspectVacuity(argv, declaration, options = {}) {
4263
4516
  // wait expired", and it is the only place that fact exists — every reader
4264
4517
  // downstream sees a check row that looks identical either way (#3716).
4265
4518
  waitExpired: read.settled === false,
4519
+ // Bound to the head the checks were read at. A push between the early read
4520
+ // and the settled one invalidates every substitute for the old head.
4521
+ // Reused only when it was COMPLETE: a substitute posted while the gate
4522
+ // waited is only seen by reading the comments again.
4523
+ substitutes:
4524
+ allStoodIn && read.headSha !== undefined && read.headSha === earlyHead
4525
+ ? earlySubstitutes
4526
+ : readSubstitutes(read.headSha),
4266
4527
  };
4267
4528
  // TWO PASSES, AND THE SECOND IS PAID FOR ONLY BY A WAIVER (#3706). The review
4268
4529
  // surface is read exactly when a waiver is on the table, which is the only
@@ -4317,7 +4578,16 @@ export function inspectVacuity(argv, declaration, options = {}) {
4317
4578
  headSha: read.headSha,
4318
4579
  checked: evaluated.checked,
4319
4580
  violations: [
4320
- ...evaluated.violations,
4581
+ // "Did not report… NOBODY LOOKED" is false once a writer's substitute
4582
+ // stands in for that exact check at this head, so it is not repeated.
4583
+ ...evaluated.violations.filter(
4584
+ violation =>
4585
+ !(
4586
+ violation.kind === VIOLATIONS.unproven &&
4587
+ gate.conditions[violation.token] ===
4588
+ REVIEW_GATE_CONDITIONS.substituted
4589
+ )
4590
+ ),
4321
4591
  ...absent.violations,
4322
4592
  ...gate.violations,
4323
4593
  ...(carried?.violations ?? []),