dev-loops 0.5.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/loop-auto.md +7 -0
- 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/loop-info.md +7 -0
- package/.claude/commands/loop-queue-status.md +24 -0
- package/.claude/commands/loop-start-spike.md +16 -0
- package/.claude/commands/loop-start.md +7 -0
- package/.claude/commands/loop-status.md +6 -0
- package/.claude/hooks/_bash-command-classify.mjs +333 -29
- package/.claude/hooks/_hook-decisions.mjs +138 -15
- package/.claude/hooks/_run-context.mjs +11 -4
- 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 +19 -10
- 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/release-runbook.md +45 -0
- package/.claude/skills/docs/retrospective-checkpoint-contract.md +90 -76
- package/.claude/skills/docs/ui-e2e-scoping-step.md +134 -0
- package/.claude/skills/docs/workflow-handoff-contract.md +39 -0
- package/.claude/skills/local-implementation/SKILL.md +26 -15
- package/.claude/skills/loop-grill/SKILL.md +163 -0
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +125 -0
- package/README.md +29 -10
- package/agents/dev-loop.agent.md +11 -3
- 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 -40
- package/extension/README.md +4 -4
- package/extension/checks.ts +1 -5
- package/extension/harness-types.ts +1 -0
- package/extension/index.ts +16 -1
- package/extension/pi-extension-adapter.ts +2 -0
- package/extension/presentation.ts +16 -13
- package/lib/dev-loops-core.mjs +141 -0
- package/package.json +10 -8
- package/scripts/claude/generate-claude-assets.mjs +15 -1
- package/scripts/github/capture-review-threads.mjs +2 -9
- package/scripts/github/comment-issue.mjs +174 -0
- 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 +208 -0
- package/scripts/github/list-issues.mjs +184 -0
- 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/_handoff-contract.mjs +1 -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 +179 -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 +13 -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 +255 -0
- 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 +179 -0
- 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 +125 -0
- package/skills/copilot-pr-followup/SKILL.md +24 -12
- package/skills/dev-loop/SKILL.md +22 -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/release-runbook.md +45 -0
- package/skills/docs/retrospective-checkpoint-contract.md +90 -76
- package/skills/docs/ui-e2e-scoping-step.md +134 -0
- package/skills/docs/workflow-handoff-contract.md +39 -0
- package/skills/local-implementation/SKILL.md +26 -15
- package/skills/loop-grill/SKILL.md +165 -0
- 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,12 +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";
|
|
19
|
+
import { UI_E2E_CHECK_NAMES } from "@dev-loops/core/loop/ui-e2e-scoping";
|
|
20
20
|
import { fetchGithubReviewThreadsPayload } from "../github/capture-review-threads.mjs";
|
|
21
|
+
import { detectPostConvergenceSignificantChange } from "./_post-convergence-change.mjs";
|
|
21
22
|
import { detectCheckpointEvidence } from "../github/detect-checkpoint-evidence.mjs";
|
|
23
|
+
import { resolveRepoRoot } from "./_repo-root-resolver.mjs";
|
|
22
24
|
import { parseArgs } from "node:util";
|
|
25
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
23
26
|
const UNMERGED_GIT_STATUS_CODES = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
|
|
24
27
|
const USAGE = `Usage: detect-pr-gate-coordination-state.mjs --repo <owner/name> --pr <number>
|
|
25
28
|
Determine which PR gate/transition is legal next for a pull request.
|
|
@@ -66,9 +69,11 @@ Output (stdout, JSON):
|
|
|
66
69
|
Error output (stderr, JSON):
|
|
67
70
|
{ "ok": false, "error": "...", "usage": "..." }
|
|
68
71
|
{ "ok": false, "error": "..." }
|
|
72
|
+
${JQ_OUTPUT_USAGE}
|
|
69
73
|
Exit codes:
|
|
70
74
|
0 Success
|
|
71
|
-
1 Argument error or gh/runtime failure
|
|
75
|
+
1 Argument error or gh/runtime failure
|
|
76
|
+
2 Invalid --jq filter`.trim();
|
|
72
77
|
const parseError = buildParseError(USAGE);
|
|
73
78
|
export function parseDetectPrGateCoordinationCliArgs(argv) {
|
|
74
79
|
const options = {
|
|
@@ -82,6 +87,7 @@ export function parseDetectPrGateCoordinationCliArgs(argv) {
|
|
|
82
87
|
help: { type: "boolean", short: "h" },
|
|
83
88
|
repo: { type: "string" },
|
|
84
89
|
pr: { type: "string" },
|
|
90
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
85
91
|
},
|
|
86
92
|
allowPositionals: true,
|
|
87
93
|
strict: false,
|
|
@@ -106,6 +112,7 @@ export function parseDetectPrGateCoordinationCliArgs(argv) {
|
|
|
106
112
|
options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
|
|
107
113
|
continue;
|
|
108
114
|
}
|
|
115
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
109
116
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
110
117
|
}
|
|
111
118
|
if (options.repo === undefined || options.pr === undefined) {
|
|
@@ -163,7 +170,7 @@ async function fetchRequestedReviewers({ repo, pr }, { env = process.env, ghComm
|
|
|
163
170
|
async function fetchPrFacts({ repo, pr }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
164
171
|
const result = await runChild(
|
|
165
172
|
ghCommand,
|
|
166
|
-
["pr", "view", String(pr), "--repo", repo, "--json", "number,state,isDraft,headRefOid,mergeable,mergeStateStatus,body,title,closingIssuesReferences,reviews,statusCheckRollup"],
|
|
173
|
+
["pr", "view", String(pr), "--repo", repo, "--json", "number,state,isDraft,headRefOid,mergeable,mergeStateStatus,body,title,closingIssuesReferences,reviews,statusCheckRollup,files"],
|
|
167
174
|
env,
|
|
168
175
|
);
|
|
169
176
|
if (result.code !== 0) {
|
|
@@ -198,26 +205,60 @@ export async function fetchPrFactsWithSettledMergeable(
|
|
|
198
205
|
}
|
|
199
206
|
return prData;
|
|
200
207
|
}
|
|
201
|
-
|
|
202
|
-
|
|
208
|
+
// Changed-file paths from `gh pr view --json files` (issue #976). Feeds the
|
|
209
|
+
// path-triggered UI e2e scoping precondition in the evaluator.
|
|
210
|
+
export function extractChangedFiles(prData) {
|
|
211
|
+
const files = Array.isArray(prData?.files) ? prData.files : [];
|
|
212
|
+
return files
|
|
213
|
+
.map((entry) => (typeof entry?.path === "string" ? entry.path : null))
|
|
214
|
+
.filter((p) => typeof p === "string" && p.length > 0);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Whether the shared UI e2e suite passed for this head, read deterministically
|
|
218
|
+
// from the statusCheckRollup: every UI e2e check that is present must be
|
|
219
|
+
// SUCCESS. Returns null when no UI e2e check is present in the rollup (unknown
|
|
220
|
+
// → the evaluator fails closed), false if any present UI e2e check is not a
|
|
221
|
+
// success, true if all present ones succeeded.
|
|
222
|
+
export function deriveUiE2ePassed(prData, checkNames = UI_E2E_CHECK_NAMES) {
|
|
223
|
+
const rollup = Array.isArray(prData?.statusCheckRollup) ? prData.statusCheckRollup : [];
|
|
224
|
+
const wanted = new Set(checkNames);
|
|
225
|
+
const present = rollup.filter((entry) => wanted.has(entry?.name) || wanted.has(entry?.context));
|
|
226
|
+
if (present.length === 0) return null;
|
|
227
|
+
return present.every((entry) => {
|
|
228
|
+
const conclusion = String(entry?.conclusion ?? "").toUpperCase();
|
|
229
|
+
const state = String(entry?.state ?? "").toUpperCase();
|
|
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";
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
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
|
+
};
|
|
203
251
|
const closing = Array.isArray(prData.closingIssuesReferences) ? prData.closingIssuesReferences : [];
|
|
204
|
-
const closingNumbers = closing
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (closingNumbers.length === 1) {
|
|
208
|
-
return closingNumbers[0];
|
|
252
|
+
const closingNumbers = dedupe(closing.map((entry) => Number(entry?.number)));
|
|
253
|
+
if (closingNumbers.length > 0) {
|
|
254
|
+
return closingNumbers;
|
|
209
255
|
}
|
|
210
256
|
const body = typeof prData.body === "string" ? prData.body : "";
|
|
211
|
-
if (body.length === 0) return
|
|
257
|
+
if (body.length === 0) return [];
|
|
212
258
|
const matches = body.match(/(?:closes|fixes|resolves)\s+#(\d+)/gi) || [];
|
|
213
|
-
|
|
214
|
-
.map((m) => Number((/(\d+)/.exec(m) || [])[1]))
|
|
215
|
-
.filter((n) => Number.isInteger(n) && n > 0);
|
|
216
|
-
if (bodyNumbers.length === 1) {
|
|
217
|
-
return bodyNumbers[0];
|
|
218
|
-
}
|
|
219
|
-
return null;
|
|
259
|
+
return dedupe(matches.map((m) => Number((/(\d+)/.exec(m) || [])[1])));
|
|
220
260
|
}
|
|
261
|
+
|
|
221
262
|
async function fetchIssueBody({ repo, issue }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
222
263
|
const result = await runChild(
|
|
223
264
|
ghCommand,
|
|
@@ -234,58 +275,118 @@ async function fetchIssueBody({ repo, issue }, { env = process.env, ghCommand =
|
|
|
234
275
|
return null;
|
|
235
276
|
}
|
|
236
277
|
}
|
|
237
|
-
async function loadRefinementArtifact({ repo, prData, prDraft, prClosed, prMerged }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
238
|
-
const
|
|
239
|
-
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) {
|
|
240
281
|
if (prDraft) {
|
|
241
282
|
return {
|
|
242
283
|
status: "missing",
|
|
243
284
|
linkedIssue: null,
|
|
244
|
-
|
|
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.",
|
|
245
287
|
finding: "missing_refinement_artifact",
|
|
246
288
|
};
|
|
247
289
|
}
|
|
248
290
|
return {
|
|
249
291
|
status: "unknown",
|
|
250
292
|
linkedIssue: null,
|
|
251
|
-
|
|
293
|
+
linkedIssues: [],
|
|
294
|
+
reason: "No deterministically resolvable linked issue (no closingIssuesReferences and no Closes/Fixes/Resolves #n reference in body).",
|
|
252
295
|
};
|
|
253
296
|
}
|
|
297
|
+
const scopeLabel = linkedIssues.map((n) => `#${n}`).join(", ");
|
|
254
298
|
if (!prDraft && !prClosed && !prMerged) {
|
|
255
299
|
return {
|
|
256
300
|
status: "unknown",
|
|
257
|
-
linkedIssue,
|
|
258
|
-
|
|
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.`,
|
|
259
304
|
};
|
|
260
305
|
}
|
|
261
|
-
const
|
|
262
|
-
|
|
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,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
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.
|
|
263
352
|
if (prDraft) {
|
|
264
353
|
return {
|
|
265
354
|
status: "missing",
|
|
266
|
-
linkedIssue,
|
|
267
|
-
|
|
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.`,
|
|
268
359
|
finding: "missing_refinement_artifact",
|
|
269
360
|
};
|
|
270
361
|
}
|
|
271
362
|
return {
|
|
272
363
|
status: "unknown",
|
|
273
|
-
linkedIssue,
|
|
274
|
-
|
|
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.`,
|
|
275
368
|
};
|
|
276
369
|
}
|
|
277
|
-
|
|
278
|
-
|
|
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;
|
|
279
376
|
return {
|
|
280
|
-
status:
|
|
281
|
-
linkedIssue,
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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",
|
|
289
390
|
_onlyEnforcedWhenDraft: prDraft === true,
|
|
290
391
|
};
|
|
291
392
|
}
|
|
@@ -305,16 +406,6 @@ async function fetchLocalConflictFiles({ env = process.env, gitCommand = "git" }
|
|
|
305
406
|
}
|
|
306
407
|
return parseGitStatusConflictFiles(result.stdout);
|
|
307
408
|
}
|
|
308
|
-
async function loadRetrospectiveCheckpoint(repoRoot) {
|
|
309
|
-
const checkpointPath = path.join(repoRoot, ".pi", "dev-loop-retrospective-checkpoint.json");
|
|
310
|
-
try {
|
|
311
|
-
const checkpointText = await readFile(checkpointPath, "utf8");
|
|
312
|
-
const checkpoint = parseJsonText(checkpointText, { label: "retrospective checkpoint" });
|
|
313
|
-
return checkpoint && typeof checkpoint === "object" ? checkpoint : null;
|
|
314
|
-
} catch (error) {
|
|
315
|
-
return null;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
409
|
export async function loadPrGateCoordinationContext(options, runtime = {}) {
|
|
319
410
|
const prData = await fetchPrFactsWithSettledMergeable(options, runtime);
|
|
320
411
|
const currentHeadSha = typeof prData?.headRefOid === "string" && prData.headRefOid.trim().length > 0
|
|
@@ -365,7 +456,7 @@ export async function loadPrGateCoordinationContext(options, runtime = {}) {
|
|
|
365
456
|
// READY_TO_REREQUEST_REVIEW, dead-ending the loop at the round cap (#896). This
|
|
366
457
|
// keeps the gate-coordination interpretation consistent with the standalone
|
|
367
458
|
// detect-copilot-loop-state path and with request-copilot-review's cap logic.
|
|
368
|
-
const interpreterRepoRoot = runtime.repoRoot ?? process.cwd();
|
|
459
|
+
const interpreterRepoRoot = runtime.repoRoot ?? resolveRepoRoot(process.cwd());
|
|
369
460
|
const interpreterConfigResult = await loadDevLoopConfig({ repoRoot: interpreterRepoRoot });
|
|
370
461
|
const interpreterRefinementConfig = (Array.isArray(interpreterConfigResult.errors) && interpreterConfigResult.errors.length > 0)
|
|
371
462
|
? resolveRefinement({ version: 1 })
|
|
@@ -419,15 +510,32 @@ async function fetchCopilotEverFormallyRequested({ repo, pr }, { env = process.e
|
|
|
419
510
|
|
|
420
511
|
export async function detectPrGateCoordinationState(options, runtime = {}) {
|
|
421
512
|
const context = await loadPrGateCoordinationContext(options, runtime);
|
|
422
|
-
const repoRoot = runtime.repoRoot ?? process.cwd();
|
|
513
|
+
const repoRoot = runtime.repoRoot ?? resolveRepoRoot(process.cwd());
|
|
423
514
|
const configLoadResult = await loadDevLoopConfig({ repoRoot });
|
|
424
515
|
const hasConfigErrors = Array.isArray(configLoadResult.errors) && configLoadResult.errors.length > 0;
|
|
425
516
|
const config = hasConfigErrors ? {} : (configLoadResult.config ?? {});
|
|
426
517
|
const draftGateConfig = resolveGateConfig(config, "draft");
|
|
427
518
|
const maxCopilotRounds = resolveRefinementConfig(config, "maxCopilotRounds");
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
+
);
|
|
431
539
|
const result = evaluatePrGateCoordination({
|
|
432
540
|
repo: context.repo,
|
|
433
541
|
pr: context.pr,
|
|
@@ -439,6 +547,9 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
|
|
|
439
547
|
mergeStateStatus: context.mergeStateStatus,
|
|
440
548
|
mergeable: context.mergeable,
|
|
441
549
|
conflictFiles: context.conflictFiles,
|
|
550
|
+
// UI e2e auto-scoping (#976): path-triggered + fail-closed precondition.
|
|
551
|
+
changedFiles: extractChangedFiles(context.prData),
|
|
552
|
+
uiE2ePassed: deriveUiE2ePassed(context.prData),
|
|
442
553
|
lifecycleState: context.interpretation.state,
|
|
443
554
|
loopDisposition: context.disposition.loopDisposition,
|
|
444
555
|
ciStatus: context.snapshot?.ciStatus ?? null,
|
|
@@ -446,14 +557,12 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
|
|
|
446
557
|
maxCopilotRounds,
|
|
447
558
|
sameHeadCleanConverged: context.interpretation.sameHeadCleanConverged,
|
|
448
559
|
draftGateRequireCi: draftGateConfig.requireCi,
|
|
449
|
-
requireRetrospectiveGate,
|
|
450
|
-
requireRetrospectiveInternalTooling,
|
|
451
|
-
retrospectiveCheckpoint,
|
|
452
560
|
draftGate: context.gateEvidence.draftGate,
|
|
453
561
|
draftGateMarker: context.gateEvidence.draftGateMarker,
|
|
454
562
|
preApprovalGate: context.gateEvidence.preApprovalGate,
|
|
455
563
|
preApprovalGateMarker: context.gateEvidence.preApprovalGateMarker,
|
|
456
564
|
refinementArtifact: context.refinementArtifact,
|
|
565
|
+
postConvergenceSignificantChange,
|
|
457
566
|
});
|
|
458
567
|
// Copilot review request guard (#613): When Copilot has reviewed the PR
|
|
459
568
|
// but no formal review request was made, block pre-approval gate entry.
|
|
@@ -465,9 +574,6 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
|
|
|
465
574
|
PR_CHECKPOINT.PRE_APPROVAL_GATE_WINDOW,
|
|
466
575
|
PR_CHECKPOINT.FINAL_APPROVAL_READY,
|
|
467
576
|
]);
|
|
468
|
-
const roundCapReached = maxCopilotRounds !== null
|
|
469
|
-
&& typeof (context.snapshot?.copilotReviewRoundCount) === "number"
|
|
470
|
-
&& context.snapshot?.copilotReviewRoundCount >= maxCopilotRounds;
|
|
471
577
|
const sameHeadCleanConverged = context.interpretation?.sameHeadCleanConverged ?? false;
|
|
472
578
|
// Round-cap clean fallback (#896): the interpreter resolved a clean post-cap head
|
|
473
579
|
// (zero unresolved threads + green CI) that Copilot will not re-review. The formal
|
|
@@ -475,7 +581,13 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
|
|
|
475
581
|
const roundCapCleanFallback = context.interpretation?.roundCapCleanEligible ?? false;
|
|
476
582
|
const copilotReviewEverFormallyRequested = copilotReviewRequestStatus === "none"
|
|
477
583
|
&& guardBoundaries.has(result.gateBoundary)
|
|
478
|
-
|
|
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)
|
|
479
591
|
? await fetchCopilotEverFormallyRequested(
|
|
480
592
|
{ repo: context.repo, pr: context.pr },
|
|
481
593
|
runtime,
|
|
@@ -488,6 +600,7 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
|
|
|
488
600
|
maxCopilotRounds,
|
|
489
601
|
sameHeadCleanConverged,
|
|
490
602
|
roundCapCleanFallback,
|
|
603
|
+
postConvergenceSignificantChange,
|
|
491
604
|
gateBoundary: result.gateBoundary,
|
|
492
605
|
})) {
|
|
493
606
|
result.gateBoundary = PR_CHECKPOINT.POST_DRAFT_EXTERNAL_REVIEW;
|
|
@@ -560,7 +673,7 @@ async function main() {
|
|
|
560
673
|
}
|
|
561
674
|
try {
|
|
562
675
|
const result = await detectPrGateCoordinationState(options);
|
|
563
|
-
process.
|
|
676
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent });
|
|
564
677
|
} catch (error) {
|
|
565
678
|
process.stderr.write(`${formatCliError(error)}\n`);
|
|
566
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)) {
|