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.
- package/.pi/dev-loop/defaults.yaml +477 -0
- package/AGENTS.md +25 -0
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/agents/dev-loop.agent.md +82 -0
- package/agents/developer.agent.md +37 -0
- package/agents/docs.agent.md +33 -0
- package/agents/fixer.agent.md +53 -0
- package/agents/quality.agent.md +28 -0
- package/agents/refiner.agent.md +87 -0
- package/agents/review.agent.md +64 -0
- package/cli/index.mjs +424 -0
- package/extension/README.md +233 -0
- package/extension/checks.ts +94 -0
- package/extension/index.ts +131 -0
- package/extension/post-merge-update.ts +512 -0
- package/extension/presentation.ts +107 -0
- package/lib/dev-loops-core.mjs +284 -0
- package/package.json +103 -0
- package/scripts/README.md +1007 -0
- package/scripts/_cli-primitives.mjs +10 -0
- package/scripts/_core-helpers.mjs +30 -0
- package/scripts/docs/validate-links.mjs +567 -0
- package/scripts/docs/validate-no-duplicate-rules.mjs +250 -0
- package/scripts/github/_review-thread-mutations.mjs +214 -0
- package/scripts/github/capture-review-threads.mjs +180 -0
- package/scripts/github/create-draft-pr.mjs +108 -0
- package/scripts/github/detect-checkpoint-evidence.mjs +393 -0
- package/scripts/github/detect-linked-issue-pr.mjs +331 -0
- package/scripts/github/manage-sub-issues.mjs +394 -0
- package/scripts/github/probe-copilot-review.mjs +323 -0
- package/scripts/github/ready-for-review.mjs +93 -0
- package/scripts/github/reconcile-draft-gate.mjs +328 -0
- package/scripts/github/reply-resolve-review-thread.mjs +42 -0
- package/scripts/github/reply-resolve-review-threads.mjs +329 -0
- package/scripts/github/request-copilot-review.mjs +551 -0
- package/scripts/github/resolve-tracker-local-spec.mjs +205 -0
- package/scripts/github/stage-reviewer-draft.mjs +191 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +694 -0
- package/scripts/github/verify-fresh-review-context.mjs +125 -0
- package/scripts/github/write-gate-findings-log.mjs +212 -0
- package/scripts/loop/_checkpoint-io.mjs +55 -0
- package/scripts/loop/_checkpoint-paths.mjs +28 -0
- package/scripts/loop/_handoff-contract.mjs +230 -0
- package/scripts/loop/_inspect-run-viewer-adapter.mjs +345 -0
- package/scripts/loop/_loop-evidence.mjs +32 -0
- package/scripts/loop/_pr-runner-coordination.mjs +611 -0
- package/scripts/loop/_stale-runner-detection.mjs +145 -0
- package/scripts/loop/_steering-state-file.mjs +134 -0
- package/scripts/loop/build-handoff-envelope.mjs +181 -0
- package/scripts/loop/checkpoint-contract.mjs +49 -0
- package/scripts/loop/conductor-monitor.mjs +1850 -0
- package/scripts/loop/conductor.mjs +214 -0
- package/scripts/loop/copilot-pr-handoff.mjs +493 -0
- package/scripts/loop/debt-remediate.mjs +304 -0
- package/scripts/loop/detect-change-scope.mjs +102 -0
- package/scripts/loop/detect-copilot-loop-state.mjs +454 -0
- package/scripts/loop/detect-copilot-session-activity.mjs +186 -0
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +318 -0
- package/scripts/loop/detect-internal-only-pr.mjs +270 -0
- package/scripts/loop/detect-issue-refinement-artifact.mjs +163 -0
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +509 -0
- package/scripts/loop/detect-reviewer-loop-state.mjs +231 -0
- package/scripts/loop/detect-stale-runner.mjs +250 -0
- package/scripts/loop/detect-tracker-first-loop-state.mjs +76 -0
- package/scripts/loop/detect-tracker-pr-state.mjs +102 -0
- package/scripts/loop/info.mjs +267 -0
- package/scripts/loop/inspect-run-viewer/cli.mjs +117 -0
- package/scripts/loop/inspect-run-viewer/constants.mjs +80 -0
- package/scripts/loop/inspect-run-viewer/graph.mjs +757 -0
- package/scripts/loop/inspect-run-viewer/handoff-envelope-renderer.mjs +398 -0
- package/scripts/loop/inspect-run-viewer/inbox.mjs +308 -0
- package/scripts/loop/inspect-run-viewer/managed-instance.mjs +750 -0
- package/scripts/loop/inspect-run-viewer/rendering.mjs +411 -0
- package/scripts/loop/inspect-run-viewer/server.mjs +638 -0
- package/scripts/loop/inspect-run-viewer/shared.mjs +103 -0
- package/scripts/loop/inspect-run-viewer/status.mjs +715 -0
- package/scripts/loop/inspect-run-viewer-ci-changes.mjs +77 -0
- package/scripts/loop/inspect-run-viewer.mjs +82 -0
- package/scripts/loop/inspect-run.mjs +382 -0
- package/scripts/loop/outer-loop.mjs +419 -0
- package/scripts/loop/pr-runner-coordination.mjs +143 -0
- package/scripts/loop/pre-commit-branch-guard.mjs +68 -0
- package/scripts/loop/pre-flight-gate.mjs +236 -0
- package/scripts/loop/pre-pr-ready-gate.mjs +183 -0
- package/scripts/loop/pre-push-main-guard.mjs +103 -0
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +32 -0
- package/scripts/loop/print-gates.mjs +42 -0
- package/scripts/loop/resolve-dev-loop-startup.mjs +533 -0
- package/scripts/loop/run-conductor-cycle.mjs +322 -0
- package/scripts/loop/run-queue.mjs +124 -0
- package/scripts/loop/run-refinement-audit.mjs +513 -0
- package/scripts/loop/run-watch-cycle.mjs +358 -0
- package/scripts/loop/steer-loop.mjs +841 -0
- package/scripts/loop/ui-designer-review-contract.mjs +76 -0
- package/scripts/loop/watch-initial-copilot-pr.mjs +253 -0
- package/scripts/projects/add-queue-item.mjs +528 -0
- package/scripts/projects/ensure-queue-board.mjs +837 -0
- package/scripts/projects/list-queue-items.mjs +489 -0
- package/scripts/projects/move-queue-item.mjs +549 -0
- package/scripts/projects/reorder-queue-item.mjs +518 -0
- package/scripts/refine/_refine-helpers.mjs +258 -0
- package/scripts/refine/prose-linkage-detector.mjs +92 -0
- package/scripts/refine/refinement-completeness-checker.mjs +88 -0
- package/scripts/refine/scope-boundary-cross-checker.mjs +163 -0
- package/scripts/refine/tree-integrity-validator.mjs +211 -0
- package/scripts/refine/verify.mjs +178 -0
- package/scripts/repo-wiki-local.mjs +156 -0
- package/scripts/repo-wiki.mjs +119 -0
- package/skills/copilot-pr-followup/SKILL.md +380 -0
- package/skills/dev-loop/SKILL.md +141 -0
- package/skills/dev-loop/scripts/dev-mode-context.mjs +152 -0
- package/skills/dev-loop/scripts/dev-mode-context.test.mjs +80 -0
- package/skills/dev-loop/scripts/init-phase.mjs +71 -0
- package/skills/dev-loop/scripts/log-bash-exit-1.mjs +25 -0
- package/skills/dev-loop/scripts/phase-files.mjs +29 -0
- package/skills/dev-loop/scripts/post-gate-verdict-fallback.mjs +480 -0
- package/skills/dev-loop/scripts/post-gate-verdict-fallback.test.mjs +732 -0
- package/skills/dev-loop/scripts/render-template.mjs +82 -0
- package/skills/dev-loop/scripts/render-template.test.mjs +63 -0
- package/skills/dev-loop/templates/bootstrap-agents.md +26 -0
- package/skills/dev-loop/templates/bootstrap-implementation-state.md +31 -0
- package/skills/dev-loop/templates/bootstrap-implementation-workflow.md +17 -0
- package/skills/dev-loop/templates/dev-mode-retrospective.md +15 -0
- package/skills/dev-loop/templates/dev-mode-review.md +17 -0
- package/skills/dev-loop/templates/dev-mode-skill-changes.md +11 -0
- package/skills/dev-loop/templates/merged-phase-plan.md +19 -0
- package/skills/dev-loop/templates/phase-doc.md +27 -0
- package/skills/dev-loop/templates/phase-summary.md +13 -0
- package/skills/dev-loop/templates/phase-variant.md +15 -0
- package/skills/dev-loop/templates/retrospective.md +11 -0
- package/skills/dev-loop/templates/review.md +32 -0
- package/skills/dev-loop/templates/ui-vision-review.md +55 -0
- package/skills/docs/acceptance-criteria-verification.md +21 -0
- package/skills/docs/anti-patterns.md +21 -0
- package/skills/docs/artifact-authority-contract.md +119 -0
- package/skills/docs/confirmation-rules.md +28 -0
- package/skills/docs/copilot-ci-status-contract.md +52 -0
- package/skills/docs/copilot-loop-operations.md +233 -0
- package/skills/docs/debt-remediation-contract.md +107 -0
- package/skills/docs/entrypoint-strategies.md +115 -0
- package/skills/docs/epic-tree-refinement-procedure.md +234 -0
- package/skills/docs/issue-intake-procedure.md +235 -0
- package/skills/docs/main-agent-contract.md +72 -0
- package/skills/docs/merge-preconditions.md +29 -0
- package/skills/docs/pr-lifecycle-contract.md +209 -0
- package/skills/docs/public-dev-loop-contract.md +497 -0
- package/skills/docs/retrospective-checkpoint-contract.md +159 -0
- package/skills/docs/stop-conditions.md +29 -0
- package/skills/docs/structural-quality.md +42 -0
- package/skills/docs/tracker-first-loop-state.md +281 -0
- package/skills/docs/validation-policy.md +27 -0
- package/skills/docs/workflow-handoff-contract.md +135 -0
- package/skills/final-approval/SKILL.md +19 -0
- package/skills/local-implementation/SKILL.md +640 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parsePrNumber, requireOptionValue, runChild } from "../_cli-primitives.mjs";
|
|
5
|
+
import {
|
|
6
|
+
buildParseError,
|
|
7
|
+
formatCliError,
|
|
8
|
+
isCopilotLogin,
|
|
9
|
+
isDirectCliRun,
|
|
10
|
+
parseJsonText,
|
|
11
|
+
classifyReviewThreadsSignal,
|
|
12
|
+
parseReviewThreads,
|
|
13
|
+
summarizeCopilotReviews,
|
|
14
|
+
} from "../_core-helpers.mjs";
|
|
15
|
+
import { fetchGithubReviewThreadsPayload } from "../github/capture-review-threads.mjs";
|
|
16
|
+
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
17
|
+
import { loadDevLoopConfig, resolveRefinement } from "@dev-loops/core/config";
|
|
18
|
+
import {
|
|
19
|
+
buildSnapshotFromPrFacts,
|
|
20
|
+
interpretLoopState,
|
|
21
|
+
normalizeSnapshot,
|
|
22
|
+
summarizeLoopInterpretation,
|
|
23
|
+
} from "@dev-loops/core/loop/copilot-loop-state";
|
|
24
|
+
import {
|
|
25
|
+
normalizeStatusCheckRollupContract,
|
|
26
|
+
summarizeHeadScopedCheckRunsSignal,
|
|
27
|
+
normalizeHeadScopedCommitStatus,
|
|
28
|
+
normalizeHeadScopedCiContract,
|
|
29
|
+
} from "@dev-loops/core/loop/copilot-ci-status";
|
|
30
|
+
const USAGE = `Usage:
|
|
31
|
+
detect-copilot-loop-state.mjs --repo <owner/name> --pr <number>
|
|
32
|
+
detect-copilot-loop-state.mjs --input <path>
|
|
33
|
+
Detect or interpret the current Copilot-loop state.
|
|
34
|
+
Modes:
|
|
35
|
+
Auto-detect Fetch live PR/GitHub facts and interpret loop state.
|
|
36
|
+
Requires: --repo, --pr
|
|
37
|
+
Snapshot Interpret a pre-built snapshot JSON without any gh calls.
|
|
38
|
+
Requires: --input
|
|
39
|
+
Required (auto-detect mode):
|
|
40
|
+
--repo <owner/name> Repository slug (e.g. owner/repo)
|
|
41
|
+
--pr <number> Pull request number
|
|
42
|
+
Required (snapshot mode):
|
|
43
|
+
--input <path> Path to snapshot JSON file
|
|
44
|
+
Optional (auto-detect mode only):
|
|
45
|
+
Optional (auto-detect mode only):
|
|
46
|
+
Output (stdout, JSON):
|
|
47
|
+
{ "ok": true, "snapshot": {..., "copilotReviewRoundCount": N}, "state": "...", "allowedTransitions": [...], "nextAction": "...",
|
|
48
|
+
"autoRerequestEligible": true|false, "sameHeadCleanConverged": true|false,
|
|
49
|
+
"loopDisposition": "...", "terminal": true|false }
|
|
50
|
+
Error output (stderr, JSON):
|
|
51
|
+
Argument/usage errors:
|
|
52
|
+
{ "ok": false, "error": "...", "usage": "..." }
|
|
53
|
+
gh/runtime failures:
|
|
54
|
+
{ "ok": false, "error": "..." }
|
|
55
|
+
Exit codes:
|
|
56
|
+
0 Success
|
|
57
|
+
1 Argument error, gh failure, or indeterminate state`.trim();
|
|
58
|
+
const VALID_OVERRIDE_STATUSES = new Set(["requested", "already-requested", "unavailable", "none", "failed"]);
|
|
59
|
+
const parseError = buildParseError(USAGE);
|
|
60
|
+
export function parseDetectCliArgs(argv) {
|
|
61
|
+
const args = [...argv];
|
|
62
|
+
const options = {
|
|
63
|
+
help: false,
|
|
64
|
+
inputPath: undefined,
|
|
65
|
+
repo: undefined,
|
|
66
|
+
pr: undefined,
|
|
67
|
+
};
|
|
68
|
+
while (args.length > 0) {
|
|
69
|
+
const token = args.shift();
|
|
70
|
+
if (token === "--help" || token === "-h") {
|
|
71
|
+
options.help = true;
|
|
72
|
+
return options;
|
|
73
|
+
}
|
|
74
|
+
if (token === "--input") {
|
|
75
|
+
options.inputPath = requireOptionValue(args, "--input", parseError);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (token === "--repo") {
|
|
79
|
+
options.repo = requireOptionValue(args, "--repo", parseError).trim();
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (token === "--pr") {
|
|
83
|
+
options.pr = parsePrNumber(requireOptionValue(args, "--pr", parseError), parseError);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
throw parseError(`Unknown argument: ${token}`);
|
|
87
|
+
}
|
|
88
|
+
if (options.inputPath !== undefined) {
|
|
89
|
+
if (options.repo !== undefined || options.pr !== undefined) {
|
|
90
|
+
throw parseError("Choose exactly one input source: --input <path> or --repo/--pr auto-detect");
|
|
91
|
+
}
|
|
92
|
+
return options;
|
|
93
|
+
}
|
|
94
|
+
const hasRepo = options.repo !== undefined;
|
|
95
|
+
const hasPr = options.pr !== undefined;
|
|
96
|
+
if (hasRepo || hasPr) {
|
|
97
|
+
if (!hasRepo || !hasPr) {
|
|
98
|
+
throw parseError("Auto-detect mode requires both --repo <owner/name> and --pr <number>");
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
parseRepoSlug(options.repo);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
throw parseError(error instanceof Error ? error.message : String(error));
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
throw parseError("Provide either --input <path> or --repo <owner/name> --pr <number>");
|
|
107
|
+
}
|
|
108
|
+
return options;
|
|
109
|
+
}
|
|
110
|
+
async function fetchPrView({ repo, pr }, { env, ghCommand }) {
|
|
111
|
+
const result = await runChild(
|
|
112
|
+
ghCommand,
|
|
113
|
+
["pr", "view", String(pr), "--repo", repo, "--json", "headRefOid,isDraft,state,number,reviews,statusCheckRollup"],
|
|
114
|
+
env,
|
|
115
|
+
);
|
|
116
|
+
if (result.code !== 0) {
|
|
117
|
+
const detail = result.stderr.trim() || `exit code ${result.code}`;
|
|
118
|
+
if (/no pull requests found/i.test(detail) || /could not find pull request/i.test(detail)) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
throw new Error(`gh command failed: ${detail}`);
|
|
122
|
+
}
|
|
123
|
+
let payload;
|
|
124
|
+
try {
|
|
125
|
+
payload = JSON.parse(result.stdout);
|
|
126
|
+
} catch {
|
|
127
|
+
throw new Error(`Invalid JSON from gh: ${result.stdout.trim() || "<empty>"}`);
|
|
128
|
+
}
|
|
129
|
+
return payload;
|
|
130
|
+
}
|
|
131
|
+
async function fetchCopilotRequested({ repo, pr }, { env, ghCommand }) {
|
|
132
|
+
const result = await runChild(
|
|
133
|
+
ghCommand,
|
|
134
|
+
["api", `repos/${repo}/pulls/${pr}/requested_reviewers`],
|
|
135
|
+
env,
|
|
136
|
+
);
|
|
137
|
+
if (result.code !== 0) {
|
|
138
|
+
const detail = result.stderr.trim() || `exit code ${result.code}`;
|
|
139
|
+
throw new Error(`gh command failed: ${detail}`);
|
|
140
|
+
}
|
|
141
|
+
let payload;
|
|
142
|
+
try {
|
|
143
|
+
payload = JSON.parse(result.stdout);
|
|
144
|
+
} catch {
|
|
145
|
+
throw new Error(`Invalid JSON from gh: ${result.stdout.trim() || "<empty>"}`);
|
|
146
|
+
}
|
|
147
|
+
const users = Array.isArray(payload?.users) ? payload.users : [];
|
|
148
|
+
return users.some((user) => isCopilotLogin(user?.login));
|
|
149
|
+
}
|
|
150
|
+
async function fetchLatestCopilotReviewRequestAt({ repo, pr }, { env, ghCommand }) {
|
|
151
|
+
const result = await runChild(
|
|
152
|
+
ghCommand,
|
|
153
|
+
["api", `repos/${repo}/issues/${pr}/timeline`, "--paginate", "--jq",
|
|
154
|
+
'.[] | select(.event == "review_requested") | select(.requested_reviewer.login != null) | {login: .requested_reviewer.login, created_at: .created_at}'],
|
|
155
|
+
env,
|
|
156
|
+
);
|
|
157
|
+
if (result.code !== 0) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
let latestAt = null;
|
|
161
|
+
for (const line of result.stdout.trim().split("\n")) {
|
|
162
|
+
if (!line) continue;
|
|
163
|
+
try {
|
|
164
|
+
const event = JSON.parse(line);
|
|
165
|
+
if (isCopilotLogin(event?.login)) {
|
|
166
|
+
if (latestAt === null || event.created_at > latestAt) {
|
|
167
|
+
latestAt = event.created_at;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return latestAt;
|
|
174
|
+
}
|
|
175
|
+
function extractPrVisibleCheckNames(statusCheckRollup) {
|
|
176
|
+
if (!Array.isArray(statusCheckRollup)) return [];
|
|
177
|
+
return statusCheckRollup
|
|
178
|
+
.map((entry) => entry?.name || entry?.context)
|
|
179
|
+
.filter((name) => typeof name === "string" && name.length > 0);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function fetchCurrentHeadCiEvidence({ repo, headSha, prVisibleCheckNames }, { env, ghCommand }) {
|
|
183
|
+
const [checkRunsResult, statusesResult] = await Promise.all([
|
|
184
|
+
runChild(
|
|
185
|
+
ghCommand,
|
|
186
|
+
["api", `repos/${repo}/commits/${headSha}/check-runs?per_page=100`],
|
|
187
|
+
env,
|
|
188
|
+
),
|
|
189
|
+
runChild(
|
|
190
|
+
ghCommand,
|
|
191
|
+
["api", `repos/${repo}/commits/${headSha}/status?per_page=100`],
|
|
192
|
+
env,
|
|
193
|
+
),
|
|
194
|
+
]);
|
|
195
|
+
let checkRunsSignal = null;
|
|
196
|
+
let checkRunsCount = null;
|
|
197
|
+
if (checkRunsResult.code === 0) {
|
|
198
|
+
try {
|
|
199
|
+
const payload = JSON.parse(checkRunsResult.stdout);
|
|
200
|
+
if (Array.isArray(payload?.check_runs)) {
|
|
201
|
+
const visibleSet = prVisibleCheckNames?.length > 0 ? new Set(prVisibleCheckNames) : null;
|
|
202
|
+
const visibleRuns = visibleSet
|
|
203
|
+
? payload.check_runs.filter((run) => !run.name || visibleSet.has(run.name))
|
|
204
|
+
: payload.check_runs;
|
|
205
|
+
// Use visible runs for the status signal, but preserve the full-set
|
|
206
|
+
// unsupportedCompleted flag so hidden check-runs still contribute to
|
|
207
|
+
// the cautious none override (#740).
|
|
208
|
+
const visibleSignal = summarizeHeadScopedCheckRunsSignal({ check_runs: visibleRuns });
|
|
209
|
+
const fullSignal = summarizeHeadScopedCheckRunsSignal(payload);
|
|
210
|
+
const excludedRuns = visibleSet
|
|
211
|
+
? payload.check_runs.filter((run) => run.name && !visibleSet.has(run.name))
|
|
212
|
+
: [];
|
|
213
|
+
const excludedSignal = summarizeHeadScopedCheckRunsSignal({ check_runs: excludedRuns });
|
|
214
|
+
checkRunsSignal = { ...visibleSignal, unsupportedCompleted: fullSignal.unsupportedCompleted, excludedFailureDetails: excludedSignal.failureDetails ?? [] };
|
|
215
|
+
checkRunsCount = payload.check_runs.length;
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
checkRunsSignal = null;
|
|
219
|
+
checkRunsCount = null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
let commitStatus = null;
|
|
223
|
+
let statusesCount = null;
|
|
224
|
+
if (statusesResult.code === 0) {
|
|
225
|
+
try {
|
|
226
|
+
const payload = JSON.parse(statusesResult.stdout);
|
|
227
|
+
if (Array.isArray(payload?.statuses)) {
|
|
228
|
+
commitStatus = normalizeHeadScopedCommitStatus(payload);
|
|
229
|
+
statusesCount = payload.statuses.length;
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
commitStatus = null;
|
|
233
|
+
statusesCount = null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (checkRunsSignal === null && commitStatus === null) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
status: normalizeHeadScopedCiContract({
|
|
241
|
+
checkRunsStatus: checkRunsSignal?.status ?? "none",
|
|
242
|
+
commitStatus: commitStatus ?? "none",
|
|
243
|
+
checkRunsUnsupportedCompleted: checkRunsSignal?.unsupportedCompleted ?? false,
|
|
244
|
+
}).overallStatus,
|
|
245
|
+
observedZeroSuitesAndStatuses: checkRunsCount === 0 && statusesCount === 0,
|
|
246
|
+
failureDetails: checkRunsSignal?.failureDetails ?? [],
|
|
247
|
+
excludedFailureDetails: checkRunsSignal?.excludedFailureDetails ?? [],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function hasLocalValidationForCurrentHead(localValidationHeadSha, currentHeadSha) {
|
|
251
|
+
if (typeof localValidationHeadSha !== "string" || typeof currentHeadSha !== "string") {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
const normalizedValidationHeadSha = localValidationHeadSha.trim().toLowerCase();
|
|
255
|
+
const normalizedCurrentHeadSha = currentHeadSha.trim().toLowerCase();
|
|
256
|
+
return normalizedValidationHeadSha.length > 0
|
|
257
|
+
&& normalizedCurrentHeadSha.length > 0
|
|
258
|
+
&& normalizedCurrentHeadSha.startsWith(normalizedValidationHeadSha);
|
|
259
|
+
}
|
|
260
|
+
function shouldPromoteCrediblyGreen({
|
|
261
|
+
refreshedCurrentHeadCi,
|
|
262
|
+
fallbackCiStatus,
|
|
263
|
+
localValidationHeadSha,
|
|
264
|
+
currentHeadSha,
|
|
265
|
+
reviewSummary,
|
|
266
|
+
unresolvedThreadCount,
|
|
267
|
+
actionableThreadCount,
|
|
268
|
+
}) {
|
|
269
|
+
return refreshedCurrentHeadCi?.status === "none"
|
|
270
|
+
&& refreshedCurrentHeadCi?.observedZeroSuitesAndStatuses === true
|
|
271
|
+
&& fallbackCiStatus === "success"
|
|
272
|
+
&& hasLocalValidationForCurrentHead(localValidationHeadSha, currentHeadSha)
|
|
273
|
+
&& reviewSummary?.hasSubmittedReviewOnCurrentHead === true
|
|
274
|
+
&& unresolvedThreadCount === 0
|
|
275
|
+
&& actionableThreadCount === 0;
|
|
276
|
+
}
|
|
277
|
+
function hasSubmittedCopilotReviewOffCurrentHead(reviewSummary, currentHeadSha) {
|
|
278
|
+
if (currentHeadSha == null) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
const reviews = Array.isArray(reviewSummary?.copilotReviews) ? reviewSummary.copilotReviews : [];
|
|
282
|
+
for (const review of reviews) {
|
|
283
|
+
const state = typeof review?.state === "string" ? review.state.toUpperCase() : "";
|
|
284
|
+
if (state === "PENDING") {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const commitSha = typeof review?.commit?.oid === "string"
|
|
288
|
+
? review.commit.oid
|
|
289
|
+
: (typeof review?.commit_id === "string" ? review.commit_id : null);
|
|
290
|
+
if (commitSha && commitSha !== currentHeadSha) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
export async function autoDetectSnapshot({ repo, pr, reviewRequestStatusOverride, localValidationHeadSha, draftGateResetAtMs }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
299
|
+
const prData = await fetchPrView({ repo, pr }, { env, ghCommand });
|
|
300
|
+
if (prData === null) {
|
|
301
|
+
return normalizeSnapshot({ prExists: false });
|
|
302
|
+
}
|
|
303
|
+
const prState = typeof prData.state === "string" ? prData.state.toUpperCase() : "OPEN";
|
|
304
|
+
const prMerged = prState === "MERGED";
|
|
305
|
+
const prClosed = prState === "CLOSED";
|
|
306
|
+
if (prMerged || prClosed) {
|
|
307
|
+
return normalizeSnapshot({
|
|
308
|
+
prExists: true,
|
|
309
|
+
prNumber: typeof prData.number === "number" ? prData.number : pr,
|
|
310
|
+
prMerged,
|
|
311
|
+
prClosed,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
const prHeadSha = typeof prData.headRefOid === "string" && prData.headRefOid.trim().length > 0
|
|
315
|
+
? prData.headRefOid.trim()
|
|
316
|
+
: null;
|
|
317
|
+
const reviewSummary = summarizeCopilotReviews(prData.reviews, { headSha: prHeadSha, draftGateResetAtMs });
|
|
318
|
+
const fallbackCiStatus = normalizeStatusCheckRollupContract(prData.statusCheckRollup).overallStatus;
|
|
319
|
+
let copilotReviewRequestStatus;
|
|
320
|
+
if (reviewRequestStatusOverride !== undefined) {
|
|
321
|
+
copilotReviewRequestStatus = reviewRequestStatusOverride;
|
|
322
|
+
} else if (reviewSummary.hasPendingReviewOnCurrentHead) {
|
|
323
|
+
copilotReviewRequestStatus = "requested";
|
|
324
|
+
} else {
|
|
325
|
+
const copilotRequested = await fetchCopilotRequested({ repo, pr }, { env, ghCommand });
|
|
326
|
+
if (!copilotRequested) {
|
|
327
|
+
copilotReviewRequestStatus = "none";
|
|
328
|
+
} else if (!reviewSummary.hasSubmittedReviewOnCurrentHead) {
|
|
329
|
+
copilotReviewRequestStatus = "requested";
|
|
330
|
+
} else {
|
|
331
|
+
const latestRequestAt = await fetchLatestCopilotReviewRequestAt({ repo, pr }, { env, ghCommand });
|
|
332
|
+
const latestReviewAt = reviewSummary.latestSubmittedReviewOnCurrentHeadAt;
|
|
333
|
+
if (latestRequestAt !== null && latestReviewAt !== null && latestRequestAt > latestReviewAt) {
|
|
334
|
+
copilotReviewRequestStatus = "requested";
|
|
335
|
+
} else if (latestRequestAt === null) {
|
|
336
|
+
copilotReviewRequestStatus = "requested";
|
|
337
|
+
} else {
|
|
338
|
+
copilotReviewRequestStatus = "none";
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
let unresolvedThreadCount = 0;
|
|
343
|
+
let actionableThreadCount = 0;
|
|
344
|
+
let lastCopilotRoundMaxSignal = null;
|
|
345
|
+
try {
|
|
346
|
+
const threadsPayload = await fetchGithubReviewThreadsPayload({ repo, pr }, { env, ghCommand });
|
|
347
|
+
const parsed = parseReviewThreads(threadsPayload);
|
|
348
|
+
unresolvedThreadCount = parsed.summary.unresolvedThreads;
|
|
349
|
+
actionableThreadCount = parsed.summary.actionableThreads;
|
|
350
|
+
lastCopilotRoundMaxSignal = classifyReviewThreadsSignal(parsed, isCopilotLogin);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
353
|
+
throw new Error(`Could not determine review-thread state: ${detail}`);
|
|
354
|
+
}
|
|
355
|
+
const shouldRefreshCurrentHeadCi =
|
|
356
|
+
prHeadSha !== null
|
|
357
|
+
&& fallbackCiStatus === "success"
|
|
358
|
+
&& (
|
|
359
|
+
hasSubmittedCopilotReviewOffCurrentHead(reviewSummary, prHeadSha)
|
|
360
|
+
|| (
|
|
361
|
+
reviewSummary.hasSubmittedReviewOnCurrentHead
|
|
362
|
+
&& hasLocalValidationForCurrentHead(localValidationHeadSha, prHeadSha)
|
|
363
|
+
)
|
|
364
|
+
);
|
|
365
|
+
const prVisibleCheckNames = extractPrVisibleCheckNames(prData.statusCheckRollup);
|
|
366
|
+
let currentHeadCiStatus = fallbackCiStatus;
|
|
367
|
+
let failureDetails = [];
|
|
368
|
+
let excludedFailureDetails = [];
|
|
369
|
+
if (shouldRefreshCurrentHeadCi) {
|
|
370
|
+
const refreshed = await fetchCurrentHeadCiEvidence({ repo, headSha: prHeadSha, prVisibleCheckNames }, { env, ghCommand });
|
|
371
|
+
currentHeadCiStatus = refreshed?.status ?? "none";
|
|
372
|
+
failureDetails = refreshed?.failureDetails ?? [];
|
|
373
|
+
excludedFailureDetails = refreshed?.excludedFailureDetails ?? [];
|
|
374
|
+
if (shouldPromoteCrediblyGreen({
|
|
375
|
+
refreshedCurrentHeadCi: refreshed,
|
|
376
|
+
fallbackCiStatus,
|
|
377
|
+
localValidationHeadSha,
|
|
378
|
+
currentHeadSha: prHeadSha,
|
|
379
|
+
reviewSummary,
|
|
380
|
+
unresolvedThreadCount,
|
|
381
|
+
actionableThreadCount,
|
|
382
|
+
})) {
|
|
383
|
+
currentHeadCiStatus = "crediblyGreen";
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return buildSnapshotFromPrFacts({
|
|
387
|
+
prData,
|
|
388
|
+
prNumber: pr,
|
|
389
|
+
copilotReviewRequestStatus,
|
|
390
|
+
copilotReviewPresent: reviewSummary.copilotReviewPresent,
|
|
391
|
+
copilotReviewOnCurrentHead: reviewSummary.hasSubmittedReviewOnCurrentHead,
|
|
392
|
+
unresolvedThreadCount,
|
|
393
|
+
actionableThreadCount,
|
|
394
|
+
copilotReviewRoundCount: reviewSummary.completedCopilotReviewRounds,
|
|
395
|
+
lastCopilotRoundMaxSignal,
|
|
396
|
+
ciStatus: currentHeadCiStatus,
|
|
397
|
+
failureDetails,
|
|
398
|
+
excludedFailureDetails,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
export async function runCli(
|
|
402
|
+
argv = process.argv.slice(2),
|
|
403
|
+
{
|
|
404
|
+
stdout = process.stdout,
|
|
405
|
+
env = process.env,
|
|
406
|
+
ghCommand = "gh",
|
|
407
|
+
} = {},
|
|
408
|
+
) {
|
|
409
|
+
const options = parseDetectCliArgs(argv);
|
|
410
|
+
if (options.help) {
|
|
411
|
+
stdout.write(`${USAGE}\n`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
let snapshot;
|
|
415
|
+
let interpretationInput;
|
|
416
|
+
if (options.inputPath !== undefined) {
|
|
417
|
+
const text = await readFile(options.inputPath, "utf8");
|
|
418
|
+
interpretationInput = parseJsonText(text);
|
|
419
|
+
snapshot = normalizeSnapshot(interpretationInput);
|
|
420
|
+
} else {
|
|
421
|
+
snapshot = await autoDetectSnapshot(
|
|
422
|
+
{
|
|
423
|
+
repo: options.repo,
|
|
424
|
+
pr: options.pr,
|
|
425
|
+
},
|
|
426
|
+
{ env, ghCommand },
|
|
427
|
+
);
|
|
428
|
+
interpretationInput = snapshot;
|
|
429
|
+
}
|
|
430
|
+
let interpretation;
|
|
431
|
+
const config = await loadDevLoopConfig({ repoRoot: path.resolve(process.cwd()) });
|
|
432
|
+
const refinementConfig = config.errors.length > 0
|
|
433
|
+
? resolveRefinement({ version: 1 })
|
|
434
|
+
: resolveRefinement(config.config);
|
|
435
|
+
interpretation = interpretLoopState(interpretationInput, refinementConfig);
|
|
436
|
+
const interpretationSummary = summarizeLoopInterpretation(interpretation);
|
|
437
|
+
stdout.write(`${JSON.stringify({
|
|
438
|
+
ok: true,
|
|
439
|
+
snapshot,
|
|
440
|
+
state: interpretation.state,
|
|
441
|
+
allowedTransitions: interpretation.allowedTransitions,
|
|
442
|
+
nextAction: interpretation.nextAction,
|
|
443
|
+
autoRerequestEligible: interpretation.autoRerequestEligible,
|
|
444
|
+
sameHeadCleanConverged: interpretation.sameHeadCleanConverged,
|
|
445
|
+
loopDisposition: interpretationSummary.loopDisposition,
|
|
446
|
+
terminal: interpretationSummary.terminal,
|
|
447
|
+
})}\n`);
|
|
448
|
+
}
|
|
449
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
450
|
+
runCli().catch((error) => {
|
|
451
|
+
process.stderr.write(`${formatCliError(error)}\n`);
|
|
452
|
+
process.exitCode = 1;
|
|
453
|
+
});
|
|
454
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
3
|
+
import { parsePositiveInteger, requireOptionValue, runChild } from "../_cli-primitives.mjs";
|
|
4
|
+
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
5
|
+
const USAGE = `Usage: detect-copilot-session-activity.mjs --repo <owner/name> --branch <name> [--limit <number>]
|
|
6
|
+
Detect Copilot GitHub Actions session activity on a branch.
|
|
7
|
+
Required:
|
|
8
|
+
--repo <owner/name> Repository slug (e.g. owner/repo)
|
|
9
|
+
--branch <name> Branch to inspect workflow runs on
|
|
10
|
+
Optional:
|
|
11
|
+
--limit <number> Number of recent runs to inspect (default: 20)
|
|
12
|
+
Activity values:
|
|
13
|
+
active Matching Copilot workflow run is currently in progress
|
|
14
|
+
concluded Most recent matching Copilot workflow run completed, or the latest
|
|
15
|
+
matching run is approval-gated in "action_required" and should be
|
|
16
|
+
treated as a non-blocking observational signal
|
|
17
|
+
idle No matching Copilot workflow run found on this branch
|
|
18
|
+
Success output (stdout, JSON):
|
|
19
|
+
{
|
|
20
|
+
"ok": true,
|
|
21
|
+
"activity": "active"|"concluded"|"idle",
|
|
22
|
+
"runId": 123456|null,
|
|
23
|
+
"runName": "..."|null,
|
|
24
|
+
"runStatus": "queued"|"in_progress"|"pending"|"requested"|"waiting"|"action_required"|"completed"|null,
|
|
25
|
+
"runConclusion": string|null,
|
|
26
|
+
"runCreatedAt": "..."|null,
|
|
27
|
+
"branch": "...",
|
|
28
|
+
"confidence": "high"
|
|
29
|
+
}
|
|
30
|
+
Error output (stderr, JSON):
|
|
31
|
+
Argument/usage errors:
|
|
32
|
+
{ "ok": false, "error": "...", "usage": "..." }
|
|
33
|
+
gh/runtime failures:
|
|
34
|
+
{ "ok": false, "error": "..." }`.trim();
|
|
35
|
+
const DEFAULT_LIMIT = 20;
|
|
36
|
+
const ACTIVE_RUN_STATUSES = new Set(["queued", "in_progress", "pending", "requested", "waiting"]);
|
|
37
|
+
const COPILOT_RUN_NAME_PATTERNS = Object.freeze([
|
|
38
|
+
/^copilot coding for issue\b/i,
|
|
39
|
+
/^addressing comment on pr\b/i,
|
|
40
|
+
/^addressing review on pr\b/i,
|
|
41
|
+
]);
|
|
42
|
+
const parseError = buildParseError(USAGE);
|
|
43
|
+
export function parseDetectCopilotSessionActivityCliArgs(argv) {
|
|
44
|
+
const args = [...argv];
|
|
45
|
+
const options = {
|
|
46
|
+
help: false,
|
|
47
|
+
repo: undefined,
|
|
48
|
+
branch: undefined,
|
|
49
|
+
limit: DEFAULT_LIMIT,
|
|
50
|
+
};
|
|
51
|
+
while (args.length > 0) {
|
|
52
|
+
const token = args.shift();
|
|
53
|
+
if (token === "--help" || token === "-h") {
|
|
54
|
+
options.help = true;
|
|
55
|
+
return options;
|
|
56
|
+
}
|
|
57
|
+
if (token === "--repo") {
|
|
58
|
+
options.repo = requireOptionValue(args, "--repo", parseError).trim();
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (token === "--branch") {
|
|
62
|
+
options.branch = requireOptionValue(args, "--branch", parseError).trim();
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (token === "--limit") {
|
|
66
|
+
options.limit = parsePositiveInteger(requireOptionValue(args, "--limit", parseError), "--limit", parseError);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
throw parseError(`Unknown argument: ${token}`);
|
|
70
|
+
}
|
|
71
|
+
if (options.repo === undefined || options.branch === undefined || options.branch.length === 0) {
|
|
72
|
+
throw parseError("detect-copilot-session-activity requires both --repo <owner/name> and --branch <name>");
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
parseRepoSlug(options.repo);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw parseError(error instanceof Error ? error.message : String(error));
|
|
78
|
+
}
|
|
79
|
+
return options;
|
|
80
|
+
}
|
|
81
|
+
function isCopilotRunName(name) {
|
|
82
|
+
if (typeof name !== "string") {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return COPILOT_RUN_NAME_PATTERNS.some((pattern) => pattern.test(name.trim()));
|
|
86
|
+
}
|
|
87
|
+
function normalizeRun(raw) {
|
|
88
|
+
if (!raw || typeof raw !== "object") {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const name = typeof raw.name === "string" ? raw.name : "";
|
|
92
|
+
if (!isCopilotRunName(name)) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const createdAt = typeof raw.createdAt === "string" ? raw.createdAt : null;
|
|
96
|
+
const createdAtMs = createdAt ? Date.parse(createdAt) : Number.NaN;
|
|
97
|
+
return {
|
|
98
|
+
runId: Number.isInteger(raw.databaseId) ? raw.databaseId : null,
|
|
99
|
+
runName: name,
|
|
100
|
+
runStatus: typeof raw.status === "string" ? raw.status : null,
|
|
101
|
+
runConclusion: typeof raw.conclusion === "string" && raw.conclusion.length > 0 ? raw.conclusion : null,
|
|
102
|
+
runCreatedAt: createdAt,
|
|
103
|
+
createdAtMs: Number.isFinite(createdAtMs) ? createdAtMs : Number.NEGATIVE_INFINITY,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function compareRunsNewestFirst(left, right) {
|
|
107
|
+
if (left.createdAtMs !== right.createdAtMs) {
|
|
108
|
+
return right.createdAtMs - left.createdAtMs;
|
|
109
|
+
}
|
|
110
|
+
return (right.runId ?? Number.NEGATIVE_INFINITY) - (left.runId ?? Number.NEGATIVE_INFINITY);
|
|
111
|
+
}
|
|
112
|
+
function toActivityPayload(activity, branch, run = null) {
|
|
113
|
+
return {
|
|
114
|
+
ok: true,
|
|
115
|
+
activity,
|
|
116
|
+
runId: run?.runId ?? null,
|
|
117
|
+
runName: run?.runName ?? null,
|
|
118
|
+
runStatus: run?.runStatus ?? null,
|
|
119
|
+
runConclusion: run?.runConclusion ?? null,
|
|
120
|
+
runCreatedAt: run?.runCreatedAt ?? null,
|
|
121
|
+
branch,
|
|
122
|
+
confidence: "high",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function isApprovalGatedActionRequired(run) {
|
|
126
|
+
const status = String(run?.runStatus ?? "").trim().toLowerCase();
|
|
127
|
+
const conclusion = String(run?.runConclusion ?? "").trim().toLowerCase();
|
|
128
|
+
return status === "action_required" || conclusion === "action_required";
|
|
129
|
+
}
|
|
130
|
+
export async function detectCopilotSessionActivity({ repo, branch, limit = DEFAULT_LIMIT }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
131
|
+
const result = await runChild(
|
|
132
|
+
ghCommand,
|
|
133
|
+
[
|
|
134
|
+
"run",
|
|
135
|
+
"list",
|
|
136
|
+
"--repo",
|
|
137
|
+
repo,
|
|
138
|
+
"--branch",
|
|
139
|
+
branch,
|
|
140
|
+
"--limit",
|
|
141
|
+
String(limit),
|
|
142
|
+
"--json",
|
|
143
|
+
"databaseId,name,status,conclusion,createdAt",
|
|
144
|
+
],
|
|
145
|
+
env,
|
|
146
|
+
);
|
|
147
|
+
if (result.code !== 0) {
|
|
148
|
+
const detail = result.stderr.trim() || `exit code ${result.code}`;
|
|
149
|
+
throw new Error(`gh command failed: ${detail}`);
|
|
150
|
+
}
|
|
151
|
+
const payload = parseJsonText(result.stdout);
|
|
152
|
+
const runs = (Array.isArray(payload) ? payload : [])
|
|
153
|
+
.map(normalizeRun)
|
|
154
|
+
.filter(Boolean)
|
|
155
|
+
.sort(compareRunsNewestFirst);
|
|
156
|
+
if (runs.length > 0) {
|
|
157
|
+
const latest = runs[0];
|
|
158
|
+
const latestStatus = String(latest.runStatus ?? "").toLowerCase();
|
|
159
|
+
if (isApprovalGatedActionRequired(latest)) {
|
|
160
|
+
return toActivityPayload("concluded", branch, latest);
|
|
161
|
+
}
|
|
162
|
+
if (ACTIVE_RUN_STATUSES.has(latestStatus)) {
|
|
163
|
+
return toActivityPayload("active", branch, latest);
|
|
164
|
+
}
|
|
165
|
+
return toActivityPayload("concluded", branch, latest);
|
|
166
|
+
}
|
|
167
|
+
return toActivityPayload("idle", branch);
|
|
168
|
+
}
|
|
169
|
+
export async function runCli(
|
|
170
|
+
argv = process.argv.slice(2),
|
|
171
|
+
{ stdout = process.stdout, env = process.env, ghCommand = "gh" } = {},
|
|
172
|
+
) {
|
|
173
|
+
const options = parseDetectCopilotSessionActivityCliArgs(argv);
|
|
174
|
+
if (options.help) {
|
|
175
|
+
stdout.write(`${USAGE}\n`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const result = await detectCopilotSessionActivity(options, { env, ghCommand });
|
|
179
|
+
stdout.write(`${JSON.stringify(result)}\n`);
|
|
180
|
+
}
|
|
181
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
182
|
+
runCli().catch((error) => {
|
|
183
|
+
process.stderr.write(`${formatCliError(error)}\n`);
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
});
|
|
186
|
+
}
|