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,28 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
2
|
+
// Resolve the board's single continue target for the live `/loop-continue` path
|
|
3
|
+
// (#988 P1, extended #1091). It lists the "In Progress" column and, failing
|
|
4
|
+
// that, the "Next Up" column via list-queue-items.mjs — with NO routing opinions
|
|
5
|
+
// beyond a single pick and NO guessing. It never touches Backlog.
|
|
3
6
|
//
|
|
4
|
-
//
|
|
5
|
-
// In
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
7
|
+
// exactly one In Progress -> { ok: true, target: {kind,number}, source: "in-progress" }
|
|
8
|
+
// multiple In Progress -> { ok: false, reason: "..." } (names the items)
|
|
9
|
+
// zero In Progress:
|
|
10
|
+
// Next Up has items -> { ok: true, target: {kind,number}, source: "next-up" }
|
|
11
|
+
// (HEAD of Next Up, by POSITION ascending)
|
|
12
|
+
// Next Up empty -> { ok: false, reason: <canonical empty-queue msg>, source: "next-up" }
|
|
13
|
+
// Next Up query errors -> propagates (fail closed — surface it, no fallback)
|
|
11
14
|
//
|
|
12
15
|
// Downstream (the dev-loop skill) resolves authoritative state from this number;
|
|
13
16
|
// this helper deliberately makes no further decisions.
|
|
14
17
|
import { formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
|
|
15
18
|
import { parseArgs } from "node:util";
|
|
16
|
-
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
19
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
17
20
|
import { main as listQueueItems } from "./list-queue-items.mjs";
|
|
21
|
+
import { EMPTY_NEXT_UP_MESSAGE } from "@dev-loops/core/loop/queue-board-ordering";
|
|
22
|
+
import { loadStateColumnMap, LOGICAL_COLUMN } from "@dev-loops/core/loop/queue-board-sync";
|
|
18
23
|
|
|
24
|
+
// Illustrative default labels for USAGE/help + comments only; the actual query
|
|
25
|
+
// columns resolve through queue.statusColumns.in_progress / .next_up at
|
|
26
|
+
// runtime (#1098, #1143).
|
|
19
27
|
const IN_PROGRESS_COLUMN = "In Progress";
|
|
28
|
+
const NEXT_UP_COLUMN = "Next Up";
|
|
29
|
+
// Canonical fail-closed empty-queue message — matches queue-driver.mjs so
|
|
30
|
+
// operators see one string regardless of which layer detects it (#1091).
|
|
31
|
+
const EMPTY_QUEUE_REASON = EMPTY_NEXT_UP_MESSAGE;
|
|
20
32
|
|
|
21
33
|
const USAGE = `Usage: dev-loops queue resolve-active --repo <owner/name> --project <number|id>
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
Resolve the board's single continue target for bare \`/loop-continue\`:
|
|
36
|
+
continues the one "${IN_PROGRESS_COLUMN}" item; if there is none, picks the HEAD
|
|
37
|
+
of "${NEXT_UP_COLUMN}" by POSITION ascending. Fails closed (no guessing) on
|
|
38
|
+
multiple in-progress items, and when "${NEXT_UP_COLUMN}" is empty. Never pulls
|
|
39
|
+
from Backlog.
|
|
26
40
|
|
|
27
41
|
Options:
|
|
28
42
|
--repo <owner/name> Required. Repository to scope the project search.
|
|
@@ -31,19 +45,21 @@ Options:
|
|
|
31
45
|
|
|
32
46
|
Output (stdout):
|
|
33
47
|
JSON, exactly one in-progress item:
|
|
34
|
-
{ ok: true, target: { kind: "issue"|"pr", number } }
|
|
35
|
-
JSON, zero
|
|
48
|
+
{ ok: true, target: { kind: "issue"|"pr", number }, source: "in-progress" }
|
|
49
|
+
JSON, zero in-progress + "${NEXT_UP_COLUMN}" head:
|
|
50
|
+
{ ok: true, target: { kind: "issue"|"pr", number }, source: "next-up" }
|
|
51
|
+
JSON, fail closed (multiple in-progress, or empty "${NEXT_UP_COLUMN}"):
|
|
36
52
|
{ ok: false, reason: "..." }
|
|
37
53
|
|
|
38
54
|
${JQ_OUTPUT_USAGE}
|
|
39
55
|
|
|
40
56
|
Exit codes (default / unfiltered output):
|
|
41
|
-
0 —
|
|
57
|
+
0 — a single continue target resolved (in-progress or "${NEXT_UP_COLUMN}" head)
|
|
42
58
|
1 — usage or argument error
|
|
43
59
|
2 — GitHub API error / invalid --jq filter
|
|
44
|
-
3 — fail closed (pass an explicit issue/PR):
|
|
45
|
-
|
|
46
|
-
field, or
|
|
60
|
+
3 — fail closed (pass an explicit issue/PR): multiple in-progress items, an
|
|
61
|
+
empty "${NEXT_UP_COLUMN}" column, or the board/project could not be
|
|
62
|
+
resolved (project, status field, or the column not found)
|
|
47
63
|
|
|
48
64
|
With --jq/--silent the result is filtered to a value/predicate, so the exit code
|
|
49
65
|
follows the shared jq-output contract (0 = truthy/ok, 1 = falsy/non-ok, 2 =
|
|
@@ -94,14 +110,10 @@ function parseCliArgs(argv) {
|
|
|
94
110
|
case "project":
|
|
95
111
|
args.project = requireValue(token, "--project requires a value (number or node ID)");
|
|
96
112
|
break;
|
|
97
|
-
|
|
98
|
-
args
|
|
99
|
-
break;
|
|
100
|
-
case "silent":
|
|
101
|
-
args.silent = true;
|
|
102
|
-
break;
|
|
103
|
-
default:
|
|
113
|
+
default: {
|
|
114
|
+
if (matchJqOutputToken(token, args, (t) => requireValue(t, "--jq requires a filter"))) break;
|
|
104
115
|
throw parseError(`Unknown flag: ${token.rawName}`);
|
|
116
|
+
}
|
|
105
117
|
}
|
|
106
118
|
}
|
|
107
119
|
return args;
|
|
@@ -112,36 +124,86 @@ function describeItem(item) {
|
|
|
112
124
|
return item.title ? `${ref} (${item.title})` : ref;
|
|
113
125
|
}
|
|
114
126
|
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
127
|
+
// Prefer the linked PR number when present (the canonical artifact once work is
|
|
128
|
+
// in flight), else the issue. No routing opinion beyond that single pick.
|
|
129
|
+
function itemToTarget(item) {
|
|
130
|
+
return item.prNumber != null
|
|
131
|
+
? { kind: "pr", number: item.prNumber }
|
|
132
|
+
: { kind: "issue", number: item.issueNumber };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Collapse the "In Progress" column to a single continue target. The caller only
|
|
136
|
+
// invokes this with a NON-EMPTY column (zero In Progress falls through to
|
|
137
|
+
// resolveNextUpHead in main()), so this only decides among the in-progress items
|
|
138
|
+
// and never guesses when there is more than one.
|
|
118
139
|
function collapseToTarget(items) {
|
|
119
|
-
if (items.length === 0) {
|
|
120
|
-
return {
|
|
121
|
-
ok: false,
|
|
122
|
-
reason: `No in-progress board item to continue. Pass an explicit issue/PR, e.g. \`/continue #N\`.`,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
140
|
if (items.length > 1) {
|
|
126
141
|
const listed = items.map(describeItem).join(", ");
|
|
127
142
|
return {
|
|
128
143
|
ok: false,
|
|
129
|
-
reason: `${items.length} in-progress board items: ${listed}. Pass an explicit issue/PR to disambiguate, e.g. \`/continue #N\`.`,
|
|
144
|
+
reason: `${items.length} in-progress board items: ${listed}. Pass an explicit issue/PR to disambiguate, e.g. \`/loop-continue #N\`.`,
|
|
130
145
|
};
|
|
131
146
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
147
|
+
return { ok: true, target: itemToTarget(items[0]), source: "in-progress" };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Zero in-progress: the live continue path falls through to the "Next Up"
|
|
151
|
+
// column, HEAD by POSITION ascending (list-queue-items returns position order).
|
|
152
|
+
// NEVER pulls from Backlog; empty Next Up fails closed with the canonical
|
|
153
|
+
// message, and a query error propagates (fail closed — surface it, no fallback).
|
|
154
|
+
async function resolveNextUpHead(args, { env, runChild, cwd = process.cwd() } = {}) {
|
|
155
|
+
// Resolve the next_up column name through the SAME statusColumns mapping
|
|
156
|
+
// board-sync uses (#1098): a repo that renamed Next Up (e.g. to "Todo") gets
|
|
157
|
+
// its configured column queried, not the literal default. Pickup SEMANTICS
|
|
158
|
+
// (position-ascending HEAD, fail-closed on empty, never Backlog) are unchanged.
|
|
159
|
+
const { columnNames, error: configError } = loadStateColumnMap(cwd);
|
|
160
|
+
if (configError) {
|
|
161
|
+
// A malformed `.devloops` must fail CLOSED — never silently fall back to the
|
|
162
|
+
// literal "Next Up" and risk selecting the wrong item from a stale/renamed
|
|
163
|
+
// column (#1098). Throw so the CLI surfaces it (exit 2), mirroring a Next Up
|
|
164
|
+
// query error's "propagate, no fallback" contract.
|
|
165
|
+
throw Object.assign(
|
|
166
|
+
new Error(`could not resolve next_up column (config read/parse error: ${configError})`),
|
|
167
|
+
{ code: "CONFIG_ERROR" },
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
const nextUpColumn = columnNames[LOGICAL_COLUMN.NEXT_UP];
|
|
171
|
+
const listed = await listQueueItems(
|
|
172
|
+
{ repo: args.repo, project: args.project, column: nextUpColumn },
|
|
173
|
+
{ env, runChild },
|
|
174
|
+
);
|
|
175
|
+
const items = listed.items ?? [];
|
|
176
|
+
if (items.length === 0) {
|
|
177
|
+
return { ok: false, reason: EMPTY_QUEUE_REASON, source: "next-up" };
|
|
178
|
+
}
|
|
179
|
+
return { ok: true, target: itemToTarget(items[0]), source: "next-up" };
|
|
137
180
|
}
|
|
138
181
|
|
|
139
|
-
async function main(args, { env = process.env, runChild } = {}) {
|
|
182
|
+
async function main(args, { env = process.env, runChild, cwd = process.cwd() } = {}) {
|
|
183
|
+
// Resolve the in_progress column name through the SAME statusColumns mapping
|
|
184
|
+
// board-sync uses (#1098, #1143): a repo that renamed In Progress gets its
|
|
185
|
+
// configured column queried, not the literal default. Fail CLOSED on a
|
|
186
|
+
// malformed `.devloops` — never silently query the literal "In Progress"
|
|
187
|
+
// and risk missing the active item on a renamed/stale column.
|
|
188
|
+
const { columnNames, error: configError } = loadStateColumnMap(cwd);
|
|
189
|
+
if (configError) {
|
|
190
|
+
throw Object.assign(
|
|
191
|
+
new Error(`could not resolve in_progress column (config read/parse error: ${configError})`),
|
|
192
|
+
{ code: "CONFIG_ERROR" },
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const inProgressColumn = columnNames[LOGICAL_COLUMN.IN_PROGRESS];
|
|
140
196
|
const listed = await listQueueItems(
|
|
141
|
-
{ repo: args.repo, project: args.project, column:
|
|
197
|
+
{ repo: args.repo, project: args.project, column: inProgressColumn },
|
|
142
198
|
{ env, runChild },
|
|
143
199
|
);
|
|
144
|
-
|
|
200
|
+
const items = listed.items ?? [];
|
|
201
|
+
// Exactly one → continue it. Multiple → fail closed (never guess). Zero →
|
|
202
|
+
// fall through to the Next Up head (the live pickup path, #1091).
|
|
203
|
+
if (items.length === 0) {
|
|
204
|
+
return resolveNextUpHead(args, { env, runChild, cwd });
|
|
205
|
+
}
|
|
206
|
+
return collapseToTarget(items);
|
|
145
207
|
}
|
|
146
208
|
|
|
147
209
|
function classifyExitCode(err) {
|
|
@@ -150,7 +212,7 @@ function classifyExitCode(err) {
|
|
|
150
212
|
return 2;
|
|
151
213
|
}
|
|
152
214
|
|
|
153
|
-
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env, runChild } = {}) {
|
|
215
|
+
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env, runChild, cwd = process.cwd() } = {}) {
|
|
154
216
|
let args;
|
|
155
217
|
try {
|
|
156
218
|
args = parseCliArgs(argv);
|
|
@@ -164,7 +226,7 @@ async function runCli(argv, { stdout = process.stdout, stderr = process.stderr,
|
|
|
164
226
|
return;
|
|
165
227
|
}
|
|
166
228
|
try {
|
|
167
|
-
const result = await main(args, { env, runChild });
|
|
229
|
+
const result = await main(args, { env, runChild, cwd });
|
|
168
230
|
// Fail closed (zero/multiple) is a clean, expected outcome — distinct exit code 3,
|
|
169
231
|
// not a crash; --jq/--silent still apply so callers can probe `.ok`.
|
|
170
232
|
process.exitCode = emitResult(result, {
|
|
@@ -190,4 +252,4 @@ if (isDirectCliRun(import.meta.url)) {
|
|
|
190
252
|
});
|
|
191
253
|
}
|
|
192
254
|
|
|
193
|
-
export { main, collapseToTarget, runCli };
|
|
255
|
+
export { main, collapseToTarget, resolveNextUpHead, runCli };
|
|
@@ -3,6 +3,7 @@ import { formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
|
|
|
3
3
|
import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
4
4
|
import { syncBoardStatus } from "@dev-loops/core/loop/queue-board-sync";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
6
7
|
|
|
7
8
|
const USAGE = `Usage: dev-loops queue sync-status --repo <owner/name> --item <number> --to-column <name>
|
|
8
9
|
(dev-loops project sync-status … is a back-compat alias)
|
|
@@ -26,9 +27,12 @@ Output (stdout):
|
|
|
26
27
|
{ ok: true, skipped: false, result: { item: { newColumn } } }
|
|
27
28
|
{ ok: true, skipped: true, reason: "board not configured" }
|
|
28
29
|
|
|
30
|
+
${JQ_OUTPUT_USAGE}
|
|
31
|
+
|
|
29
32
|
Exit codes:
|
|
30
33
|
0 — always on a parsed command (best-effort sync; skips/failures are reported in JSON)
|
|
31
34
|
1 — usage or argument error
|
|
35
|
+
2 — invalid --jq filter
|
|
32
36
|
`.trim();
|
|
33
37
|
|
|
34
38
|
function parseCliArgs(argv) {
|
|
@@ -48,6 +52,7 @@ function parseCliArgs(argv) {
|
|
|
48
52
|
item: { type: "string" },
|
|
49
53
|
"to-column": { type: "string" },
|
|
50
54
|
help: { type: "boolean", short: "h" },
|
|
55
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
51
56
|
},
|
|
52
57
|
allowPositionals: true,
|
|
53
58
|
strict: false,
|
|
@@ -77,8 +82,10 @@ function parseCliArgs(argv) {
|
|
|
77
82
|
case "to-column":
|
|
78
83
|
args.toColumn = requireValue(token, "--to-column requires a value", "INVALID_COLUMN");
|
|
79
84
|
break;
|
|
80
|
-
default:
|
|
85
|
+
default: {
|
|
86
|
+
if (matchJqOutputToken(token, args, (t) => requireValue(t, "--jq requires a filter", "INVALID_ARGS"))) break;
|
|
81
87
|
throw Object.assign(new Error(`Unknown flag: ${token.rawName}`), { code: "INVALID_ARGS", usage: USAGE });
|
|
88
|
+
}
|
|
82
89
|
}
|
|
83
90
|
}
|
|
84
91
|
return args;
|
|
@@ -174,17 +181,15 @@ async function runCli(argv, { stdout = process.stdout, stderr = process.stderr,
|
|
|
174
181
|
return;
|
|
175
182
|
}
|
|
176
183
|
// Defensive: any unexpected error stays best-effort/non-fatal — report it
|
|
177
|
-
// as a skip and keep exit 0 so it never blocks the PR/merge caller
|
|
178
|
-
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
process.exitCode = 0;
|
|
184
|
+
// as a skip and keep exit 0 (ok:true) so it never blocks the PR/merge caller,
|
|
185
|
+
// unless --jq/--silent turns a filter/predicate into a non-zero exit — same
|
|
186
|
+
// shared contract as the normal result path below.
|
|
187
|
+
process.exitCode = emitResult({ ok: true, skipped: true, reason: err.message ?? "board sync failed" }, { jq: args.jq, silent: args.silent, stdout, stderr });
|
|
182
188
|
return;
|
|
183
189
|
}
|
|
184
|
-
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
process.exitCode = 0;
|
|
190
|
+
// Best-effort contract: result.ok is always true, so without --jq/--silent this
|
|
191
|
+
// always exits 0. An invalid --jq filter still fails closed (exit 2).
|
|
192
|
+
process.exitCode = emitResult(result, { jq: args.jq, silent: args.silent, stdout, stderr });
|
|
188
193
|
}
|
|
189
194
|
|
|
190
195
|
if (isDirectCliRun(import.meta.url)) {
|
|
@@ -5,6 +5,7 @@ import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "
|
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
import { parsePositiveInteger, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
7
7
|
import { detectRepoSlug, parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
8
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
8
9
|
|
|
9
10
|
export const FORBIDDEN_PROSE_PATTERNS = [
|
|
10
11
|
/Child of #/iu,
|
|
@@ -16,7 +17,12 @@ export const FORBIDDEN_PROSE_PATTERNS = [
|
|
|
16
17
|
export const DEFAULT_USAGE_SUFFIX = `
|
|
17
18
|
Output:
|
|
18
19
|
Default output is human-readable text.
|
|
19
|
-
Add --json for machine-readable JSON
|
|
20
|
+
Add --json for machine-readable JSON.
|
|
21
|
+
|
|
22
|
+
${JQ_OUTPUT_USAGE}
|
|
23
|
+
(--jq/--silent only apply together with --json; the verdict is always in the
|
|
24
|
+
payload, never the exit code — a parsed --json run always exits 0 unless
|
|
25
|
+
--jq/--silent explicitly turns the verdict into the exit code.)`.trim();
|
|
20
26
|
|
|
21
27
|
export function normalizeIssueNumber(value, label, parseError) {
|
|
22
28
|
return parsePositiveInteger(value, label, parseError);
|
|
@@ -26,7 +32,12 @@ export function parseCheckerCliArgs(argv, usage, checkerName) {
|
|
|
26
32
|
const parseError = buildParseError(usage);
|
|
27
33
|
const { tokens } = parseArgs({
|
|
28
34
|
args: [...argv],
|
|
29
|
-
options: {
|
|
35
|
+
options: {
|
|
36
|
+
help: { type: "boolean", short: "h" },
|
|
37
|
+
input: { type: "string" },
|
|
38
|
+
json: { type: "boolean" },
|
|
39
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
40
|
+
},
|
|
30
41
|
allowPositionals: true,
|
|
31
42
|
strict: false,
|
|
32
43
|
tokens: true,
|
|
@@ -51,6 +62,7 @@ export function parseCheckerCliArgs(argv, usage, checkerName) {
|
|
|
51
62
|
options.json = true;
|
|
52
63
|
continue;
|
|
53
64
|
}
|
|
65
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
54
66
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
55
67
|
}
|
|
56
68
|
if (typeof options.input !== "string" || options.input.trim().length === 0) {
|
|
@@ -262,10 +274,13 @@ export function normalizeScopeToken(value) {
|
|
|
262
274
|
.replace(/\s+/gu, " ");
|
|
263
275
|
}
|
|
264
276
|
|
|
265
|
-
|
|
277
|
+
// Returns the process exit code to use. Documented contract: the verdict lives
|
|
278
|
+
// in the payload, not the exit code — a parsed --json run exits 0 even when
|
|
279
|
+
// result.ok is false, unless --jq/--silent explicitly turns the verdict into
|
|
280
|
+
// the exit code (predicate/--silent semantics from the shared jq-output contract).
|
|
281
|
+
export function writeCheckerOutput(result, { stdout = process.stdout, stderr = process.stderr, json = false, jq, silent }) {
|
|
266
282
|
if (json) {
|
|
267
|
-
|
|
268
|
-
return;
|
|
283
|
+
return emitResult(result, { jq, silent, stdout, stderr, ok: true });
|
|
269
284
|
}
|
|
270
285
|
|
|
271
286
|
const status = result.ok ? "PASS" : "FAIL";
|
|
@@ -279,6 +294,7 @@ export function writeCheckerOutput(result, { stdout = process.stdout, json = fal
|
|
|
279
294
|
}
|
|
280
295
|
}
|
|
281
296
|
stdout.write(`${lines.join("\n")}\n`);
|
|
297
|
+
return 0;
|
|
282
298
|
}
|
|
283
299
|
|
|
284
300
|
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
SPIKE_EXIT_ACTION,
|
|
18
18
|
SPIKE_EXIT_DISPOSITION,
|
|
19
19
|
} from "@dev-loops/core/loop/spike-exit-contract";
|
|
20
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
20
21
|
|
|
21
22
|
const USAGE = `Usage:
|
|
22
23
|
exit-spike.mjs --spike-file <path> --disposition <discard|graduate> [--plan-file <path>] [--json]
|
|
@@ -41,9 +42,14 @@ Optional:
|
|
|
41
42
|
--plan-file <path> Output plan-file path (required for graduate)
|
|
42
43
|
--json Machine-readable JSON output
|
|
43
44
|
--help Show this help
|
|
45
|
+
|
|
46
|
+
${JQ_OUTPUT_USAGE}
|
|
47
|
+
(--jq/--silent only apply together with --json; the default text output is unaffected.)
|
|
48
|
+
|
|
44
49
|
Exit codes:
|
|
45
50
|
0 Exit succeeded (discard recorded; or plan file written for graduate)
|
|
46
|
-
1 Argument error, or fail-closed (not ready / unknown disposition)
|
|
51
|
+
1 Argument error, or fail-closed (not ready / unknown disposition)
|
|
52
|
+
2 Invalid --jq filter`.trim();
|
|
47
53
|
|
|
48
54
|
const parseError = buildParseError(USAGE);
|
|
49
55
|
|
|
@@ -56,6 +62,7 @@ export function parseExitSpikeCliArgs(argv) {
|
|
|
56
62
|
disposition: { type: "string" },
|
|
57
63
|
"plan-file": { type: "string" },
|
|
58
64
|
json: { type: "boolean" },
|
|
65
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
59
66
|
},
|
|
60
67
|
allowPositionals: true,
|
|
61
68
|
strict: false,
|
|
@@ -87,6 +94,7 @@ export function parseExitSpikeCliArgs(argv) {
|
|
|
87
94
|
options.json = true;
|
|
88
95
|
continue;
|
|
89
96
|
}
|
|
97
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
90
98
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
91
99
|
}
|
|
92
100
|
if (typeof options.spikeFile !== "string" || options.spikeFile.trim().length === 0) {
|
|
@@ -104,11 +112,14 @@ export function parseExitSpikeCliArgs(argv) {
|
|
|
104
112
|
return options;
|
|
105
113
|
}
|
|
106
114
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
// Sets process.exitCode from `summary.ok` (0/1), or the shared --jq/--silent
|
|
116
|
+
// contract's exit code (0/1/2) when --json + --jq/--silent are in play.
|
|
117
|
+
function emit(stdout, options, summary, humanLines) {
|
|
118
|
+
if (options.json) {
|
|
119
|
+
process.exitCode = emitResult(summary, { jq: options.jq, silent: options.silent, stdout });
|
|
110
120
|
} else {
|
|
111
121
|
stdout.write(`${humanLines.join("\n")}\n`);
|
|
122
|
+
process.exitCode = summary.ok ? 0 : 1;
|
|
112
123
|
}
|
|
113
124
|
}
|
|
114
125
|
|
|
@@ -136,15 +147,14 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
|
|
|
136
147
|
if (!decision.ok) {
|
|
137
148
|
// Fail closed: no tracker artifact, no plan file — surface the reason.
|
|
138
149
|
const summary = { ok: false, reason: decision.reason, spikeIntakeState: decision.spikeIntakeState ?? spikeIntakeState };
|
|
139
|
-
emit(stdout, options
|
|
140
|
-
process.exitCode = 1;
|
|
150
|
+
emit(stdout, options, summary, [`exit-spike: FAIL (${decision.reason})`]);
|
|
141
151
|
return summary;
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
// DISCARD: zero artifacts. The findings doc on disk is the whole record.
|
|
145
155
|
if (decision.action === SPIKE_EXIT_ACTION.DISCARD) {
|
|
146
156
|
const summary = { ok: true, action: SPIKE_EXIT_ACTION.DISCARD, spikeFile: spikePath };
|
|
147
|
-
emit(stdout, options
|
|
157
|
+
emit(stdout, options, summary, [
|
|
148
158
|
"exit-spike: PASS (discard)",
|
|
149
159
|
` spike: ${spikePath}`,
|
|
150
160
|
" no tracker artifact created (findings doc is the record)",
|
|
@@ -170,7 +180,7 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
|
|
|
170
180
|
spikeFile: spikePath,
|
|
171
181
|
planFile: planPath,
|
|
172
182
|
};
|
|
173
|
-
emit(stdout, options
|
|
183
|
+
emit(stdout, options, summary, [
|
|
174
184
|
"exit-spike: PASS (graduate)",
|
|
175
185
|
` spike: ${spikePath}`,
|
|
176
186
|
` plan: ${planPath} (local-first; promote with scripts/refine/promote-plan.mjs)`,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
writeLinkedPrNumber,
|
|
18
18
|
PLAN_FILE_PROMOTE_ACTION,
|
|
19
19
|
} from "@dev-loops/core/loop/plan-file-promote-contract";
|
|
20
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
20
21
|
|
|
21
22
|
const CREATE_PR_PATH = fileURLToPath(new URL("../github/create-pr.mjs", import.meta.url));
|
|
22
23
|
|
|
@@ -42,9 +43,14 @@ Optional:
|
|
|
42
43
|
--branch <name> Branch name to commit the plan on (default: derived from the plan path)
|
|
43
44
|
--json Machine-readable JSON output
|
|
44
45
|
--help Show this help
|
|
46
|
+
|
|
47
|
+
${JQ_OUTPUT_USAGE}
|
|
48
|
+
(--jq/--silent only apply together with --json; the default text output is unaffected.)
|
|
49
|
+
|
|
45
50
|
Exit codes:
|
|
46
51
|
0 Promotion succeeded (or idempotent no-op for an already-linked plan)
|
|
47
|
-
1 Argument error, or fail-closed (plan not ready / git / PR-create failure); no issue ever created
|
|
52
|
+
1 Argument error, or fail-closed (plan not ready / git / PR-create failure); no issue ever created
|
|
53
|
+
2 Invalid --jq filter`.trim();
|
|
48
54
|
|
|
49
55
|
const parseError = buildParseError(USAGE);
|
|
50
56
|
|
|
@@ -57,6 +63,7 @@ export function parsePromotePlanCliArgs(argv) {
|
|
|
57
63
|
base: { type: "string" },
|
|
58
64
|
branch: { type: "string" },
|
|
59
65
|
json: { type: "boolean" },
|
|
66
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
60
67
|
},
|
|
61
68
|
allowPositionals: true,
|
|
62
69
|
strict: false,
|
|
@@ -88,6 +95,7 @@ export function parsePromotePlanCliArgs(argv) {
|
|
|
88
95
|
options.json = true;
|
|
89
96
|
continue;
|
|
90
97
|
}
|
|
98
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
91
99
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
92
100
|
}
|
|
93
101
|
if (typeof options.planFile !== "string" || options.planFile.trim().length === 0) {
|
|
@@ -123,11 +131,14 @@ function parsePrNumberFromGhOutput(text) {
|
|
|
123
131
|
return match ? Number.parseInt(match[1], 10) : null;
|
|
124
132
|
}
|
|
125
133
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
// Sets process.exitCode from `summary.ok` (0/1), or the shared --jq/--silent
|
|
135
|
+
// contract's exit code (0/1/2) when --json + --jq/--silent are in play.
|
|
136
|
+
function emit(stdout, options, summary, humanLines) {
|
|
137
|
+
if (options.json) {
|
|
138
|
+
process.exitCode = emitResult(summary, { jq: options.jq, silent: options.silent, stdout });
|
|
129
139
|
} else {
|
|
130
140
|
stdout.write(`${humanLines.join("\n")}\n`);
|
|
141
|
+
process.exitCode = summary.ok ? 0 : 1;
|
|
131
142
|
}
|
|
132
143
|
}
|
|
133
144
|
|
|
@@ -161,8 +172,7 @@ export async function runCli(argv = process.argv.slice(2), {
|
|
|
161
172
|
if (!decision.ok) {
|
|
162
173
|
// Fail closed: no git, no gh, no issue — surface the reason and stop.
|
|
163
174
|
const summary = { ok: false, reason: decision.reason, planFileIntakeState: decision.planFileIntakeState ?? null };
|
|
164
|
-
emit(stdout, options
|
|
165
|
-
process.exitCode = 1;
|
|
175
|
+
emit(stdout, options, summary, [`promote-plan: FAIL (${decision.reason})`]);
|
|
166
176
|
return summary;
|
|
167
177
|
}
|
|
168
178
|
|
|
@@ -174,7 +184,7 @@ export async function runCli(argv = process.argv.slice(2), {
|
|
|
174
184
|
planFile: planPath,
|
|
175
185
|
prNumber: decision.existingPrNumber,
|
|
176
186
|
};
|
|
177
|
-
emit(stdout, options
|
|
187
|
+
emit(stdout, options, summary, [
|
|
178
188
|
"promote-plan: PASS (already promoted)",
|
|
179
189
|
` plan: ${planPath}`,
|
|
180
190
|
` pr: #${decision.existingPrNumber} (existing; opened nothing)`,
|
|
@@ -207,8 +217,7 @@ export async function runCli(argv = process.argv.slice(2), {
|
|
|
207
217
|
|
|
208
218
|
const fail = (reason, detail) => {
|
|
209
219
|
const summary = { ok: false, reason, detail: detail ?? null, planFile: planPath };
|
|
210
|
-
emit(stdout, options
|
|
211
|
-
process.exitCode = 1;
|
|
220
|
+
emit(stdout, options, summary, [`promote-plan: FAIL (${reason})${detail ? `: ${detail}` : ""}`]);
|
|
212
221
|
return summary;
|
|
213
222
|
};
|
|
214
223
|
|
|
@@ -294,11 +303,10 @@ export async function runCli(argv = process.argv.slice(2), {
|
|
|
294
303
|
branch,
|
|
295
304
|
recovery: `gh pr create failed, so no PR was opened, but the plan is committed and branch ${branch} is pushed. Re-run promote-plan to recover: the commit step is idempotent (skips when nothing is staged) and the push is idempotent, so a clean re-run will retry opening the PR.`,
|
|
296
305
|
};
|
|
297
|
-
emit(stdout, options
|
|
306
|
+
emit(stdout, options, summary, [
|
|
298
307
|
`promote-plan: FAIL (pr_create_failed)${detail ? `: ${detail}` : ""}`,
|
|
299
308
|
` branch: ${branch} (committed and pushed; no PR opened — re-run promote-plan to recover, it is idempotent and will retry opening the PR)`,
|
|
300
309
|
]);
|
|
301
|
-
process.exitCode = 1;
|
|
302
310
|
return summary;
|
|
303
311
|
}
|
|
304
312
|
const prNumber = parsePrNumberFromGhOutput(prCreate.stdout);
|
|
@@ -315,11 +323,10 @@ export async function runCli(argv = process.argv.slice(2), {
|
|
|
315
323
|
branch,
|
|
316
324
|
recovery: `A draft PR was opened on branch ${branch} but its number could not be parsed from gh output. Find it with: gh pr list --head "${branch}" --json number,url. Then record the link with: node scripts/refine/promote-plan.mjs is idempotent only once the front-matter prNumber is set, so add it manually (prNumber: <n>) and commit ${planDocRelPath}.`,
|
|
317
325
|
};
|
|
318
|
-
emit(stdout, options
|
|
326
|
+
emit(stdout, options, summary, [
|
|
319
327
|
`promote-plan: FAIL (pr_number_unparseable)`,
|
|
320
328
|
` branch: ${branch} (a draft PR is open; its number could not be parsed — recover via 'gh pr list --head "${branch}"')`,
|
|
321
329
|
]);
|
|
322
|
-
process.exitCode = 1;
|
|
323
330
|
return summary;
|
|
324
331
|
}
|
|
325
332
|
|
|
@@ -340,11 +347,10 @@ export async function runCli(argv = process.argv.slice(2), {
|
|
|
340
347
|
prNumber,
|
|
341
348
|
recovery: `PR #${prNumber} is open but the plan->PR link commit failed. ${planDocRelPath} now carries prNumber: ${prNumber} in its front-matter; record the link with: git add -- "${planDocRelPath}" && git commit -m "docs(plan): link to PR #${prNumber}" && git push.`,
|
|
342
349
|
};
|
|
343
|
-
emit(stdout, options
|
|
350
|
+
emit(stdout, options, summary, [
|
|
344
351
|
`promote-plan: FAIL (${reason})${detail ? `: ${detail}` : ""}`,
|
|
345
352
|
` pr: #${prNumber} (open; plan->PR link NOT committed — recover manually)`,
|
|
346
353
|
]);
|
|
347
|
-
process.exitCode = 1;
|
|
348
354
|
return summary;
|
|
349
355
|
};
|
|
350
356
|
const linkedMarkdown = writeLinkedPrNumber(markdownText, prNumber);
|
|
@@ -370,7 +376,7 @@ export async function runCli(argv = process.argv.slice(2), {
|
|
|
370
376
|
branch,
|
|
371
377
|
prNumber,
|
|
372
378
|
};
|
|
373
|
-
emit(stdout, options
|
|
379
|
+
emit(stdout, options, summary, [
|
|
374
380
|
"promote-plan: PASS",
|
|
375
381
|
` plan: ${planPath}`,
|
|
376
382
|
` branch: ${branch}`,
|
|
@@ -80,7 +80,7 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
|
|
|
80
80
|
}
|
|
81
81
|
const tree = await loadTreeFromInput(options.input);
|
|
82
82
|
const result = runProseLinkageDetector(tree);
|
|
83
|
-
writeCheckerOutput(result, { stdout, json: options.json });
|
|
83
|
+
process.exitCode = writeCheckerOutput(result, { stdout, json: options.json, jq: options.jq, silent: options.silent });
|
|
84
84
|
return result;
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "@dev-loops/core/loop/plan-file-refine-contract";
|
|
14
14
|
import { PLAN_FILE_REFINEMENT_SECTIONS } from "@dev-loops/core/loop/plan-file-intake-contract";
|
|
15
15
|
import { classifyDocsGrillFinding } from "../loop/docs-grill-contract.mjs";
|
|
16
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
16
17
|
|
|
17
18
|
const USAGE = `Usage:
|
|
18
19
|
refine-plan-file.mjs --plan-file <path> --payload <path> [--json]
|
|
@@ -31,9 +32,14 @@ Required:
|
|
|
31
32
|
Optional:
|
|
32
33
|
--json Machine-readable JSON output
|
|
33
34
|
--help Show this help
|
|
35
|
+
|
|
36
|
+
${JQ_OUTPUT_USAGE}
|
|
37
|
+
(--jq/--silent only apply together with --json; the default text output is unaffected.)
|
|
38
|
+
|
|
34
39
|
Exit codes:
|
|
35
40
|
0 Refinement succeeded; plan written in place; stop for local human review
|
|
36
|
-
1 Argument error, or fail-closed refine/grill/ambiguous state (no write)
|
|
41
|
+
1 Argument error, or fail-closed refine/grill/ambiguous state (no write)
|
|
42
|
+
2 Invalid --jq filter`.trim();
|
|
37
43
|
|
|
38
44
|
const parseError = buildParseError(USAGE);
|
|
39
45
|
|
|
@@ -45,6 +51,7 @@ export function parseRefinePlanFileCliArgs(argv) {
|
|
|
45
51
|
"plan-file": { type: "string" },
|
|
46
52
|
payload: { type: "string" },
|
|
47
53
|
json: { type: "boolean" },
|
|
54
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
48
55
|
},
|
|
49
56
|
allowPositionals: true,
|
|
50
57
|
strict: false,
|
|
@@ -72,6 +79,7 @@ export function parseRefinePlanFileCliArgs(argv) {
|
|
|
72
79
|
options.json = true;
|
|
73
80
|
continue;
|
|
74
81
|
}
|
|
82
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
75
83
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
76
84
|
}
|
|
77
85
|
if (typeof options.planFile !== "string" || options.planFile.trim().length === 0) {
|
|
@@ -123,11 +131,12 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
|
|
|
123
131
|
if (!result.ok) {
|
|
124
132
|
// Fail closed: surface the reason, do not write, do not advance, do not promote.
|
|
125
133
|
if (options.json) {
|
|
126
|
-
|
|
134
|
+
const failurePayload = { ok: false, reason: result.reason, planFileIntakeState: result.planFileIntakeState ?? null };
|
|
135
|
+
process.exitCode = emitResult(failurePayload, { jq: options.jq, silent: options.silent, stdout });
|
|
127
136
|
} else {
|
|
128
137
|
stdout.write(`refine-plan-file: FAIL (${result.reason})\n`);
|
|
138
|
+
process.exitCode = 1;
|
|
129
139
|
}
|
|
130
|
-
process.exitCode = 1;
|
|
131
140
|
return result;
|
|
132
141
|
}
|
|
133
142
|
|
|
@@ -142,7 +151,7 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
|
|
|
142
151
|
grillDispositions: result.grillDispositions,
|
|
143
152
|
};
|
|
144
153
|
if (options.json) {
|
|
145
|
-
|
|
154
|
+
process.exitCode = emitResult(summary, { jq: options.jq, silent: options.silent, stdout });
|
|
146
155
|
} else {
|
|
147
156
|
stdout.write(
|
|
148
157
|
[
|