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,267 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
6
+ import { requireOptionValue, parsePositiveInteger } from "../_cli-primitives.mjs";
7
+ import { detectRepoSlug, normalizeRepoSlug } from "@dev-loops/core/github/repo-slug";
8
+
9
+ // REPO_ROOT resolves to the git repo root (scripts/loop/info.mjs → scripts/ → repo/)
10
+ const REPO_ROOT = path.resolve(fileURLToPath(new URL("..", import.meta.url)), "..");
11
+
12
+ const USAGE = `Usage:
13
+ dev-loops loop info --issue <number>
14
+ dev-loops loop info --pr <number>
15
+ Read-only state inspection for issues and PRs.
16
+ Required (exactly one):
17
+ --issue <n> Issue number
18
+ --pr <n> PR number
19
+ Optional:
20
+ --json Machine-readable JSON output (default: human-readable summary)
21
+ --repo <slug> Repository slug (auto-detected from git remote when omitted)
22
+ Exit codes:
23
+ 0 Success
24
+ 1 Argument error or runtime failure`.trim();
25
+
26
+ const parseError = buildParseError(USAGE);
27
+
28
+ function parseCliArgs(argv) {
29
+ const args = [...argv];
30
+ const opts = { help: false, issue: undefined, pr: undefined, json: false, repo: undefined };
31
+ while (args.length > 0) {
32
+ const token = args.shift();
33
+ if (token === "--help" || token === "-h") { opts.help = true; return opts; }
34
+ if (token === "--json") { opts.json = true; continue; }
35
+ if (token === "--issue") { opts.issue = parsePositiveInteger(requireOptionValue(args, "--issue", parseError), "--issue", parseError); continue; }
36
+ if (token === "--pr") { opts.pr = parsePositiveInteger(requireOptionValue(args, "--pr", parseError), "--pr", parseError); continue; }
37
+ if (token === "--repo") { opts.repo = requireOptionValue(args, "--repo", parseError); continue; }
38
+ throw parseError(`Unknown argument: ${token}`);
39
+ }
40
+ const modes = [opts.issue, opts.pr].filter(v => v !== undefined).length;
41
+ if (modes > 1) throw parseError("--issue and --pr are mutually exclusive");
42
+ if (modes === 0) throw parseError("--issue <n> or --pr <n> is required");
43
+ return opts;
44
+ }
45
+
46
+ function validateRepo(repo) {
47
+ if (!repo) {
48
+ throw parseError("Repo auto-detection failed. Set origin remote or use --repo.");
49
+ }
50
+ try {
51
+ // Normalize (trim) the slug and validate structure
52
+ return normalizeRepoSlug(repo, { errorMessage: "--repo must match <owner/name>" });
53
+ } catch (err) {
54
+ throw parseError(`Invalid repo slug: ${err instanceof Error ? err.message : String(err)}`);
55
+ }
56
+ }
57
+
58
+ function ghJson(args, cwd) {
59
+ try {
60
+ const stdout = execFileSync("gh", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
61
+ return JSON.parse(stdout);
62
+ } catch (err) {
63
+ throw new Error(`gh command failed: ${err instanceof Error ? err.message : String(err)}`);
64
+ }
65
+ }
66
+
67
+ function runNode(scriptPath, args, cwd) {
68
+ const stdout = execFileSync(process.execPath, [scriptPath, ...args], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
69
+ return JSON.parse(stdout);
70
+ }
71
+
72
+ function formatBranchDisplay(headRefName, baseRefName) {
73
+ return `${headRefName} ← ${baseRefName}`;
74
+ }
75
+
76
+ function formatCiDisplay(ciStatus, ciConclusion) {
77
+ if (!ciStatus || ciStatus === "none") return "no CI";
78
+ if (ciStatus === "pending") return "CI pending";
79
+ if (ciStatus === "failure") return `CI ❌ (${ciConclusion || "failed"})`;
80
+ if (ciStatus === "crediblyGreen") return "CI ✅ (local)";
81
+ return `CI ${ciStatus}`;
82
+ }
83
+
84
+ function formatPrSummary(prData, handoffResult) {
85
+ const lines = [];
86
+ lines.push(`PR #${prData.number}: ${prData.title}`);
87
+ lines.push(` Branch: ${formatBranchDisplay(prData.headRefName, prData.baseRefName)}`);
88
+ lines.push(` State: ${prData.state}${prData.isDraft ? " (draft)" : ""}`);
89
+ lines.push(` Author: ${prData.author?.login || "unknown"}`);
90
+
91
+ if (handoffResult?.snapshot) {
92
+ const s = handoffResult.snapshot;
93
+ if (s.ciStatus !== undefined) {
94
+ lines.push(` CI: ${formatCiDisplay(s.ciStatus, s.ciConclusion)}`);
95
+ }
96
+ if (s.unresolvedThreadCount !== undefined) {
97
+ lines.push(` Unresolved threads: ${s.unresolvedThreadCount}`);
98
+ }
99
+ if (s.completedCopilotRoundCount !== undefined && s.completedCopilotRoundCount > 0) {
100
+ lines.push(` Copilot rounds: ${s.completedCopilotRoundCount}`);
101
+ }
102
+ if (s.reviewRoundCount !== undefined && s.reviewRoundCount > 0) {
103
+ lines.push(` Review rounds: ${s.reviewRoundCount}`);
104
+ }
105
+ if (s.copilotReviewOnCurrentHead) {
106
+ lines.push(` Copilot review: requested on current head`);
107
+ }
108
+ } else if (handoffResult?.error) {
109
+ lines.push(` Handoff: unavailable (${handoffResult.error})`);
110
+ }
111
+
112
+ if (handoffResult?.action) {
113
+ lines.push(` Action: ${handoffResult.action}`);
114
+ }
115
+ if (handoffResult?.nextAction) {
116
+ lines.push(` Next: ${handoffResult.nextAction}`);
117
+ }
118
+ if (handoffResult?.state) {
119
+ lines.push(` Loop state: ${handoffResult.state}`);
120
+ }
121
+
122
+ return lines.join("\n");
123
+ }
124
+
125
+ function formatIssueSummary(issueData, startupBundle, linkedPrData) {
126
+ const lines = [];
127
+ lines.push(`Issue #${issueData.number}: ${issueData.title}`);
128
+ lines.push(` State: ${issueData.state}`);
129
+
130
+ if (issueData.assignees?.length > 0) {
131
+ const names = issueData.assignees.map(a => a.login).join(", ");
132
+ lines.push(` Assignees: ${names}`);
133
+ }
134
+
135
+ const bundle = startupBundle?.bundle || startupBundle;
136
+ if (bundle) {
137
+ if (bundle.loopState) lines.push(` Loop state: ${bundle.loopState}`);
138
+ if (bundle.selectedStrategy) lines.push(` Strategy: ${bundle.selectedStrategy}`);
139
+ if (bundle.routeKind) lines.push(` Route: ${bundle.routeKind}`);
140
+ if (bundle.nextAction) lines.push(` Next: ${bundle.nextAction}`);
141
+ } else if (startupBundle?.error) {
142
+ lines.push(` Startup: unavailable (${startupBundle.error})`);
143
+ }
144
+
145
+ if (issueData.body) {
146
+ const hasAc = /##\s*Acceptance Criteria|##\s*AC\b|###\s*Acceptance Criteria|###\s*AC\b/i.test(issueData.body);
147
+ lines.push(` Acceptance criteria: ${hasAc ? "present" : "missing"}`);
148
+ }
149
+
150
+ if (linkedPrData) {
151
+ lines.push(` Linked PR: #${linkedPrData.number} (${linkedPrData.state}${linkedPrData.isDraft ? ", draft" : ""})`);
152
+ if (linkedPrData.headRefName) {
153
+ lines.push(` Branch: ${formatBranchDisplay(linkedPrData.headRefName, linkedPrData.baseRefName)}`);
154
+ }
155
+ if (linkedPrData.ciStatus !== undefined) {
156
+ lines.push(` CI: ${formatCiDisplay(linkedPrData.ciStatus, linkedPrData.ciConclusion)}`);
157
+ }
158
+ if (linkedPrData.unresolvedThreadCount !== undefined) {
159
+ lines.push(` Unresolved threads: ${linkedPrData.unresolvedThreadCount}`);
160
+ }
161
+ if (linkedPrData.loopState) {
162
+ lines.push(` Loop state: ${linkedPrData.loopState}`);
163
+ }
164
+ if (linkedPrData.action) {
165
+ lines.push(` Action: ${linkedPrData.action}`);
166
+ }
167
+ }
168
+
169
+ return lines.join("\n");
170
+ }
171
+
172
+ function buildPrInfo(prNumber, repo, cwd) {
173
+ const prData = ghJson(["pr", "view", String(prNumber), "--repo", repo, "--json", "number,title,body,state,isDraft,headRefName,baseRefName,author,mergedAt,url,reviewRequests"], cwd);
174
+
175
+ let handoffResult = null;
176
+ try {
177
+ const handoffScript = path.join(REPO_ROOT, "scripts/loop/copilot-pr-handoff.mjs");
178
+ handoffResult = runNode(handoffScript, ["--pr", String(prNumber), "--repo", repo, "--watch-status", "idle"], cwd);
179
+ } catch (err) {
180
+ handoffResult = { error: err instanceof Error ? err.message : String(err) };
181
+ }
182
+
183
+ return { prData, handoffResult };
184
+ }
185
+
186
+ function buildIssueInfo(issueNumber, repo, cwd) {
187
+ const issueData = ghJson(["issue", "view", String(issueNumber), "--repo", repo, "--json", "number,title,body,state,labels,assignees,milestone,url"], cwd);
188
+
189
+ // Run startup resolver with synthetic PI_SUBAGENT_RUN_ID to avoid
190
+ // async-start contract rejection for GitHub-first issue routes.
191
+ let startupBundle = null;
192
+ try {
193
+ const startupScript = path.join(REPO_ROOT, "scripts/loop/resolve-dev-loop-startup.mjs");
194
+ const env = { ...process.env, PI_SUBAGENT_RUN_ID: "info-readonly-placeholder" };
195
+ const raw = execFileSync(process.execPath, [startupScript, "--issue", String(issueNumber)], {
196
+ cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env,
197
+ });
198
+ startupBundle = JSON.parse(raw);
199
+ } catch (err) {
200
+ startupBundle = { error: err instanceof Error ? err.message : String(err) };
201
+ }
202
+
203
+ let linkedPrInfo = null;
204
+ try {
205
+ const linkageScript = path.join(REPO_ROOT, "scripts/github/detect-linked-issue-pr.mjs");
206
+ const linkage = runNode(linkageScript, ["--repo", repo, "--issue", String(issueNumber)], cwd);
207
+ if (linkage.hasOpenLinkedPr && linkage.prNumber) {
208
+ const prData = ghJson(["pr", "view", String(linkage.prNumber), "--repo", repo, "--json", "number,title,state,isDraft,headRefName,baseRefName,author,url"], cwd);
209
+
210
+ let handoffResult = null;
211
+ try {
212
+ const handoffScript = path.join(REPO_ROOT, "scripts/loop/copilot-pr-handoff.mjs");
213
+ handoffResult = runNode(handoffScript, ["--pr", String(linkage.prNumber), "--repo", repo, "--watch-status", "idle"], cwd);
214
+ } catch {
215
+ handoffResult = null;
216
+ }
217
+
218
+ linkedPrInfo = {
219
+ ...prData,
220
+ ciStatus: handoffResult?.snapshot?.ciStatus,
221
+ ciConclusion: handoffResult?.snapshot?.ciConclusion,
222
+ unresolvedThreadCount: handoffResult?.snapshot?.unresolvedThreadCount,
223
+ loopState: handoffResult?.state,
224
+ action: handoffResult?.action,
225
+ };
226
+ }
227
+ } catch {
228
+ // Linked PR detection unavailable
229
+ }
230
+
231
+ return { issueData, startupBundle, linkedPrInfo: linkedPrInfo };
232
+ }
233
+
234
+ export async function runCli(argv = process.argv.slice(2), { stdout = process.stdout, stderr = process.stderr } = {}) {
235
+ const opts = parseCliArgs(argv);
236
+ if (opts.help) { stdout.write(`${USAGE}\n`); return; }
237
+
238
+ const cwd = process.cwd();
239
+ const rawRepo = opts.repo || detectRepoSlug(cwd);
240
+ // validateRepo normalizes (trims) the slug
241
+ const repo = validateRepo(rawRepo);
242
+
243
+ if (opts.issue !== undefined) {
244
+ const { issueData, startupBundle, linkedPrInfo } = buildIssueInfo(opts.issue, repo, cwd);
245
+
246
+ if (opts.json) {
247
+ stdout.write(JSON.stringify({ ok: true, kind: "issue", issue: issueData, startup: startupBundle, linkedPr: linkedPrInfo }) + "\n");
248
+ } else {
249
+ stdout.write(formatIssueSummary(issueData, startupBundle, linkedPrInfo) + "\n");
250
+ }
251
+ } else {
252
+ const { prData, handoffResult } = buildPrInfo(opts.pr, repo, cwd);
253
+
254
+ if (opts.json) {
255
+ stdout.write(JSON.stringify({ ok: true, kind: "pr", pr: prData, handoff: handoffResult }) + "\n");
256
+ } else {
257
+ stdout.write(formatPrSummary(prData, handoffResult) + "\n");
258
+ }
259
+ }
260
+ }
261
+
262
+ if (isDirectCliRun(import.meta.url)) {
263
+ runCli().catch((error) => {
264
+ process.stderr.write(`${formatCliError(error)}\n`);
265
+ process.exitCode = 1;
266
+ });
267
+ }
@@ -0,0 +1,117 @@
1
+ import {
2
+ DEFAULT_HOST,
3
+ DEFAULT_PORT,
4
+ USAGE,
5
+ } from "./constants.mjs";
6
+ import { requireOptionValue } from "../../_cli-primitives.mjs";
7
+ import { normalizeInspectionTarget } from "../_inspect-run-viewer-adapter.mjs";
8
+
9
+ export function parseInspectRunViewerCliError(message) {
10
+ return Object.assign(new Error(message), { usage: USAGE });
11
+ }
12
+
13
+ function parsePort(rawPort) {
14
+ if (!/^\d+$/.test(rawPort)) {
15
+ throw parseInspectRunViewerCliError("--port must be a positive integer");
16
+ }
17
+ const port = Number(rawPort);
18
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
19
+ throw parseInspectRunViewerCliError("--port must be between 1 and 65535");
20
+ }
21
+ return port;
22
+ }
23
+
24
+ function parseHost(rawHost) {
25
+ const host = rawHost.trim();
26
+ if (host.length === 0) {
27
+ throw parseInspectRunViewerCliError("--host must not be empty");
28
+ }
29
+ if (/^\[[^\]]+\]$/.test(host)) {
30
+ return host.slice(1, -1);
31
+ }
32
+ return host;
33
+ }
34
+
35
+ function isLoopbackHost(host) {
36
+ return host === "localhost"
37
+ || host === "::1"
38
+ || /^127(?:\.\d{1,3}){3}$/.test(host);
39
+ }
40
+
41
+ export function normalizeCliRepoOption(rawRepo) {
42
+ try {
43
+ return normalizeInspectionTarget({ repo: rawRepo, pr: 1 }).repo;
44
+ } catch (error) {
45
+ throw parseInspectRunViewerCliError(error instanceof Error ? error.message : String(error));
46
+ }
47
+ }
48
+
49
+ export function parseInspectRunViewerCliArgs(argv) {
50
+ const args = [...argv];
51
+ const options = {
52
+ help: false,
53
+ repo: undefined,
54
+ host: DEFAULT_HOST,
55
+ port: DEFAULT_PORT,
56
+ steeringStateFile: undefined,
57
+ copilotInputPath: undefined,
58
+ reviewerInputPath: undefined,
59
+ allowNonLocalhost: false,
60
+ restart: false,
61
+ };
62
+
63
+ while (args.length > 0) {
64
+ const token = args.shift();
65
+ if (token === "--help" || token === "-h") {
66
+ options.help = true;
67
+ return options;
68
+ }
69
+ if (token === "--repo") {
70
+ options.repo = requireOptionValue(args, "--repo", parseInspectRunViewerCliError);
71
+ continue;
72
+ }
73
+ if (token === "--pr") {
74
+ throw parseInspectRunViewerCliError("--pr is no longer supported on the CLI; choose a PR with ?pr=<number> in the viewer URL");
75
+ }
76
+ if (token === "--host") {
77
+ options.host = parseHost(requireOptionValue(args, "--host", parseInspectRunViewerCliError));
78
+ continue;
79
+ }
80
+ if (token === "--port") {
81
+ options.port = parsePort(requireOptionValue(args, "--port", parseInspectRunViewerCliError));
82
+ continue;
83
+ }
84
+ if (token === "--allow-non-localhost") {
85
+ options.allowNonLocalhost = true;
86
+ continue;
87
+ }
88
+ if (token === "--restart") {
89
+ options.restart = true;
90
+ continue;
91
+ }
92
+ if (token === "--steering-state-file") {
93
+ options.steeringStateFile = requireOptionValue(args, "--steering-state-file", parseInspectRunViewerCliError);
94
+ continue;
95
+ }
96
+ if (token === "--copilot-input") {
97
+ options.copilotInputPath = requireOptionValue(args, "--copilot-input", parseInspectRunViewerCliError);
98
+ continue;
99
+ }
100
+ if (token === "--reviewer-input") {
101
+ options.reviewerInputPath = requireOptionValue(args, "--reviewer-input", parseInspectRunViewerCliError);
102
+ continue;
103
+ }
104
+ throw parseInspectRunViewerCliError(`Unknown argument: ${token}`);
105
+ }
106
+
107
+ if (!options.help) {
108
+ options.repo = options.repo === undefined ? undefined : normalizeCliRepoOption(options.repo);
109
+ if (!options.allowNonLocalhost && !isLoopbackHost(options.host)) {
110
+ throw parseInspectRunViewerCliError("--host must stay on localhost/loopback unless --allow-non-localhost is set");
111
+ }
112
+ }
113
+
114
+ return options;
115
+ }
116
+
117
+ export { USAGE };
@@ -0,0 +1,80 @@
1
+ import path from "node:path";
2
+ import { createRequire } from "node:module";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ export const USAGE = `Usage: inspect-run-viewer.mjs [--repo <owner/name>]
6
+ [--host <host>] [--port <port>] [--allow-non-localhost] [--restart]
7
+ [--steering-state-file <path>]
8
+ [--copilot-input <path>] [--reviewer-input <path>]
9
+
10
+ Owned read-only local/operator inspection dashboard for inspect-run snapshots.
11
+ inspect-run remains authoritative for inspection/status state; this viewer owns local inbox discovery plus read-only presentation/prioritization.
12
+ Inbox-first mode works with no PR selected. Use ?pr=<number> to deep-link a selected PR and optionally ?repo=<owner/name> to scope the inbox.
13
+
14
+ Optional:
15
+ --repo <owner/name> Restrict the inbox to one repo
16
+ --host <host> Bind host (default: 127.0.0.1)
17
+ --port <port> Bind port (default: 4311)
18
+ --allow-non-localhost Permit non-loopback binds
19
+ (otherwise rejected)
20
+ --restart Stop any existing listener on the
21
+ chosen port before starting
22
+ (requires lsof/POSIX; sends
23
+ SIGTERM to all listeners)
24
+ --steering-state-file <path> Pass-through to inspect-run
25
+
26
+ --copilot-input <path> Pass-through to inspect-run
27
+ --reviewer-input <path> Pass-through to inspect-run
28
+ (pass-through to inspect-run)
29
+ )`.trim();
30
+
31
+ export const DEFAULT_HOST = "127.0.0.1";
32
+ export const DEFAULT_PORT = 4311;
33
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
34
+ export const MERMAID_BROWSER_ASSET_ROUTE = "/assets/mermaid.min.js";
35
+ const require = createRequire(import.meta.url);
36
+ const DEFAULT_MERMAID_BROWSER_ASSET_FALLBACK_PATH = path.join(
37
+ REPO_ROOT,
38
+ "node_modules",
39
+ "mermaid",
40
+ "dist",
41
+ "mermaid.min.js",
42
+ );
43
+
44
+ export function resolveMermaidBrowserAssetPath({ resolveImpl = require.resolve.bind(require) } = {}) {
45
+ try {
46
+ return resolveImpl("mermaid/dist/mermaid.min.js");
47
+ } catch {
48
+ return DEFAULT_MERMAID_BROWSER_ASSET_FALLBACK_PATH;
49
+ }
50
+ }
51
+
52
+ export const MERMAID_BROWSER_ASSET_PATH = resolveMermaidBrowserAssetPath();
53
+ export const DEFAULT_INBOX_UPDATED_WITHIN_DAYS = 7;
54
+ export const DEFAULT_INBOX_PAGE_SIZE = 25;
55
+ export const MAX_INBOX_RESULT_LIMIT = 100;
56
+ export const DEFAULT_INBOX_PR_STATE = "open";
57
+ export const DEFAULT_INBOX_MODE = "assignee";
58
+ export const DEFAULT_INBOX_PAGE = 1;
59
+ export const INBOX_UPDATED_FILTER_PRESETS = [
60
+ { label: "7d", value: 7 },
61
+ { label: "30d", value: 30 },
62
+ { label: "90d", value: 90 },
63
+ { label: "All", value: null },
64
+ ];
65
+ export const INBOX_STATE_FILTER_PRESETS = [
66
+ { label: "Open", value: "open" },
67
+ { label: "Closed", value: "closed" },
68
+ { label: "All", value: "all" },
69
+ ];
70
+ export const INBOX_STATE_FILTER_VALUES = new Set(
71
+ INBOX_STATE_FILTER_PRESETS.map((preset) => preset.value),
72
+ );
73
+ export const INBOX_MODE_FILTER_PRESETS = [
74
+ { label: "Assigned", value: "assignee" },
75
+ { label: "Reviewer", value: "reviewer" },
76
+ { label: "Involved", value: "involved" },
77
+ ];
78
+ export const INBOX_MODE_FILTER_VALUES = new Set(
79
+ INBOX_MODE_FILTER_PRESETS.map((preset) => preset.value),
80
+ );