dev-loops 0.6.0 → 0.7.2
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 +8 -7
- package/.claude/agents/fixer.md +1 -0
- package/.claude/agents/refiner.md +2 -2
- package/.claude/agents/review.md +4 -3
- 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} +2 -2
- 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 +60 -34
- package/.claude/skills/dev-loop/SKILL.md +16 -12
- package/.claude/skills/docs/acceptance-criteria-verification.md +8 -3
- package/.claude/skills/docs/anti-patterns.md +5 -3
- package/.claude/skills/docs/artifact-authority-contract.md +30 -11
- package/.claude/skills/docs/confirmation-rules.md +1 -0
- package/.claude/skills/docs/contract-style-guide.md +37 -0
- package/.claude/skills/docs/copilot-loop-operations.md +19 -15
- package/.claude/skills/docs/cross-harness-regression-contract.md +60 -0
- package/.claude/skills/docs/debt-remediation-contract.md +1 -1
- package/.claude/skills/docs/epic-tree-refinement-procedure.md +25 -22
- package/.claude/skills/docs/issue-intake-procedure.md +26 -19
- 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 +30 -12
- package/.claude/skills/docs/plan-file-contract.md +1 -1
- package/.claude/skills/docs/pr-lifecycle-contract.md +44 -35
- package/.claude/skills/docs/public-dev-loop-contract.md +60 -44
- package/.claude/skills/docs/retrospective-checkpoint-contract.md +106 -79
- package/.claude/skills/docs/spike-mode-contract.md +14 -7
- package/.claude/skills/docs/stop-conditions.md +30 -15
- package/.claude/skills/docs/tracker-first-loop-state.md +13 -8
- package/.claude/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/.claude/skills/docs/validation-policy.md +4 -5
- package/.claude/skills/docs/workflow-handoff-contract.md +39 -0
- package/.claude/skills/local-implementation/SKILL.md +62 -145
- package/.claude/skills/loop-grill/SKILL.md +163 -0
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +101 -0
- package/README.md +9 -9
- package/agents/dev-loop.agent.md +11 -10
- package/agents/developer.agent.md +1 -1
- package/agents/docs.agent.md +1 -1
- package/agents/fixer.agent.md +2 -1
- package/agents/quality.agent.md +1 -1
- package/agents/refiner.agent.md +3 -3
- package/agents/review.agent.md +5 -4
- 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 +12 -12
- package/scripts/_core-helpers.mjs +2 -0
- package/scripts/claude/headless-dev-loop.mjs +1 -1
- package/scripts/docs/validate-rule-ownership.mjs +442 -0
- package/scripts/docs/validate-state-machine-conformance.mjs +1092 -0
- package/scripts/github/_review-thread-mutations.mjs +5 -2
- 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/create-pr.mjs +119 -10
- package/scripts/github/detect-checkpoint-evidence.mjs +259 -18
- 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/list-review-threads.mjs +277 -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 +20 -5
- 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 +98 -26
- 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 +105 -15
- package/scripts/github/verify-briefing-prefixes.mjs +191 -0
- package/scripts/github/verify-fresh-review-context.mjs +226 -32
- package/scripts/github/view-pr.mjs +150 -0
- package/scripts/github/wait-pr-checks.mjs +171 -0
- package/scripts/github/write-gate-context.mjs +458 -65
- package/scripts/github/write-gate-findings-log.mjs +128 -2
- 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 +162 -14
- package/scripts/loop/debt-remediate.mjs +24 -12
- package/scripts/loop/detect-change-scope.mjs +38 -9
- package/scripts/loop/detect-copilot-loop-state.mjs +34 -8
- 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 +184 -69
- 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 +216 -10
- 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 +106 -0
- package/scripts/loop/steer-loop.mjs +29 -16
- package/scripts/loop/validate-pr-body-spec.mjs +223 -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 +443 -0
- package/scripts/projects/_resolve-project.mjs +1 -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 +59 -382
- package/scripts/projects/move-queue-item.mjs +21 -449
- package/scripts/projects/reconcile-queue.mjs +253 -0
- package/scripts/projects/reorder-queue-item.mjs +43 -68
- 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 +60 -34
- package/skills/dev-loop/SKILL.md +11 -7
- package/skills/docs/acceptance-criteria-verification.md +8 -3
- package/skills/docs/anti-patterns.md +5 -3
- package/skills/docs/artifact-authority-contract.md +30 -11
- package/skills/docs/confirmation-rules.md +1 -0
- package/skills/docs/contract-style-guide.md +37 -0
- package/skills/docs/copilot-loop-operations.md +19 -15
- package/skills/docs/cross-harness-regression-contract.md +60 -0
- package/skills/docs/debt-remediation-contract.md +1 -1
- package/skills/docs/epic-tree-refinement-procedure.md +25 -22
- package/skills/docs/issue-intake-procedure.md +26 -19
- 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 +30 -12
- package/skills/docs/plan-file-contract.md +1 -1
- package/skills/docs/pr-lifecycle-contract.md +44 -35
- package/skills/docs/public-dev-loop-contract.md +60 -44
- package/skills/docs/required-rules.json +149 -0
- package/skills/docs/retrospective-checkpoint-contract.md +106 -79
- package/skills/docs/spike-mode-contract.md +14 -7
- package/skills/docs/stop-conditions.md +30 -15
- package/skills/docs/tracker-first-loop-state.md +13 -8
- package/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/skills/docs/validation-policy.md +4 -5
- package/skills/docs/workflow-handoff-contract.md +39 -0
- package/skills/local-implementation/SKILL.md +62 -145
- package/skills/loop-grill/SKILL.md +165 -0
- package/.claude/commands/continue.md +0 -15
- package/scripts/docs/validate-no-duplicate-rules.mjs +0 -250
- 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
|
@@ -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,
|
|
@@ -12,15 +11,18 @@ import {
|
|
|
12
11
|
summarizeCopilotReviews,
|
|
13
12
|
} from "../_core-helpers.mjs";
|
|
14
13
|
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
15
|
-
import { loadDevLoopConfig, resolveGateConfig, resolveRefinement, resolveRefinementConfig, resolveWorkflowConfig } from "@dev-loops/core/config";
|
|
14
|
+
import { loadDevLoopConfig, resolveEffectiveCopilotRoundCap, 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.
|
|
@@ -28,6 +30,12 @@ Required:
|
|
|
28
30
|
--repo <owner/name> Repository slug (e.g. owner/repo)
|
|
29
31
|
--pr <number> Pull request number
|
|
30
32
|
Optional:
|
|
33
|
+
--lightweight This PR is light-dispatched (#1210): compose the Copilot
|
|
34
|
+
round cap with localImplementation.lightMode.maxCopilotRounds
|
|
35
|
+
(default 1) via min(lightMode.maxCopilotRounds,
|
|
36
|
+
refinement.maxCopilotRounds) instead of using
|
|
37
|
+
refinement.maxCopilotRounds alone. refinement.maxCopilotRounds:
|
|
38
|
+
0 still disables Copilot rounds even with --lightweight.
|
|
31
39
|
Output (stdout, JSON):
|
|
32
40
|
{
|
|
33
41
|
"ok": true,
|
|
@@ -67,15 +75,18 @@ Output (stdout, JSON):
|
|
|
67
75
|
Error output (stderr, JSON):
|
|
68
76
|
{ "ok": false, "error": "...", "usage": "..." }
|
|
69
77
|
{ "ok": false, "error": "..." }
|
|
78
|
+
${JQ_OUTPUT_USAGE}
|
|
70
79
|
Exit codes:
|
|
71
80
|
0 Success
|
|
72
|
-
1 Argument error or gh/runtime failure
|
|
81
|
+
1 Argument error or gh/runtime failure
|
|
82
|
+
2 Invalid --jq filter`.trim();
|
|
73
83
|
const parseError = buildParseError(USAGE);
|
|
74
84
|
export function parseDetectPrGateCoordinationCliArgs(argv) {
|
|
75
85
|
const options = {
|
|
76
86
|
help: false,
|
|
77
87
|
repo: undefined,
|
|
78
88
|
pr: undefined,
|
|
89
|
+
lightweight: false,
|
|
79
90
|
};
|
|
80
91
|
const { tokens } = parseArgs({
|
|
81
92
|
args: [...argv],
|
|
@@ -83,6 +94,8 @@ export function parseDetectPrGateCoordinationCliArgs(argv) {
|
|
|
83
94
|
help: { type: "boolean", short: "h" },
|
|
84
95
|
repo: { type: "string" },
|
|
85
96
|
pr: { type: "string" },
|
|
97
|
+
lightweight: { type: "boolean" },
|
|
98
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
86
99
|
},
|
|
87
100
|
allowPositionals: true,
|
|
88
101
|
strict: false,
|
|
@@ -107,6 +120,11 @@ export function parseDetectPrGateCoordinationCliArgs(argv) {
|
|
|
107
120
|
options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
|
|
108
121
|
continue;
|
|
109
122
|
}
|
|
123
|
+
if (token.name === "lightweight") {
|
|
124
|
+
options.lightweight = true;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
110
128
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
111
129
|
}
|
|
112
130
|
if (options.repo === undefined || options.pr === undefined) {
|
|
@@ -221,30 +239,38 @@ export function deriveUiE2ePassed(prData, checkNames = UI_E2E_CHECK_NAMES) {
|
|
|
221
239
|
return present.every((entry) => {
|
|
222
240
|
const conclusion = String(entry?.conclusion ?? "").toUpperCase();
|
|
223
241
|
const state = String(entry?.state ?? "").toUpperCase();
|
|
224
|
-
|
|
242
|
+
// SKIPPED = "not applicable to this run" (e.g. viewer-smoke when no viewer files changed) — not a failure.
|
|
243
|
+
return conclusion === "SUCCESS" || conclusion === "SKIPPED" || state === "SUCCESS" || state === "SKIPPED";
|
|
225
244
|
});
|
|
226
245
|
}
|
|
227
246
|
|
|
228
|
-
|
|
229
|
-
|
|
247
|
+
// Ordered, de-duplicated list of ALL closing-referenced issue numbers for a PR.
|
|
248
|
+
// Umbrella PRs legitimately close multiple issues (#1052), so the refinement
|
|
249
|
+
// guard resolves against every one of them, not just a unique single ref.
|
|
250
|
+
export function resolveLinkedIssuesFromPr(prData) {
|
|
251
|
+
if (!prData || typeof prData !== "object") return [];
|
|
252
|
+
const dedupe = (nums) => {
|
|
253
|
+
const seen = new Set();
|
|
254
|
+
const out = [];
|
|
255
|
+
for (const n of nums) {
|
|
256
|
+
if (Number.isInteger(n) && n > 0 && !seen.has(n)) {
|
|
257
|
+
seen.add(n);
|
|
258
|
+
out.push(n);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
};
|
|
230
263
|
const closing = Array.isArray(prData.closingIssuesReferences) ? prData.closingIssuesReferences : [];
|
|
231
|
-
const closingNumbers = closing
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (closingNumbers.length === 1) {
|
|
235
|
-
return closingNumbers[0];
|
|
264
|
+
const closingNumbers = dedupe(closing.map((entry) => Number(entry?.number)));
|
|
265
|
+
if (closingNumbers.length > 0) {
|
|
266
|
+
return closingNumbers;
|
|
236
267
|
}
|
|
237
268
|
const body = typeof prData.body === "string" ? prData.body : "";
|
|
238
|
-
if (body.length === 0) return
|
|
269
|
+
if (body.length === 0) return [];
|
|
239
270
|
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;
|
|
271
|
+
return dedupe(matches.map((m) => Number((/(\d+)/.exec(m) || [])[1])));
|
|
247
272
|
}
|
|
273
|
+
|
|
248
274
|
async function fetchIssueBody({ repo, issue }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
249
275
|
const result = await runChild(
|
|
250
276
|
ghCommand,
|
|
@@ -261,58 +287,118 @@ async function fetchIssueBody({ repo, issue }, { env = process.env, ghCommand =
|
|
|
261
287
|
return null;
|
|
262
288
|
}
|
|
263
289
|
}
|
|
264
|
-
async function loadRefinementArtifact({ repo, prData, prDraft, prClosed, prMerged }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
265
|
-
const
|
|
266
|
-
if (
|
|
290
|
+
export async function loadRefinementArtifact({ repo, prData, prDraft, prClosed, prMerged }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
291
|
+
const linkedIssues = resolveLinkedIssuesFromPr(prData);
|
|
292
|
+
if (linkedIssues.length === 0) {
|
|
267
293
|
if (prDraft) {
|
|
268
294
|
return {
|
|
269
295
|
status: "missing",
|
|
270
296
|
linkedIssue: null,
|
|
271
|
-
|
|
297
|
+
linkedIssues: [],
|
|
298
|
+
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
299
|
finding: "missing_refinement_artifact",
|
|
273
300
|
};
|
|
274
301
|
}
|
|
275
302
|
return {
|
|
276
303
|
status: "unknown",
|
|
277
304
|
linkedIssue: null,
|
|
278
|
-
|
|
305
|
+
linkedIssues: [],
|
|
306
|
+
reason: "No deterministically resolvable linked issue (no closingIssuesReferences and no Closes/Fixes/Resolves #n reference in body).",
|
|
279
307
|
};
|
|
280
308
|
}
|
|
309
|
+
const scopeLabel = linkedIssues.map((n) => `#${n}`).join(", ");
|
|
281
310
|
if (!prDraft && !prClosed && !prMerged) {
|
|
282
311
|
return {
|
|
283
312
|
status: "unknown",
|
|
284
|
-
linkedIssue,
|
|
285
|
-
|
|
313
|
+
linkedIssue: linkedIssues.length === 1 ? linkedIssues[0] : null,
|
|
314
|
+
linkedIssues,
|
|
315
|
+
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.`,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const { detectIssueRefinementArtifact } = await import("@dev-loops/core/loop/issue-refinement-artifact");
|
|
319
|
+
// Fetch and evaluate every closing-referenced issue. An umbrella PR's scope
|
|
320
|
+
// is refined if AT LEAST ONE linked issue carries a refinement artifact.
|
|
321
|
+
const evaluated = [];
|
|
322
|
+
for (const issue of linkedIssues) {
|
|
323
|
+
const body = await fetchIssueBody({ repo, issue }, { env, ghCommand });
|
|
324
|
+
if (body === null) {
|
|
325
|
+
evaluated.push({ issue, artifact: null });
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
evaluated.push({ issue, artifact: detectIssueRefinementArtifact({ body, issueNumber: issue }) });
|
|
329
|
+
}
|
|
330
|
+
const refinedIssues = evaluated
|
|
331
|
+
.filter((e) => e.artifact && e.artifact.hasACs === true)
|
|
332
|
+
.map((e) => e.issue);
|
|
333
|
+
const firstPresent = evaluated.find((e) => e.artifact && e.artifact.hasACs === true);
|
|
334
|
+
const isUmbrella = linkedIssues.length > 1;
|
|
335
|
+
|
|
336
|
+
if (firstPresent) {
|
|
337
|
+
const a = firstPresent.artifact;
|
|
338
|
+
return {
|
|
339
|
+
status: "present",
|
|
340
|
+
linkedIssue: firstPresent.issue,
|
|
341
|
+
linkedIssues,
|
|
342
|
+
refinedIssues,
|
|
343
|
+
source: a.source,
|
|
344
|
+
acItems: a.acItems,
|
|
345
|
+
dodItems: a.dodItems,
|
|
346
|
+
sections: a.sections,
|
|
347
|
+
linkedDoc: a.linkedDoc,
|
|
348
|
+
reason: isUmbrella
|
|
349
|
+
? `Refinement artifact present via linked issue #${firstPresent.issue} (umbrella PR closes ${scopeLabel}).`
|
|
350
|
+
: a.reason,
|
|
351
|
+
finding: a.finding,
|
|
352
|
+
_onlyEnforcedWhenDraft: prDraft === true,
|
|
286
353
|
};
|
|
287
354
|
}
|
|
288
|
-
|
|
289
|
-
|
|
355
|
+
|
|
356
|
+
// None of the linked issues carry a refinement artifact (or all bodies failed
|
|
357
|
+
// to fetch). Report against the first linked issue for single-value consumers.
|
|
358
|
+
// Note: `finding`/`missing` here is only enforced by the gate when the PR is
|
|
359
|
+
// draft (`_onlyEnforcedWhenDraft`); closed/merged PRs surface it informationally.
|
|
360
|
+
const firstEvaluated = evaluated[0];
|
|
361
|
+
const allFailed = evaluated.every((e) => e.artifact === null);
|
|
362
|
+
if (allFailed) {
|
|
363
|
+
// Preserve prior single-issue semantics: draft → missing, else unknown.
|
|
290
364
|
if (prDraft) {
|
|
291
365
|
return {
|
|
292
366
|
status: "missing",
|
|
293
|
-
linkedIssue,
|
|
294
|
-
|
|
367
|
+
linkedIssue: firstEvaluated.issue,
|
|
368
|
+
linkedIssues,
|
|
369
|
+
refinedIssues,
|
|
370
|
+
reason: `Failed to fetch body for linked issue(s) ${scopeLabel}; draft gate cannot verify a refinement artifact, treating as missing.`,
|
|
295
371
|
finding: "missing_refinement_artifact",
|
|
296
372
|
};
|
|
297
373
|
}
|
|
298
374
|
return {
|
|
299
375
|
status: "unknown",
|
|
300
|
-
linkedIssue,
|
|
301
|
-
|
|
376
|
+
linkedIssue: linkedIssues.length === 1 ? linkedIssues[0] : firstEvaluated.issue,
|
|
377
|
+
linkedIssues,
|
|
378
|
+
refinedIssues,
|
|
379
|
+
reason: `Failed to fetch body for linked issue(s) ${scopeLabel}; refinement status is unknown.`,
|
|
302
380
|
};
|
|
303
381
|
}
|
|
304
|
-
|
|
305
|
-
|
|
382
|
+
// Mixed branch: not allFailed, so at least one body fetched but none is
|
|
383
|
+
// refined. Report against the first successfully-fetched (non-null) issue —
|
|
384
|
+
// `evaluated[0]` may be a failed fetch: it still retains its `issue` field but
|
|
385
|
+
// has `artifact: null` (body fetch / artifact detection failed for that issue).
|
|
386
|
+
const firstFetched = evaluated.find((e) => e.artifact !== null);
|
|
387
|
+
const first = firstFetched.artifact;
|
|
306
388
|
return {
|
|
307
|
-
status:
|
|
308
|
-
linkedIssue,
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
389
|
+
status: "missing",
|
|
390
|
+
linkedIssue: firstFetched.issue,
|
|
391
|
+
linkedIssues,
|
|
392
|
+
refinedIssues,
|
|
393
|
+
source: first.source,
|
|
394
|
+
acItems: first.acItems,
|
|
395
|
+
dodItems: first.dodItems,
|
|
396
|
+
sections: first.sections,
|
|
397
|
+
linkedDoc: first.linkedDoc,
|
|
398
|
+
reason: isUmbrella
|
|
399
|
+
? `No linked issue (${scopeLabel}) carries a refinement artifact (ACs/DoD); draft gate cannot verify a refinement artifact.`
|
|
400
|
+
: first.reason,
|
|
401
|
+
finding: "missing_refinement_artifact",
|
|
316
402
|
_onlyEnforcedWhenDraft: prDraft === true,
|
|
317
403
|
};
|
|
318
404
|
}
|
|
@@ -332,16 +418,6 @@ async function fetchLocalConflictFiles({ env = process.env, gitCommand = "git" }
|
|
|
332
418
|
}
|
|
333
419
|
return parseGitStatusConflictFiles(result.stdout);
|
|
334
420
|
}
|
|
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
421
|
export async function loadPrGateCoordinationContext(options, runtime = {}) {
|
|
346
422
|
const prData = await fetchPrFactsWithSettledMergeable(options, runtime);
|
|
347
423
|
const currentHeadSha = typeof prData?.headRefOid === "string" && prData.headRefOid.trim().length > 0
|
|
@@ -392,11 +468,22 @@ export async function loadPrGateCoordinationContext(options, runtime = {}) {
|
|
|
392
468
|
// READY_TO_REREQUEST_REVIEW, dead-ending the loop at the round cap (#896). This
|
|
393
469
|
// keeps the gate-coordination interpretation consistent with the standalone
|
|
394
470
|
// detect-copilot-loop-state path and with request-copilot-review's cap logic.
|
|
395
|
-
const interpreterRepoRoot = runtime.repoRoot ?? process.cwd();
|
|
471
|
+
const interpreterRepoRoot = runtime.repoRoot ?? resolveRepoRoot(process.cwd());
|
|
396
472
|
const interpreterConfigResult = await loadDevLoopConfig({ repoRoot: interpreterRepoRoot });
|
|
397
|
-
const
|
|
473
|
+
const interpreterConfigHasErrors = Array.isArray(interpreterConfigResult.errors) && interpreterConfigResult.errors.length > 0;
|
|
474
|
+
const interpreterRefinementConfig = interpreterConfigHasErrors
|
|
398
475
|
? resolveRefinement({ version: 1 })
|
|
399
476
|
: resolveRefinement(interpreterConfigResult.config ?? { version: 1 });
|
|
477
|
+
if (options.lightweight) {
|
|
478
|
+
// Compose (not replace) the round cap for light-dispatched PRs (#1210):
|
|
479
|
+
// min(lightMode.maxCopilotRounds ?? 1, refinement.maxCopilotRounds), so
|
|
480
|
+
// maxCopilotRounds: 0 still disables Copilot rounds everywhere. Shared with
|
|
481
|
+
// the maxCopilotRounds resolution below (#1126 requires the two to agree).
|
|
482
|
+
interpreterRefinementConfig.maxCopilotRounds = resolveEffectiveCopilotRoundCap(
|
|
483
|
+
interpreterConfigHasErrors ? { version: 1 } : (interpreterConfigResult.config ?? { version: 1 }),
|
|
484
|
+
{ lightweight: true },
|
|
485
|
+
);
|
|
486
|
+
}
|
|
400
487
|
const interpretation = interpretLoopState(snapshot, interpreterRefinementConfig);
|
|
401
488
|
const disposition = summarizeLoopInterpretation(interpretation, interpreterRefinementConfig);
|
|
402
489
|
const mergeStateStatus = typeof prData?.mergeStateStatus === "string" && prData.mergeStateStatus.trim().length > 0
|
|
@@ -446,15 +533,37 @@ async function fetchCopilotEverFormallyRequested({ repo, pr }, { env = process.e
|
|
|
446
533
|
|
|
447
534
|
export async function detectPrGateCoordinationState(options, runtime = {}) {
|
|
448
535
|
const context = await loadPrGateCoordinationContext(options, runtime);
|
|
449
|
-
const repoRoot = runtime.repoRoot ?? process.cwd();
|
|
536
|
+
const repoRoot = runtime.repoRoot ?? resolveRepoRoot(process.cwd());
|
|
450
537
|
const configLoadResult = await loadDevLoopConfig({ repoRoot });
|
|
451
538
|
const hasConfigErrors = Array.isArray(configLoadResult.errors) && configLoadResult.errors.length > 0;
|
|
452
539
|
const config = hasConfigErrors ? {} : (configLoadResult.config ?? {});
|
|
453
540
|
const draftGateConfig = resolveGateConfig(config, "draft");
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
const
|
|
541
|
+
// Shared with interpreterRefinementConfig.maxCopilotRounds in
|
|
542
|
+
// loadPrGateCoordinationContext (#1126: the two must never disagree at the
|
|
543
|
+
// cap boundary) — the same lightweight composition (#1210) is applied here.
|
|
544
|
+
const maxCopilotRounds = options.lightweight
|
|
545
|
+
? resolveEffectiveCopilotRoundCap(config, { lightweight: true })
|
|
546
|
+
: resolveRefinementConfig(config, "maxCopilotRounds");
|
|
547
|
+
// Shared with interpretLoopState (consumed by copilot-pr-handoff.mjs) and
|
|
548
|
+
// evaluatePrGateCoordination — the single source of truth for "is the
|
|
549
|
+
// Copilot round cap reached" so this detector cannot disagree with the
|
|
550
|
+
// handoff at the cap boundary (#1126).
|
|
551
|
+
const roundCapReached = isCopilotRoundCapReached({
|
|
552
|
+
copilotReviewRoundCount: context.snapshot?.copilotReviewRoundCount,
|
|
553
|
+
maxCopilotRounds,
|
|
554
|
+
});
|
|
555
|
+
const postConvergenceSignificantChange = await detectPostConvergenceSignificantChange(
|
|
556
|
+
{
|
|
557
|
+
repo: context.repo,
|
|
558
|
+
pr: context.pr,
|
|
559
|
+
currentHeadSha: context.currentHeadSha,
|
|
560
|
+
reviews: context.prData?.reviews,
|
|
561
|
+
changedFiles: context.prData?.files,
|
|
562
|
+
roundCapReached: roundCapReached && context.interpretation?.roundCapCleanEligible === true,
|
|
563
|
+
regularCopilotRounds: (context.snapshot?.copilotReviewRoundCount ?? 0) > 0,
|
|
564
|
+
},
|
|
565
|
+
runtime,
|
|
566
|
+
);
|
|
458
567
|
const result = evaluatePrGateCoordination({
|
|
459
568
|
repo: context.repo,
|
|
460
569
|
pr: context.pr,
|
|
@@ -475,15 +584,17 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
|
|
|
475
584
|
copilotReviewRoundCount: context.snapshot?.copilotReviewRoundCount ?? 0,
|
|
476
585
|
maxCopilotRounds,
|
|
477
586
|
sameHeadCleanConverged: context.interpretation.sameHeadCleanConverged,
|
|
587
|
+
// Independent gate-ENTRY re-check (#1190): fed alongside (not derived from)
|
|
588
|
+
// sameHeadCleanConverged, so an outstanding request on the current head refuses
|
|
589
|
+
// RUN_PRE_APPROVAL_GATE even if sameHeadCleanConverged were somehow stale/wrong.
|
|
590
|
+
copilotReviewRequestStatus: context.snapshot?.copilotReviewRequestStatus ?? "none",
|
|
478
591
|
draftGateRequireCi: draftGateConfig.requireCi,
|
|
479
|
-
requireRetrospectiveGate,
|
|
480
|
-
requireRetrospectiveInternalTooling,
|
|
481
|
-
retrospectiveCheckpoint,
|
|
482
592
|
draftGate: context.gateEvidence.draftGate,
|
|
483
593
|
draftGateMarker: context.gateEvidence.draftGateMarker,
|
|
484
594
|
preApprovalGate: context.gateEvidence.preApprovalGate,
|
|
485
595
|
preApprovalGateMarker: context.gateEvidence.preApprovalGateMarker,
|
|
486
596
|
refinementArtifact: context.refinementArtifact,
|
|
597
|
+
postConvergenceSignificantChange,
|
|
487
598
|
});
|
|
488
599
|
// Copilot review request guard (#613): When Copilot has reviewed the PR
|
|
489
600
|
// but no formal review request was made, block pre-approval gate entry.
|
|
@@ -495,9 +606,6 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
|
|
|
495
606
|
PR_CHECKPOINT.PRE_APPROVAL_GATE_WINDOW,
|
|
496
607
|
PR_CHECKPOINT.FINAL_APPROVAL_READY,
|
|
497
608
|
]);
|
|
498
|
-
const roundCapReached = maxCopilotRounds !== null
|
|
499
|
-
&& typeof (context.snapshot?.copilotReviewRoundCount) === "number"
|
|
500
|
-
&& context.snapshot?.copilotReviewRoundCount >= maxCopilotRounds;
|
|
501
609
|
const sameHeadCleanConverged = context.interpretation?.sameHeadCleanConverged ?? false;
|
|
502
610
|
// Round-cap clean fallback (#896): the interpreter resolved a clean post-cap head
|
|
503
611
|
// (zero unresolved threads + green CI) that Copilot will not re-review. The formal
|
|
@@ -505,7 +613,13 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
|
|
|
505
613
|
const roundCapCleanFallback = context.interpretation?.roundCapCleanEligible ?? false;
|
|
506
614
|
const copilotReviewEverFormallyRequested = copilotReviewRequestStatus === "none"
|
|
507
615
|
&& guardBoundaries.has(result.gateBoundary)
|
|
508
|
-
|
|
616
|
+
// cap-0 disables the Copilot gate, so shouldGuardCopilotReviewRequest always
|
|
617
|
+
// returns false here — skip the timeline fetch it would never need (#1126).
|
|
618
|
+
// (Restores the suppression the roundCapReached predicate swap dropped.)
|
|
619
|
+
&& maxCopilotRounds !== 0
|
|
620
|
+
&& !(roundCapReached
|
|
621
|
+
&& (sameHeadCleanConverged || roundCapCleanFallback)
|
|
622
|
+
&& !postConvergenceSignificantChange)
|
|
509
623
|
? await fetchCopilotEverFormallyRequested(
|
|
510
624
|
{ repo: context.repo, pr: context.pr },
|
|
511
625
|
runtime,
|
|
@@ -518,6 +632,7 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
|
|
|
518
632
|
maxCopilotRounds,
|
|
519
633
|
sameHeadCleanConverged,
|
|
520
634
|
roundCapCleanFallback,
|
|
635
|
+
postConvergenceSignificantChange,
|
|
521
636
|
gateBoundary: result.gateBoundary,
|
|
522
637
|
})) {
|
|
523
638
|
result.gateBoundary = PR_CHECKPOINT.POST_DRAFT_EXTERNAL_REVIEW;
|
|
@@ -590,7 +705,7 @@ async function main() {
|
|
|
590
705
|
}
|
|
591
706
|
try {
|
|
592
707
|
const result = await detectPrGateCoordinationState(options);
|
|
593
|
-
process.
|
|
708
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent });
|
|
594
709
|
} catch (error) {
|
|
595
710
|
process.stderr.write(`${formatCliError(error)}\n`);
|
|
596
711
|
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)) {
|