dev-loops 0.6.0 → 0.7.1
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/.claude/.claude-plugin/plugin.json +1 -1
- package/.claude/agents/dev-loop.md +2 -1
- package/.claude/agents/review.md +2 -1
- package/.claude/commands/{auto.md → loop-auto.md} +1 -1
- package/.claude/commands/loop-continue.md +15 -0
- package/.claude/commands/loop-enqueue.md +22 -0
- package/.claude/commands/loop-grill.md +17 -0
- package/.claude/commands/{info.md → loop-info.md} +2 -2
- package/.claude/commands/loop-queue-status.md +24 -0
- package/.claude/commands/{start-spike.md → loop-start-spike.md} +1 -1
- package/.claude/commands/{start.md → loop-start.md} +1 -1
- package/.claude/commands/{status.md → loop-status.md} +1 -1
- package/.claude/hooks/_bash-command-classify.mjs +333 -29
- package/.claude/hooks/_hook-decisions.mjs +138 -15
- package/.claude/hooks/pre-tool-use-bash-gate.mjs +36 -15
- package/.claude/skills/copilot-pr-followup/SKILL.md +24 -12
- package/.claude/skills/dev-loop/SKILL.md +15 -11
- package/.claude/skills/docs/acceptance-criteria-verification.md +6 -2
- package/.claude/skills/docs/anti-patterns.md +2 -1
- package/.claude/skills/docs/artifact-authority-contract.md +22 -4
- package/.claude/skills/docs/copilot-loop-operations.md +1 -1
- package/.claude/skills/docs/cross-harness-regression-contract.md +60 -0
- package/.claude/skills/docs/issue-intake-procedure.md +1 -0
- package/.claude/skills/docs/local-planning-flow.md +1 -1
- package/.claude/skills/docs/local-planning-worked-example.md +1 -1
- package/.claude/skills/docs/merge-preconditions.md +17 -1
- package/.claude/skills/docs/plan-file-contract.md +1 -1
- package/.claude/skills/docs/public-dev-loop-contract.md +1 -1
- package/.claude/skills/docs/retrospective-checkpoint-contract.md +90 -76
- package/.claude/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/.claude/skills/docs/workflow-handoff-contract.md +39 -0
- package/.claude/skills/local-implementation/SKILL.md +25 -14
- package/.claude/skills/loop-grill/SKILL.md +163 -0
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +95 -0
- package/README.md +9 -9
- package/agents/dev-loop.agent.md +3 -2
- package/agents/developer.agent.md +1 -1
- package/agents/docs.agent.md +1 -1
- package/agents/fixer.agent.md +1 -1
- package/agents/quality.agent.md +1 -1
- package/agents/refiner.agent.md +1 -1
- package/agents/review.agent.md +3 -2
- package/cli/index.mjs +35 -42
- package/extension/README.md +4 -4
- package/extension/checks.ts +1 -5
- package/extension/harness-types.ts +1 -0
- package/extension/index.ts +6 -0
- package/extension/pi-extension-adapter.ts +2 -0
- package/extension/presentation.ts +7 -19
- package/package.json +9 -10
- package/scripts/github/capture-review-threads.mjs +2 -9
- package/scripts/github/comment-issue.mjs +2 -9
- package/scripts/github/create-label.mjs +133 -0
- package/scripts/github/detect-checkpoint-evidence.mjs +170 -14
- package/scripts/github/detect-linked-issue-pr.mjs +22 -6
- package/scripts/github/edit-pr.mjs +259 -0
- package/scripts/github/fetch-ci-logs.mjs +2 -9
- package/scripts/github/list-issues.mjs +2 -9
- package/scripts/github/manage-sub-issues.mjs +46 -10
- package/scripts/github/offer-human-handoff.mjs +8 -5
- package/scripts/github/post-gate-findings.mjs +14 -2
- package/scripts/github/probe-ci-status.mjs +8 -2
- package/scripts/github/probe-copilot-review.mjs +21 -14
- package/scripts/github/ready-for-review.mjs +26 -8
- package/scripts/github/reconcile-draft-gate.mjs +7 -3
- package/scripts/github/reply-resolve-review-thread.mjs +16 -5
- package/scripts/github/reply-resolve-review-threads.mjs +69 -7
- package/scripts/github/request-copilot-review.mjs +8 -2
- package/scripts/github/resolve-handoff-candidates.mjs +7 -3
- package/scripts/github/resolve-tracker-local-spec.mjs +9 -3
- package/scripts/github/stage-reviewer-draft.mjs +10 -2
- package/scripts/github/tick-verified-checkboxes.mjs +202 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +31 -10
- package/scripts/github/verify-fresh-review-context.mjs +150 -31
- package/scripts/github/view-pr.mjs +150 -0
- package/scripts/github/write-gate-context.mjs +248 -61
- package/scripts/github/write-gate-findings-log.mjs +75 -1
- package/scripts/lib/jq-output.mjs +18 -0
- package/scripts/loop/_post-convergence-change.mjs +211 -0
- package/scripts/loop/_pr-runner-coordination.mjs +70 -0
- package/scripts/loop/_repo-root-resolver.mjs +47 -0
- package/scripts/loop/build-handoff-envelope.mjs +14 -4
- package/scripts/loop/check-retro-tooling.mjs +14 -3
- package/scripts/loop/checkpoint-contract.mjs +7 -4
- package/scripts/loop/cleanup-worktree.mjs +12 -3
- package/scripts/loop/conductor-monitor.mjs +12 -18
- package/scripts/loop/copilot-pr-handoff.mjs +135 -14
- package/scripts/loop/debt-remediate.mjs +24 -12
- package/scripts/loop/detect-change-scope.mjs +36 -7
- package/scripts/loop/detect-copilot-loop-state.mjs +11 -5
- package/scripts/loop/detect-copilot-session-activity.mjs +7 -3
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +7 -3
- package/scripts/loop/detect-internal-only-pr.mjs +8 -2
- package/scripts/loop/detect-issue-refinement-artifact.mjs +6 -3
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +149 -66
- package/scripts/loop/detect-reviewer-loop-state.mjs +12 -2
- package/scripts/loop/detect-tracker-first-loop-state.mjs +9 -1
- package/scripts/loop/detect-tracker-pr-state.mjs +10 -3
- package/scripts/loop/ensure-worktree.mjs +8 -3
- package/scripts/loop/info.mjs +12 -4
- package/scripts/loop/inspect-run-viewer/constants.mjs +4 -18
- package/scripts/loop/inspect-run-viewer/vendor/mermaid.min.js +3405 -0
- package/scripts/loop/inspect-run-viewer-ci-changes.mjs +18 -6
- package/scripts/loop/inspect-run-viewer.mjs +67 -4
- package/scripts/loop/inspect-run.mjs +8 -2
- package/scripts/loop/outer-loop.mjs +8 -4
- package/scripts/loop/pr-runner-coordination.mjs +2 -9
- package/scripts/loop/pre-commit-branch-guard.mjs +16 -10
- package/scripts/loop/pre-flight-gate.mjs +9 -9
- package/scripts/loop/pre-pr-ready-gate.mjs +8 -4
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +13 -4
- package/scripts/loop/provision-worktree.mjs +74 -3
- package/scripts/loop/resolve-dev-loop-startup.mjs +76 -7
- package/scripts/loop/resolve-gate-dispatch.mjs +134 -0
- package/scripts/loop/resolve-pr-conflicts.mjs +14 -7
- package/scripts/loop/run-conductor-cycle.mjs +8 -2
- package/scripts/loop/run-queue.mjs +18 -9
- package/scripts/loop/run-refinement-audit.mjs +8 -9
- package/scripts/loop/run-watch-cycle.mjs +2 -9
- package/scripts/loop/sanctioned-commands.mjs +104 -0
- package/scripts/loop/steer-loop.mjs +29 -16
- package/scripts/loop/validate-pr-body-spec.mjs +207 -0
- package/scripts/loop/watch-initial-copilot-pr.mjs +8 -3
- package/scripts/pages/build-site.mjs +59 -8
- package/scripts/pages/build-state-atlas.mjs +441 -0
- package/scripts/projects/_resolve-project.mjs +148 -0
- package/scripts/projects/add-queue-item.mjs +60 -54
- package/scripts/projects/archive-done-items.mjs +49 -84
- package/scripts/projects/ensure-queue-board.mjs +10 -36
- package/scripts/projects/list-queue-items.mjs +116 -65
- package/scripts/projects/move-queue-item.mjs +28 -49
- package/scripts/projects/reconcile-queue.mjs +253 -0
- package/scripts/projects/reorder-queue-item.mjs +41 -47
- package/scripts/projects/resolve-active-board-item.mjs +108 -46
- package/scripts/projects/sync-item-status.mjs +15 -10
- package/scripts/refine/_refine-helpers.mjs +21 -5
- package/scripts/refine/exit-spike.mjs +18 -8
- package/scripts/refine/promote-plan.mjs +22 -16
- package/scripts/refine/prose-linkage-detector.mjs +1 -1
- package/scripts/refine/refine-plan-file.mjs +13 -4
- package/scripts/refine/refinement-completeness-checker.mjs +1 -1
- package/scripts/refine/scaffold-spike-file.mjs +4 -8
- package/scripts/refine/scope-boundary-cross-checker.mjs +1 -1
- package/scripts/refine/tree-integrity-validator.mjs +1 -1
- package/scripts/refine/validate-plan-file.mjs +1 -1
- package/scripts/refine/validate-spike-file.mjs +1 -1
- package/scripts/refine/verify.mjs +11 -6
- package/scripts/release/assert-core-dependency-version.mjs +123 -0
- package/scripts/release/extract-changelog-section.mjs +16 -2
- package/skills/copilot-pr-followup/SKILL.md +24 -12
- package/skills/dev-loop/SKILL.md +10 -6
- package/skills/docs/acceptance-criteria-verification.md +6 -2
- package/skills/docs/anti-patterns.md +2 -1
- package/skills/docs/artifact-authority-contract.md +22 -4
- package/skills/docs/copilot-loop-operations.md +1 -1
- package/skills/docs/cross-harness-regression-contract.md +60 -0
- package/skills/docs/issue-intake-procedure.md +1 -0
- package/skills/docs/local-planning-flow.md +1 -1
- package/skills/docs/local-planning-worked-example.md +1 -1
- package/skills/docs/merge-preconditions.md +17 -1
- package/skills/docs/plan-file-contract.md +1 -1
- package/skills/docs/public-dev-loop-contract.md +1 -1
- package/skills/docs/retrospective-checkpoint-contract.md +90 -76
- package/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/skills/docs/workflow-handoff-contract.md +39 -0
- package/skills/local-implementation/SKILL.md +25 -14
- package/skills/loop-grill/SKILL.md +165 -0
- package/.claude/commands/continue.md +0 -15
- package/scripts/loop/conductor.mjs +0 -233
- package/scripts/loop/detect-stale-runner.mjs +0 -265
- package/scripts/loop/pre-push-main-guard.mjs +0 -117
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
detectIssueRefinementArtifact,
|
|
9
9
|
REFINEMENT_SOURCE,
|
|
10
10
|
} from "@dev-loops/core/loop/issue-refinement-artifact";
|
|
11
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
11
12
|
const USAGE = `Usage:
|
|
12
13
|
detect-issue-refinement-artifact.mjs --repo <owner/name> --issue <number>
|
|
13
14
|
detect-issue-refinement-artifact.mjs --input <path>
|
|
@@ -31,7 +32,8 @@ Success output (stdout, JSON):
|
|
|
31
32
|
"reason": "..."
|
|
32
33
|
}
|
|
33
34
|
Error output (stderr, JSON):
|
|
34
|
-
{ "ok": false, "error": "...", "usage": "..." }
|
|
35
|
+
{ "ok": false, "error": "...", "usage": "..." }
|
|
36
|
+
${JQ_OUTPUT_USAGE}`.trim();
|
|
35
37
|
const parseError = buildParseError(USAGE);
|
|
36
38
|
export function parseDetectIssueRefinementArtifactCliArgs(argv) {
|
|
37
39
|
const options = {
|
|
@@ -47,6 +49,7 @@ export function parseDetectIssueRefinementArtifactCliArgs(argv) {
|
|
|
47
49
|
repo: { type: "string" },
|
|
48
50
|
issue: { type: "string" },
|
|
49
51
|
input: { type: "string" },
|
|
52
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
50
53
|
},
|
|
51
54
|
allowPositionals: true,
|
|
52
55
|
strict: false,
|
|
@@ -79,6 +82,7 @@ export function parseDetectIssueRefinementArtifactCliArgs(argv) {
|
|
|
79
82
|
options.input = requireTokenValue(token, parseError).trim();
|
|
80
83
|
continue;
|
|
81
84
|
}
|
|
85
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
82
86
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
83
87
|
}
|
|
84
88
|
const hasInput = typeof options.input === "string" && options.input.length > 0;
|
|
@@ -165,8 +169,7 @@ export async function runCli(
|
|
|
165
169
|
}
|
|
166
170
|
try {
|
|
167
171
|
const result = await detectIssueRefinementArtifactFromOptions(options, { env, ghCommand });
|
|
168
|
-
|
|
169
|
-
return 0;
|
|
172
|
+
return emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
170
173
|
} catch (error) {
|
|
171
174
|
stderr.write(`${formatCliError(error)}\n`);
|
|
172
175
|
return 1;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import {
|
|
5
4
|
buildParseError,
|
|
@@ -14,13 +13,16 @@ import {
|
|
|
14
13
|
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
15
14
|
import { loadDevLoopConfig, resolveGateConfig, resolveRefinement, resolveRefinementConfig, resolveWorkflowConfig } from "@dev-loops/core/config";
|
|
16
15
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
17
|
-
import { buildSnapshotFromPrFacts, interpretLoopState, summarizeLoopInterpretation } from "@dev-loops/core/loop/copilot-loop-state";
|
|
16
|
+
import { buildSnapshotFromPrFacts, interpretLoopState, isCopilotRoundCapReached, summarizeLoopInterpretation } from "@dev-loops/core/loop/copilot-loop-state";
|
|
18
17
|
import { evaluatePrGateCoordination, PR_CHECKPOINT, PR_CHECKPOINT_ACTION } from "@dev-loops/core/loop/pr-gate-coordination";
|
|
19
18
|
import { shouldGuardCopilotReviewRequest } from "@dev-loops/core/loop/pr-gate-coordination";
|
|
20
19
|
import { UI_E2E_CHECK_NAMES } from "@dev-loops/core/loop/ui-e2e-scoping";
|
|
21
20
|
import { fetchGithubReviewThreadsPayload } from "../github/capture-review-threads.mjs";
|
|
21
|
+
import { detectPostConvergenceSignificantChange } from "./_post-convergence-change.mjs";
|
|
22
22
|
import { detectCheckpointEvidence } from "../github/detect-checkpoint-evidence.mjs";
|
|
23
|
+
import { resolveRepoRoot } from "./_repo-root-resolver.mjs";
|
|
23
24
|
import { parseArgs } from "node:util";
|
|
25
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
24
26
|
const UNMERGED_GIT_STATUS_CODES = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
|
|
25
27
|
const USAGE = `Usage: detect-pr-gate-coordination-state.mjs --repo <owner/name> --pr <number>
|
|
26
28
|
Determine which PR gate/transition is legal next for a pull request.
|
|
@@ -67,9 +69,11 @@ Output (stdout, JSON):
|
|
|
67
69
|
Error output (stderr, JSON):
|
|
68
70
|
{ "ok": false, "error": "...", "usage": "..." }
|
|
69
71
|
{ "ok": false, "error": "..." }
|
|
72
|
+
${JQ_OUTPUT_USAGE}
|
|
70
73
|
Exit codes:
|
|
71
74
|
0 Success
|
|
72
|
-
1 Argument error or gh/runtime failure
|
|
75
|
+
1 Argument error or gh/runtime failure
|
|
76
|
+
2 Invalid --jq filter`.trim();
|
|
73
77
|
const parseError = buildParseError(USAGE);
|
|
74
78
|
export function parseDetectPrGateCoordinationCliArgs(argv) {
|
|
75
79
|
const options = {
|
|
@@ -83,6 +87,7 @@ export function parseDetectPrGateCoordinationCliArgs(argv) {
|
|
|
83
87
|
help: { type: "boolean", short: "h" },
|
|
84
88
|
repo: { type: "string" },
|
|
85
89
|
pr: { type: "string" },
|
|
90
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
86
91
|
},
|
|
87
92
|
allowPositionals: true,
|
|
88
93
|
strict: false,
|
|
@@ -107,6 +112,7 @@ export function parseDetectPrGateCoordinationCliArgs(argv) {
|
|
|
107
112
|
options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
|
|
108
113
|
continue;
|
|
109
114
|
}
|
|
115
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
110
116
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
111
117
|
}
|
|
112
118
|
if (options.repo === undefined || options.pr === undefined) {
|
|
@@ -221,30 +227,38 @@ export function deriveUiE2ePassed(prData, checkNames = UI_E2E_CHECK_NAMES) {
|
|
|
221
227
|
return present.every((entry) => {
|
|
222
228
|
const conclusion = String(entry?.conclusion ?? "").toUpperCase();
|
|
223
229
|
const state = String(entry?.state ?? "").toUpperCase();
|
|
224
|
-
|
|
230
|
+
// SKIPPED = "not applicable to this run" (e.g. viewer-smoke when no viewer files changed) — not a failure.
|
|
231
|
+
return conclusion === "SUCCESS" || conclusion === "SKIPPED" || state === "SUCCESS" || state === "SKIPPED";
|
|
225
232
|
});
|
|
226
233
|
}
|
|
227
234
|
|
|
228
|
-
|
|
229
|
-
|
|
235
|
+
// Ordered, de-duplicated list of ALL closing-referenced issue numbers for a PR.
|
|
236
|
+
// Umbrella PRs legitimately close multiple issues (#1052), so the refinement
|
|
237
|
+
// guard resolves against every one of them, not just a unique single ref.
|
|
238
|
+
export function resolveLinkedIssuesFromPr(prData) {
|
|
239
|
+
if (!prData || typeof prData !== "object") return [];
|
|
240
|
+
const dedupe = (nums) => {
|
|
241
|
+
const seen = new Set();
|
|
242
|
+
const out = [];
|
|
243
|
+
for (const n of nums) {
|
|
244
|
+
if (Number.isInteger(n) && n > 0 && !seen.has(n)) {
|
|
245
|
+
seen.add(n);
|
|
246
|
+
out.push(n);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
};
|
|
230
251
|
const closing = Array.isArray(prData.closingIssuesReferences) ? prData.closingIssuesReferences : [];
|
|
231
|
-
const closingNumbers = closing
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (closingNumbers.length === 1) {
|
|
235
|
-
return closingNumbers[0];
|
|
252
|
+
const closingNumbers = dedupe(closing.map((entry) => Number(entry?.number)));
|
|
253
|
+
if (closingNumbers.length > 0) {
|
|
254
|
+
return closingNumbers;
|
|
236
255
|
}
|
|
237
256
|
const body = typeof prData.body === "string" ? prData.body : "";
|
|
238
|
-
if (body.length === 0) return
|
|
257
|
+
if (body.length === 0) return [];
|
|
239
258
|
const matches = body.match(/(?:closes|fixes|resolves)\s+#(\d+)/gi) || [];
|
|
240
|
-
|
|
241
|
-
.map((m) => Number((/(\d+)/.exec(m) || [])[1]))
|
|
242
|
-
.filter((n) => Number.isInteger(n) && n > 0);
|
|
243
|
-
if (bodyNumbers.length === 1) {
|
|
244
|
-
return bodyNumbers[0];
|
|
245
|
-
}
|
|
246
|
-
return null;
|
|
259
|
+
return dedupe(matches.map((m) => Number((/(\d+)/.exec(m) || [])[1])));
|
|
247
260
|
}
|
|
261
|
+
|
|
248
262
|
async function fetchIssueBody({ repo, issue }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
249
263
|
const result = await runChild(
|
|
250
264
|
ghCommand,
|
|
@@ -261,58 +275,118 @@ async function fetchIssueBody({ repo, issue }, { env = process.env, ghCommand =
|
|
|
261
275
|
return null;
|
|
262
276
|
}
|
|
263
277
|
}
|
|
264
|
-
async function loadRefinementArtifact({ repo, prData, prDraft, prClosed, prMerged }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
265
|
-
const
|
|
266
|
-
if (
|
|
278
|
+
export async function loadRefinementArtifact({ repo, prData, prDraft, prClosed, prMerged }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
279
|
+
const linkedIssues = resolveLinkedIssuesFromPr(prData);
|
|
280
|
+
if (linkedIssues.length === 0) {
|
|
267
281
|
if (prDraft) {
|
|
268
282
|
return {
|
|
269
283
|
status: "missing",
|
|
270
284
|
linkedIssue: null,
|
|
271
|
-
|
|
285
|
+
linkedIssues: [],
|
|
286
|
+
reason: "Draft PR has no deterministically resolvable linked issue (no closingIssuesReferences and no Closes/Fixes/Resolves #n reference in body); draft gate cannot verify a refinement artifact.",
|
|
272
287
|
finding: "missing_refinement_artifact",
|
|
273
288
|
};
|
|
274
289
|
}
|
|
275
290
|
return {
|
|
276
291
|
status: "unknown",
|
|
277
292
|
linkedIssue: null,
|
|
278
|
-
|
|
293
|
+
linkedIssues: [],
|
|
294
|
+
reason: "No deterministically resolvable linked issue (no closingIssuesReferences and no Closes/Fixes/Resolves #n reference in body).",
|
|
279
295
|
};
|
|
280
296
|
}
|
|
297
|
+
const scopeLabel = linkedIssues.map((n) => `#${n}`).join(", ");
|
|
281
298
|
if (!prDraft && !prClosed && !prMerged) {
|
|
282
299
|
return {
|
|
283
300
|
status: "unknown",
|
|
284
|
-
linkedIssue,
|
|
285
|
-
|
|
301
|
+
linkedIssue: linkedIssues.length === 1 ? linkedIssues[0] : null,
|
|
302
|
+
linkedIssues,
|
|
303
|
+
reason: `Linked issue(s) ${scopeLabel} detected (${linkedIssues.length}); refinement check is a draft-gate boundary and the PR is not draft, so the check is informational only and does not fetch issue bodies.`,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const { detectIssueRefinementArtifact } = await import("@dev-loops/core/loop/issue-refinement-artifact");
|
|
307
|
+
// Fetch and evaluate every closing-referenced issue. An umbrella PR's scope
|
|
308
|
+
// is refined if AT LEAST ONE linked issue carries a refinement artifact.
|
|
309
|
+
const evaluated = [];
|
|
310
|
+
for (const issue of linkedIssues) {
|
|
311
|
+
const body = await fetchIssueBody({ repo, issue }, { env, ghCommand });
|
|
312
|
+
if (body === null) {
|
|
313
|
+
evaluated.push({ issue, artifact: null });
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
evaluated.push({ issue, artifact: detectIssueRefinementArtifact({ body, issueNumber: issue }) });
|
|
317
|
+
}
|
|
318
|
+
const refinedIssues = evaluated
|
|
319
|
+
.filter((e) => e.artifact && e.artifact.hasACs === true)
|
|
320
|
+
.map((e) => e.issue);
|
|
321
|
+
const firstPresent = evaluated.find((e) => e.artifact && e.artifact.hasACs === true);
|
|
322
|
+
const isUmbrella = linkedIssues.length > 1;
|
|
323
|
+
|
|
324
|
+
if (firstPresent) {
|
|
325
|
+
const a = firstPresent.artifact;
|
|
326
|
+
return {
|
|
327
|
+
status: "present",
|
|
328
|
+
linkedIssue: firstPresent.issue,
|
|
329
|
+
linkedIssues,
|
|
330
|
+
refinedIssues,
|
|
331
|
+
source: a.source,
|
|
332
|
+
acItems: a.acItems,
|
|
333
|
+
dodItems: a.dodItems,
|
|
334
|
+
sections: a.sections,
|
|
335
|
+
linkedDoc: a.linkedDoc,
|
|
336
|
+
reason: isUmbrella
|
|
337
|
+
? `Refinement artifact present via linked issue #${firstPresent.issue} (umbrella PR closes ${scopeLabel}).`
|
|
338
|
+
: a.reason,
|
|
339
|
+
finding: a.finding,
|
|
340
|
+
_onlyEnforcedWhenDraft: prDraft === true,
|
|
286
341
|
};
|
|
287
342
|
}
|
|
288
|
-
|
|
289
|
-
|
|
343
|
+
|
|
344
|
+
// None of the linked issues carry a refinement artifact (or all bodies failed
|
|
345
|
+
// to fetch). Report against the first linked issue for single-value consumers.
|
|
346
|
+
// Note: `finding`/`missing` here is only enforced by the gate when the PR is
|
|
347
|
+
// draft (`_onlyEnforcedWhenDraft`); closed/merged PRs surface it informationally.
|
|
348
|
+
const firstEvaluated = evaluated[0];
|
|
349
|
+
const allFailed = evaluated.every((e) => e.artifact === null);
|
|
350
|
+
if (allFailed) {
|
|
351
|
+
// Preserve prior single-issue semantics: draft → missing, else unknown.
|
|
290
352
|
if (prDraft) {
|
|
291
353
|
return {
|
|
292
354
|
status: "missing",
|
|
293
|
-
linkedIssue,
|
|
294
|
-
|
|
355
|
+
linkedIssue: firstEvaluated.issue,
|
|
356
|
+
linkedIssues,
|
|
357
|
+
refinedIssues,
|
|
358
|
+
reason: `Failed to fetch body for linked issue(s) ${scopeLabel}; draft gate cannot verify a refinement artifact, treating as missing.`,
|
|
295
359
|
finding: "missing_refinement_artifact",
|
|
296
360
|
};
|
|
297
361
|
}
|
|
298
362
|
return {
|
|
299
363
|
status: "unknown",
|
|
300
|
-
linkedIssue,
|
|
301
|
-
|
|
364
|
+
linkedIssue: linkedIssues.length === 1 ? linkedIssues[0] : firstEvaluated.issue,
|
|
365
|
+
linkedIssues,
|
|
366
|
+
refinedIssues,
|
|
367
|
+
reason: `Failed to fetch body for linked issue(s) ${scopeLabel}; refinement status is unknown.`,
|
|
302
368
|
};
|
|
303
369
|
}
|
|
304
|
-
|
|
305
|
-
|
|
370
|
+
// Mixed branch: not allFailed, so at least one body fetched but none is
|
|
371
|
+
// refined. Report against the first successfully-fetched (non-null) issue —
|
|
372
|
+
// `evaluated[0]` may be a failed fetch: it still retains its `issue` field but
|
|
373
|
+
// has `artifact: null` (body fetch / artifact detection failed for that issue).
|
|
374
|
+
const firstFetched = evaluated.find((e) => e.artifact !== null);
|
|
375
|
+
const first = firstFetched.artifact;
|
|
306
376
|
return {
|
|
307
|
-
status:
|
|
308
|
-
linkedIssue,
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
377
|
+
status: "missing",
|
|
378
|
+
linkedIssue: firstFetched.issue,
|
|
379
|
+
linkedIssues,
|
|
380
|
+
refinedIssues,
|
|
381
|
+
source: first.source,
|
|
382
|
+
acItems: first.acItems,
|
|
383
|
+
dodItems: first.dodItems,
|
|
384
|
+
sections: first.sections,
|
|
385
|
+
linkedDoc: first.linkedDoc,
|
|
386
|
+
reason: isUmbrella
|
|
387
|
+
? `No linked issue (${scopeLabel}) carries a refinement artifact (ACs/DoD); draft gate cannot verify a refinement artifact.`
|
|
388
|
+
: first.reason,
|
|
389
|
+
finding: "missing_refinement_artifact",
|
|
316
390
|
_onlyEnforcedWhenDraft: prDraft === true,
|
|
317
391
|
};
|
|
318
392
|
}
|
|
@@ -332,16 +406,6 @@ async function fetchLocalConflictFiles({ env = process.env, gitCommand = "git" }
|
|
|
332
406
|
}
|
|
333
407
|
return parseGitStatusConflictFiles(result.stdout);
|
|
334
408
|
}
|
|
335
|
-
async function loadRetrospectiveCheckpoint(repoRoot) {
|
|
336
|
-
const checkpointPath = path.join(repoRoot, ".pi", "dev-loop-retrospective-checkpoint.json");
|
|
337
|
-
try {
|
|
338
|
-
const checkpointText = await readFile(checkpointPath, "utf8");
|
|
339
|
-
const checkpoint = parseJsonText(checkpointText, { label: "retrospective checkpoint" });
|
|
340
|
-
return checkpoint && typeof checkpoint === "object" ? checkpoint : null;
|
|
341
|
-
} catch (error) {
|
|
342
|
-
return null;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
409
|
export async function loadPrGateCoordinationContext(options, runtime = {}) {
|
|
346
410
|
const prData = await fetchPrFactsWithSettledMergeable(options, runtime);
|
|
347
411
|
const currentHeadSha = typeof prData?.headRefOid === "string" && prData.headRefOid.trim().length > 0
|
|
@@ -392,7 +456,7 @@ export async function loadPrGateCoordinationContext(options, runtime = {}) {
|
|
|
392
456
|
// READY_TO_REREQUEST_REVIEW, dead-ending the loop at the round cap (#896). This
|
|
393
457
|
// keeps the gate-coordination interpretation consistent with the standalone
|
|
394
458
|
// detect-copilot-loop-state path and with request-copilot-review's cap logic.
|
|
395
|
-
const interpreterRepoRoot = runtime.repoRoot ?? process.cwd();
|
|
459
|
+
const interpreterRepoRoot = runtime.repoRoot ?? resolveRepoRoot(process.cwd());
|
|
396
460
|
const interpreterConfigResult = await loadDevLoopConfig({ repoRoot: interpreterRepoRoot });
|
|
397
461
|
const interpreterRefinementConfig = (Array.isArray(interpreterConfigResult.errors) && interpreterConfigResult.errors.length > 0)
|
|
398
462
|
? resolveRefinement({ version: 1 })
|
|
@@ -446,15 +510,32 @@ async function fetchCopilotEverFormallyRequested({ repo, pr }, { env = process.e
|
|
|
446
510
|
|
|
447
511
|
export async function detectPrGateCoordinationState(options, runtime = {}) {
|
|
448
512
|
const context = await loadPrGateCoordinationContext(options, runtime);
|
|
449
|
-
const repoRoot = runtime.repoRoot ?? process.cwd();
|
|
513
|
+
const repoRoot = runtime.repoRoot ?? resolveRepoRoot(process.cwd());
|
|
450
514
|
const configLoadResult = await loadDevLoopConfig({ repoRoot });
|
|
451
515
|
const hasConfigErrors = Array.isArray(configLoadResult.errors) && configLoadResult.errors.length > 0;
|
|
452
516
|
const config = hasConfigErrors ? {} : (configLoadResult.config ?? {});
|
|
453
517
|
const draftGateConfig = resolveGateConfig(config, "draft");
|
|
454
518
|
const maxCopilotRounds = resolveRefinementConfig(config, "maxCopilotRounds");
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
519
|
+
// Shared with interpretLoopState (consumed by copilot-pr-handoff.mjs) and
|
|
520
|
+
// evaluatePrGateCoordination — the single source of truth for "is the
|
|
521
|
+
// Copilot round cap reached" so this detector cannot disagree with the
|
|
522
|
+
// handoff at the cap boundary (#1126).
|
|
523
|
+
const roundCapReached = isCopilotRoundCapReached({
|
|
524
|
+
copilotReviewRoundCount: context.snapshot?.copilotReviewRoundCount,
|
|
525
|
+
maxCopilotRounds,
|
|
526
|
+
});
|
|
527
|
+
const postConvergenceSignificantChange = await detectPostConvergenceSignificantChange(
|
|
528
|
+
{
|
|
529
|
+
repo: context.repo,
|
|
530
|
+
pr: context.pr,
|
|
531
|
+
currentHeadSha: context.currentHeadSha,
|
|
532
|
+
reviews: context.prData?.reviews,
|
|
533
|
+
changedFiles: context.prData?.files,
|
|
534
|
+
roundCapReached: roundCapReached && context.interpretation?.roundCapCleanEligible === true,
|
|
535
|
+
regularCopilotRounds: (context.snapshot?.copilotReviewRoundCount ?? 0) > 0,
|
|
536
|
+
},
|
|
537
|
+
runtime,
|
|
538
|
+
);
|
|
458
539
|
const result = evaluatePrGateCoordination({
|
|
459
540
|
repo: context.repo,
|
|
460
541
|
pr: context.pr,
|
|
@@ -476,14 +557,12 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
|
|
|
476
557
|
maxCopilotRounds,
|
|
477
558
|
sameHeadCleanConverged: context.interpretation.sameHeadCleanConverged,
|
|
478
559
|
draftGateRequireCi: draftGateConfig.requireCi,
|
|
479
|
-
requireRetrospectiveGate,
|
|
480
|
-
requireRetrospectiveInternalTooling,
|
|
481
|
-
retrospectiveCheckpoint,
|
|
482
560
|
draftGate: context.gateEvidence.draftGate,
|
|
483
561
|
draftGateMarker: context.gateEvidence.draftGateMarker,
|
|
484
562
|
preApprovalGate: context.gateEvidence.preApprovalGate,
|
|
485
563
|
preApprovalGateMarker: context.gateEvidence.preApprovalGateMarker,
|
|
486
564
|
refinementArtifact: context.refinementArtifact,
|
|
565
|
+
postConvergenceSignificantChange,
|
|
487
566
|
});
|
|
488
567
|
// Copilot review request guard (#613): When Copilot has reviewed the PR
|
|
489
568
|
// but no formal review request was made, block pre-approval gate entry.
|
|
@@ -495,9 +574,6 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
|
|
|
495
574
|
PR_CHECKPOINT.PRE_APPROVAL_GATE_WINDOW,
|
|
496
575
|
PR_CHECKPOINT.FINAL_APPROVAL_READY,
|
|
497
576
|
]);
|
|
498
|
-
const roundCapReached = maxCopilotRounds !== null
|
|
499
|
-
&& typeof (context.snapshot?.copilotReviewRoundCount) === "number"
|
|
500
|
-
&& context.snapshot?.copilotReviewRoundCount >= maxCopilotRounds;
|
|
501
577
|
const sameHeadCleanConverged = context.interpretation?.sameHeadCleanConverged ?? false;
|
|
502
578
|
// Round-cap clean fallback (#896): the interpreter resolved a clean post-cap head
|
|
503
579
|
// (zero unresolved threads + green CI) that Copilot will not re-review. The formal
|
|
@@ -505,7 +581,13 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
|
|
|
505
581
|
const roundCapCleanFallback = context.interpretation?.roundCapCleanEligible ?? false;
|
|
506
582
|
const copilotReviewEverFormallyRequested = copilotReviewRequestStatus === "none"
|
|
507
583
|
&& guardBoundaries.has(result.gateBoundary)
|
|
508
|
-
|
|
584
|
+
// cap-0 disables the Copilot gate, so shouldGuardCopilotReviewRequest always
|
|
585
|
+
// returns false here — skip the timeline fetch it would never need (#1126).
|
|
586
|
+
// (Restores the suppression the roundCapReached predicate swap dropped.)
|
|
587
|
+
&& maxCopilotRounds !== 0
|
|
588
|
+
&& !(roundCapReached
|
|
589
|
+
&& (sameHeadCleanConverged || roundCapCleanFallback)
|
|
590
|
+
&& !postConvergenceSignificantChange)
|
|
509
591
|
? await fetchCopilotEverFormallyRequested(
|
|
510
592
|
{ repo: context.repo, pr: context.pr },
|
|
511
593
|
runtime,
|
|
@@ -518,6 +600,7 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
|
|
|
518
600
|
maxCopilotRounds,
|
|
519
601
|
sameHeadCleanConverged,
|
|
520
602
|
roundCapCleanFallback,
|
|
603
|
+
postConvergenceSignificantChange,
|
|
521
604
|
gateBoundary: result.gateBoundary,
|
|
522
605
|
})) {
|
|
523
606
|
result.gateBoundary = PR_CHECKPOINT.POST_DRAFT_EXTERNAL_REVIEW;
|
|
@@ -590,7 +673,7 @@ async function main() {
|
|
|
590
673
|
}
|
|
591
674
|
try {
|
|
592
675
|
const result = await detectPrGateCoordinationState(options);
|
|
593
|
-
process.
|
|
676
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent });
|
|
594
677
|
} catch (error) {
|
|
595
678
|
process.stderr.write(`${formatCliError(error)}\n`);
|
|
596
679
|
process.exitCode = 1;
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
interpretReviewerLoopState,
|
|
9
9
|
normalizeReviewerSnapshot,
|
|
10
10
|
} from "@dev-loops/core/loop/reviewer-loop-state";
|
|
11
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
11
12
|
const HELP = `Usage: detect-reviewer-loop-state.mjs [--input <path> | --repo <owner/name> --pr <number>] [--review-requested <true|false>] [--local-state <path>]
|
|
12
13
|
Detect reviewer loop state for a pull request.
|
|
13
14
|
Modes:
|
|
@@ -17,9 +18,13 @@ Options (auto-detect mode only):
|
|
|
17
18
|
--review-requested <bool> Override review-requested detection (true/false)
|
|
18
19
|
--local-state <path> Path to local state file for snapshot merging
|
|
19
20
|
Reviewer scope is auto-resolved from PR requested reviewers.
|
|
21
|
+
|
|
22
|
+
${JQ_OUTPUT_USAGE}
|
|
23
|
+
|
|
20
24
|
Exit codes:
|
|
21
25
|
0 Success
|
|
22
26
|
1 Error
|
|
27
|
+
2 Invalid --jq filter
|
|
23
28
|
`;
|
|
24
29
|
function parseBool(value, flag) {
|
|
25
30
|
if (value === "true") return true;
|
|
@@ -36,6 +41,7 @@ export function parseDetectReviewerCliArgs(argv) {
|
|
|
36
41
|
pr: { type: "string" },
|
|
37
42
|
"review-requested": { type: "string" },
|
|
38
43
|
"local-state": { type: "string" },
|
|
44
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
39
45
|
},
|
|
40
46
|
allowPositionals: true,
|
|
41
47
|
strict: false,
|
|
@@ -83,6 +89,7 @@ export function parseDetectReviewerCliArgs(argv) {
|
|
|
83
89
|
options.localStatePath = requireTokenValue(token);
|
|
84
90
|
continue;
|
|
85
91
|
}
|
|
92
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t))) continue;
|
|
86
93
|
throw new Error(`Unknown argument: ${token.rawName}`);
|
|
87
94
|
}
|
|
88
95
|
if (options.inputPath !== undefined) {
|
|
@@ -231,7 +238,7 @@ export async function autoDetectReviewerSnapshot(
|
|
|
231
238
|
}
|
|
232
239
|
export async function runCli(
|
|
233
240
|
argv = process.argv.slice(2),
|
|
234
|
-
{ stdout = process.stdout, env = process.env, ghCommand = "gh" } = {},
|
|
241
|
+
{ stdout = process.stdout, stderr = process.stderr, env = process.env, ghCommand = "gh" } = {},
|
|
235
242
|
) {
|
|
236
243
|
const options = parseDetectReviewerCliArgs(argv);
|
|
237
244
|
if (options.help) { stdout.write(HELP); return; }
|
|
@@ -243,7 +250,10 @@ export async function runCli(
|
|
|
243
250
|
snapshot = await autoDetectReviewerSnapshot(options, { env, ghCommand });
|
|
244
251
|
}
|
|
245
252
|
const interpretation = interpretReviewerLoopState(snapshot);
|
|
246
|
-
|
|
253
|
+
process.exitCode = emitResult(
|
|
254
|
+
{ ok: true, snapshot, state: interpretation.state, allowedTransitions: interpretation.allowedTransitions, nextAction: interpretation.nextAction },
|
|
255
|
+
{ jq: options.jq, silent: options.silent, stdout, stderr },
|
|
256
|
+
);
|
|
247
257
|
}
|
|
248
258
|
if (isDirectCliRun(import.meta.url)) {
|
|
249
259
|
runCli().catch((error) => { process.stderr.write(`${formatCliError(error)}\n`); process.exitCode = 1; });
|
|
@@ -3,6 +3,7 @@ import process from "node:process";
|
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
import { interpretTrackerLoopState } from "@dev-loops/core/loop/tracker-first-loop-state";
|
|
6
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
6
7
|
|
|
7
8
|
function showHelp() {
|
|
8
9
|
process.stdout.write(`Usage: detect-tracker-first-loop-state.mjs --repo <owner/name> --issue <number>
|
|
@@ -11,9 +12,13 @@ Options:
|
|
|
11
12
|
--repo <owner/name> GitHub repository slug
|
|
12
13
|
--issue <number> GitHub issue number
|
|
13
14
|
--help, -h Show this help
|
|
15
|
+
|
|
16
|
+
${JQ_OUTPUT_USAGE}
|
|
17
|
+
|
|
14
18
|
Exit codes:
|
|
15
19
|
0 Success
|
|
16
20
|
1 Error
|
|
21
|
+
2 Invalid --jq filter
|
|
17
22
|
`);
|
|
18
23
|
process.exit(0);
|
|
19
24
|
}
|
|
@@ -25,6 +30,7 @@ function parseCliArgs(argv) {
|
|
|
25
30
|
repo: { type: "string" },
|
|
26
31
|
issue: { type: "string" },
|
|
27
32
|
help: { type: "boolean", short: "h" },
|
|
33
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
28
34
|
},
|
|
29
35
|
allowPositionals: true,
|
|
30
36
|
strict: false,
|
|
@@ -46,7 +52,9 @@ function parseCliArgs(argv) {
|
|
|
46
52
|
}
|
|
47
53
|
if (token.name === "issue") {
|
|
48
54
|
opts.issue = token.value ?? null;
|
|
55
|
+
continue;
|
|
49
56
|
}
|
|
57
|
+
if (matchJqOutputToken(token, opts)) continue;
|
|
50
58
|
}
|
|
51
59
|
}
|
|
52
60
|
return opts;
|
|
@@ -90,7 +98,7 @@ async function main() {
|
|
|
90
98
|
return;
|
|
91
99
|
}
|
|
92
100
|
const result = interpretTrackerLoopState({ trackerState: rawState, prContext });
|
|
93
|
-
process.
|
|
101
|
+
process.exitCode = emitResult(result, { jq: rawOpts.jq, silent: rawOpts.silent });
|
|
94
102
|
}
|
|
95
103
|
const isDirectRun =
|
|
96
104
|
process.argv[1] && process.argv[1].includes("detect-tracker-first-loop-state.mjs");
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
interpretTrackerPrState,
|
|
10
10
|
normalizeTrackerPrSnapshot,
|
|
11
11
|
} from "@dev-loops/core/loop/tracker-pr-state";
|
|
12
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
12
13
|
const USAGE = `Usage:
|
|
13
14
|
detect-tracker-pr-state.mjs --input <path>
|
|
14
15
|
Interpret a pre-built tracker-PR snapshot JSON and emit the current lifecycle
|
|
@@ -47,9 +48,11 @@ Output (stdout, JSON):
|
|
|
47
48
|
Error output (stderr, JSON):
|
|
48
49
|
Argument/usage errors: { "ok": false, "error": "...", "usage": "..." }
|
|
49
50
|
Runtime failures: { "ok": false, "error": "..." }
|
|
51
|
+
${JQ_OUTPUT_USAGE}
|
|
50
52
|
Exit codes:
|
|
51
53
|
0 Success
|
|
52
|
-
1 Argument error or runtime failure
|
|
54
|
+
1 Argument error or runtime failure
|
|
55
|
+
2 Invalid --jq filter`.trim();
|
|
53
56
|
const parseError = buildParseError(USAGE);
|
|
54
57
|
export function parseDetectTrackerPrCliArgs(argv) {
|
|
55
58
|
const options = {
|
|
@@ -61,6 +64,7 @@ export function parseDetectTrackerPrCliArgs(argv) {
|
|
|
61
64
|
options: {
|
|
62
65
|
help: { type: "boolean", short: "h" },
|
|
63
66
|
input: { type: "string" },
|
|
67
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
64
68
|
},
|
|
65
69
|
allowPositionals: true,
|
|
66
70
|
strict: false,
|
|
@@ -81,6 +85,7 @@ export function parseDetectTrackerPrCliArgs(argv) {
|
|
|
81
85
|
options.inputPath = requireTokenValue(token, parseError);
|
|
82
86
|
continue;
|
|
83
87
|
}
|
|
88
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
84
89
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
85
90
|
}
|
|
86
91
|
if (options.inputPath === undefined) {
|
|
@@ -92,6 +97,7 @@ export async function runCli(
|
|
|
92
97
|
argv = process.argv.slice(2),
|
|
93
98
|
{
|
|
94
99
|
stdout = process.stdout,
|
|
100
|
+
stderr = process.stderr,
|
|
95
101
|
} = {},
|
|
96
102
|
) {
|
|
97
103
|
const options = parseDetectTrackerPrCliArgs(argv);
|
|
@@ -103,8 +109,9 @@ export async function runCli(
|
|
|
103
109
|
const raw = parseJsonText(text);
|
|
104
110
|
const snapshot = normalizeTrackerPrSnapshot(raw);
|
|
105
111
|
const { state, allowedTransitions, nextAction, reverseSyncAction } = interpretTrackerPrState(snapshot);
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
process.exitCode = emitResult(
|
|
113
|
+
{ ok: true, snapshot, state, allowedTransitions, nextAction, reverseSyncAction },
|
|
114
|
+
{ jq: options.jq, silent: options.silent, stdout, stderr },
|
|
108
115
|
);
|
|
109
116
|
}
|
|
110
117
|
const isDirectRun =
|
|
@@ -27,6 +27,7 @@ import { parseArgs } from "node:util";
|
|
|
27
27
|
import { resolveWorktreePath } from "@dev-loops/core/loop/handoff-envelope";
|
|
28
28
|
import { provisionWorktree } from "./provision-worktree.mjs";
|
|
29
29
|
import { canonicalize } from "./_worktree-path.mjs";
|
|
30
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
30
31
|
|
|
31
32
|
const USAGE = `Usage:
|
|
32
33
|
ensure-worktree.mjs --repo-root <p> (--issue <n> | --pr <n>) [--branch <name>] [--base <ref>]
|
|
@@ -43,7 +44,9 @@ Optional:
|
|
|
43
44
|
-h, --help Show this help.
|
|
44
45
|
Output (stdout, JSON):
|
|
45
46
|
{ "ok": true, "path": <p>, "created": bool, "reused": bool,
|
|
46
|
-
"provision": { "actions": [...], "summary": {...} } }
|
|
47
|
+
"provision": { "actions": [...], "summary": {...} } }
|
|
48
|
+
|
|
49
|
+
${JQ_OUTPUT_USAGE}`.trim();
|
|
47
50
|
|
|
48
51
|
const parseError = buildParseError(USAGE);
|
|
49
52
|
|
|
@@ -71,6 +74,7 @@ export function parseEnsureWorktreeCliArgs(argv) {
|
|
|
71
74
|
pr: { type: "string" },
|
|
72
75
|
branch: { type: "string" },
|
|
73
76
|
base: { type: "string" },
|
|
77
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
74
78
|
},
|
|
75
79
|
allowPositionals: true,
|
|
76
80
|
strict: false,
|
|
@@ -103,6 +107,7 @@ export function parseEnsureWorktreeCliArgs(argv) {
|
|
|
103
107
|
options.base = requireTokenValue(token, parseError, { flagPattern: /^-/u });
|
|
104
108
|
continue;
|
|
105
109
|
}
|
|
110
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
106
111
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
107
112
|
}
|
|
108
113
|
if (options.help) return options;
|
|
@@ -201,14 +206,14 @@ export async function ensureWorktree(
|
|
|
201
206
|
return { ok: true, path: target, created: true, reused: false, provision: summary };
|
|
202
207
|
}
|
|
203
208
|
|
|
204
|
-
export async function runCli(argv = process.argv.slice(2), { stdout = process.stdout } = {}) {
|
|
209
|
+
export async function runCli(argv = process.argv.slice(2), { stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
205
210
|
const options = parseEnsureWorktreeCliArgs(argv);
|
|
206
211
|
if (options.help) {
|
|
207
212
|
stdout.write(`${USAGE}\n`);
|
|
208
213
|
return;
|
|
209
214
|
}
|
|
210
215
|
const result = await ensureWorktree(options);
|
|
211
|
-
|
|
216
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
212
217
|
}
|
|
213
218
|
|
|
214
219
|
if (isDirectCliRun(import.meta.url)) {
|