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,358 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { parsePrNumber, requireOptionValue, runChild } from "../_cli-primitives.mjs";
4
+ import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
5
+ import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
6
+ import { DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION } from "@dev-loops/core/loop/public-dev-loop-routing";
7
+ import { watchCopilotReview } from "../github/probe-copilot-review.mjs";
8
+ import { runHandoff } from "./copilot-pr-handoff.mjs";
9
+ import { detectCopilotSessionActivity } from "./detect-copilot-session-activity.mjs";
10
+ import {
11
+ EXTERNAL_HEALTHY_WAIT_TIMEOUT_POLICY,
12
+ enforceExternalHealthyWaitTimeout,
13
+ } from "@dev-loops/core/loop/timeout-policy";
14
+ const REMOVED_FLAGS = new Set([
15
+ "--force-rerequest-review",
16
+ "--probe-only",
17
+ ]);
18
+ const USAGE = `Usage: run-watch-cycle.mjs --repo <owner/name> --pr <number>
19
+ Run one deterministic Copilot wait-cycle boundary.
20
+ Required:
21
+ --repo <owner/name> Repository slug (e.g. owner/repo)
22
+ --pr <number> Pull request number
23
+ Output (stdout, JSON):
24
+ { "ok": true, "handoffAction": "watch"|"fix"|"stop", "state": "...",
25
+ "allowedTransitions": [...], "nextAction": "...", "snapshot": {...},
26
+ "requestWatchContract"?: { ... },
27
+ "reviewRequestStatus"?: "...", "watchArgs"?: { ... },
28
+ "watchTimeoutPolicy"?: { "classification": "...", "minimumTimeoutMs": N, "defaultTimeoutMs": N },
29
+ "contractTrace"?: { ... },
30
+ "sessionActivity"?: { ... },
31
+ "watchStatus"?: "changed"|"timeout"|"idle", "watch"?: { ... },
32
+ "loopDisposition": "pending"|"unresolved_feedback"|"clean_converged"|"blocked"|"action_required"|"done",
33
+ "cycleDisposition": "pending"|"needs_followup"|"terminal",
34
+ "roundCapCleanEligible": true|false,
35
+ "terminal": true|false }
36
+ Cycle disposition:
37
+ pending Watch state persists; keep waiting or re-enter later
38
+ needs_followup Fresh review activity or fix-state follow-up needs action
39
+ terminal No automatic next step remains
40
+ Error output (stderr, JSON):
41
+ Argument/usage errors:
42
+ { "ok": false, "error": "...", "usage": "..." }
43
+ runtime failures:
44
+ { "ok": false, "error": "..." }
45
+ Exit codes:
46
+ 0 Success
47
+ 1 Argument error or runtime failure`.trim();
48
+ const parseError = buildParseError(USAGE);
49
+ function rejectRemovedFlag(token) {
50
+ throw parseError(
51
+ `${token} has been removed. Copilot re-requests and probe-only mode are managed internally. Omit the flag.`,
52
+ );
53
+ }
54
+ async function fetchPrHeadBranch({ repo, pr }, { env, ghCommand }) {
55
+ const result = await runChild(
56
+ ghCommand,
57
+ ["pr", "view", String(pr), "--repo", repo, "--json", "headRefName"],
58
+ env,
59
+ );
60
+ if (result.code !== 0) {
61
+ const detail = result.stderr.trim() || `exit code ${result.code}`;
62
+ throw new Error(`gh command failed: ${detail}`);
63
+ }
64
+ let payload;
65
+ try {
66
+ payload = JSON.parse(result.stdout);
67
+ } catch {
68
+ throw new Error(`Invalid JSON from gh: ${result.stdout.trim() || "<empty>"}`);
69
+ }
70
+ if (typeof payload.headRefName !== "string" || payload.headRefName.trim().length === 0) {
71
+ throw new Error("Missing required PR facts: headRefName");
72
+ }
73
+ return payload.headRefName.trim();
74
+ }
75
+ async function watchWorkflowRun({ repo, runId, timeoutMs = null }, { env, ghCommand }) {
76
+ return new Promise((resolve, reject) => {
77
+ const child = spawn(
78
+ ghCommand,
79
+ ["run", "watch", String(runId), "--repo", repo],
80
+ { env, stdio: ["ignore", "ignore", "pipe"] },
81
+ );
82
+ let stderr = "";
83
+ let timedOut = false;
84
+ let timeoutId = null;
85
+ child.stderr.on("data", (chunk) => {
86
+ stderr += String(chunk);
87
+ });
88
+ if (Number.isInteger(timeoutMs) && timeoutMs >= 0) {
89
+ timeoutId = setTimeout(() => {
90
+ timedOut = true;
91
+ child.kill("SIGTERM");
92
+ }, timeoutMs);
93
+ }
94
+ child.on("error", reject);
95
+ child.on("close", (code) => {
96
+ if (timeoutId !== null) {
97
+ clearTimeout(timeoutId);
98
+ }
99
+ if (timedOut) {
100
+ resolve({ status: "timed_out" });
101
+ return;
102
+ }
103
+ if (code !== 0) {
104
+ const detail = stderr.trim() || `exit code ${code}`;
105
+ reject(new Error(`gh command failed: ${detail}`));
106
+ return;
107
+ }
108
+ resolve({ status: "completed" });
109
+ });
110
+ });
111
+ }
112
+ function determineWatchTimeout(defaultTimeoutMs) {
113
+ return enforceExternalHealthyWaitTimeout({
114
+ timeoutMs: defaultTimeoutMs,
115
+ contextLabel: "Copilot review wait",
116
+ });
117
+ }
118
+ function buildWatchCycleContractTrace({
119
+ handoff,
120
+ watchArgs = null,
121
+ watchTimeoutPolicy = null,
122
+ watchStatus,
123
+ cycleDisposition,
124
+ sessionActivity = null,
125
+ workflowRunWatch = null,
126
+ }) {
127
+ const boundaryClassification = handoff.action !== "watch"
128
+ ? (handoff.loopDisposition === "blocked"
129
+ ? DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.BLOCKED
130
+ : handoff.terminal
131
+ ? DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.TERMINAL
132
+ : DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.ROUTED_FOLLOWUP)
133
+ : watchStatus === "changed"
134
+ ? DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.ROUTED_FOLLOWUP
135
+ : DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.HEALTHY_WAIT;
136
+ return {
137
+ handoff: {
138
+ action: handoff.action,
139
+ state: handoff.state,
140
+ loopDisposition: handoff.loopDisposition,
141
+ terminal: Boolean(handoff.terminal),
142
+ },
143
+ waitStrategy: {
144
+ helper: handoff.action === "watch" ? "scripts/github/probe-copilot-review.mjs" : null,
145
+ mode: handoff.action === "watch"
146
+ ? "persistent_watch"
147
+ : "not_applicable",
148
+ effectiveTimeoutMs: watchArgs?.timeoutMs ?? null,
149
+ effectivePollIntervalMs: watchArgs?.pollIntervalMs ?? null,
150
+ timeoutPolicyClassification: watchTimeoutPolicy?.classification ?? null,
151
+ },
152
+ orchestration: {
153
+ emittedWatchArgs: handoff.watchArgs ?? null,
154
+ effectiveWatchArgs: watchArgs,
155
+ sessionActivity,
156
+ workflowRunWatch,
157
+ },
158
+ stateRefresh: handoff.action === "watch"
159
+ ? {
160
+ boundaryKind: "post_watch_or_probe",
161
+ observedStatus: watchStatus,
162
+ refreshRequired: true,
163
+ refreshReason: watchStatus === "changed"
164
+ ? "Watch boundaries with fresh activity require an authoritative state refresh before routing the follow-up path."
165
+ : "Healthy watch boundaries are observational only; refresh authoritative state before treating timeout/idle as stop or completion.",
166
+ }
167
+ : null,
168
+ stopReason: {
169
+ classification: boundaryClassification,
170
+ terminal: Boolean(handoff.terminal),
171
+ cycleDisposition,
172
+ reason: handoff.action === "watch"
173
+ ? (watchStatus === "changed"
174
+ ? "Fresh watcher activity requires follow-up instead of staying in a healthy wait boundary."
175
+ : "Quiet watcher boundaries remain healthy waits and must not be treated as terminal completion by themselves.")
176
+ : handoff.nextAction,
177
+ },
178
+ };
179
+ }
180
+ export function parseWatchCycleCliArgs(argv) {
181
+ const args = [...argv];
182
+ const options = {
183
+ help: false,
184
+ repo: undefined,
185
+ pr: undefined,
186
+ };
187
+ while (args.length > 0) {
188
+ const token = args.shift();
189
+ if (token === "--help" || token === "-h") {
190
+ options.help = true;
191
+ return options;
192
+ }
193
+ if (REMOVED_FLAGS.has(token)) {
194
+ rejectRemovedFlag(token);
195
+ }
196
+ if (token === "--repo") {
197
+ options.repo = requireOptionValue(args, "--repo", parseError).trim();
198
+ continue;
199
+ }
200
+ if (token === "--pr") {
201
+ options.pr = parsePrNumber(requireOptionValue(args, "--pr", parseError), parseError);
202
+ continue;
203
+ }
204
+ throw parseError(`Unknown argument: ${token}`);
205
+ }
206
+ if (options.repo === undefined || options.pr === undefined) {
207
+ throw parseError("run-watch-cycle requires both --repo <owner/name> and --pr <number>");
208
+ }
209
+ try {
210
+ parseRepoSlug(options.repo);
211
+ } catch (error) {
212
+ throw parseError(error instanceof Error ? error.message : String(error));
213
+ }
214
+ return options;
215
+ }
216
+ export async function runWatchCycle(
217
+ options,
218
+ {
219
+ env = process.env,
220
+ ghCommand = "gh",
221
+ runHandoffImpl = runHandoff,
222
+ watchCopilotReviewImpl = watchCopilotReview,
223
+ detectCopilotSessionActivityImpl = detectCopilotSessionActivity,
224
+ fetchPrHeadBranchImpl = fetchPrHeadBranch,
225
+ watchWorkflowRunImpl = watchWorkflowRun,
226
+ detectSessionActivity = false,
227
+ } = {},
228
+ ) {
229
+ const handoff = await runHandoffImpl(options, { env, ghCommand });
230
+ const result = {
231
+ ok: true,
232
+ handoffAction: handoff.action,
233
+ state: handoff.state,
234
+ allowedTransitions: handoff.allowedTransitions,
235
+ nextAction: handoff.nextAction,
236
+ snapshot: handoff.snapshot,
237
+ roundCapCleanEligible: handoff.roundCapCleanEligible ?? false,
238
+ loopDisposition: handoff.loopDisposition,
239
+ cycleDisposition: handoff.action === "stop" ? "terminal" : "needs_followup",
240
+ terminal: Boolean(handoff.terminal),
241
+ };
242
+ if (handoff.requestWatchContract !== undefined) {
243
+ result.requestWatchContract = handoff.requestWatchContract;
244
+ }
245
+ if (handoff.reviewRequestStatus !== undefined) {
246
+ result.reviewRequestStatus = handoff.reviewRequestStatus;
247
+ }
248
+ if (handoff.watchArgs !== undefined) {
249
+ result.watchArgs = handoff.watchArgs;
250
+ }
251
+ if (handoff.watchTimeoutPolicy !== undefined) {
252
+ result.watchTimeoutPolicy = handoff.watchTimeoutPolicy;
253
+ }
254
+ if (handoff.action !== "watch") {
255
+ result.contractTrace = buildWatchCycleContractTrace({
256
+ handoff,
257
+ watchArgs: result.watchArgs ?? null,
258
+ watchTimeoutPolicy: result.watchTimeoutPolicy ?? null,
259
+ watchStatus: result.watchStatus,
260
+ cycleDisposition: result.cycleDisposition,
261
+ });
262
+ return result;
263
+ }
264
+ if (result.watchTimeoutPolicy === undefined) {
265
+ result.watchTimeoutPolicy = EXTERNAL_HEALTHY_WAIT_TIMEOUT_POLICY;
266
+ }
267
+ const persistentWatchTimeoutMs = determineWatchTimeout(
268
+ handoff.watchArgs.timeoutMs,
269
+ );
270
+ let workflowRunWatch = null;
271
+ if (detectSessionActivity) {
272
+ const headBranch = await fetchPrHeadBranchImpl({ repo: options.repo, pr: options.pr }, { env, ghCommand });
273
+ const session = await detectCopilotSessionActivityImpl(
274
+ {
275
+ repo: options.repo,
276
+ branch: headBranch,
277
+ },
278
+ { env, ghCommand },
279
+ );
280
+ result.sessionActivity = session;
281
+ if (
282
+ session.activity === "active"
283
+ && Number.isInteger(session.runId)
284
+ ) {
285
+ const workflowWatchResult = await watchWorkflowRunImpl(
286
+ {
287
+ repo: options.repo,
288
+ runId: session.runId,
289
+ timeoutMs: persistentWatchTimeoutMs,
290
+ },
291
+ { env, ghCommand },
292
+ );
293
+ workflowRunWatch = {
294
+ attempted: true,
295
+ timeoutMs: persistentWatchTimeoutMs,
296
+ runId: session.runId,
297
+ status: workflowWatchResult?.status ?? "unknown",
298
+ };
299
+ }
300
+ }
301
+ const watchOptions = {
302
+ ...handoff.watchArgs,
303
+ timeoutMs: persistentWatchTimeoutMs,
304
+ };
305
+ const watch = await watchCopilotReviewImpl(watchOptions, { env, ghCommand });
306
+ result.watchArgs = watchOptions;
307
+ result.watchStatus = watch.status;
308
+ result.watch = watch;
309
+ result.cycleDisposition = watch.status === "changed" ? "needs_followup" : "pending";
310
+ result.terminal = false;
311
+ result.contractTrace = buildWatchCycleContractTrace({
312
+ handoff,
313
+ watchArgs: watchOptions,
314
+ watchTimeoutPolicy: result.watchTimeoutPolicy,
315
+ watchStatus: watch.status,
316
+ cycleDisposition: result.cycleDisposition,
317
+ sessionActivity: result.sessionActivity ?? null,
318
+ workflowRunWatch: detectSessionActivity
319
+ ? (workflowRunWatch ?? {
320
+ attempted: false,
321
+ timeoutMs: persistentWatchTimeoutMs,
322
+ runId: result.sessionActivity?.runId ?? null,
323
+ status: null,
324
+ })
325
+ : null,
326
+ });
327
+ return result;
328
+ }
329
+ export async function runCli(
330
+ argv = process.argv.slice(2),
331
+ {
332
+ stdout = process.stdout,
333
+ env = process.env,
334
+ ghCommand = "gh",
335
+ runHandoffImpl = runHandoff,
336
+ watchCopilotReviewImpl = watchCopilotReview,
337
+ } = {},
338
+ ) {
339
+ const options = parseWatchCycleCliArgs(argv);
340
+ if (options.help) {
341
+ stdout.write(`${USAGE}\n`);
342
+ return;
343
+ }
344
+ const result = await runWatchCycle(options, {
345
+ env,
346
+ ghCommand,
347
+ runHandoffImpl,
348
+ watchCopilotReviewImpl,
349
+ detectSessionActivity: true,
350
+ });
351
+ stdout.write(`${JSON.stringify(result)}\n`);
352
+ }
353
+ if (isDirectCliRun(import.meta.url)) {
354
+ runCli().catch((error) => {
355
+ process.stderr.write(`${formatCliError(error)}\n`);
356
+ process.exitCode = 1;
357
+ });
358
+ }