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
|
@@ -21,7 +21,8 @@ import {
|
|
|
21
21
|
} from "@dev-loops/core/loop/async-start-contract";
|
|
22
22
|
import { detectRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
23
23
|
import { isCopilotLogin } from "@dev-loops/core/github/copilot-helpers";
|
|
24
|
-
import { loadDevLoopConfig, resolveWorkflowConfig } from "@dev-loops/core/config";
|
|
24
|
+
import { loadDevLoopConfig, resolveLightMode, resolveWorkflowConfig } from "@dev-loops/core/config";
|
|
25
|
+
import { detectScope } from "./detect-change-scope.mjs";
|
|
25
26
|
import { createPiAdapter } from "@dev-loops/core/harness";
|
|
26
27
|
import { validatePlanFile } from "../refine/validate-plan-file.mjs";
|
|
27
28
|
import {
|
|
@@ -34,7 +35,10 @@ import {
|
|
|
34
35
|
PLAN_FILE_REFINEMENT_SECTIONS,
|
|
35
36
|
} from "@dev-loops/core/loop/plan-file-intake-contract";
|
|
36
37
|
import { evaluateSpikeIntakeState } from "@dev-loops/core/loop/spike-intake-contract";
|
|
38
|
+
import { loadBoardConfig } from "@dev-loops/core/loop/queue-board-sync";
|
|
39
|
+
import { main as reconcileQueue } from "../projects/reconcile-queue.mjs";
|
|
37
40
|
import { parseArgs } from "node:util";
|
|
41
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
38
42
|
const USAGE = `Usage:
|
|
39
43
|
resolve-dev-loop-startup.mjs --issue <number>
|
|
40
44
|
resolve-dev-loop-startup.mjs --pr <number>
|
|
@@ -54,9 +58,31 @@ Required (exactly one):
|
|
|
54
58
|
--input <path> Path to a JSON file with canonical-state payload
|
|
55
59
|
--plan-file <path> Path to a phase-doc-format plan to start locally
|
|
56
60
|
--spike <path> Path to a spike artifact to start a spike loop locally
|
|
61
|
+
Optional modifier:
|
|
62
|
+
--lightweight With --issue: use the PR body as the spec-of-record
|
|
63
|
+
(canonicalSpecSource: pr_body) — no phase/plan doc minted or
|
|
64
|
+
committed. Same gate sequence; only the backing artifact
|
|
65
|
+
differs. Rejected with --plan-file (its opposite). The secondary
|
|
66
|
+
heuristic (chore/fix commit type + no --plan-file + small change)
|
|
67
|
+
is a documented manual signal; --lightweight is the explicit,
|
|
68
|
+
deterministic trigger.
|
|
69
|
+
Used ALONE (no --issue/--pr/--input/--plan-file/--spike):
|
|
70
|
+
issue-less PR-first (#1210) — no tracker binding at all
|
|
71
|
+
(canonicalSpecSource: pr_body, no issue-keyed worktree
|
|
72
|
+
requirement). Gated on localImplementation.lightMode being
|
|
73
|
+
enabled AND the live change scope (git diff) staying within
|
|
74
|
+
its maxFiles/maxLines threshold; fails closed with a distinct
|
|
75
|
+
reason (light mode disabled / scope undetectable / over
|
|
76
|
+
threshold) requiring --issue above the threshold.
|
|
77
|
+
${JQ_OUTPUT_USAGE}
|
|
78
|
+
|
|
57
79
|
Exit codes:
|
|
58
80
|
0 Success
|
|
59
|
-
1 Argument error, runtime failure, or async-start contract rejection
|
|
81
|
+
1 Argument error, runtime failure, or async-start contract rejection
|
|
82
|
+
2 Invalid --jq filter`.trim();
|
|
83
|
+
// Upper bound on the awaited best-effort startup reconcile so a slow or hung gh
|
|
84
|
+
// can never delay startup completion.
|
|
85
|
+
const STARTUP_RECONCILE_BUDGET_MS = 20000;
|
|
60
86
|
const SHARED_PUBLIC_CONTRACT = "skills/docs/public-dev-loop-contract.md";
|
|
61
87
|
const SHARED_RETROSPECTIVE_CONTRACT = "skills/docs/retrospective-checkpoint-contract.md";
|
|
62
88
|
const STRATEGY_REQUIRED_READS = {
|
|
@@ -123,6 +149,7 @@ export function parseResolveDevLoopStartupCliArgs(argv) {
|
|
|
123
149
|
pr: undefined,
|
|
124
150
|
planFile: undefined,
|
|
125
151
|
spike: undefined,
|
|
152
|
+
lightweight: false,
|
|
126
153
|
};
|
|
127
154
|
const { tokens } = parseArgs({
|
|
128
155
|
args: [...argv],
|
|
@@ -133,6 +160,8 @@ export function parseResolveDevLoopStartupCliArgs(argv) {
|
|
|
133
160
|
pr: { type: "string" },
|
|
134
161
|
"plan-file": { type: "string" },
|
|
135
162
|
spike: { type: "string" },
|
|
163
|
+
lightweight: { type: "boolean" },
|
|
164
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
136
165
|
},
|
|
137
166
|
allowPositionals: true,
|
|
138
167
|
strict: false,
|
|
@@ -169,14 +198,34 @@ export function parseResolveDevLoopStartupCliArgs(argv) {
|
|
|
169
198
|
options.spike = requireTokenValue(token, parseError);
|
|
170
199
|
continue;
|
|
171
200
|
}
|
|
201
|
+
if (token.name === "lightweight") {
|
|
202
|
+
options.lightweight = true;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
172
206
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
173
207
|
}
|
|
174
208
|
const modeCount = [options.inputPath, options.issue, options.pr, options.planFile, options.spike].filter(v => v !== undefined).length;
|
|
175
209
|
if (modeCount > 1) {
|
|
176
210
|
throw parseError("--issue, --pr, --input, --plan-file, and --spike are mutually exclusive; provide exactly one");
|
|
177
211
|
}
|
|
178
|
-
|
|
179
|
-
|
|
212
|
+
// --lightweight is normally a MODIFIER (not a 6th mode): it makes the PR body
|
|
213
|
+
// the spec-of-record for the --issue local path. Used ALONE (modeCount === 0,
|
|
214
|
+
// issue #1210) it is instead the issue-less PR-first trigger — no tracker
|
|
215
|
+
// binding at all — so the "no mode selected" error is skipped in that case.
|
|
216
|
+
if (modeCount === 0 && !options.lightweight) {
|
|
217
|
+
throw parseError("--input <path>, --issue <n>, --pr <n>, --plan-file <path>, --spike <path>, or --lightweight (issue-less PR-first) is required");
|
|
218
|
+
}
|
|
219
|
+
// --lightweight is the opposite of --plan-file (which commits a durable plan
|
|
220
|
+
// doc as the spec) regardless of mode.
|
|
221
|
+
if (options.lightweight && options.planFile !== undefined) {
|
|
222
|
+
throw parseError("--lightweight and --plan-file are opposites: --plan-file commits a durable plan doc as the spec-of-record, --lightweight makes the PR body the spec. Provide only one.");
|
|
223
|
+
}
|
|
224
|
+
// When another mode IS selected, --lightweight only composes with --issue
|
|
225
|
+
// (not --pr/--input/--spike). Used with no mode selected at all, it is the
|
|
226
|
+
// issue-less PR-first trigger handled above.
|
|
227
|
+
if (options.lightweight && modeCount > 0 && options.issue === undefined) {
|
|
228
|
+
throw parseError("--lightweight is a modifier for the --issue path (the PR body becomes the spec-of-record). Combine it with --issue <n>, or use --lightweight alone (no other mode flag) for the issue-less PR-first path.");
|
|
180
229
|
}
|
|
181
230
|
return options;
|
|
182
231
|
}
|
|
@@ -264,7 +313,7 @@ function resolveTargetPreference(cwd) {
|
|
|
264
313
|
} catch {
|
|
265
314
|
}
|
|
266
315
|
}
|
|
267
|
-
return "
|
|
316
|
+
return "prefer_local";
|
|
268
317
|
}
|
|
269
318
|
function normalizeConfigInputSource(value) {
|
|
270
319
|
if (value === "phase-docs") return "phase-docs";
|
|
@@ -511,6 +560,120 @@ export function buildSpikeInput({ spikeFilePath }) {
|
|
|
511
560
|
},
|
|
512
561
|
};
|
|
513
562
|
}
|
|
563
|
+
// Candidate default-branch refs for issue-less scope measurement, in preference
|
|
564
|
+
// order. origin/HEAD tracks the remote default branch when set; the rest cover
|
|
565
|
+
// the common names.
|
|
566
|
+
const ISSUELESS_BASE_REF_CANDIDATES = ["origin/HEAD", "origin/main", "origin/master", "main", "master"];
|
|
567
|
+
|
|
568
|
+
function resolveIssuelessMergeBase(cwd) {
|
|
569
|
+
for (const ref of ISSUELESS_BASE_REF_CANDIDATES) {
|
|
570
|
+
try {
|
|
571
|
+
return execFileSync("git", ["merge-base", ref, "HEAD"], {
|
|
572
|
+
cwd,
|
|
573
|
+
encoding: "utf8",
|
|
574
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
575
|
+
}).trim();
|
|
576
|
+
} catch {
|
|
577
|
+
// try the next candidate
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Decide whether the live change scope is eligible for issue-less lightweight
|
|
585
|
+
* PR-first (#1210): reuses the same localImplementation.lightMode threshold
|
|
586
|
+
* that gates inline vs full-fanout gate dispatch, so "genuinely small" means
|
|
587
|
+
* the same thing everywhere in the repo.
|
|
588
|
+
*
|
|
589
|
+
* Scope is measured from the merge-base with the default branch to the WORKING
|
|
590
|
+
* TREE (`git diff --stat <merge-base>`), so multi-commit branches and
|
|
591
|
+
* uncommitted changes are both counted — a HEAD~1-only measure would fail OPEN
|
|
592
|
+
* on a branch whose earlier commits already exceed the threshold. Fails CLOSED
|
|
593
|
+
* on every negative path (disabled / no resolvable base / undetectable diff /
|
|
594
|
+
* over threshold) with a distinct reason so the caller can report why --issue
|
|
595
|
+
* is required instead of silently defaulting one way or the other.
|
|
596
|
+
*
|
|
597
|
+
* Every git invocation is bound to `cwd` (the adapter-resolved repoRoot),
|
|
598
|
+
* matching the rest of this file's git calls — process.cwd() is never relied
|
|
599
|
+
* on implicitly so this stays correct when invoked from a subdirectory or
|
|
600
|
+
* with a harness cwd that differs from the OS process cwd.
|
|
601
|
+
*
|
|
602
|
+
* @param {import("@dev-loops/core/config").DevLoopConfig} config
|
|
603
|
+
* @param {string} [cwd] - Repo root to run git commands in (defaults to process.cwd() when omitted).
|
|
604
|
+
* @returns {{ eligible: true, scope: object, threshold: {maxFiles:number,maxLines:number} } | { eligible: false, reason: "light_mode_disabled"|"scope_detection_failed"|"over_threshold", scope?: object, threshold?: object, detail?: string }}
|
|
605
|
+
*/
|
|
606
|
+
export function resolveIssuelessLightweightEligibility(config, cwd) {
|
|
607
|
+
const threshold = resolveLightMode(config);
|
|
608
|
+
if (!threshold) {
|
|
609
|
+
return { eligible: false, reason: "light_mode_disabled" };
|
|
610
|
+
}
|
|
611
|
+
const mergeBase = resolveIssuelessMergeBase(cwd);
|
|
612
|
+
if (mergeBase === null) {
|
|
613
|
+
return {
|
|
614
|
+
eligible: false,
|
|
615
|
+
reason: "scope_detection_failed",
|
|
616
|
+
detail: `no merge-base with any default-branch candidate (${ISSUELESS_BASE_REF_CANDIDATES.join(", ")})`,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
// detectScope with only `base` diffs base..working-tree in one measure
|
|
620
|
+
// (committed branch delta + uncommitted changes).
|
|
621
|
+
const scope = detectScope({ base: mergeBase, cwd });
|
|
622
|
+
if (scope.ok === false) {
|
|
623
|
+
return { eligible: false, reason: "scope_detection_failed", detail: scope.error };
|
|
624
|
+
}
|
|
625
|
+
if (scope.filesChanged > threshold.maxFiles || scope.linesChanged > threshold.maxLines) {
|
|
626
|
+
return { eligible: false, reason: "over_threshold", scope, threshold };
|
|
627
|
+
}
|
|
628
|
+
return { eligible: true, scope, threshold };
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Build a `--lightweight` startup input with NO tracker binding at all
|
|
633
|
+
* (issue-less PR-first, #1210): `--lightweight` used alone, no --issue.
|
|
634
|
+
*
|
|
635
|
+
* Read-only: no tracker mutation, no GitHub calls, no issue/PR number. Gated
|
|
636
|
+
* by {@link resolveIssuelessLightweightEligibility} — an ineligible change
|
|
637
|
+
* throws so the CLI fails closed (exit 1, no readiness bundle) with a message
|
|
638
|
+
* naming the distinct reason, mirroring buildPlanFileInput/buildSpikeInput's
|
|
639
|
+
* fail-closed-on-invalid-input convention. Exempt from the worktree-isolation
|
|
640
|
+
* guard like the plan-file/spike paths: there is no issue number to key a
|
|
641
|
+
* worktree on.
|
|
642
|
+
*
|
|
643
|
+
* @param {{ config: import("@dev-loops/core/config").DevLoopConfig, cwd?: string }} params
|
|
644
|
+
* @returns {object} startup input with canonicalSpecSource: "pr_body"
|
|
645
|
+
*/
|
|
646
|
+
export function buildLightweightIssuelessInput({ config, cwd }) {
|
|
647
|
+
const eligibility = resolveIssuelessLightweightEligibility(config, cwd);
|
|
648
|
+
if (!eligibility.eligible) {
|
|
649
|
+
if (eligibility.reason === "light_mode_disabled") {
|
|
650
|
+
throw new Error("--lightweight without --issue (issue-less PR-first) requires localImplementation.lightMode.enabled in .devloops; enable light mode or provide --issue <n>.");
|
|
651
|
+
}
|
|
652
|
+
if (eligibility.reason === "scope_detection_failed") {
|
|
653
|
+
throw new Error(`--lightweight without --issue (issue-less PR-first) requires a measurable change scope; git diff failed (${eligibility.detail}). Provide --issue <n> instead.`);
|
|
654
|
+
}
|
|
655
|
+
throw new Error(`--lightweight without --issue (issue-less PR-first) requires the change to stay within the light-mode threshold (maxFiles=${eligibility.threshold.maxFiles}, maxLines=${eligibility.threshold.maxLines}); this change is ${eligibility.scope.filesChanged} files / ${eligibility.scope.linesChanged} lines. Provide --issue <n> for above-threshold changes.`);
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
intent: "start_issue_locally",
|
|
659
|
+
mode: "bounded_handoff",
|
|
660
|
+
targetPreference: "prefer_local",
|
|
661
|
+
artifactState: "not_applicable",
|
|
662
|
+
issueLinkageResolution: "not_applicable",
|
|
663
|
+
issueReadiness: "not_applicable",
|
|
664
|
+
issueAssignmentState: "not_applicable",
|
|
665
|
+
loopState: "implementation_pending",
|
|
666
|
+
canonicalSpecSource: "pr_body",
|
|
667
|
+
planFileExempt: true,
|
|
668
|
+
currentState: {
|
|
669
|
+
target: { kind: "local_phase", issue: null, pr: null, linkedPr: null, branch: null, phase: "lightweight-issueless" },
|
|
670
|
+
ownership: "local",
|
|
671
|
+
nextActor: "local",
|
|
672
|
+
status: "active",
|
|
673
|
+
authorization: "authorized",
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
}
|
|
514
677
|
export function summarizeCanonicalState(bundle) {
|
|
515
678
|
return {
|
|
516
679
|
target: bundle.canonicalState?.target ?? null,
|
|
@@ -541,7 +704,10 @@ export function buildResolveDevLoopStartupResult(input, { adapter = createPiAdap
|
|
|
541
704
|
// evaluator does not model. Strip them before evaluation and re-apply them to
|
|
542
705
|
// the result; `planFileExempt` waives the worktree-isolation guard because a
|
|
543
706
|
// pre-promotion plan has no issue to key a worktree on.
|
|
544
|
-
|
|
707
|
+
// `canonicalSpecSource` (issue #1025) is a resolver-only field the pure routing
|
|
708
|
+
// evaluator does not model — strip it before evaluation and re-attach to the
|
|
709
|
+
// result, mirroring planFileIntakeState/spikeIntakeState.
|
|
710
|
+
const { planFileExempt = false, planFileIntakeState = null, spikeIntakeState = null, canonicalSpecSource = null, ...routingInput } = input;
|
|
545
711
|
input = routingInput;
|
|
546
712
|
try {
|
|
547
713
|
const checkpointText = readFileSync(
|
|
@@ -652,6 +818,7 @@ export function buildResolveDevLoopStartupResult(input, { adapter = createPiAdap
|
|
|
652
818
|
canonicalStateSummary: summarizeCanonicalState(bundle),
|
|
653
819
|
...(planFileIntakeState !== null ? { planFileIntakeState } : {}),
|
|
654
820
|
...(spikeIntakeState !== null ? { spikeIntakeState } : {}),
|
|
821
|
+
...(canonicalSpecSource !== null ? { canonicalSpecSource } : {}),
|
|
655
822
|
bundle,
|
|
656
823
|
};
|
|
657
824
|
}
|
|
@@ -672,7 +839,7 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
|
|
|
672
839
|
? devLoopConfig?.strategy?.default === "local-first"
|
|
673
840
|
? "prefer_local"
|
|
674
841
|
: "prefer_github_first"
|
|
675
|
-
: "
|
|
842
|
+
: "prefer_local";
|
|
676
843
|
const inputSource = configErrors.length === 0
|
|
677
844
|
? normalizeConfigInputSource(devLoopConfig?.inputSource?.default)
|
|
678
845
|
: "tracker";
|
|
@@ -693,6 +860,7 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
|
|
|
693
860
|
delete parsed.planFileExempt;
|
|
694
861
|
delete parsed.planFileIntakeState;
|
|
695
862
|
delete parsed.spikeIntakeState;
|
|
863
|
+
delete parsed.canonicalSpecSource;
|
|
696
864
|
}
|
|
697
865
|
input = parsed;
|
|
698
866
|
} else if (options.issue !== undefined) {
|
|
@@ -702,6 +870,19 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
|
|
|
702
870
|
targetPreference,
|
|
703
871
|
inputSource,
|
|
704
872
|
});
|
|
873
|
+
// --lightweight modifier (issue #1025): the PR body becomes the
|
|
874
|
+
// spec-of-record for this local session — no phase/plan doc minted.
|
|
875
|
+
if (options.lightweight) {
|
|
876
|
+
input = { ...input, canonicalSpecSource: "pr_body" };
|
|
877
|
+
}
|
|
878
|
+
} else if (options.lightweight) {
|
|
879
|
+
// --lightweight used ALONE (no other mode flag): issue-less PR-first (#1210).
|
|
880
|
+
// A broken config must surface as ITS OWN failure, not decay to the
|
|
881
|
+
// misleading light_mode_disabled reason a bare {version:1} would produce.
|
|
882
|
+
if (configErrors.length > 0) {
|
|
883
|
+
throw new Error(`--lightweight without --issue (issue-less PR-first) requires a loadable dev-loop config, but config loading failed: ${configErrors.map((e) => e?.message ?? String(e)).join("; ")}. Fix the config or provide --issue <n>.`);
|
|
884
|
+
}
|
|
885
|
+
input = buildLightweightIssuelessInput({ config: devLoopConfig, cwd: repoRoot });
|
|
705
886
|
} else {
|
|
706
887
|
input = buildAutoResolvedInput({
|
|
707
888
|
pr: options.pr,
|
|
@@ -711,11 +892,36 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
|
|
|
711
892
|
}
|
|
712
893
|
const result = buildResolveDevLoopStartupResult(input, { asyncStartMode, adapter });
|
|
713
894
|
if (result.ok === false) {
|
|
714
|
-
|
|
715
|
-
process.exitCode = 1;
|
|
895
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout: stderr, stderr });
|
|
716
896
|
return;
|
|
717
897
|
}
|
|
718
|
-
|
|
898
|
+
// Emit the deterministic bundle FIRST, before the best-effort self-heal below,
|
|
899
|
+
// so a slow or hung gh reconcile can never delay the startup result.
|
|
900
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
901
|
+
// #1069: best-effort startup self-heal — converge the board from live GitHub
|
|
902
|
+
// state so merged→Done / ready→In Progress land deterministically. Gated on a
|
|
903
|
+
// configured board so it never shells out to gh in the no-.devloops unit tests;
|
|
904
|
+
// never writes stdout, never changes exit code, never throws. Skips
|
|
905
|
+
// --input/--plan-file/--spike modes.
|
|
906
|
+
if (options.issue !== undefined || options.pr !== undefined) {
|
|
907
|
+
let reconcileRoot = sessionCwd;
|
|
908
|
+
try {
|
|
909
|
+
reconcileRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
910
|
+
cwd: sessionCwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
|
|
911
|
+
}).trim();
|
|
912
|
+
} catch { /* fall back to sessionCwd */ }
|
|
913
|
+
if (loadBoardConfig(reconcileRoot).enabled === true) {
|
|
914
|
+
try {
|
|
915
|
+
// The budget bounds startup latency; reconcile is best-effort and the
|
|
916
|
+
// board self-heals on the next entry if it doesn't finish. The timer is
|
|
917
|
+
// unref'd so it never keeps the event loop alive on its own.
|
|
918
|
+
await Promise.race([
|
|
919
|
+
reconcileQueue({ repo: detectRepoSlug(reconcileRoot) }, { env: adapter.getEnv(), cwd: reconcileRoot, skipTerminalColumn: true }),
|
|
920
|
+
new Promise((resolve) => { const t = setTimeout(resolve, STARTUP_RECONCILE_BUDGET_MS); t.unref?.(); }),
|
|
921
|
+
]);
|
|
922
|
+
} catch { /* best-effort */ }
|
|
923
|
+
}
|
|
924
|
+
}
|
|
719
925
|
}
|
|
720
926
|
if (isDirectCliRun(import.meta.url)) {
|
|
721
927
|
runCli().catch((error) => {
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import {
|
|
5
|
+
loadDevLoopConfig,
|
|
6
|
+
resolveGateDispatchMode,
|
|
7
|
+
GATE_FULL_LABEL,
|
|
8
|
+
} from "@dev-loops/core/config";
|
|
9
|
+
import { detectScope } from "./detect-change-scope.mjs";
|
|
10
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
11
|
+
|
|
12
|
+
const USAGE = `Usage: resolve-gate-dispatch.mjs --gate <draft|preApproval> [--base <ref>] [--head <ref>] [--full-label] [--inline-severities <csv>]
|
|
13
|
+
Decide inline vs full fan-out for a gate from lightMode config + PR facts.
|
|
14
|
+
Options:
|
|
15
|
+
--gate <draft|preApproval> Gate to resolve dispatch for (required)
|
|
16
|
+
--base <ref> Base ref for scope detection (default: HEAD~1)
|
|
17
|
+
--head <ref> Head ref; ignored unless --base is also set
|
|
18
|
+
--full-label PR has the ${GATE_FULL_LABEL} label (forces full fan-out)
|
|
19
|
+
--inline-severities <csv> Comma-separated severities from the inline pass (escalation phase)
|
|
20
|
+
--help, -h Show this help
|
|
21
|
+
Output (stdout, JSON):
|
|
22
|
+
{ "ok": true, "gate": "draft", "scope": { "ok": true, "filesChanged": 1, "linesChanged": 5 }, "mode": "inline", "reason": "under_threshold", "threshold": { "maxFiles": 2, "maxLines": 20 } }
|
|
23
|
+
{ "ok": true, "gate": "draft", "scope": { "ok": true, "filesChanged": 9, "linesChanged": 300 }, "mode": "full_fanout", "reason": "over_threshold", "threshold": { "maxFiles": 2, "maxLines": 20 } }
|
|
24
|
+
{ "ok": true, "gate": "draft", "scope": { "ok": false, ... }, "mode": "full_fanout", "reason": "scope_detection_failed", "threshold": null }
|
|
25
|
+
Error output (stderr, JSON):
|
|
26
|
+
{ "ok": false, "error": "...", "usage"?: "..." }
|
|
27
|
+
|
|
28
|
+
${JQ_OUTPUT_USAGE}
|
|
29
|
+
|
|
30
|
+
Exit codes:
|
|
31
|
+
0 Success
|
|
32
|
+
1 Error
|
|
33
|
+
2 Invalid --jq filter
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const VALID_GATES = new Set(["draft", "preApproval"]);
|
|
37
|
+
|
|
38
|
+
/** Parse a comma-separated severity list. Returns `undefined` when the flag is absent, otherwise a trimmed array (which may be empty `[]` for empty/whitespace-only input). */
|
|
39
|
+
export function parseSeverities(csv) {
|
|
40
|
+
if (csv == null) return undefined;
|
|
41
|
+
const list = String(csv)
|
|
42
|
+
.split(",")
|
|
43
|
+
.map((s) => s.trim())
|
|
44
|
+
.filter((s) => s.length > 0);
|
|
45
|
+
return list.length > 0 ? list : [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseCliArgs(argv) {
|
|
49
|
+
const { values } = parseArgs({
|
|
50
|
+
args: [...argv],
|
|
51
|
+
options: {
|
|
52
|
+
gate: { type: "string" },
|
|
53
|
+
base: { type: "string" },
|
|
54
|
+
head: { type: "string" },
|
|
55
|
+
"full-label": { type: "boolean", default: false },
|
|
56
|
+
"inline-severities": { type: "string" },
|
|
57
|
+
help: { type: "boolean", short: "h" },
|
|
58
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
59
|
+
},
|
|
60
|
+
allowPositionals: true,
|
|
61
|
+
strict: true,
|
|
62
|
+
});
|
|
63
|
+
if (values.help) {
|
|
64
|
+
process.stdout.write(USAGE);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
if (!values.gate || !VALID_GATES.has(values.gate)) {
|
|
68
|
+
throw new Error("--gate must be one of: draft, preApproval");
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
gate: values.gate,
|
|
72
|
+
base: values.base ?? null,
|
|
73
|
+
head: values.head ?? null,
|
|
74
|
+
hasFullLabel: Boolean(values["full-label"]),
|
|
75
|
+
inlineFindingSeverities: parseSeverities(values["inline-severities"]),
|
|
76
|
+
jq: values.jq,
|
|
77
|
+
silent: values.silent === true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function run(argv) {
|
|
82
|
+
let opts;
|
|
83
|
+
try {
|
|
84
|
+
opts = parseCliArgs(argv);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
process.stderr.write(
|
|
87
|
+
JSON.stringify({ ok: false, error: err.message, usage: USAGE.trim() }) + "\n"
|
|
88
|
+
);
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const { config } = await loadDevLoopConfig({ repoRoot: process.cwd() });
|
|
94
|
+
const scope = detectScope({ base: opts.base, head: opts.head });
|
|
95
|
+
// Fail CLOSED on unmeasurable scope: a broken/failed diff must route to the
|
|
96
|
+
// full gate, never silently collapse to inline (which would bypass review).
|
|
97
|
+
if (scope.ok === false) {
|
|
98
|
+
process.exitCode = emitResult({
|
|
99
|
+
ok: true,
|
|
100
|
+
gate: opts.gate,
|
|
101
|
+
scope,
|
|
102
|
+
mode: "full_fanout",
|
|
103
|
+
reason: "scope_detection_failed",
|
|
104
|
+
threshold: null,
|
|
105
|
+
}, { jq: opts.jq, silent: opts.silent });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const decision = resolveGateDispatchMode(config, opts.gate, {
|
|
109
|
+
scope,
|
|
110
|
+
hasFullLabel: opts.hasFullLabel,
|
|
111
|
+
inlineFindingSeverities: opts.inlineFindingSeverities,
|
|
112
|
+
});
|
|
113
|
+
process.exitCode = emitResult(
|
|
114
|
+
{ ok: true, gate: opts.gate, scope, ...decision },
|
|
115
|
+
{ jq: opts.jq, silent: opts.silent },
|
|
116
|
+
);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
process.stderr.write(
|
|
119
|
+
JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }) + "\n"
|
|
120
|
+
);
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const isDirectRun =
|
|
126
|
+
process.argv[1] && process.argv[1].includes("resolve-gate-dispatch.mjs");
|
|
127
|
+
if (isDirectRun) {
|
|
128
|
+
run(process.argv.slice(2)).catch((err) => {
|
|
129
|
+
process.stderr.write(
|
|
130
|
+
JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }) + "\n"
|
|
131
|
+
);
|
|
132
|
+
process.exitCode = 1;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -6,6 +6,7 @@ import { parseArgs } from "node:util";
|
|
|
6
6
|
|
|
7
7
|
import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
|
|
8
8
|
import { requireTokenValue } from "../_cli-primitives.mjs";
|
|
9
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
9
10
|
|
|
10
11
|
const USAGE = `Usage: resolve-pr-conflicts.mjs [--base <branch>] [--repo-root <dir>] [--no-verify] [--push] [--json]
|
|
11
12
|
|
|
@@ -36,9 +37,14 @@ Output (stdout, JSON with --json):
|
|
|
36
37
|
Error output:
|
|
37
38
|
{ "ok": false, "error": "...", "conflictFiles": ["..."] }
|
|
38
39
|
|
|
40
|
+
${JQ_OUTPUT_USAGE}
|
|
41
|
+
(--jq/--silent apply to the JSON error output always, and to the success output
|
|
42
|
+
with --json; the default non-JSON success summary is unaffected.)
|
|
43
|
+
|
|
39
44
|
Exit codes:
|
|
40
45
|
0 Clean merge (incl. already up to date) or auto-resolved CHANGELOG
|
|
41
|
-
1 Argument error, git failure, or UNRESOLVABLE conflict (fail closed)
|
|
46
|
+
1 Argument error, git failure, or UNRESOLVABLE conflict (fail closed)
|
|
47
|
+
2 Invalid --jq filter`.trim();
|
|
42
48
|
|
|
43
49
|
const parseError = buildParseError(USAGE);
|
|
44
50
|
|
|
@@ -70,6 +76,7 @@ export function parseResolvePrConflictsCliArgs(argv) {
|
|
|
70
76
|
"no-verify": { type: "boolean" },
|
|
71
77
|
push: { type: "boolean" },
|
|
72
78
|
json: { type: "boolean" },
|
|
79
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
73
80
|
},
|
|
74
81
|
allowPositionals: true,
|
|
75
82
|
strict: false,
|
|
@@ -101,8 +108,10 @@ export function parseResolvePrConflictsCliArgs(argv) {
|
|
|
101
108
|
case "json":
|
|
102
109
|
options.json = true;
|
|
103
110
|
break;
|
|
104
|
-
default:
|
|
111
|
+
default: {
|
|
112
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) break;
|
|
105
113
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
114
|
+
}
|
|
106
115
|
}
|
|
107
116
|
}
|
|
108
117
|
return options;
|
|
@@ -337,18 +346,16 @@ export async function runCli(argv, { stdout = process.stdout, stderr = process.s
|
|
|
337
346
|
try {
|
|
338
347
|
const result = await resolvePrConflicts(options, { env });
|
|
339
348
|
if (options.json) {
|
|
340
|
-
|
|
341
|
-
} else {
|
|
342
|
-
stdout.write(`${result.action} (base ${result.base}${result.resolvedFiles.length ? `, resolved ${result.resolvedFiles.join(", ")}` : ""}${result.pushed ? ", pushed" : ""})\n`);
|
|
349
|
+
return emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
343
350
|
}
|
|
351
|
+
stdout.write(`${result.action} (base ${result.base}${result.resolvedFiles.length ? `, resolved ${result.resolvedFiles.join(", ")}` : ""}${result.pushed ? ", pushed" : ""})\n`);
|
|
344
352
|
return 0;
|
|
345
353
|
} catch (error) {
|
|
346
354
|
const payload = { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
347
355
|
if (Array.isArray(error?.conflictFiles)) {
|
|
348
356
|
payload.conflictFiles = error.conflictFiles;
|
|
349
357
|
}
|
|
350
|
-
|
|
351
|
-
return 1;
|
|
358
|
+
return emitResult(payload, { jq: options.jq, silent: options.silent, stdout: stderr, stderr });
|
|
352
359
|
}
|
|
353
360
|
}
|
|
354
361
|
|
|
@@ -15,9 +15,12 @@ import {
|
|
|
15
15
|
} from "./_handoff-contract.mjs";
|
|
16
16
|
import { listOpenPrs } from "./_loop-pr-aggregation.mjs";
|
|
17
17
|
import { parseArgs } from "node:util";
|
|
18
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
18
19
|
export { listOpenPrs };
|
|
19
20
|
const USAGE = `Usage: run-conductor-cycle.mjs --repo <owner/name>
|
|
20
|
-
Poll all open PRs, detect state, and output an ordered action queue
|
|
21
|
+
Poll all open PRs, detect state, and output an ordered action queue.
|
|
22
|
+
|
|
23
|
+
${JQ_OUTPUT_USAGE}`.trim();
|
|
21
24
|
export const CHECKPOINT_ACTION_TO_CONDUCTOR_ACTION = Object.freeze({
|
|
22
25
|
[PR_CHECKPOINT_ACTION.ADDRESS_REVIEW_FEEDBACK]: "fix_threads",
|
|
23
26
|
[PR_CHECKPOINT_ACTION.REPLY_RESOLVE_REVIEW_THREADS]: "fix_threads",
|
|
@@ -78,6 +81,7 @@ export function parseCliArgs(argv) {
|
|
|
78
81
|
options: {
|
|
79
82
|
help: { type: "boolean", short: "h" },
|
|
80
83
|
repo: { type: "string" },
|
|
84
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
81
85
|
},
|
|
82
86
|
allowPositionals: true,
|
|
83
87
|
strict: false,
|
|
@@ -98,6 +102,7 @@ export function parseCliArgs(argv) {
|
|
|
98
102
|
options.repo = requireTokenValue(token, parseError).trim();
|
|
99
103
|
continue;
|
|
100
104
|
}
|
|
105
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
101
106
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
102
107
|
}
|
|
103
108
|
if (options.repo === undefined) {
|
|
@@ -280,6 +285,7 @@ export async function runCli(
|
|
|
280
285
|
argv = process.argv.slice(2),
|
|
281
286
|
{
|
|
282
287
|
stdout = process.stdout,
|
|
288
|
+
stderr = process.stderr,
|
|
283
289
|
env = process.env,
|
|
284
290
|
ghCommand = "gh",
|
|
285
291
|
cwd = process.cwd(),
|
|
@@ -295,7 +301,7 @@ export async function runCli(
|
|
|
295
301
|
ghCommand,
|
|
296
302
|
repoRoot: cwd,
|
|
297
303
|
});
|
|
298
|
-
|
|
304
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
299
305
|
}
|
|
300
306
|
if (isDirectCliRun(import.meta.url)) {
|
|
301
307
|
runCli().catch((error) => {
|
|
@@ -21,6 +21,11 @@ import { readQueue } from "@dev-loops/core/loop/queue-state";
|
|
|
21
21
|
import { reconcileBoardMembership } from "@dev-loops/core/loop/queue-membership";
|
|
22
22
|
import { parsePositiveInteger } from "@dev-loops/core/cli/primitives";
|
|
23
23
|
import { loadDevLoopConfig, resolveEffectiveMergeAuthorizedFromLoad } from "@dev-loops/core/config";
|
|
24
|
+
import {
|
|
25
|
+
REASON_NEXT_UP_EMPTY,
|
|
26
|
+
REASON_BOARD_QUERY_ERROR,
|
|
27
|
+
EMPTY_NEXT_UP_MESSAGE,
|
|
28
|
+
} from "@dev-loops/core/loop/queue-board-ordering";
|
|
24
29
|
|
|
25
30
|
const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url));
|
|
26
31
|
|
|
@@ -129,24 +134,28 @@ async function main() {
|
|
|
129
134
|
// resolution failure (which falls through to the local queue below).
|
|
130
135
|
console.log(JSON.stringify({
|
|
131
136
|
ok: true,
|
|
132
|
-
|
|
137
|
+
// Canonical empty-Next-Up outcome — matches queue-driver.mjs so operators
|
|
138
|
+
// see one message regardless of which layer detects it.
|
|
139
|
+
message: EMPTY_NEXT_UP_MESSAGE,
|
|
133
140
|
boardConfigured: true,
|
|
134
|
-
reason:
|
|
141
|
+
reason: REASON_NEXT_UP_EMPTY,
|
|
135
142
|
results: [],
|
|
136
143
|
}));
|
|
137
144
|
return;
|
|
138
145
|
}
|
|
139
146
|
|
|
140
147
|
if (membership.emptiness === "board_unavailable") {
|
|
141
|
-
// The board IS configured but Next Up resolution failed
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
148
|
+
// The board IS configured but Next Up resolution failed and the local queue
|
|
149
|
+
// had nothing to fall back to. Align with the driver's canonical fail-closed
|
|
150
|
+
// board-query-error outcome (reason + framing) so operators see one shape
|
|
151
|
+
// regardless of which layer detects the outage — an outage halts the run
|
|
152
|
+
// rather than draining Backlog/local order.
|
|
145
153
|
console.log(JSON.stringify({
|
|
146
|
-
ok:
|
|
147
|
-
|
|
154
|
+
ok: false,
|
|
155
|
+
stopped: true,
|
|
156
|
+
reason: REASON_BOARD_QUERY_ERROR,
|
|
157
|
+
message: `Next Up query failed (${membership.reason}); refusing to fall back to Backlog/local order — nothing to run`,
|
|
148
158
|
boardConfigured: true,
|
|
149
|
-
reason: membership.reason ?? null,
|
|
150
159
|
results: [],
|
|
151
160
|
}));
|
|
152
161
|
return;
|