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,493 @@
1
+ #!/usr/bin/env node
2
+ import { buildParseError, formatCliError, isCopilotLogin, isDirectCliRun, normalizeTimestamp } from "../_core-helpers.mjs";
3
+ import { parsePrNumber, requireOptionValue, runChild } from "../_cli-primitives.mjs";
4
+ import { detectRepoSlug, parseRepoSlug } from "@dev-loops/core/github/repo-slug";
5
+ import path from "node:path";
6
+ import { loadDevLoopConfig, resolveRefinement } from "@dev-loops/core/config";
7
+ import { autoDetectSnapshot } from "./detect-copilot-loop-state.mjs";
8
+ import { performCopilotReviewRequest } from "../github/request-copilot-review.mjs";
9
+ import { detectInternalOnly as detectPrInternalOnly } from "./detect-internal-only-pr.mjs";
10
+ import { applyConfirmedReviewRequest, interpretLoopState, NEXT_ACTIONS, STATE, summarizeLoopInterpretation, TRANSITIONS } from "@dev-loops/core/loop/copilot-loop-state";
11
+ import { ensureAsyncRunnerOwnership } from "./_pr-runner-coordination.mjs";
12
+
13
+
14
+ import {
15
+ EXTERNAL_HEALTHY_WAIT_TIMEOUT_POLICY,
16
+ enforceExternalHealthyWaitTimeout,
17
+ } from "@dev-loops/core/loop/timeout-policy";
18
+ import {
19
+ DEFAULT_POLL_INTERVAL_MS,
20
+ COPILOT_REVIEW_WAIT_TIMEOUT_MS,
21
+ } from "@dev-loops/core/loop/policy-constants";
22
+ const VALID_WATCH_STATUSES = new Set(["changed", "timeout", "idle"]);
23
+ const REMOVED_FLAGS = new Set([
24
+ "--force-rerequest-review",
25
+ ]);
26
+ const USAGE = `Usage: copilot-pr-handoff.mjs --pr <number> [--repo <owner/name>] [--watch-status <changed|timeout|idle>]
27
+ Detect the Copilot-loop state for a PR, request Copilot review only when
28
+ a new request is still needed, and emit the recommended next action with
29
+ exact parameters.
30
+ Required:
31
+ --pr <number> Pull request number
32
+ Optional:
33
+ --repo <owner/name> Repository slug (e.g. owner/repo). Auto-detected from git remote when omitted.
34
+ --watch-status <status> Refresh deterministic loop state after a prior
35
+ watcher result (changed|timeout|idle). This mode
36
+ never requests review; it only re-detects state.
37
+ Output (stdout, JSON):
38
+ { "ok": true, "action": "watch"|"fix"|"stop", "state": "...",
39
+ "allowedTransitions": [...], "nextAction": "...", "snapshot": {...},
40
+ "reviewRequestStatus"?: "...", "watchStatus"?: "...",
41
+ "autoRerequestEligible": true|false, "sameHeadCleanConverged": true|false,
42
+ "roundCapCleanEligible": true|false, "loopDisposition": "...", "terminal": true|false,
43
+ "requestWatchContract": {
44
+ "action": "watch"|"fix"|"stop",
45
+ "nextAction": "...",
46
+ "requestStatus": "requested"|"already-requested"|"unavailable"|"failed"|"none",
47
+ "routingState": "copilot_request_confirmed_waiting"|"ready_state_needs_copilot_request"|"draft_reset_requires_ready_state_reentry"|"non_ready_state",
48
+ "watchEntryConfirmed": true|false,
49
+ "watchArgs": { ... }|null,
50
+ "stopState"?: "unavailable"|"blocked"|"draft_requires_ready_state_reentry"|"no_automatic_next_step"
51
+ },
52
+ "watchTimeoutPolicy"?: { "classification": "...", "minimumTimeoutMs": N, "defaultTimeoutMs": N },
53
+ "watchArgs"?: { "repo": "...", "pr": N, "pollIntervalMs": N, "timeoutMs": N } }
54
+ Actions:
55
+ watch Copilot review was requested; use watchArgs with probe-copilot-review.mjs
56
+ fix Unresolved feedback exists; address it before re-requesting review
57
+ stop No automatic next step; report the current state (terminal, blocked, or operator-decision-required) and do not proceed
58
+ Watch refresh rule:
59
+ watcher timeout/idle is observational only. Re-run this helper with
60
+ --watch-status and stop only when terminal=true. Pending or unresolved
61
+ states remain non-terminal even after a timeout.
62
+ Watch defaults:
63
+ pollIntervalMs 60000 (1 minute)
64
+ timeoutMs 1800000 (30 minutes)
65
+ Error output (stderr, JSON):
66
+ Argument/usage errors:
67
+ { "ok": false, "error": "...", "usage": "..." }
68
+ gh/runtime failures:
69
+ { "ok": false, "error": "..." }
70
+ Exit codes:
71
+ 0 Success
72
+ 1 Argument error or gh failure`.trim();
73
+ const WATCH_STATES = new Set([
74
+ STATE.WAITING_FOR_COPILOT_REVIEW,
75
+ ]);
76
+ const FIX_STATES = new Set([
77
+ STATE.UNRESOLVED_FEEDBACK_PRESENT,
78
+ STATE.ALREADY_FIXED_NEEDS_REPLY_RESOLVE,
79
+ STATE.INTERNAL_TOOLING_DIRECT_GATE,
80
+ ]);
81
+ function summarizeRequestWatchContract({
82
+ interpretation,
83
+ action,
84
+ requestStatus,
85
+ watchArgs,
86
+ }) {
87
+ let routingState = "non_ready_state";
88
+ if (action === "watch" && (requestStatus === "requested" || requestStatus === "already-requested")) {
89
+ routingState = "copilot_request_confirmed_waiting";
90
+ } else if (interpretation.state === STATE.PR_DRAFT) {
91
+ routingState = "draft_reset_requires_ready_state_reentry";
92
+ } else if (
93
+ interpretation.state === STATE.PR_READY_NO_FEEDBACK
94
+ || interpretation.state === STATE.READY_TO_REREQUEST_REVIEW
95
+ && interpretation.sameHeadCleanConverged !== true
96
+ ) {
97
+ routingState = "ready_state_needs_copilot_request";
98
+ } else if (interpretation.state === STATE.INTERNAL_TOOLING_DIRECT_GATE) {
99
+ routingState = "internal_tooling_skip_copilot";
100
+ }
101
+ let stopState;
102
+ if (action === "stop") {
103
+ if (interpretation.state === STATE.REVIEW_REQUEST_UNAVAILABLE) {
104
+ stopState = "unavailable";
105
+ } else if (interpretation.state === STATE.BLOCKED_NEEDS_USER_DECISION) {
106
+ stopState = "blocked";
107
+ } else if (interpretation.state === STATE.PR_DRAFT) {
108
+ stopState = "draft_requires_ready_state_reentry";
109
+ } else {
110
+ stopState = "no_automatic_next_step";
111
+ }
112
+ }
113
+ return {
114
+ action,
115
+ nextAction: interpretation.nextAction,
116
+ requestStatus,
117
+ routingState,
118
+ watchEntryConfirmed: action === "watch" && watchArgs !== undefined,
119
+ watchArgs: watchArgs ?? null,
120
+ stopState,
121
+ };
122
+ }
123
+ const parseError = buildParseError(USAGE);
124
+ function rejectRemovedFlag(token) {
125
+ throw parseError(
126
+ `${token} has been removed. Copilot re-requests are managed internally. Omit the flag.`,
127
+ );
128
+ }
129
+ export function parseHandoffCliArgs(argv, { cwd = process.cwd() } = {}) {
130
+ const args = [...argv];
131
+ const options = {
132
+ help: false,
133
+ repo: undefined,
134
+ pr: undefined,
135
+ watchStatus: undefined,
136
+ };
137
+ while (args.length > 0) {
138
+ const token = args.shift();
139
+ if (token === "--help" || token === "-h") {
140
+ options.help = true;
141
+ return options;
142
+ }
143
+ if (REMOVED_FLAGS.has(token)) {
144
+ rejectRemovedFlag(token);
145
+ }
146
+ if (token === "--repo") {
147
+ options.repo = requireOptionValue(args, "--repo", parseError).trim();
148
+ continue;
149
+ }
150
+ if (token === "--pr") {
151
+ options.pr = parsePrNumber(requireOptionValue(args, "--pr", parseError), parseError);
152
+ continue;
153
+ }
154
+ if (token === "--watch-status") {
155
+ const watchStatus = requireOptionValue(args, "--watch-status", parseError).trim().toLowerCase();
156
+ if (!VALID_WATCH_STATUSES.has(watchStatus)) {
157
+ throw parseError(`--watch-status must be one of: ${[...VALID_WATCH_STATUSES].join(", ")}`);
158
+ }
159
+ options.watchStatus = watchStatus;
160
+ continue;
161
+ }
162
+ throw parseError(`Unknown argument: ${token}`);
163
+ }
164
+ if (options.pr === undefined) {
165
+ throw parseError("copilot-pr-handoff requires --pr <number>");
166
+ }
167
+ if (options.repo === undefined) {
168
+ options.repo = detectRepoSlug(cwd);
169
+ if (!options.repo) {
170
+ throw parseError(
171
+ "Repo auto-detection failed. " +
172
+ "Run from a git repo checkout or provide --repo <owner/name>."
173
+ );
174
+ }
175
+ }
176
+ try {
177
+ parseRepoSlug(options.repo);
178
+ } catch (error) {
179
+ throw parseError(error instanceof Error ? error.message : String(error));
180
+ }
181
+ return options;
182
+ }
183
+ /**
184
+ * Detect recent human (non-bot) comments on a PR since the last subagent action.
185
+ * Determines "last subagent action" by finding the most recent bot/Copilot comment
186
+ * on the PR issue comments. If a human comment exists after that timestamp, the
187
+ * loop should pause for operator review.
188
+ * Returns { paused: true, humanComments: [...] } when human comments need attention.
189
+ */
190
+ export async function detectRecentHumanComments({ repo, pr, claimedAtMs }, { env = process.env, ghCommand = "gh" } = {}) {
191
+ try {
192
+ const result = await runChild(
193
+ ghCommand,
194
+ ["api", `repos/${repo}/issues/${pr}/comments`, "--paginate", "--jq", ".[]"],
195
+ env,
196
+ );
197
+ if (result.code !== 0) {
198
+ return { paused: false, error: "comment_fetch_failed", detail: "Failed to fetch PR comments; human comment detection unavailable." };
199
+ }
200
+ const lines = result.stdout.trim().split("\n").filter(Boolean);
201
+ if (lines.length === 0) {
202
+ return { paused: false };
203
+ }
204
+ let comments;
205
+ try {
206
+ comments = lines.map((line) => JSON.parse(line));
207
+ } catch {
208
+ return { paused: false, error: "comment_parse_failed", detail: "Failed to parse PR comments JSON; human comment detection unavailable." };
209
+ }
210
+ if (!Array.isArray(comments)) {
211
+ return { paused: false };
212
+ }
213
+
214
+ // Find the most recent bot/Copilot comment timestamp (last subagent action)
215
+ let lastBotActionMs = null;
216
+ for (const comment of comments) {
217
+ const authorLogin = comment?.user?.login ?? "";
218
+ if (!isCopilotLogin(authorLogin)) {
219
+ continue;
220
+ }
221
+ const createdAt = normalizeTimestamp(comment?.created_at);
222
+ if (createdAt !== null && (lastBotActionMs === null || createdAt > lastBotActionMs)) {
223
+ lastBotActionMs = createdAt;
224
+ }
225
+ }
226
+
227
+ // If no Copilot comments found, use the run's claim time as baseline when available
228
+ if (lastBotActionMs === null) {
229
+ if (typeof claimedAtMs === "number" && claimedAtMs > 0) {
230
+ lastBotActionMs = claimedAtMs;
231
+ } else {
232
+ return { paused: false };
233
+ }
234
+ }
235
+
236
+ // Find human comments after the last bot action
237
+ const humanComments = [];
238
+ for (const comment of comments) {
239
+ const authorLogin = comment?.user?.login ?? "";
240
+ const authorType = comment?.user?.type ?? "";
241
+ // Skip bot authors (GitHub type "Bot") and Copilot
242
+ if (authorType === "Bot" || isCopilotLogin(authorLogin)) {
243
+ continue;
244
+ }
245
+ // Skip if comment body is a gate verdict comment (system action, not operator input)
246
+ const body = typeof comment?.body === "string" ? comment.body : "";
247
+ if (body.includes("Gate review:") || body.includes("**draft_gate**") || body.includes("**pre_approval_gate**")) {
248
+ continue;
249
+ }
250
+ const createdAt = normalizeTimestamp(comment?.created_at);
251
+ if (createdAt !== null && createdAt > lastBotActionMs) {
252
+ humanComments.push({
253
+ id: comment.id,
254
+ author: authorLogin,
255
+ createdAt: comment.created_at,
256
+ bodyPreview: body.slice(0, 200),
257
+ });
258
+ }
259
+ }
260
+
261
+ return {
262
+ paused: humanComments.length > 0,
263
+ humanComments: humanComments.length > 0 ? humanComments : undefined,
264
+ lastBotCommentAt: lastBotActionMs !== null ? new Date(lastBotActionMs).toISOString() : undefined,
265
+ };
266
+ } catch {
267
+ return { paused: false, error: "unexpected_error", detail: "Unexpected error during human comment detection." };
268
+ }
269
+ }
270
+
271
+ export async function runHandoff(options, { env = process.env, ghCommand = "gh" } = {}) {
272
+ const runnerOwnership = await ensureAsyncRunnerOwnership({
273
+ repo: options.repo,
274
+ pr: options.pr,
275
+ env,
276
+ cwd: path.resolve(process.cwd()),
277
+ claimIfMissing: true,
278
+ });
279
+ if (!runnerOwnership.ok) {
280
+ return {
281
+ ok: true,
282
+ action: "stop",
283
+ state: STATE.BLOCKED_NEEDS_USER_DECISION,
284
+ allowedTransitions: [],
285
+ nextAction: runnerOwnership.message,
286
+ autoRerequestEligible: false,
287
+ sameHeadCleanConverged: false,
288
+ roundCapCleanEligible: false,
289
+ loopDisposition: "blocked",
290
+ terminal: true,
291
+ snapshot: { repo: options.repo, pr: options.pr },
292
+ runnerOwnership,
293
+ requestWatchContract: {
294
+ action: "stop",
295
+ nextAction: runnerOwnership.message,
296
+ requestStatus: "none",
297
+ routingState: "non_ready_state",
298
+ watchEntryConfirmed: false,
299
+ watchArgs: null,
300
+ stopState: "blocked",
301
+ },
302
+ };
303
+ }
304
+ let snapshot = await autoDetectSnapshot(
305
+ { repo: options.repo, pr: options.pr },
306
+ { env, ghCommand },
307
+ );
308
+ const config = await loadDevLoopConfig({ repoRoot: path.resolve(process.cwd()) });
309
+ if (config.errors?.length > 0) {
310
+ console.error("[copilot-pr-handoff] config warnings:", JSON.stringify(config.errors));
311
+ }
312
+ const refinementConfig = config.errors?.length > 0
313
+ ? resolveRefinement({ version: 1 })
314
+ : resolveRefinement(config.config);
315
+ let interpretation = interpretLoopState(snapshot, refinementConfig);
316
+
317
+ // Check for human comments since last subagent action
318
+ // Only active in async subagent context (PI_SUBAGENT_RUN_ID set)
319
+ let humanCommentCheck = { paused: false };
320
+ if (env.PI_SUBAGENT_RUN_ID) {
321
+ humanCommentCheck = await detectRecentHumanComments(
322
+ { repo: options.repo, pr: options.pr, claimedAtMs: runnerOwnership?.activeRun?.claimedAt ? new Date(runnerOwnership.activeRun.claimedAt).getTime() : undefined },
323
+ { env, ghCommand },
324
+ );
325
+ }
326
+ const TERMINAL_STATES = new Set([STATE.NO_PR, STATE.DONE, STATE.BLOCKED_NEEDS_USER_DECISION]);
327
+ const humanCommentUnavailable = humanCommentCheck.error && !humanCommentCheck.paused;
328
+ if ((humanCommentCheck.paused || humanCommentUnavailable) && !TERMINAL_STATES.has(interpretation.state)) {
329
+ return {
330
+ ok: true,
331
+ action: "stop",
332
+ state: STATE.BLOCKED_NEEDS_USER_DECISION,
333
+ allowedTransitions: [],
334
+ nextAction: humanCommentCheck.paused
335
+ ? "Human comment detected on PR since last subagent action; review the comment(s) before continuing the automated loop."
336
+ : `Human comment detection unavailable (${humanCommentCheck.error}); review PR comments manually before continuing.`,
337
+ autoRerequestEligible: false,
338
+ sameHeadCleanConverged: false,
339
+ roundCapCleanEligible: false,
340
+ loopDisposition: "blocked",
341
+ terminal: true,
342
+ snapshot,
343
+ runnerOwnership,
344
+ humanCommentPause: {
345
+ reason: humanCommentCheck.paused ? "human_comment_detected" : "human_comment_check_unavailable",
346
+ error: humanCommentCheck.error || undefined,
347
+ humanComments: humanCommentCheck.humanComments,
348
+ lastBotCommentAt: humanCommentCheck.lastBotCommentAt,
349
+ },
350
+ requestWatchContract: {
351
+ action: "stop",
352
+ nextAction: humanCommentCheck.paused
353
+ ? "Human comment detected on PR since last subagent action; review the comment(s) before continuing the automated loop."
354
+ : `Human comment detection unavailable (${humanCommentCheck.error}); review PR comments manually before continuing.`,
355
+ requestStatus: "none",
356
+ routingState: "non_ready_state",
357
+ watchEntryConfirmed: false,
358
+ watchArgs: null,
359
+ stopState: "blocked",
360
+ },
361
+ };
362
+ }
363
+
364
+
365
+ // Detect internal tooling PRs — suppress Copilot review request step entirely.
366
+ // Internal-only PRs (scripts/docs/tests/config) skip the request, not just the wait.
367
+ let internalOnlySkipCopilot = false;
368
+ // Skip internal detection in sequential stub/test mode to avoid consuming stub entries.
369
+ // Claims-mode stubs handle interleaved calls; detection runs normally.
370
+ if (!env.GH_SEQUENCE_PATH || env.GH_STUB_MODE === "claims") {
371
+ if (options.watchStatus === undefined &&
372
+ (interpretation.state === STATE.PR_READY_NO_FEEDBACK ||
373
+ interpretation.state === STATE.READY_TO_REREQUEST_REVIEW)) {
374
+ try {
375
+ const internalCheck = await detectPrInternalOnly(options, { env, ghCommand });
376
+ if (internalCheck.ok && internalCheck.internalOnly) {
377
+ internalOnlySkipCopilot = true;
378
+ interpretation = {
379
+ ...interpretation,
380
+ state: STATE.INTERNAL_TOOLING_DIRECT_GATE,
381
+ nextAction: NEXT_ACTIONS[STATE.INTERNAL_TOOLING_DIRECT_GATE],
382
+ allowedTransitions: TRANSITIONS[STATE.INTERNAL_TOOLING_DIRECT_GATE] || [STATE.DONE],
383
+ };
384
+ }
385
+ } catch {
386
+ // Best-effort: if detection fails, fall through to normal request behavior
387
+ }
388
+ }
389
+ }
390
+
391
+ let reviewRequestStatus;
392
+ const shouldRequestReview = !internalOnlySkipCopilot && options.watchStatus === undefined
393
+ && (interpretation.state === STATE.PR_READY_NO_FEEDBACK
394
+ || interpretation.state === STATE.READY_TO_REREQUEST_REVIEW
395
+ && interpretation.autoRerequestEligible);
396
+ if (shouldRequestReview) {
397
+ const requestResult = await performCopilotReviewRequest(
398
+ {
399
+ repo: options.repo,
400
+ pr: options.pr,
401
+ sameHeadCleanConverged: interpretation.sameHeadCleanConverged,
402
+ },
403
+ { env, ghCommand },
404
+ );
405
+ reviewRequestStatus = requestResult.status;
406
+ snapshot = applyConfirmedReviewRequest(snapshot, reviewRequestStatus);
407
+ interpretation = interpretLoopState(snapshot, refinementConfig);
408
+ }
409
+ const interpretationSummary = summarizeLoopInterpretation(interpretation, refinementConfig);
410
+ const effectiveReviewRequestStatus = reviewRequestStatus
411
+ ?? (snapshot.copilotReviewRequestStatus === "requested" || snapshot.copilotReviewRequestStatus === "already-requested"
412
+ ? snapshot.copilotReviewRequestStatus
413
+ : undefined);
414
+ let action;
415
+ if (reviewRequestStatus === "requested" || reviewRequestStatus === "already-requested") {
416
+ action = "watch";
417
+ } else if (WATCH_STATES.has(interpretation.state)) {
418
+ action = "watch";
419
+ } else if (FIX_STATES.has(interpretation.state)) {
420
+ action = "fix";
421
+ } else {
422
+ action = "stop";
423
+ }
424
+ const result = {
425
+ ok: true,
426
+ action,
427
+ state: interpretation.state,
428
+ allowedTransitions: interpretation.allowedTransitions,
429
+ nextAction: interpretation.nextAction,
430
+ autoRerequestEligible: interpretation.autoRerequestEligible,
431
+ sameHeadCleanConverged: interpretation.sameHeadCleanConverged,
432
+ roundCapCleanEligible: interpretation.roundCapCleanEligible ?? false,
433
+ loopDisposition: interpretationSummary.loopDisposition,
434
+ terminal: interpretationSummary.terminal,
435
+ snapshot,
436
+ internalOnlySkipCopilot: internalOnlySkipCopilot || undefined,
437
+ };
438
+ if (runnerOwnership.status !== "skipped_no_async_run_id") {
439
+ result.runnerOwnership = runnerOwnership;
440
+ }
441
+ if (effectiveReviewRequestStatus !== undefined) {
442
+ result.reviewRequestStatus = effectiveReviewRequestStatus;
443
+ }
444
+ if (options.watchStatus !== undefined) {
445
+ result.watchStatus = options.watchStatus;
446
+ }
447
+ if (action === "watch") {
448
+ result.watchTimeoutPolicy = EXTERNAL_HEALTHY_WAIT_TIMEOUT_POLICY;
449
+ result.watchArgs = {
450
+ repo: options.repo,
451
+ pr: options.pr,
452
+ pollIntervalMs: DEFAULT_POLL_INTERVAL_MS,
453
+ timeoutMs: enforceExternalHealthyWaitTimeout({
454
+ timeoutMs: COPILOT_REVIEW_WAIT_TIMEOUT_MS,
455
+ contextLabel: "Copilot review wait",
456
+ }),
457
+ };
458
+ }
459
+ const normalizedRequestStatus = effectiveReviewRequestStatus
460
+ ?? (snapshot.copilotReviewRequestStatus === "unavailable"
461
+ || snapshot.copilotReviewRequestStatus === "failed"
462
+ ? snapshot.copilotReviewRequestStatus
463
+ : "none");
464
+ result.requestWatchContract = summarizeRequestWatchContract({
465
+ interpretation,
466
+ action,
467
+ requestStatus: normalizedRequestStatus,
468
+ watchArgs: result.watchArgs,
469
+ });
470
+ return result;
471
+ }
472
+ export async function runCli(
473
+ argv = process.argv.slice(2),
474
+ {
475
+ stdout = process.stdout,
476
+ env = process.env,
477
+ ghCommand = "gh",
478
+ } = {},
479
+ ) {
480
+ const options = parseHandoffCliArgs(argv);
481
+ if (options.help) {
482
+ stdout.write(`${USAGE}\n`);
483
+ return;
484
+ }
485
+ const result = await runHandoff(options, { env, ghCommand });
486
+ stdout.write(`${JSON.stringify(result)}\n`);
487
+ }
488
+ if (isDirectCliRun(import.meta.url)) {
489
+ runCli().catch((error) => {
490
+ process.stderr.write(`${formatCliError(error)}\n`);
491
+ process.exitCode = 1;
492
+ });
493
+ }