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,77 @@
1
+ #!/usr/bin/env node
2
+ import { appendFile, readFile } from "node:fs/promises";
3
+ import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
4
+ export const INSPECT_RUN_VIEWER_RELEVANT_EXACT_PATHS = Object.freeze([
5
+ ".github/workflows/ci.yml",
6
+ "package.json",
7
+ "package-lock.json",
8
+ "playwright.inspect-run-viewer.config.mjs",
9
+ "scripts/loop/_inspect-run-viewer-adapter.mjs",
10
+ "scripts/loop/inspect-run-viewer.mjs",
11
+ "scripts/loop/inspect-run-viewer-ci-changes.mjs",
12
+ "test/playwright/harness/webkit-smoke-harness.mjs",
13
+ "test/playwright/inspect-run-viewer.spec.mjs",
14
+ ]);
15
+ export const INSPECT_RUN_VIEWER_RELEVANT_PREFIXES = Object.freeze([
16
+ "scripts/loop/inspect-run-viewer/",
17
+ "test/playwright/fixtures/",
18
+ ]);
19
+ const USAGE = "Usage: inspect-run-viewer-ci-changes.mjs <changed-files-path>";
20
+ const HELP = `Usage: inspect-run-viewer-ci-changes.mjs <changed-files-path>
21
+ Classify changed files to determine if inspect-run-viewer tests should run.
22
+ Reads a newline-delimited file list and checks against known relevant paths.
23
+ Options:
24
+ --help, -h Show this help
25
+ Exit codes:
26
+ 0 Success
27
+ 1 Error
28
+ `;
29
+ const parseError = buildParseError(USAGE);
30
+ export function normalizeInspectRunViewerPath(filePath) {
31
+ return String(filePath ?? "")
32
+ .trim()
33
+ .replace(/^\.\/+/u, "");
34
+ }
35
+ function isInspectRunViewerRelevantNormalizedPath(normalizedPath) {
36
+ return normalizedPath.length > 0 && (
37
+ INSPECT_RUN_VIEWER_RELEVANT_EXACT_PATHS.includes(normalizedPath)
38
+ || INSPECT_RUN_VIEWER_RELEVANT_PREFIXES.some((prefix) => normalizedPath.startsWith(prefix))
39
+ );
40
+ }
41
+ export function isInspectRunViewerRelevantPath(filePath) {
42
+ return isInspectRunViewerRelevantNormalizedPath(normalizeInspectRunViewerPath(filePath));
43
+ }
44
+ export function classifyInspectRunViewerCiChanges(changedPaths = []) {
45
+ const relevantPaths = [...new Set(changedPaths
46
+ .map((entry) => normalizeInspectRunViewerPath(entry))
47
+ .filter((entry) => isInspectRunViewerRelevantNormalizedPath(entry)))].sort();
48
+ return {
49
+ shouldRun: relevantPaths.length > 0,
50
+ relevantPaths,
51
+ };
52
+ }
53
+ export async function runCli(
54
+ argv = process.argv.slice(2),
55
+ { env = process.env, stdout = process.stdout } = {},
56
+ ) {
57
+ if (argv.length === 1 && (argv[0] === "--help" || argv[0] === "-h")) {
58
+ stdout.write(HELP);
59
+ return;
60
+ }
61
+ if (argv.length !== 1) {
62
+ throw parseError("inspect-run-viewer-ci-changes requires exactly one changed-files path argument");
63
+ }
64
+ const rawPaths = await readFile(argv[0], "utf8");
65
+ const result = classifyInspectRunViewerCiChanges(rawPaths.split(/\r?\n/u));
66
+ if (env.GITHUB_OUTPUT) {
67
+ await appendFile(env.GITHUB_OUTPUT, `inspect_run_viewer=${result.shouldRun}\n`, "utf8");
68
+ }
69
+ stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
70
+ return result;
71
+ }
72
+ if (isDirectCliRun(import.meta.url)) {
73
+ runCli().catch((error) => {
74
+ process.stderr.write(`${formatCliError(error)}\n`);
75
+ process.exitCode = 1;
76
+ });
77
+ }
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { formatCliError } from "../_core-helpers.mjs";
5
+ import { parseInspectRunViewerCliArgs, parseInspectRunViewerCliError, USAGE } from "./inspect-run-viewer/cli.mjs";
6
+ import {
7
+ createInspectRunViewerServer,
8
+ formatInspectRunViewerUrl,
9
+ listListeningPidsForPort,
10
+ restartExistingPortListener,
11
+ } from "./inspect-run-viewer/server.mjs";
12
+ import {
13
+ buildInspectionMermaidGraph,
14
+ loadMermaidBrowserScript,
15
+ renderInspectRunViewerHtml,
16
+ resetMermaidBrowserScriptCache,
17
+ } from "./inspect-run-viewer/rendering.mjs";
18
+ function normalizeRestartCapabilityError(error) {
19
+ const missingLsof = error?.code === "ENOENT"
20
+ && (error?.path === "lsof" || /(^|\b)lsof(\b|$)/i.test(String(error?.message ?? "")));
21
+ if (!missingLsof) {
22
+ return error;
23
+ }
24
+ const parseFriendlyError = parseInspectRunViewerCliError(
25
+ "--restart requires lsof/POSIX support; install lsof or rerun without --restart",
26
+ );
27
+ parseFriendlyError.cause = error;
28
+ return parseFriendlyError;
29
+ }
30
+ export {
31
+ buildInspectionMermaidGraph,
32
+ createInspectRunViewerServer,
33
+ formatInspectRunViewerUrl,
34
+ listListeningPidsForPort,
35
+ loadMermaidBrowserScript,
36
+ parseInspectRunViewerCliArgs,
37
+ renderInspectRunViewerHtml,
38
+ resetMermaidBrowserScriptCache,
39
+ restartExistingPortListener,
40
+ };
41
+ export async function runCli(
42
+ argv = process.argv.slice(2),
43
+ {
44
+ stdout = process.stdout,
45
+ restartExistingPortListenerImpl = restartExistingPortListener,
46
+ } = {},
47
+ ) {
48
+ const options = parseInspectRunViewerCliArgs(argv);
49
+ if (options.help) {
50
+ stdout.write(`${USAGE}\n`);
51
+ return null;
52
+ }
53
+ if (options.restart) {
54
+ try {
55
+ await restartExistingPortListenerImpl(options.port);
56
+ } catch (error) {
57
+ throw normalizeRestartCapabilityError(error);
58
+ }
59
+ }
60
+ const server = createInspectRunViewerServer(options);
61
+ await new Promise((resolve, reject) => {
62
+ server.once("error", reject);
63
+ server.listen(options.port, options.host, resolve);
64
+ });
65
+ stdout.write(
66
+ `${JSON.stringify({
67
+ ok: true,
68
+ message: "read-only inspect-run dashboard started",
69
+ scope: { repo: options.repo },
70
+ url: formatInspectRunViewerUrl(options.host, options.port),
71
+ reload: "manual",
72
+ })}\n`,
73
+ );
74
+ return server;
75
+ }
76
+ const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
77
+ if (isDirectRun) {
78
+ runCli().catch((error) => {
79
+ process.stderr.write(`${formatCliError(error)}\n`);
80
+ process.exitCode = 1;
81
+ });
82
+ }
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { parsePrNumber, requireOptionValue, runChild } from "../_cli-primitives.mjs";
6
+ import { buildParseError, formatCliError, parseJsonText, parseReviewThreads } from "../_core-helpers.mjs";
7
+ import { fetchGithubReviewThreadsPayload } from "../github/capture-review-threads.mjs";
8
+ import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
9
+ import { readExistingCheckpoint } from "./_checkpoint-io.mjs";
10
+ import { loadCopilotEvidence, loadReviewerEvidence } from "./_loop-evidence.mjs";
11
+ import { interpretOuterLoopState } from "@dev-loops/core/loop/conductor-routing";
12
+ import {
13
+ composeRunInspectionSnapshot,
14
+ deriveRunIdForInspectionTarget,
15
+ } from "@dev-loops/core/loop/run-inspection";
16
+ import { summarizeCopilotLoopIterations } from "@dev-loops/core/loop/copilot-loop-iterations";
17
+ import {
18
+ classifySafePoint,
19
+ getSteeringStatus,
20
+ normalizeSteeringState,
21
+ resolveEffectiveLoopState,
22
+ STEERING_KIND,
23
+ } from "@dev-loops/core/loop/steering";
24
+ import { validateSteeringStateTarget } from "./_steering-state-file.mjs";
25
+ const USAGE = `Usage: inspect-run.mjs --repo <owner/name> --pr <number>
26
+ Read-only run inspection for the Copilot PR outer-loop family.
27
+ Produces a single JSON snapshot describing the current state of one
28
+ explicitly targeted run without attaching to a live worker process or
29
+ rewriting any local artifacts.
30
+ Required:
31
+ --repo <owner/name> Repository slug (e.g. owner/repo)
32
+ --pr <number> Pull request number
33
+ Optional:
34
+ --steering-state-file <path> Path to a durable steering state JSON
35
+ file (as written by steer-loop.mjs).
36
+ When absent, steering is reported as
37
+ unavailable (no_steering_locator).
38
+ Test / snapshot-mode flags:
39
+ --copilot-input <path> Pre-built copilot snapshot JSON
40
+ (skips live copilot detection)
41
+ --reviewer-input <path> Pre-built reviewer snapshot JSON
42
+ (skips live reviewer detection)
43
+ Output (stdout, JSON):
44
+ Always-present fields:
45
+ ok, schemaVersion, target, inspectedAt, activeStateFamily,
46
+ outerState, outerAction, activeFamilyState, statusClass, needsAttention,
47
+ sourceMode, trust, evidence, markers, loopIterations
48
+ Best-effort fields:
49
+ allowedTransitions (when authoritative outerState is available),
50
+ layers (copilot, reviewer, steering drill-down)
51
+ statusClass values:
52
+ active Orchestrator needs to re-enter an inner loop
53
+ waiting Orchestrator is waiting on an external event
54
+ blocked Orchestrator is stopped and requires attention
55
+ done PR is merged or closed; loop complete
56
+ unknown Cannot determine state from available evidence
57
+ sourceMode values:
58
+ live-detector-backed All facts from live detectors (authoritative)
59
+ checkpoint-only Checkpoint drill-down only; top-level state stays unknown
60
+ partial Degraded mode. Mixed live + checkpoint fallback keeps top-level
61
+ state unknown; complete current-state input supplied by the
62
+ caller (including mixed live + input coverage) can still derive
63
+ a top-level state.
64
+ unavailable No usable evidence available
65
+ Error output (stderr, JSON):
66
+ Argument/usage errors:
67
+ { "ok": false, "error": "...", "usage": "..." }
68
+ Runtime failures:
69
+ { "ok": false, "error": "..." }
70
+ Exit codes:
71
+ 0 Success
72
+ 1 Argument error or unexpected runtime failure`.trim();
73
+ const parseError = buildParseError(USAGE);
74
+ export function parseInspectRunCliArgs(argv) {
75
+ const args = [...argv];
76
+ const options = {
77
+ help: false,
78
+ repo: undefined,
79
+ pr: undefined,
80
+ steeringStateFile: undefined,
81
+ copilotInputPath: undefined,
82
+ reviewerInputPath: undefined,
83
+ };
84
+ while (args.length > 0) {
85
+ const token = args.shift();
86
+ if (token === "--help" || token === "-h") {
87
+ options.help = true;
88
+ return options;
89
+ }
90
+ if (token === "--repo") {
91
+ options.repo = requireOptionValue(args, "--repo", parseError).trim();
92
+ continue;
93
+ }
94
+ if (token === "--pr") {
95
+ options.pr = parsePrNumber(requireOptionValue(args, "--pr", parseError), parseError);
96
+ continue;
97
+ }
98
+ if (token === "--steering-state-file") {
99
+ options.steeringStateFile = requireOptionValue(args, "--steering-state-file", parseError);
100
+ continue;
101
+ }
102
+ if (token === "--copilot-input") {
103
+ options.copilotInputPath = requireOptionValue(args, "--copilot-input", parseError);
104
+ continue;
105
+ }
106
+ if (token === "--reviewer-input") {
107
+ options.reviewerInputPath = requireOptionValue(args, "--reviewer-input", parseError);
108
+ continue;
109
+ }
110
+ throw parseError(`Unknown argument: ${token}`);
111
+ }
112
+ if (!options.help) {
113
+ if (options.repo === undefined || options.pr === undefined) {
114
+ throw parseError("inspect-run requires both --repo <owner/name> and --pr <number>");
115
+ }
116
+ try {
117
+ parseRepoSlug(options.repo);
118
+ } catch (error) {
119
+ throw parseError(error instanceof Error ? error.message : String(error));
120
+ }
121
+ }
122
+ return options;
123
+ }
124
+ async function runGhJson(args, { env, ghCommand }) {
125
+ const result = await runChild(ghCommand, args, env);
126
+ if (result.code !== 0) {
127
+ const detail = result.stderr.trim() || `exit code ${result.code}`;
128
+ throw new Error(`gh command failed: ${detail}`);
129
+ }
130
+ try {
131
+ return JSON.parse(result.stdout);
132
+ } catch {
133
+ throw new Error(`Invalid JSON from gh: ${result.stdout.trim() || "<empty>"}`);
134
+ }
135
+ }
136
+ function normalizeTimelineReviewRequestEvents(payload) {
137
+ const events = Array.isArray(payload) ? payload : [];
138
+ return events
139
+ .filter((event) => event?.event === "review_requested")
140
+ .map((event) => ({
141
+ createdAt: event?.created_at,
142
+ requestedReviewerLogin: event?.requested_reviewer?.login,
143
+ }));
144
+ }
145
+ function normalizeReviewPayload(payload) {
146
+ const reviews = Array.isArray(payload) ? payload : [];
147
+ return reviews.map((review) => ({
148
+ state: review?.state,
149
+ submittedAt: review?.submitted_at ?? review?.created_at,
150
+ authorLogin: review?.user?.login ?? review?.author?.login,
151
+ commitSha: review?.commit_id ?? review?.commit?.oid,
152
+ }));
153
+ }
154
+ function normalizeReviewCommentsPayload(payload) {
155
+ const comments = Array.isArray(payload) ? payload : [];
156
+ return comments.map((comment) => ({
157
+ createdAt: comment?.created_at ?? comment?.createdAt,
158
+ authorLogin: comment?.user?.login ?? comment?.author?.login,
159
+ }));
160
+ }
161
+ function normalizeCommitsPayload(payload) {
162
+ const commits = Array.isArray(payload) ? payload : [];
163
+ return commits.map((item) => ({
164
+ sha: item?.sha ?? item?.commit?.oid,
165
+ committedAt: item?.commit?.committer?.date ?? item?.commit?.author?.date ?? item?.committed_at,
166
+ authorLogin: item?.author?.login ?? item?.committer?.login ?? "",
167
+ }));
168
+ }
169
+ async function fetchCopilotLoopIterations({ repo, pr, snapshot }, { env, ghCommand }) {
170
+ const [prViewPayload, timelinePayload, reviewsPayload, reviewCommentsPayload, commitsPayload, reviewThreadsPayload] = await Promise.all([
171
+ runGhJson(["pr", "view", String(pr), "--repo", repo, "--json", "headRefOid"], { env, ghCommand }),
172
+ runGhJson(
173
+ ["api", "-H", "Accept: application/vnd.github+json", `repos/${repo}/issues/${pr}/timeline?per_page=100`],
174
+ { env, ghCommand },
175
+ ),
176
+ runGhJson(["api", `repos/${repo}/pulls/${pr}/reviews?per_page=100`], { env, ghCommand }),
177
+ runGhJson(["api", `repos/${repo}/pulls/${pr}/comments?per_page=100`], { env, ghCommand }),
178
+ runGhJson(["api", `repos/${repo}/pulls/${pr}/commits?per_page=100`], { env, ghCommand }),
179
+ fetchGithubReviewThreadsPayload({ repo, pr }, { env, ghCommand }),
180
+ ]);
181
+ const reviewThreads = parseReviewThreads(reviewThreadsPayload);
182
+ const degradedReasons = [];
183
+ if (Array.isArray(timelinePayload) && timelinePayload.length >= 100) degradedReasons.push("timeline_page_cap");
184
+ if (Array.isArray(reviewsPayload) && reviewsPayload.length >= 100) degradedReasons.push("reviews_page_cap");
185
+ if (Array.isArray(reviewCommentsPayload) && reviewCommentsPayload.length >= 100) degradedReasons.push("review_comments_page_cap");
186
+ if (Array.isArray(commitsPayload) && commitsPayload.length >= 100) degradedReasons.push("commits_page_cap");
187
+ if (reviewThreadsPayload?.data?.repository?.pullRequest?.reviewThreads?.pageInfo?.hasNextPage) {
188
+ degradedReasons.push("review_threads_has_next_page");
189
+ }
190
+ return summarizeCopilotLoopIterations({
191
+ reviewRequestEvents: normalizeTimelineReviewRequestEvents(timelinePayload),
192
+ reviews: normalizeReviewPayload(reviewsPayload),
193
+ reviewComments: normalizeReviewCommentsPayload(reviewCommentsPayload),
194
+ commits: normalizeCommitsPayload(commitsPayload),
195
+ reviewThreadSummary: reviewThreads.summary,
196
+ currentHeadSha: typeof prViewPayload?.headRefOid === "string" ? prViewPayload.headRefOid : null,
197
+ currentReviewRequestStatus: snapshot?.copilotReviewRequestStatus ?? "none",
198
+ degraded: degradedReasons.length > 0,
199
+ degradedReasons,
200
+ });
201
+ }
202
+ async function loadSteeringState(steeringStateFile) {
203
+ try {
204
+ const text = await readFile(steeringStateFile, "utf8");
205
+ const raw = parseJsonText(text);
206
+ return { state: normalizeSteeringState(raw), loadFailed: false };
207
+ } catch (error) {
208
+ if (error && error.code === "ENOENT") {
209
+ return { state: null, loadFailed: false };
210
+ }
211
+ return { state: null, loadFailed: true };
212
+ }
213
+ }
214
+ export async function inspectRun(options, { env = process.env, ghCommand = "gh" } = {}) {
215
+ const { repo, pr, steeringStateFile, copilotInputPath, reviewerInputPath } = options;
216
+ parseRepoSlug(repo);
217
+ const inspectedAt = new Date().toISOString();
218
+ const evidenceSourceKinds = {
219
+ copilot: copilotInputPath !== undefined ? "input" : "live",
220
+ reviewer: reviewerInputPath !== undefined ? "input" : "live",
221
+ };
222
+ let copilotEvidence = null;
223
+ let copilotLiveStatus = "failed";
224
+ try {
225
+ copilotEvidence = await loadCopilotEvidence({ repo, pr, copilotInputPath }, { env, ghCommand });
226
+ copilotLiveStatus = "ok";
227
+ } catch {
228
+ }
229
+ let reviewerEvidence = null;
230
+ let reviewerLiveStatus = "failed";
231
+ try {
232
+ reviewerEvidence = await loadReviewerEvidence({ repo, pr, reviewerInputPath }, { env, ghCommand });
233
+ reviewerLiveStatus = "ok";
234
+ } catch {
235
+ }
236
+ let loopIterations = {
237
+ available: false,
238
+ source: "github_pr_timeline",
239
+ reason: "requires_live_github_facts",
240
+ };
241
+ if (copilotEvidence?.snapshot?.prExists === false) {
242
+ loopIterations = {
243
+ available: false,
244
+ source: "github_pr_timeline",
245
+ reason: "no_pr",
246
+ };
247
+ } else if (copilotInputPath === undefined && copilotEvidence !== null) {
248
+ try {
249
+ loopIterations = await fetchCopilotLoopIterations(
250
+ { repo, pr, snapshot: copilotEvidence.snapshot },
251
+ { env, ghCommand },
252
+ );
253
+ } catch {
254
+ loopIterations = {
255
+ available: false,
256
+ source: "github_pr_timeline",
257
+ reason: "github_fact_capture_failed",
258
+ };
259
+ }
260
+ }
261
+ const { checkpoint: existingCheckpoint, filePath: checkpointEvidencePath } = await readExistingCheckpoint(repo, pr, { failSilently: true });
262
+ let outerState;
263
+ let outerAllowedTransitions;
264
+ let outerAction;
265
+ let outerReason;
266
+ const explicitTargetMissing =
267
+ copilotEvidence?.snapshot?.prExists === false
268
+ || reviewerEvidence?.snapshot?.prExists === false;
269
+ const hasCompleteCurrentInnerLoopState =
270
+ !explicitTargetMissing
271
+ && copilotLiveStatus === "ok"
272
+ && reviewerLiveStatus === "ok"
273
+ && copilotEvidence !== null
274
+ && reviewerEvidence !== null;
275
+ if (hasCompleteCurrentInnerLoopState) {
276
+ const outerInterpretation = interpretOuterLoopState({
277
+ target: { repo, pr },
278
+ copilotState: copilotEvidence.interpretation.state,
279
+ reviewerState: reviewerEvidence.interpretation.state,
280
+ sourceMode: evidenceSourceKinds.copilot === "live" && evidenceSourceKinds.reviewer === "live"
281
+ ? "authoritative"
282
+ : "snapshot",
283
+ requiresLocalIsolation: false,
284
+ });
285
+ outerState = outerInterpretation.state;
286
+ outerAllowedTransitions = outerInterpretation.allowedTransitions;
287
+ outerAction = outerInterpretation.outerAction;
288
+ outerReason = outerInterpretation.stopReason;
289
+ }
290
+ let steeringEvidence = null;
291
+ let steeringLoadFailed = false;
292
+ let steeringUnavailableReason = null;
293
+ let steeringReadback = null;
294
+ const steeringLocatorPath = steeringStateFile ?? null;
295
+ if (steeringLocatorPath !== null) {
296
+ const result = await loadSteeringState(steeringLocatorPath);
297
+ steeringEvidence = result.state;
298
+ steeringLoadFailed = result.loadFailed;
299
+ if (steeringEvidence !== null) {
300
+ const validation = validateSteeringStateTarget(steeringEvidence, {
301
+ repo,
302
+ pr,
303
+ runId: deriveRunIdForInspectionTarget({ repo, pr }),
304
+ });
305
+ if (!validation.ok) {
306
+ steeringEvidence = null;
307
+ steeringUnavailableReason = "mismatched_steering_target";
308
+ } else {
309
+ const steeringStatus = getSteeringStatus(steeringEvidence);
310
+ const resolved = copilotEvidence !== null
311
+ ? resolveEffectiveLoopState(copilotEvidence.snapshot, steeringEvidence)
312
+ : null;
313
+ const queuedStopAtNextSafeGate = steeringEvidence.queuedEvents.some(
314
+ (event) => event.kind === STEERING_KIND.STOP_AT_NEXT_SAFE_GATE,
315
+ );
316
+ const effectiveConstraints = resolved?.effectiveConstraints ?? steeringStatus.effectiveConstraints;
317
+ const safePointCategory = copilotEvidence !== null
318
+ ? classifySafePoint(copilotEvidence.interpretation.state)
319
+ : null;
320
+ steeringReadback = {
321
+ latestAcknowledgement: steeringStatus.latestResult,
322
+ effectiveConstraints,
323
+ pendingSummary: {
324
+ queuedCount: steeringStatus.queuedCount,
325
+ queuedKinds: [...new Set(steeringEvidence.queuedEvents.map((event) => event.kind))],
326
+ stopAtNextSafeGateQueued: queuedStopAtNextSafeGate,
327
+ },
328
+ stopAtNextSafeGate: {
329
+ effective: effectiveConstraints.stopAtNextSafeGate,
330
+ queued: queuedStopAtNextSafeGate,
331
+ terminal: resolved?.terminalStopAtNextSafeGate ?? false,
332
+ safePointCategory,
333
+ },
334
+ };
335
+ }
336
+ }
337
+ }
338
+ return composeRunInspectionSnapshot({
339
+ target: { repo, pr },
340
+ inspectedAt,
341
+ outerState,
342
+ outerAllowedTransitions,
343
+ outerAction,
344
+ outerReason,
345
+ copilotEvidence,
346
+ reviewerEvidence,
347
+ existingCheckpoint,
348
+ checkpointEvidencePath,
349
+ liveAvailability: { copilot: copilotLiveStatus, reviewer: reviewerLiveStatus },
350
+ evidenceSourceKinds,
351
+ explicitTargetMissing,
352
+ steeringLocatorPath,
353
+ steeringEvidence,
354
+ steeringLoadFailed,
355
+ steeringUnavailableReason,
356
+ steeringReadback,
357
+ loopIterations,
358
+ });
359
+ }
360
+ export async function runCli(
361
+ argv = process.argv.slice(2),
362
+ {
363
+ stdout = process.stdout,
364
+ env = process.env,
365
+ ghCommand = "gh",
366
+ } = {},
367
+ ) {
368
+ const options = parseInspectRunCliArgs(argv);
369
+ if (options.help) {
370
+ stdout.write(`${USAGE}\n`);
371
+ return;
372
+ }
373
+ const snapshot = await inspectRun(options, { env, ghCommand });
374
+ stdout.write(`${JSON.stringify(snapshot)}\n`);
375
+ }
376
+ const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
377
+ if (isDirectRun) {
378
+ runCli().catch((error) => {
379
+ process.stderr.write(`${formatCliError(error)}\n`);
380
+ process.exitCode = 1;
381
+ });
382
+ }