dev-loops 0.1.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.
Files changed (156) hide show
  1. package/.pi/dev-loop/defaults.yaml +477 -0
  2. package/AGENTS.md +25 -0
  3. package/CHANGELOG.md +18 -0
  4. package/LICENSE +21 -0
  5. package/README.md +178 -0
  6. package/agents/dev-loop.agent.md +82 -0
  7. package/agents/developer.agent.md +37 -0
  8. package/agents/docs.agent.md +33 -0
  9. package/agents/fixer.agent.md +53 -0
  10. package/agents/quality.agent.md +28 -0
  11. package/agents/refiner.agent.md +87 -0
  12. package/agents/review.agent.md +64 -0
  13. package/cli/index.mjs +424 -0
  14. package/extension/README.md +233 -0
  15. package/extension/checks.ts +94 -0
  16. package/extension/index.ts +131 -0
  17. package/extension/post-merge-update.ts +512 -0
  18. package/extension/presentation.ts +107 -0
  19. package/lib/dev-loops-core.mjs +284 -0
  20. package/package.json +103 -0
  21. package/scripts/README.md +1007 -0
  22. package/scripts/_cli-primitives.mjs +10 -0
  23. package/scripts/_core-helpers.mjs +30 -0
  24. package/scripts/docs/validate-links.mjs +567 -0
  25. package/scripts/docs/validate-no-duplicate-rules.mjs +250 -0
  26. package/scripts/github/_review-thread-mutations.mjs +214 -0
  27. package/scripts/github/capture-review-threads.mjs +180 -0
  28. package/scripts/github/create-draft-pr.mjs +108 -0
  29. package/scripts/github/detect-checkpoint-evidence.mjs +393 -0
  30. package/scripts/github/detect-linked-issue-pr.mjs +331 -0
  31. package/scripts/github/manage-sub-issues.mjs +394 -0
  32. package/scripts/github/probe-copilot-review.mjs +323 -0
  33. package/scripts/github/ready-for-review.mjs +93 -0
  34. package/scripts/github/reconcile-draft-gate.mjs +328 -0
  35. package/scripts/github/reply-resolve-review-thread.mjs +42 -0
  36. package/scripts/github/reply-resolve-review-threads.mjs +329 -0
  37. package/scripts/github/request-copilot-review.mjs +551 -0
  38. package/scripts/github/resolve-tracker-local-spec.mjs +205 -0
  39. package/scripts/github/stage-reviewer-draft.mjs +191 -0
  40. package/scripts/github/upsert-checkpoint-verdict.mjs +694 -0
  41. package/scripts/github/verify-fresh-review-context.mjs +125 -0
  42. package/scripts/github/write-gate-findings-log.mjs +212 -0
  43. package/scripts/loop/_checkpoint-io.mjs +55 -0
  44. package/scripts/loop/_checkpoint-paths.mjs +28 -0
  45. package/scripts/loop/_handoff-contract.mjs +230 -0
  46. package/scripts/loop/_inspect-run-viewer-adapter.mjs +345 -0
  47. package/scripts/loop/_loop-evidence.mjs +32 -0
  48. package/scripts/loop/_pr-runner-coordination.mjs +611 -0
  49. package/scripts/loop/_stale-runner-detection.mjs +145 -0
  50. package/scripts/loop/_steering-state-file.mjs +134 -0
  51. package/scripts/loop/build-handoff-envelope.mjs +181 -0
  52. package/scripts/loop/checkpoint-contract.mjs +49 -0
  53. package/scripts/loop/conductor-monitor.mjs +1850 -0
  54. package/scripts/loop/conductor.mjs +214 -0
  55. package/scripts/loop/copilot-pr-handoff.mjs +493 -0
  56. package/scripts/loop/debt-remediate.mjs +304 -0
  57. package/scripts/loop/detect-change-scope.mjs +102 -0
  58. package/scripts/loop/detect-copilot-loop-state.mjs +454 -0
  59. package/scripts/loop/detect-copilot-session-activity.mjs +186 -0
  60. package/scripts/loop/detect-initial-copilot-pr-state.mjs +318 -0
  61. package/scripts/loop/detect-internal-only-pr.mjs +270 -0
  62. package/scripts/loop/detect-issue-refinement-artifact.mjs +163 -0
  63. package/scripts/loop/detect-pr-gate-coordination-state.mjs +509 -0
  64. package/scripts/loop/detect-reviewer-loop-state.mjs +231 -0
  65. package/scripts/loop/detect-stale-runner.mjs +250 -0
  66. package/scripts/loop/detect-tracker-first-loop-state.mjs +76 -0
  67. package/scripts/loop/detect-tracker-pr-state.mjs +102 -0
  68. package/scripts/loop/info.mjs +267 -0
  69. package/scripts/loop/inspect-run-viewer/cli.mjs +117 -0
  70. package/scripts/loop/inspect-run-viewer/constants.mjs +80 -0
  71. package/scripts/loop/inspect-run-viewer/graph.mjs +757 -0
  72. package/scripts/loop/inspect-run-viewer/handoff-envelope-renderer.mjs +398 -0
  73. package/scripts/loop/inspect-run-viewer/inbox.mjs +308 -0
  74. package/scripts/loop/inspect-run-viewer/managed-instance.mjs +750 -0
  75. package/scripts/loop/inspect-run-viewer/rendering.mjs +411 -0
  76. package/scripts/loop/inspect-run-viewer/server.mjs +638 -0
  77. package/scripts/loop/inspect-run-viewer/shared.mjs +103 -0
  78. package/scripts/loop/inspect-run-viewer/status.mjs +715 -0
  79. package/scripts/loop/inspect-run-viewer-ci-changes.mjs +77 -0
  80. package/scripts/loop/inspect-run-viewer.mjs +82 -0
  81. package/scripts/loop/inspect-run.mjs +382 -0
  82. package/scripts/loop/outer-loop.mjs +419 -0
  83. package/scripts/loop/pr-runner-coordination.mjs +143 -0
  84. package/scripts/loop/pre-commit-branch-guard.mjs +68 -0
  85. package/scripts/loop/pre-flight-gate.mjs +236 -0
  86. package/scripts/loop/pre-pr-ready-gate.mjs +183 -0
  87. package/scripts/loop/pre-push-main-guard.mjs +103 -0
  88. package/scripts/loop/pre-write-remote-freshness-guard.mjs +32 -0
  89. package/scripts/loop/print-gates.mjs +42 -0
  90. package/scripts/loop/resolve-dev-loop-startup.mjs +533 -0
  91. package/scripts/loop/run-conductor-cycle.mjs +322 -0
  92. package/scripts/loop/run-queue.mjs +124 -0
  93. package/scripts/loop/run-refinement-audit.mjs +513 -0
  94. package/scripts/loop/run-watch-cycle.mjs +358 -0
  95. package/scripts/loop/steer-loop.mjs +841 -0
  96. package/scripts/loop/ui-designer-review-contract.mjs +76 -0
  97. package/scripts/loop/watch-initial-copilot-pr.mjs +253 -0
  98. package/scripts/projects/add-queue-item.mjs +528 -0
  99. package/scripts/projects/ensure-queue-board.mjs +837 -0
  100. package/scripts/projects/list-queue-items.mjs +489 -0
  101. package/scripts/projects/move-queue-item.mjs +549 -0
  102. package/scripts/projects/reorder-queue-item.mjs +518 -0
  103. package/scripts/refine/_refine-helpers.mjs +258 -0
  104. package/scripts/refine/prose-linkage-detector.mjs +92 -0
  105. package/scripts/refine/refinement-completeness-checker.mjs +88 -0
  106. package/scripts/refine/scope-boundary-cross-checker.mjs +163 -0
  107. package/scripts/refine/tree-integrity-validator.mjs +211 -0
  108. package/scripts/refine/verify.mjs +178 -0
  109. package/scripts/repo-wiki-local.mjs +156 -0
  110. package/scripts/repo-wiki.mjs +119 -0
  111. package/skills/copilot-pr-followup/SKILL.md +380 -0
  112. package/skills/dev-loop/SKILL.md +141 -0
  113. package/skills/dev-loop/scripts/dev-mode-context.mjs +152 -0
  114. package/skills/dev-loop/scripts/dev-mode-context.test.mjs +80 -0
  115. package/skills/dev-loop/scripts/init-phase.mjs +71 -0
  116. package/skills/dev-loop/scripts/log-bash-exit-1.mjs +25 -0
  117. package/skills/dev-loop/scripts/phase-files.mjs +29 -0
  118. package/skills/dev-loop/scripts/post-gate-verdict-fallback.mjs +480 -0
  119. package/skills/dev-loop/scripts/post-gate-verdict-fallback.test.mjs +732 -0
  120. package/skills/dev-loop/scripts/render-template.mjs +82 -0
  121. package/skills/dev-loop/scripts/render-template.test.mjs +63 -0
  122. package/skills/dev-loop/templates/bootstrap-agents.md +26 -0
  123. package/skills/dev-loop/templates/bootstrap-implementation-state.md +31 -0
  124. package/skills/dev-loop/templates/bootstrap-implementation-workflow.md +17 -0
  125. package/skills/dev-loop/templates/dev-mode-retrospective.md +15 -0
  126. package/skills/dev-loop/templates/dev-mode-review.md +17 -0
  127. package/skills/dev-loop/templates/dev-mode-skill-changes.md +11 -0
  128. package/skills/dev-loop/templates/merged-phase-plan.md +19 -0
  129. package/skills/dev-loop/templates/phase-doc.md +27 -0
  130. package/skills/dev-loop/templates/phase-summary.md +13 -0
  131. package/skills/dev-loop/templates/phase-variant.md +15 -0
  132. package/skills/dev-loop/templates/retrospective.md +11 -0
  133. package/skills/dev-loop/templates/review.md +32 -0
  134. package/skills/dev-loop/templates/ui-vision-review.md +55 -0
  135. package/skills/docs/acceptance-criteria-verification.md +21 -0
  136. package/skills/docs/anti-patterns.md +21 -0
  137. package/skills/docs/artifact-authority-contract.md +119 -0
  138. package/skills/docs/confirmation-rules.md +28 -0
  139. package/skills/docs/copilot-ci-status-contract.md +52 -0
  140. package/skills/docs/copilot-loop-operations.md +233 -0
  141. package/skills/docs/debt-remediation-contract.md +107 -0
  142. package/skills/docs/entrypoint-strategies.md +115 -0
  143. package/skills/docs/epic-tree-refinement-procedure.md +234 -0
  144. package/skills/docs/issue-intake-procedure.md +235 -0
  145. package/skills/docs/main-agent-contract.md +72 -0
  146. package/skills/docs/merge-preconditions.md +29 -0
  147. package/skills/docs/pr-lifecycle-contract.md +209 -0
  148. package/skills/docs/public-dev-loop-contract.md +497 -0
  149. package/skills/docs/retrospective-checkpoint-contract.md +159 -0
  150. package/skills/docs/stop-conditions.md +29 -0
  151. package/skills/docs/structural-quality.md +42 -0
  152. package/skills/docs/tracker-first-loop-state.md +281 -0
  153. package/skills/docs/validation-policy.md +27 -0
  154. package/skills/docs/workflow-handoff-contract.md +135 -0
  155. package/skills/final-approval/SKILL.md +19 -0
  156. package/skills/local-implementation/SKILL.md +640 -0
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import { spawn } from "node:child_process";
4
+ import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
5
+ const USAGE = `Usage: create-draft-pr.mjs [gh pr create args...]
6
+ Thin wrapper around \`gh pr create\` that enforces draft-first PR creation.
7
+ Behavior:
8
+ - injects exactly one \`--draft\` when absent
9
+ - rejects \`--ready\` before invoking \`gh\`
10
+ - detects missing \`Closes #N\` / \`Fixes #N\` in \`--body\` or \`--body-file\` content (non-fatal stderr warning)
11
+ - forwards every other argument to \`gh pr create\` unchanged
12
+ - preserves the underlying \`gh pr create\` stdout, stderr, and exit code
13
+ Examples:
14
+ node scripts/github/create-draft-pr.mjs --repo owner/repo --assignee @me --base main --head feature --title "..." --body-file pr.md
15
+ node <resolved-skill-scripts>/github/create-draft-pr.mjs --repo owner/repo --assignee @me --base main --head feature --title "..." --body-file pr.md
16
+ Notes:
17
+ - Use \`gh pr ready\` later to leave draft state; this wrapper never opens a ready PR.
18
+ - Wrapper-owned validation is limited to \`--ready\`; all other argument validation is left to \`gh pr create\`.
19
+ - Closing-keyword warning is advisory only and does not change exit code.
20
+ Exit codes:
21
+ 0 \`gh pr create\` succeeded
22
+ 1 wrapper validation failed or \`gh\` could not be spawned
23
+ N same non-zero exit code returned by \`gh pr create\``.trim();
24
+ const parseError = buildParseError(USAGE);
25
+ const READY_FLAG_PATTERN = /^--ready(?:$|=)/u;
26
+ const DRAFT_FLAG_PATTERN = /^--draft(?:=(.*))?$/iu;
27
+ const DRAFT_TRUE_VALUE_PATTERN = /^(?:true|1)$/iu;
28
+ const CLOSING_KEYWORD_PATTERN = /Closes\s+#\d+|Fixes\s+#\d+/i;
29
+ const MAX_BODY_SCAN_BYTES = 16 * 1024;
30
+ export function detectClosingKeyword(body) {
31
+ if (!body || typeof body !== "string") return false;
32
+ return CLOSING_KEYWORD_PATTERN.test(body.slice(0, MAX_BODY_SCAN_BYTES));
33
+ }
34
+ async function resolveBody(args) {
35
+ const bodyIdx = args.indexOf("--body");
36
+ if (bodyIdx !== -1 && bodyIdx + 1 < args.length) {
37
+ return args[bodyIdx + 1];
38
+ }
39
+ const bodyFileIdx = args.indexOf("--body-file");
40
+ if (bodyFileIdx !== -1 && bodyFileIdx + 1 < args.length) {
41
+ try {
42
+ const content = await readFile(args[bodyFileIdx + 1], "utf8");
43
+ return content;
44
+ } catch {
45
+ return "";
46
+ }
47
+ }
48
+ return null; // unreadable → warn
49
+ }
50
+ async function warnMissingClosingKeyword(args) {
51
+ const body = await resolveBody(args);
52
+ if (body === null) return; // no --body or --body-file, skip
53
+ if (!detectClosingKeyword(body)) {
54
+ process.stderr.write(
55
+ "[create-draft-pr] Warning: PR body missing `Closes #N` or `Fixes #N`. " +
56
+ "GitHub will not auto-close the linked issue on merge.\n",
57
+ );
58
+ }
59
+ }
60
+ export function buildCreateDraftPrArgs(argv) {
61
+ const args = [...argv];
62
+ if (args.includes("--help") || args.includes("-h")) {
63
+ return {
64
+ help: true,
65
+ ghArgs: null,
66
+ };
67
+ }
68
+ if (args.some((token) => READY_FLAG_PATTERN.test(token))) {
69
+ throw parseError("create-draft-pr rejects --ready; open the PR as draft first, then run `gh pr ready` after the draft gate is satisfied");
70
+ }
71
+ const draftTokens = args.filter((token) => DRAFT_FLAG_PATTERN.test(token));
72
+ const lastDraftToken = draftTokens.length > 0 ? draftTokens.at(-1) : null;
73
+ const lastDraftSuppliesDraft = lastDraftToken === "--draft" || (typeof lastDraftToken === "string" && DRAFT_TRUE_VALUE_PATTERN.test(lastDraftToken.slice("--draft=".length)));
74
+ return {
75
+ help: false,
76
+ ghArgs: ["pr", "create", ...args, ...(lastDraftSuppliesDraft ? [] : ["--draft"])],
77
+ };
78
+ }
79
+ export function spawnCreateDraftPr(ghArgs, { ghCommand = "gh", env = process.env } = {}) {
80
+ return new Promise((resolve, reject) => {
81
+ const child = spawn(ghCommand, ghArgs, {
82
+ env,
83
+ stdio: "inherit",
84
+ });
85
+ child.on("error", reject);
86
+ child.on("close", (code) => {
87
+ resolve(typeof code === "number" ? code : 1);
88
+ });
89
+ });
90
+ }
91
+ export async function main(argv = process.argv.slice(2), runtime = {}) {
92
+ const { help, ghArgs } = buildCreateDraftPrArgs(argv);
93
+ if (help) {
94
+ process.stdout.write(`${USAGE}\n`);
95
+ return 0;
96
+ }
97
+ await warnMissingClosingKeyword(argv);
98
+ return spawnCreateDraftPr(ghArgs, runtime);
99
+ }
100
+ if (isDirectCliRun(import.meta.url)) {
101
+ try {
102
+ const exitCode = await main();
103
+ process.exitCode = exitCode;
104
+ } catch (error) {
105
+ process.stderr.write(`${formatCliError(error)}\n`);
106
+ process.exitCode = 1;
107
+ }
108
+ }
@@ -0,0 +1,393 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildParseError,
4
+ formatCliError,
5
+ isDirectCliRun,
6
+ parseJsonText,
7
+ parseReviewThreads,
8
+ summarizeGateReviewCommentMarkers,
9
+ summarizeGateReviewComments,
10
+ } from "../_core-helpers.mjs";
11
+ import { parsePrNumber, requireOptionValue, runChild } from "../_cli-primitives.mjs";
12
+ import { fetchGithubReviewThreadsPayload } from "./capture-review-threads.mjs";
13
+ import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
14
+ import { ensureAsyncRunnerOwnership } from "../loop/_pr-runner-coordination.mjs";
15
+ import { detectStaleRunner } from "../loop/_stale-runner-detection.mjs";
16
+ const USAGE = `Usage: detect-checkpoint-evidence.mjs --repo <owner/name> --pr <number>
17
+ Fetch the live PR head SHA and visible PR issue comments, then summarize the
18
+ latest valid draft-gate and pre-approval checkpoint verdict comments. Always fail
19
+ closed (exit 1) unless both required gate comments exist: a clean draft_gate
20
+ comment for the one-time draft boundary and a clean current-head
21
+ pre_approval_gate comment.
22
+ Required:
23
+ --repo <owner/name> Repository slug (e.g. owner/repo)
24
+ --pr <number> Pull request number
25
+ Output (stdout, JSON; always includes preMergeGateCheck):
26
+ {
27
+ "ok": true,
28
+ "repo": "owner/repo",
29
+ "pr": 17,
30
+ "currentHeadSha": "abc1234",
31
+ "draftGate": {
32
+ "visible": true,
33
+ "headSha": "abc1234",
34
+ "verdict": "clean",
35
+ "findingsSummary": "no issues found",
36
+ "nextAction": "mark ready for review",
37
+ "commentId": 101,
38
+ "commentUrl": "https://github.com/owner/repo/pull/17#issuecomment-101",
39
+ "updatedAt": "2026-05-29T22:00:00Z"
40
+ },
41
+ "draftGateMarker": {
42
+ "visible": true,
43
+ "headSha": "abc1234",
44
+ "verdict": "clean",
45
+ "findingsSummary": "no issues found",
46
+ "nextAction": "mark ready for review",
47
+ "contractComplete": true,
48
+ "commentId": 101,
49
+ "commentUrl": "https://github.com/owner/repo/pull/17#issuecomment-101",
50
+ "updatedAt": "2026-05-29T22:00:00Z"
51
+ },
52
+ "draftGateSatisfied": true,
53
+ "preApprovalGate": {
54
+ "visible": true,
55
+ "headSha": "abc1234",
56
+ "verdict": "clean",
57
+ "findingsSummary": "no issues found",
58
+ "nextAction": "await final human approval",
59
+ "commentId": 102,
60
+ "commentUrl": "https://github.com/owner/repo/pull/17#issuecomment-102",
61
+ "updatedAt": "2026-05-29T22:00:00Z"
62
+ },
63
+ "preApprovalGateMarker": {
64
+ "visible": true,
65
+ "headSha": "abc1234",
66
+ "verdict": "clean",
67
+ "findingsSummary": "no issues found",
68
+ "nextAction": "await final human approval",
69
+ "contractComplete": true,
70
+ "commentId": 102,
71
+ "commentUrl": "https://github.com/owner/repo/pull/17#issuecomment-102",
72
+ "updatedAt": "2026-05-29T22:00:00Z"
73
+ },
74
+ "preMergeGateCheck": {
75
+ "ok": true,
76
+ "failures": []
77
+ }
78
+ }
79
+ Error output (stderr, JSON):
80
+ { "ok": false, "error": "...", "usage": "..." }
81
+ { "ok": false, "error": "..." }
82
+ Exit codes:
83
+ 0 Success (gate evidence is valid)
84
+ 1 Argument error, gh failure, malformed gh JSON, or missing required pre-merge gate evidence.`.trim();
85
+ const parseError = buildParseError(USAGE);
86
+ export function parseDetectCheckpointEvidenceCliArgs(argv) {
87
+ const args = [...argv];
88
+ const options = {
89
+ help: false,
90
+ repo: undefined,
91
+ pr: undefined,
92
+ };
93
+ while (args.length > 0) {
94
+ const token = args.shift();
95
+ if (token === "--help" || token === "-h") {
96
+ options.help = true;
97
+ return options;
98
+ }
99
+ if (token === "--repo") {
100
+ options.repo = requireOptionValue(args, "--repo", parseError).trim();
101
+ continue;
102
+ }
103
+ if (token === "--pr") {
104
+ options.pr = parsePrNumber(requireOptionValue(args, "--pr", parseError), parseError);
105
+ continue;
106
+ }
107
+ if (token === "--require-before-merge") {
108
+ throw parseError(`--require-before-merge has been removed: gate evidence enforcement is now always-on by default. Omit the flag.`);
109
+ }
110
+ throw parseError(`Unknown argument: ${token}`);
111
+ }
112
+ if (options.repo === undefined || options.pr === undefined) {
113
+ throw parseError("detect-checkpoint-evidence requires both --repo <owner/name> and --pr <number>");
114
+ }
115
+ try {
116
+ parseRepoSlug(options.repo);
117
+ } catch (error) {
118
+ throw parseError(error instanceof Error ? error.message : String(error));
119
+ }
120
+ return options;
121
+ }
122
+ async function runGhJson(args, { env, ghCommand }) {
123
+ const result = await runChild(ghCommand, args, env);
124
+ if (result.code !== 0) {
125
+ const detail = result.stderr.trim() || `exit code ${result.code}`;
126
+ throw new Error(`gh command failed: ${detail}`);
127
+ }
128
+ return parseJsonText(result.stdout, { label: `gh ${args.slice(0, 2).join(" ")}` });
129
+ }
130
+ function normalizeIssueCommentsPayload(payload) {
131
+ if (!Array.isArray(payload)) {
132
+ throw new Error("Invalid gh issue comments payload: expected an array");
133
+ }
134
+ if (payload.every((entry) => Array.isArray(entry))) {
135
+ return payload.flat();
136
+ }
137
+ return payload;
138
+ }
139
+ function normalizePrReviewsPayload(payload) {
140
+ if (!Array.isArray(payload)) return [];
141
+ const flat = payload.every((entry) => Array.isArray(entry)) ? payload.flat() : payload;
142
+ return flat
143
+ .filter((r) => r && typeof r === "object" && r.state !== "PENDING" && typeof r.submitted_at === "string" && r.submitted_at.trim().length > 0 && typeof r.body === "string" && r.body.trim().length > 0)
144
+ .map((r) => ({
145
+ id: r.id,
146
+ body: r.body,
147
+ html_url: typeof r.html_url === "string" ? r.html_url : null,
148
+ created_at: typeof r.submitted_at === "string" ? r.submitted_at : null,
149
+ updated_at: typeof r.submitted_at === "string" ? r.submitted_at : null,
150
+ }));
151
+ }
152
+ function emptyGateSummary() {
153
+ return {
154
+ visible: false,
155
+ headSha: null,
156
+ verdict: null,
157
+ findingsSummary: null,
158
+ nextAction: null,
159
+ commentId: null,
160
+ commentUrl: null,
161
+ updatedAt: null,
162
+ };
163
+ }
164
+ function normalizeGateSummary(summary) {
165
+ if (!summary) {
166
+ return emptyGateSummary();
167
+ }
168
+ return {
169
+ visible: true,
170
+ headSha: summary.headSha,
171
+ verdict: summary.verdict,
172
+ findingsSummary: summary.findingsSummary,
173
+ nextAction: summary.nextAction,
174
+ commentId: summary.commentId,
175
+ commentUrl: summary.commentUrl,
176
+ updatedAt: summary.updatedAt,
177
+ };
178
+ }
179
+ function emptyGateMarkerSummary() {
180
+ return {
181
+ visible: false,
182
+ headSha: null,
183
+ verdict: null,
184
+ findingsSummary: null,
185
+ nextAction: null,
186
+ contractComplete: false,
187
+ commentId: null,
188
+ commentUrl: null,
189
+ updatedAt: null,
190
+ };
191
+ }
192
+ function normalizeGateMarkerSummary(summary) {
193
+ if (!summary) {
194
+ return emptyGateMarkerSummary();
195
+ }
196
+ return {
197
+ visible: true,
198
+ headSha: summary.headSha,
199
+ verdict: summary.verdict,
200
+ findingsSummary: summary.findingsSummary,
201
+ nextAction: summary.nextAction,
202
+ contractComplete: summary.contractComplete === true,
203
+ commentId: summary.commentId,
204
+ commentUrl: summary.commentUrl,
205
+ updatedAt: summary.updatedAt,
206
+ };
207
+ }
208
+ export function buildPreMergeGateCheck(evidence, unresolvedThreadCount = null, staleRunnerCheck = null) {
209
+ const failures = [];
210
+ if (!(evidence.draftGate.visible && evidence.draftGate.verdict === "clean")) {
211
+ failures.push("missing visible clean draft_gate comment");
212
+ }
213
+ const preApproval = evidence.preApprovalGateMarker;
214
+ if (!(
215
+ preApproval.visible
216
+ && preApproval.contractComplete
217
+ && preApproval.verdict === "clean"
218
+ && preApproval.headSha === evidence.currentHeadSha
219
+ )) {
220
+ failures.push("missing visible clean current-head pre_approval_gate comment");
221
+ }
222
+ if (typeof unresolvedThreadCount === "number" && unresolvedThreadCount !== 0) {
223
+ if (unresolvedThreadCount === -1) {
224
+ failures.push("could not fetch review thread state from GitHub API; re-run gate evidence check when API connectivity is restored");
225
+ } else {
226
+ failures.push(`unresolved review threads present (${unresolvedThreadCount}); must resolve all threads before merge`);
227
+ }
228
+ }
229
+ if (staleRunnerCheck && !staleRunnerCheck.ok) {
230
+ for (const failure of staleRunnerCheck.failures) {
231
+ failures.push(failure);
232
+ }
233
+ }
234
+ return {
235
+ ok: failures.length === 0,
236
+ failures,
237
+ };
238
+ }
239
+ export async function detectCheckpointEvidence(options, { env = process.env, ghCommand = "gh", cwd = process.cwd() } = {}) {
240
+ const runnerOwnership = await ensureAsyncRunnerOwnership({
241
+ repo: options.repo,
242
+ pr: options.pr,
243
+ env,
244
+ cwd,
245
+ claimIfMissing: false,
246
+ requireExisting: false,
247
+ });
248
+ if (!runnerOwnership.ok) {
249
+ const error = new Error(runnerOwnership.message);
250
+ error.runnerOwnership = runnerOwnership;
251
+ throw error;
252
+ }
253
+ const staleRunnerDetection = await detectStaleRunner({
254
+ repo: options.repo,
255
+ pr: options.pr,
256
+ cwd,
257
+ });
258
+ if (!staleRunnerDetection.ok) {
259
+ const error = new Error(staleRunnerDetection.message);
260
+ error.staleRunner = staleRunnerDetection;
261
+ throw error;
262
+ }
263
+ const prPayload = await runGhJson(["pr", "view", String(options.pr), "--repo", options.repo, "--json", "headRefOid"], { env, ghCommand });
264
+ const commentsPayload = normalizeIssueCommentsPayload(await runGhJson(["api", "--paginate", "--slurp", `repos/${options.repo}/issues/${options.pr}/comments?per_page=100`], { env, ghCommand }));
265
+ const currentHeadSha = typeof prPayload?.headRefOid === "string" && prPayload.headRefOid.trim().length > 0
266
+ ? prPayload.headRefOid.trim()
267
+ : null;
268
+ if (!currentHeadSha) {
269
+ throw new Error("Invalid gh pr view payload: missing headRefOid");
270
+ }
271
+ // Also scan PR reviews for gate comments posted via the review API.
272
+ // This prevents duplicates when an escape-hatch path posted a gate verdict
273
+ // as a PR review rather than an issue comment (root cause 3 from issue #692).
274
+ let prReviews = [];
275
+ try {
276
+ const reviewsRaw = await runGhJson(["api", "--paginate", "--slurp", `repos/${options.repo}/pulls/${options.pr}/reviews?per_page=100`], { env, ghCommand });
277
+ prReviews = normalizePrReviewsPayload(reviewsRaw);
278
+ } catch {
279
+ // Graceful fallback: PR reviews fetch failure is non-fatal.
280
+ // We continue with issue comments only.
281
+ }
282
+ const allComments = [...commentsPayload, ...prReviews];
283
+ const commentSummary = summarizeGateReviewComments(allComments);
284
+ const markerSummary = summarizeGateReviewCommentMarkers(allComments, { headSha: currentHeadSha });
285
+ return {
286
+ ok: true,
287
+ repo: options.repo,
288
+ pr: options.pr,
289
+ currentHeadSha,
290
+ draftGate: normalizeGateSummary(commentSummary.draft_gate),
291
+ preApprovalGate: normalizeGateSummary(commentSummary.pre_approval_gate),
292
+ draftGateMarker: normalizeGateMarkerSummary(markerSummary.draft_gate),
293
+ preApprovalGateMarker: normalizeGateMarkerSummary(markerSummary.pre_approval_gate),
294
+ draftGateSatisfied: commentSummary.draft_gate?.verdict === "clean" && typeof commentSummary.draft_gate?.headSha === "string",
295
+ ...(runnerOwnership.status !== "skipped_no_async_run_id" ? { runnerOwnership } : {}),
296
+ staleRunner: {
297
+ status: staleRunnerDetection.status,
298
+ activeRun: staleRunnerDetection.activeRun,
299
+ exitSignals: staleRunnerDetection.exitSignal?.signals ?? [],
300
+ staleRunner: staleRunnerDetection.staleRunner,
301
+ maxAgeMs: staleRunnerDetection.maxAgeMs,
302
+ filePath: staleRunnerDetection.filePath,
303
+ },
304
+ };
305
+ }
306
+ async function main() {
307
+ let options;
308
+ try {
309
+ options = parseDetectCheckpointEvidenceCliArgs(process.argv.slice(2));
310
+ } catch (error) {
311
+ process.stderr.write(`${formatCliError(error, { usage: USAGE })}\n`);
312
+ process.exitCode = 1;
313
+ return;
314
+ }
315
+ if (options.help) {
316
+ process.stdout.write(`${USAGE}\n`);
317
+ return;
318
+ }
319
+ try {
320
+ const result = await detectCheckpointEvidence(options);
321
+ let unresolvedThreadCount = -1;
322
+ try {
323
+ const threadsPayload = await fetchGithubReviewThreadsPayload(options, { env: process.env });
324
+ const parsedThreads = parseReviewThreads(threadsPayload);
325
+ unresolvedThreadCount = parsedThreads?.summary?.unresolvedThreads ?? 0;
326
+ } catch {
327
+ unresolvedThreadCount = -1;
328
+ }
329
+ const staleRunnerCheck = {
330
+ ok: result.staleRunner.status === "fresh_runner" || result.staleRunner.status === "no_owner_record",
331
+ failures: result.staleRunner.status === "stale_runner"
332
+ ? [`stale runner: ${result.staleRunner.staleRunner?.runId} claimed ${result.staleRunner.staleRunner?.claimedAgeMs}ms ago, last updated ${result.staleRunner.staleRunner?.updatedAgeMs}ms ago (max age ${result.staleRunner.staleRunner?.maxAgeMs}ms)`]
333
+ : result.staleRunner.status === "exit_signal_recorded"
334
+ ? [`exit signal recorded for run ${result.staleRunner.activeRun?.runId}: refuse to merge`]
335
+ : [],
336
+ };
337
+ const preMergeGateCheck = buildPreMergeGateCheck(result, unresolvedThreadCount, staleRunnerCheck);
338
+ const output = { ...result, preMergeGateCheck, staleRunnerCheck };
339
+ if (!preMergeGateCheck.ok) {
340
+ process.stderr.write(`${JSON.stringify({
341
+ ok: false,
342
+ error: `Pre-merge gate evidence check failed: ${preMergeGateCheck.failures.join("; ")}`,
343
+ repo: result.repo,
344
+ pr: result.pr,
345
+ currentHeadSha: result.currentHeadSha,
346
+ preMergeGateCheck,
347
+ staleRunnerCheck,
348
+ staleRunner: result.staleRunner,
349
+ })}\n`);
350
+ process.exitCode = 1;
351
+ return;
352
+ }
353
+ process.stdout.write(`${JSON.stringify(output)}\n`);
354
+ } catch (error) {
355
+ if (error && typeof error === "object" && "staleRunner" in error && error.staleRunner) {
356
+ const staleRunnerCheck = {
357
+ ok: false,
358
+ failures: error.staleRunner.status === "stale_runner"
359
+ ? [`stale runner: ${error.staleRunner.staleRunner?.runId} claimed ${error.staleRunner.staleRunner?.claimedAgeMs}ms ago, last updated ${error.staleRunner.staleRunner?.updatedAgeMs}ms ago (max age ${error.staleRunner.staleRunner?.maxAgeMs}ms)`]
360
+ : error.staleRunner.status === "exit_signal_recorded"
361
+ ? [`exit signal recorded for run ${error.staleRunner.activeRun?.runId}: refuse to merge`]
362
+ : [],
363
+ };
364
+ process.stderr.write(`${JSON.stringify({
365
+ ok: false,
366
+ error: error.staleRunner.error,
367
+ status: error.staleRunner.status,
368
+ message: error.staleRunner.message,
369
+ staleRunner: {
370
+ status: error.staleRunner.status,
371
+ activeRun: error.staleRunner.activeRun,
372
+ exitSignals: error.staleRunner.exitSignal?.signals ?? [],
373
+ staleRunner: error.staleRunner.staleRunner,
374
+ maxAgeMs: error.staleRunner.maxAgeMs,
375
+ filePath: error.staleRunner.filePath,
376
+ },
377
+ staleRunnerCheck,
378
+ })}\n`);
379
+ process.exitCode = 1;
380
+ return;
381
+ }
382
+ if (error && typeof error === "object" && "runnerOwnership" in error && error.runnerOwnership) {
383
+ process.stderr.write(`${JSON.stringify(error.runnerOwnership)}\n`);
384
+ process.exitCode = 1;
385
+ return;
386
+ }
387
+ process.stderr.write(`${JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) })}\n`);
388
+ process.exitCode = 1;
389
+ }
390
+ }
391
+ if (isDirectCliRun(import.meta.url)) {
392
+ await main();
393
+ }