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,205 @@
1
+ #!/usr/bin/env node
2
+ import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
3
+ import { parseIssueNumber, requireOptionValue, runChild } from "../_cli-primitives.mjs";
4
+ import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
5
+ const ISSUE_JSON_FIELDS = "number,title,body,url,state";
6
+ const USAGE = `Usage: resolve-tracker-local-spec.mjs (--repo <owner/name> --issue <number> | --issue-url <github-issue-url>)
7
+ Resolve the canonical tracker-backed local spec bundle from one GitHub issue reference.
8
+ This helper is intentionally bounded to the GitHub-backed tracker path and does not
9
+ create or read docs/phases/phase-<n>.md.
10
+ Allowed inputs:
11
+ --repo <owner/name> Repository slug (must be paired with --issue)
12
+ --issue <number> Issue number (must be paired with --repo)
13
+ --issue-url <url> Full GitHub issue URL (alternative to --repo/--issue)
14
+ Success output (stdout, JSON):
15
+ {
16
+ "ok": true,
17
+ "repo": "owner/name",
18
+ "issue": 85,
19
+ "issueUrl": "https://github.com/owner/repo/issues/85",
20
+ "state": "OPEN"|"CLOSED",
21
+ "title": "...",
22
+ "body": "...",
23
+ "canonicalSpecSource": "tracker_issue",
24
+ "localImplementationMode": "tracker_backed",
25
+ "localPhaseDocAllowed": false,
26
+ "stateSync": "tracker_issue_is_canonical"
27
+ }
28
+ Error output (stderr, JSON):
29
+ Argument/usage errors:
30
+ { "ok": false, "error": "...", "usage": "..." }
31
+ gh/runtime failures:
32
+ { "ok": false, "error": "..." }`.trim();
33
+ const parseError = buildParseError(USAGE);
34
+ export function parseGitHubIssueUrl(value) {
35
+ let parsedUrl;
36
+ try {
37
+ parsedUrl = new URL(value);
38
+ } catch {
39
+ throw parseError("--issue-url must be a valid GitHub issue URL");
40
+ }
41
+ if (!/^https?:$/i.test(parsedUrl.protocol) || parsedUrl.hostname.toLowerCase() !== "github.com") {
42
+ throw parseError("--issue-url must be a valid GitHub issue URL");
43
+ }
44
+ const [owner, name, issueMarker, issueNumber, ...rest] = parsedUrl.pathname.split("/").filter(Boolean);
45
+ if (rest.length > 0 || issueMarker !== "issues") {
46
+ throw parseError("--issue-url must be a valid GitHub issue URL");
47
+ }
48
+ const repo = `${owner ?? ""}/${name ?? ""}`;
49
+ try {
50
+ parseRepoSlug(repo, { errorMessage: "--issue-url must be a valid GitHub issue URL" });
51
+ } catch (error) {
52
+ throw parseError("--issue-url must be a valid GitHub issue URL");
53
+ }
54
+ if (!/^\d+$/.test(issueNumber ?? "") || Number(issueNumber) === 0) {
55
+ throw parseError("--issue-url must be a valid GitHub issue URL");
56
+ }
57
+ return {
58
+ repo,
59
+ issue: Number(issueNumber),
60
+ };
61
+ }
62
+ export function parseResolveTrackerLocalSpecCliArgs(argv) {
63
+ const args = [...argv];
64
+ const options = {
65
+ help: false,
66
+ repo: undefined,
67
+ issue: undefined,
68
+ issueUrl: undefined,
69
+ };
70
+ while (args.length > 0) {
71
+ const token = args.shift();
72
+ if (token === "--help" || token === "-h") {
73
+ options.help = true;
74
+ return options;
75
+ }
76
+ if (token === "--repo") {
77
+ options.repo = requireOptionValue(args, "--repo", parseError).trim();
78
+ continue;
79
+ }
80
+ if (token === "--issue") {
81
+ options.issue = parseIssueNumber(requireOptionValue(args, "--issue", parseError), parseError);
82
+ continue;
83
+ }
84
+ if (token === "--issue-url") {
85
+ options.issueUrl = requireOptionValue(args, "--issue-url", parseError).trim();
86
+ continue;
87
+ }
88
+ throw parseError(`Unknown argument: ${token}`);
89
+ }
90
+ const usingIssueUrl = typeof options.issueUrl === "string";
91
+ const usingRepoIssue = options.repo !== undefined || options.issue !== undefined;
92
+ if (usingIssueUrl && usingRepoIssue) {
93
+ throw parseError("Use either --issue-url <url> or --repo <owner/name> with --issue <number>, but not both");
94
+ }
95
+ if (!usingIssueUrl && (options.repo === undefined || options.issue === undefined)) {
96
+ throw parseError("Tracker spec resolution requires either --issue-url <url> or both --repo <owner/name> and --issue <number>");
97
+ }
98
+ if (usingIssueUrl) {
99
+ const { repo, issue } = parseGitHubIssueUrl(options.issueUrl);
100
+ return {
101
+ help: false,
102
+ repo,
103
+ issue,
104
+ issueUrl: options.issueUrl,
105
+ };
106
+ }
107
+ try {
108
+ parseRepoSlug(options.repo);
109
+ } catch (error) {
110
+ throw parseError(error instanceof Error ? error.message : String(error));
111
+ }
112
+ return options;
113
+ }
114
+ function buildIssueViewArgs({ repo, issue }) {
115
+ return [
116
+ "issue",
117
+ "view",
118
+ String(issue),
119
+ "--repo",
120
+ repo,
121
+ "--json",
122
+ ISSUE_JSON_FIELDS,
123
+ ];
124
+ }
125
+ function readIssuePayload(payload) {
126
+ if (!payload || typeof payload !== "object") {
127
+ throw new Error("Invalid tracker issue payload: expected object");
128
+ }
129
+ const number = payload.number;
130
+ const title = payload.title;
131
+ const body = payload.body;
132
+ const url = payload.url;
133
+ const state = payload.state;
134
+ if (!Number.isInteger(number) || number <= 0) {
135
+ throw new Error("Invalid tracker issue payload: missing positive issue number");
136
+ }
137
+ if (typeof title !== "string") {
138
+ throw new Error("Invalid tracker issue payload: missing title");
139
+ }
140
+ if (typeof url !== "string" || url.length === 0) {
141
+ throw new Error("Invalid tracker issue payload: missing issue URL");
142
+ }
143
+ if (typeof state !== "string" || state.length === 0) {
144
+ throw new Error("Invalid tracker issue payload: missing state");
145
+ }
146
+ return {
147
+ number,
148
+ title,
149
+ body: typeof body === "string" ? body : "",
150
+ url,
151
+ state,
152
+ };
153
+ }
154
+ export async function resolveTrackerLocalSpec(
155
+ { repo, issue },
156
+ { env = process.env, ghCommand = "gh" } = {},
157
+ ) {
158
+ const { owner, name } = parseRepoSlug(repo);
159
+ const canonicalRepo = `${owner}/${name}`;
160
+ const result = await runChild(
161
+ ghCommand,
162
+ buildIssueViewArgs({ repo: canonicalRepo, issue }),
163
+ env,
164
+ );
165
+ if (result.code !== 0) {
166
+ const detail = result.stderr.trim() || `exit code ${result.code}`;
167
+ throw new Error(`gh command failed: ${detail}`);
168
+ }
169
+ const payload = parseJsonText(result.stdout);
170
+ const resolvedIssue = readIssuePayload(payload);
171
+ return {
172
+ ok: true,
173
+ repo: canonicalRepo,
174
+ issue: resolvedIssue.number,
175
+ issueUrl: resolvedIssue.url,
176
+ state: resolvedIssue.state,
177
+ title: resolvedIssue.title,
178
+ body: resolvedIssue.body,
179
+ canonicalSpecSource: "tracker_issue",
180
+ localImplementationMode: "tracker_backed",
181
+ localPhaseDocAllowed: false,
182
+ stateSync: "tracker_issue_is_canonical",
183
+ };
184
+ }
185
+ export async function runCli(
186
+ argv = process.argv.slice(2),
187
+ { stdout = process.stdout, env = process.env, ghCommand = "gh" } = {},
188
+ ) {
189
+ const options = parseResolveTrackerLocalSpecCliArgs(argv);
190
+ if (options.help) {
191
+ stdout.write(`${USAGE}\n`);
192
+ return;
193
+ }
194
+ const result = await resolveTrackerLocalSpec(
195
+ { repo: options.repo, issue: options.issue },
196
+ { env, ghCommand },
197
+ );
198
+ stdout.write(`${JSON.stringify(result)}\n`);
199
+ }
200
+ if (isDirectCliRun(import.meta.url)) {
201
+ runCli().catch((error) => {
202
+ process.stderr.write(`${formatCliError(error)}\n`);
203
+ process.exitCode = 1;
204
+ });
205
+ }
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
6
+ import { parsePositiveInteger, requireOptionValue } from "../_cli-primitives.mjs";
7
+ import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
8
+ import { buildDraftReviewPayload } from "@dev-loops/core/loop/reviewer-loop-state";
9
+ const HELP = `Usage: stage-reviewer-draft.mjs --repo <owner/name> --pr <number> --review-file <path> [--local-state-output <path>]
10
+ Stage a pending draft review on a GitHub pull request.
11
+ Options:
12
+ --repo <owner/name> GitHub repository slug (required)
13
+ --pr <number> Pull request number (required)
14
+ --review-file <path> Path to JSON file containing review payload (required)
15
+ --local-state-output <path> Path to write local state snapshot (optional)
16
+ --help, -h Show this help
17
+ Exit codes:
18
+ 0 Success
19
+ 1 Error
20
+ `;
21
+ export function parseStageDraftCliArgs(argv) {
22
+ const args = [...argv];
23
+ const options = {
24
+ repo: undefined,
25
+ pr: undefined,
26
+ reviewFile: undefined,
27
+ localStateOutput: undefined,
28
+ help: false,
29
+ };
30
+ while (args.length > 0) {
31
+ const token = args.shift();
32
+ if (token === "--help" || token === "-h") {
33
+ options.help = true;
34
+ return options;
35
+ }
36
+ if (token === "--repo") {
37
+ options.repo = requireOptionValue(args, "--repo").trim();
38
+ continue;
39
+ }
40
+ if (token === "--pr") {
41
+ options.pr = parsePositiveInteger(requireOptionValue(args, "--pr"), "--pr");
42
+ continue;
43
+ }
44
+ if (token === "--review-file") {
45
+ options.reviewFile = requireOptionValue(args, "--review-file");
46
+ continue;
47
+ }
48
+ if (token === "--local-state-output") {
49
+ options.localStateOutput = requireOptionValue(args, "--local-state-output");
50
+ continue;
51
+ }
52
+ throw new Error(`Unknown argument: ${token}`);
53
+ }
54
+ if (!options.repo || !options.pr || !options.reviewFile) {
55
+ throw new Error(
56
+ "Staging a reviewer draft requires --repo <owner/name>, --pr <number>, and --review-file <path>",
57
+ );
58
+ }
59
+ parseRepoSlug(options.repo);
60
+ return options;
61
+ }
62
+ function runChild(command, args, env, stdinText) {
63
+ return new Promise((resolve, reject) => {
64
+ const child = spawn(command, args, {
65
+ env,
66
+ stdio: ["pipe", "pipe", "pipe"],
67
+ });
68
+ let stdout = "";
69
+ let stderr = "";
70
+ child.stdout.on("data", (chunk) => {
71
+ stdout += String(chunk);
72
+ });
73
+ child.stderr.on("data", (chunk) => {
74
+ stderr += String(chunk);
75
+ });
76
+ if (stdinText === undefined) {
77
+ child.stdin.end();
78
+ } else {
79
+ child.stdin.end(stdinText);
80
+ }
81
+ child.on("error", reject);
82
+ child.on("close", (code) => {
83
+ resolve({ code, stdout, stderr });
84
+ });
85
+ });
86
+ }
87
+ function parseJson(text) {
88
+ try {
89
+ return JSON.parse(text);
90
+ } catch {
91
+ throw new Error(`Invalid JSON from gh: ${text.trim() || "<empty>"}`);
92
+ }
93
+ }
94
+ function parseDraftReviewResponse(payload) {
95
+ const reviewId = payload?.id;
96
+ const reviewUrl = typeof payload?.html_url === "string"
97
+ ? payload.html_url
98
+ : (typeof payload?._links?.html?.href === "string" ? payload._links.html.href : null);
99
+ const state = typeof payload?.state === "string" ? payload.state.toUpperCase() : null;
100
+ const commitSha = typeof payload?.commit_id === "string" && payload.commit_id.trim().length > 0
101
+ ? payload.commit_id.trim()
102
+ : null;
103
+ if (!Number.isFinite(reviewId) || !reviewUrl || state !== "PENDING" || !commitSha) {
104
+ throw new Error("Draft review payload from gh did not include id, url, PENDING state, and commit_id");
105
+ }
106
+ return { reviewId, reviewUrl, state, commitSha };
107
+ }
108
+ async function postDraftReview({ repo, pr, reviewPayload }, { env = process.env, ghCommand = "gh" } = {}) {
109
+ const result = await runChild(
110
+ ghCommand,
111
+ ["api", "-X", "POST", `repos/${repo}/pulls/${pr}/reviews`, "--input", "-"],
112
+ env,
113
+ `${JSON.stringify(reviewPayload)}\n`,
114
+ );
115
+ if (result.code !== 0) {
116
+ const detail = result.stderr.trim() || `exit code ${result.code}`;
117
+ throw new Error(`gh command failed: ${detail}`);
118
+ }
119
+ return parseJson(result.stdout);
120
+ }
121
+ async function writeLocalState(pathname, fragment) {
122
+ if (!pathname) {
123
+ return null;
124
+ }
125
+ let current = {};
126
+ try {
127
+ const text = await readFile(pathname, "utf8");
128
+ const parsed = parseJsonText(text);
129
+ if (parsed && typeof parsed === "object") {
130
+ current = parsed;
131
+ }
132
+ } catch (error) {
133
+ if (error && error.code !== "ENOENT") {
134
+ throw error;
135
+ }
136
+ }
137
+ const next = {
138
+ ...current,
139
+ draftReviewPrepared: true,
140
+ draftReviewPosted: true,
141
+ draftReviewId: fragment.reviewId,
142
+ draftReviewUrl: fragment.reviewUrl,
143
+ draftReviewCommitSha: fragment.commitSha,
144
+ draftReviewNotificationStatus: "none",
145
+ };
146
+ await mkdir(path.dirname(pathname), { recursive: true });
147
+ await writeFile(pathname, `${JSON.stringify(next, null, 2)}\n`, "utf8");
148
+ return pathname;
149
+ }
150
+ export async function runCli(
151
+ argv = process.argv.slice(2),
152
+ {
153
+ stdout = process.stdout,
154
+ env = process.env,
155
+ ghCommand = "gh",
156
+ } = {},
157
+ ) {
158
+ const options = parseStageDraftCliArgs(argv);
159
+ if (options.help) {
160
+ stdout.write(HELP);
161
+ return;
162
+ }
163
+ const rawReview = parseJsonText(await readFile(options.reviewFile, "utf8"));
164
+ if (!rawReview || typeof rawReview !== "object") {
165
+ throw new Error("--review-file must contain a JSON object");
166
+ }
167
+ const reviewPayload = buildDraftReviewPayload(rawReview);
168
+ if (!reviewPayload.commit_id) {
169
+ throw new Error("Merged review payload must include headSha so the pending review is pinned to a commit");
170
+ }
171
+ const draftReview = parseDraftReviewResponse(
172
+ await postDraftReview({ repo: options.repo, pr: options.pr, reviewPayload }, { env, ghCommand }),
173
+ );
174
+ const localStatePath = await writeLocalState(options.localStateOutput, draftReview);
175
+ stdout.write(`${JSON.stringify({
176
+ ok: true,
177
+ repo: options.repo,
178
+ pr: options.pr,
179
+ reviewId: draftReview.reviewId,
180
+ reviewUrl: draftReview.reviewUrl,
181
+ reviewState: draftReview.state,
182
+ commitSha: draftReview.commitSha,
183
+ localStatePath,
184
+ })}\n`);
185
+ }
186
+ if (isDirectCliRun(import.meta.url)) {
187
+ runCli().catch((error) => {
188
+ process.stderr.write(`${formatCliError(error)}\n`);
189
+ process.exitCode = 1;
190
+ });
191
+ }