dev-loops 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/.claude-plugin/plugin.json +1 -1
- package/.claude/agents/dev-loop.md +2 -1
- package/.claude/agents/review.md +2 -1
- package/.claude/commands/{auto.md → loop-auto.md} +1 -1
- package/.claude/commands/loop-continue.md +15 -0
- package/.claude/commands/loop-enqueue.md +22 -0
- package/.claude/commands/loop-grill.md +17 -0
- package/.claude/commands/{info.md → loop-info.md} +2 -2
- package/.claude/commands/loop-queue-status.md +24 -0
- package/.claude/commands/{start-spike.md → loop-start-spike.md} +1 -1
- package/.claude/commands/{start.md → loop-start.md} +1 -1
- package/.claude/commands/{status.md → loop-status.md} +1 -1
- package/.claude/hooks/_bash-command-classify.mjs +333 -29
- package/.claude/hooks/_hook-decisions.mjs +138 -15
- package/.claude/hooks/pre-tool-use-bash-gate.mjs +36 -15
- package/.claude/skills/copilot-pr-followup/SKILL.md +24 -12
- package/.claude/skills/dev-loop/SKILL.md +15 -11
- package/.claude/skills/docs/acceptance-criteria-verification.md +6 -2
- package/.claude/skills/docs/anti-patterns.md +2 -1
- package/.claude/skills/docs/artifact-authority-contract.md +22 -4
- package/.claude/skills/docs/copilot-loop-operations.md +1 -1
- package/.claude/skills/docs/cross-harness-regression-contract.md +60 -0
- package/.claude/skills/docs/issue-intake-procedure.md +1 -0
- package/.claude/skills/docs/local-planning-flow.md +1 -1
- package/.claude/skills/docs/local-planning-worked-example.md +1 -1
- package/.claude/skills/docs/merge-preconditions.md +17 -1
- package/.claude/skills/docs/plan-file-contract.md +1 -1
- package/.claude/skills/docs/public-dev-loop-contract.md +1 -1
- package/.claude/skills/docs/retrospective-checkpoint-contract.md +90 -76
- package/.claude/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/.claude/skills/docs/workflow-handoff-contract.md +39 -0
- package/.claude/skills/local-implementation/SKILL.md +25 -14
- package/.claude/skills/loop-grill/SKILL.md +163 -0
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +95 -0
- package/README.md +9 -9
- package/agents/dev-loop.agent.md +3 -2
- package/agents/developer.agent.md +1 -1
- package/agents/docs.agent.md +1 -1
- package/agents/fixer.agent.md +1 -1
- package/agents/quality.agent.md +1 -1
- package/agents/refiner.agent.md +1 -1
- package/agents/review.agent.md +3 -2
- package/cli/index.mjs +35 -42
- package/extension/README.md +4 -4
- package/extension/checks.ts +1 -5
- package/extension/harness-types.ts +1 -0
- package/extension/index.ts +6 -0
- package/extension/pi-extension-adapter.ts +2 -0
- package/extension/presentation.ts +7 -19
- package/package.json +9 -10
- package/scripts/github/capture-review-threads.mjs +2 -9
- package/scripts/github/comment-issue.mjs +2 -9
- package/scripts/github/create-label.mjs +133 -0
- package/scripts/github/detect-checkpoint-evidence.mjs +170 -14
- package/scripts/github/detect-linked-issue-pr.mjs +22 -6
- package/scripts/github/edit-pr.mjs +259 -0
- package/scripts/github/fetch-ci-logs.mjs +2 -9
- package/scripts/github/list-issues.mjs +2 -9
- package/scripts/github/manage-sub-issues.mjs +46 -10
- package/scripts/github/offer-human-handoff.mjs +8 -5
- package/scripts/github/post-gate-findings.mjs +14 -2
- package/scripts/github/probe-ci-status.mjs +8 -2
- package/scripts/github/probe-copilot-review.mjs +21 -14
- package/scripts/github/ready-for-review.mjs +26 -8
- package/scripts/github/reconcile-draft-gate.mjs +7 -3
- package/scripts/github/reply-resolve-review-thread.mjs +16 -5
- package/scripts/github/reply-resolve-review-threads.mjs +69 -7
- package/scripts/github/request-copilot-review.mjs +8 -2
- package/scripts/github/resolve-handoff-candidates.mjs +7 -3
- package/scripts/github/resolve-tracker-local-spec.mjs +9 -3
- package/scripts/github/stage-reviewer-draft.mjs +10 -2
- package/scripts/github/tick-verified-checkboxes.mjs +202 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +31 -10
- package/scripts/github/verify-fresh-review-context.mjs +150 -31
- package/scripts/github/view-pr.mjs +150 -0
- package/scripts/github/write-gate-context.mjs +248 -61
- package/scripts/github/write-gate-findings-log.mjs +75 -1
- package/scripts/lib/jq-output.mjs +18 -0
- package/scripts/loop/_post-convergence-change.mjs +211 -0
- package/scripts/loop/_pr-runner-coordination.mjs +70 -0
- package/scripts/loop/_repo-root-resolver.mjs +47 -0
- package/scripts/loop/build-handoff-envelope.mjs +14 -4
- package/scripts/loop/check-retro-tooling.mjs +14 -3
- package/scripts/loop/checkpoint-contract.mjs +7 -4
- package/scripts/loop/cleanup-worktree.mjs +12 -3
- package/scripts/loop/conductor-monitor.mjs +12 -18
- package/scripts/loop/copilot-pr-handoff.mjs +135 -14
- package/scripts/loop/debt-remediate.mjs +24 -12
- package/scripts/loop/detect-change-scope.mjs +36 -7
- package/scripts/loop/detect-copilot-loop-state.mjs +11 -5
- package/scripts/loop/detect-copilot-session-activity.mjs +7 -3
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +7 -3
- package/scripts/loop/detect-internal-only-pr.mjs +8 -2
- package/scripts/loop/detect-issue-refinement-artifact.mjs +6 -3
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +149 -66
- package/scripts/loop/detect-reviewer-loop-state.mjs +12 -2
- package/scripts/loop/detect-tracker-first-loop-state.mjs +9 -1
- package/scripts/loop/detect-tracker-pr-state.mjs +10 -3
- package/scripts/loop/ensure-worktree.mjs +8 -3
- package/scripts/loop/info.mjs +12 -4
- package/scripts/loop/inspect-run-viewer/constants.mjs +4 -18
- package/scripts/loop/inspect-run-viewer/vendor/mermaid.min.js +3405 -0
- package/scripts/loop/inspect-run-viewer-ci-changes.mjs +18 -6
- package/scripts/loop/inspect-run-viewer.mjs +67 -4
- package/scripts/loop/inspect-run.mjs +8 -2
- package/scripts/loop/outer-loop.mjs +8 -4
- package/scripts/loop/pr-runner-coordination.mjs +2 -9
- package/scripts/loop/pre-commit-branch-guard.mjs +16 -10
- package/scripts/loop/pre-flight-gate.mjs +9 -9
- package/scripts/loop/pre-pr-ready-gate.mjs +8 -4
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +13 -4
- package/scripts/loop/provision-worktree.mjs +74 -3
- package/scripts/loop/resolve-dev-loop-startup.mjs +76 -7
- package/scripts/loop/resolve-gate-dispatch.mjs +134 -0
- package/scripts/loop/resolve-pr-conflicts.mjs +14 -7
- package/scripts/loop/run-conductor-cycle.mjs +8 -2
- package/scripts/loop/run-queue.mjs +18 -9
- package/scripts/loop/run-refinement-audit.mjs +8 -9
- package/scripts/loop/run-watch-cycle.mjs +2 -9
- package/scripts/loop/sanctioned-commands.mjs +104 -0
- package/scripts/loop/steer-loop.mjs +29 -16
- package/scripts/loop/validate-pr-body-spec.mjs +207 -0
- package/scripts/loop/watch-initial-copilot-pr.mjs +8 -3
- package/scripts/pages/build-site.mjs +59 -8
- package/scripts/pages/build-state-atlas.mjs +441 -0
- package/scripts/projects/_resolve-project.mjs +148 -0
- package/scripts/projects/add-queue-item.mjs +60 -54
- package/scripts/projects/archive-done-items.mjs +49 -84
- package/scripts/projects/ensure-queue-board.mjs +10 -36
- package/scripts/projects/list-queue-items.mjs +116 -65
- package/scripts/projects/move-queue-item.mjs +28 -49
- package/scripts/projects/reconcile-queue.mjs +253 -0
- package/scripts/projects/reorder-queue-item.mjs +41 -47
- package/scripts/projects/resolve-active-board-item.mjs +108 -46
- package/scripts/projects/sync-item-status.mjs +15 -10
- package/scripts/refine/_refine-helpers.mjs +21 -5
- package/scripts/refine/exit-spike.mjs +18 -8
- package/scripts/refine/promote-plan.mjs +22 -16
- package/scripts/refine/prose-linkage-detector.mjs +1 -1
- package/scripts/refine/refine-plan-file.mjs +13 -4
- package/scripts/refine/refinement-completeness-checker.mjs +1 -1
- package/scripts/refine/scaffold-spike-file.mjs +4 -8
- package/scripts/refine/scope-boundary-cross-checker.mjs +1 -1
- package/scripts/refine/tree-integrity-validator.mjs +1 -1
- package/scripts/refine/validate-plan-file.mjs +1 -1
- package/scripts/refine/validate-spike-file.mjs +1 -1
- package/scripts/refine/verify.mjs +11 -6
- package/scripts/release/assert-core-dependency-version.mjs +123 -0
- package/scripts/release/extract-changelog-section.mjs +16 -2
- package/skills/copilot-pr-followup/SKILL.md +24 -12
- package/skills/dev-loop/SKILL.md +10 -6
- package/skills/docs/acceptance-criteria-verification.md +6 -2
- package/skills/docs/anti-patterns.md +2 -1
- package/skills/docs/artifact-authority-contract.md +22 -4
- package/skills/docs/copilot-loop-operations.md +1 -1
- package/skills/docs/cross-harness-regression-contract.md +60 -0
- package/skills/docs/issue-intake-procedure.md +1 -0
- package/skills/docs/local-planning-flow.md +1 -1
- package/skills/docs/local-planning-worked-example.md +1 -1
- package/skills/docs/merge-preconditions.md +17 -1
- package/skills/docs/plan-file-contract.md +1 -1
- package/skills/docs/public-dev-loop-contract.md +1 -1
- package/skills/docs/retrospective-checkpoint-contract.md +90 -76
- package/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/skills/docs/workflow-handoff-contract.md +39 -0
- package/skills/local-implementation/SKILL.md +25 -14
- package/skills/loop-grill/SKILL.md +165 -0
- package/.claude/commands/continue.md +0 -15
- package/scripts/loop/conductor.mjs +0 -233
- package/scripts/loop/detect-stale-runner.mjs +0 -265
- package/scripts/loop/pre-push-main-guard.mjs +0 -117
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
3
3
|
import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
4
|
+
import { resolveProjectSelector, findProject, applyDevloopsBoard } from "./_resolve-project.mjs";
|
|
4
5
|
import { parseArgs } from "node:util";
|
|
6
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
5
7
|
|
|
6
8
|
const USAGE = `Usage:
|
|
7
|
-
dev-loops queue reorder --repo <owner/name> --project <number|id> --item <number|node-id> [--after <number|node-id>]
|
|
8
|
-
dev-loops queue reorder move-to-top <ref> --repo <owner/name> --project <number|id>
|
|
9
|
-
dev-loops queue reorder move-after <ref> <after-ref> --repo <owner/name> --project <number|id>
|
|
10
|
-
dev-loops queue reorder order <ref1> <ref2> ... --repo <owner/name> --project <number|id>
|
|
9
|
+
dev-loops queue reorder --repo <owner/name> --project <number|id|board-uri> --item <number|node-id> [--after <number|node-id>]
|
|
10
|
+
dev-loops queue reorder move-to-top <ref> --repo <owner/name> --project <number|id|board-uri>
|
|
11
|
+
dev-loops queue reorder move-after <ref> <after-ref> --repo <owner/name> --project <number|id|board-uri>
|
|
12
|
+
dev-loops queue reorder order <ref1> <ref2> ... --repo <owner/name> --project <number|id|board-uri>
|
|
11
13
|
(dev-loops project reorder … is a back-compat alias)
|
|
12
14
|
|
|
13
15
|
Reorder GitHub Projects V2 items by board position via updateProjectV2ItemPosition.
|
|
@@ -22,22 +24,27 @@ Forms:
|
|
|
22
24
|
A <ref> is an issue/PR number OR a project item node ID. Works for both issues and PRs.
|
|
23
25
|
|
|
24
26
|
Options:
|
|
25
|
-
--repo <owner/name>
|
|
26
|
-
--project <number|id>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
--
|
|
27
|
+
--repo <owner/name> Required. Repository to scope the project search.
|
|
28
|
+
--project <number|id|board-uri> Project number, node ID, or board URI
|
|
29
|
+
(e.g. https://github.com/users/me/projects/3).
|
|
30
|
+
When omitted, resolved from .devloops
|
|
31
|
+
queue.projectNumber / queue.boardTitle.
|
|
32
|
+
--item <number|node-id> Flag form: item to reorder.
|
|
33
|
+
--after <number|node-id> Flag form: position after this item. When omitted, move to top.
|
|
34
|
+
--dry-run Print the intended GraphQL mutation(s) without executing.
|
|
35
|
+
--help, -h Show this help.
|
|
31
36
|
|
|
32
37
|
Output (stdout):
|
|
33
38
|
JSON. Move/move-to-top: { ok, item, after_ref|null, before, after }.
|
|
34
39
|
order: { ok, moves: [...], before, after }.
|
|
35
40
|
dry-run: { ok, dryRun: true, mutations: [{ query, variables }], before }.
|
|
36
41
|
|
|
42
|
+
${JQ_OUTPUT_USAGE}
|
|
43
|
+
|
|
37
44
|
Exit codes:
|
|
38
45
|
0 — success
|
|
39
46
|
1 — usage or argument error
|
|
40
|
-
2 — GitHub API error
|
|
47
|
+
2 — GitHub API error / invalid --jq filter
|
|
41
48
|
3 — project, item, or after-item not found
|
|
42
49
|
`.trim();
|
|
43
50
|
|
|
@@ -70,6 +77,7 @@ function parseCliArgs(argv) {
|
|
|
70
77
|
after: { type: "string" },
|
|
71
78
|
"dry-run": { type: "boolean" },
|
|
72
79
|
help: { type: "boolean", short: "h" },
|
|
80
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
73
81
|
},
|
|
74
82
|
allowPositionals: true,
|
|
75
83
|
strict: false,
|
|
@@ -109,8 +117,10 @@ function parseCliArgs(argv) {
|
|
|
109
117
|
}
|
|
110
118
|
args.dryRun = true;
|
|
111
119
|
break;
|
|
112
|
-
default:
|
|
120
|
+
default: {
|
|
121
|
+
if (matchJqOutputToken(token, args, (t) => requireValue(t, "--jq requires a filter"))) break;
|
|
113
122
|
throw parseError(`Unknown flag: ${token.rawName}`);
|
|
123
|
+
}
|
|
114
124
|
}
|
|
115
125
|
}
|
|
116
126
|
return args;
|
|
@@ -144,24 +154,6 @@ function validateRepo(repo) {
|
|
|
144
154
|
return repo;
|
|
145
155
|
}
|
|
146
156
|
|
|
147
|
-
function parseProjectRef(raw) {
|
|
148
|
-
if (!raw || typeof raw !== "string" || raw.trim().length === 0) {
|
|
149
|
-
throw Object.assign(new Error("--project is required"), { code: "INVALID_PROJECT" });
|
|
150
|
-
}
|
|
151
|
-
const trimmed = raw.trim();
|
|
152
|
-
const asNum = Number(trimmed);
|
|
153
|
-
if (Number.isInteger(asNum) && asNum > 0 && String(asNum) === trimmed) {
|
|
154
|
-
return { kind: "number", value: asNum };
|
|
155
|
-
}
|
|
156
|
-
if (trimmed === "0") {
|
|
157
|
-
throw Object.assign(new Error(`--project must be a positive integer or a node ID, got "${raw}"`), { code: "INVALID_PROJECT" });
|
|
158
|
-
}
|
|
159
|
-
if (GLOBAL_NODE_ID_RE.test(trimmed)) {
|
|
160
|
-
return { kind: "id", value: trimmed };
|
|
161
|
-
}
|
|
162
|
-
throw Object.assign(new Error(`--project must be a positive integer or a node ID, got "${raw}"`), { code: "INVALID_PROJECT" });
|
|
163
|
-
}
|
|
164
|
-
|
|
165
157
|
function parseItemRef(raw) {
|
|
166
158
|
if (!raw || typeof raw !== "string" || raw.trim().length === 0) {
|
|
167
159
|
throw Object.assign(new Error("--item is required"), { code: "INVALID_ITEM" });
|
|
@@ -522,19 +514,17 @@ function classifyExitCode(err) {
|
|
|
522
514
|
|
|
523
515
|
// ── Resolve owner + project (shared) ──────────────────────────────────────
|
|
524
516
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
return project;
|
|
517
|
+
// Resolve the project based on a selector (from resolveProjectSelector).
|
|
518
|
+
// When the selector contains a URI ref, the URI-encoded owner overrides the
|
|
519
|
+
// repo-derived owner so cross-scope boards (user vs org) resolve unambiguously.
|
|
520
|
+
async function resolveProject(repoOwner, selector, env, child) {
|
|
521
|
+
const projectRef = selector.projectRef;
|
|
522
|
+
const effectiveOwner = projectRef?.kind === "uri" ? projectRef.owner : repoOwner;
|
|
523
|
+
const ownerKind = projectRef?.kind === "uri"
|
|
524
|
+
? projectRef.ownerKind
|
|
525
|
+
: (await resolveOwner(repoOwner, env, child)).kind;
|
|
526
|
+
const projects = await listAllProjects(effectiveOwner, ownerKind, env, child);
|
|
527
|
+
return findProject(projects, selector, effectiveOwner);
|
|
538
528
|
}
|
|
539
529
|
|
|
540
530
|
function executePosition(projectId, itemId, afterId) {
|
|
@@ -742,7 +732,7 @@ async function main(args, { env = process.env, runChild } = {}) {
|
|
|
742
732
|
const child = runChild ?? _runChild;
|
|
743
733
|
const repo = validateRepo(args.repo);
|
|
744
734
|
const [owner, repoName] = repo.split("/");
|
|
745
|
-
const
|
|
735
|
+
const selector = resolveProjectSelector(args);
|
|
746
736
|
|
|
747
737
|
// Fail closed: the legacy flag form takes no positional arguments. A stray
|
|
748
738
|
// token (e.g. `reorder 630 --item ...`) must not be silently ignored.
|
|
@@ -753,7 +743,7 @@ async function main(args, { env = process.env, runChild } = {}) {
|
|
|
753
743
|
);
|
|
754
744
|
}
|
|
755
745
|
|
|
756
|
-
const project = await resolveProject(owner,
|
|
746
|
+
const project = await resolveProject(owner, selector, env, child);
|
|
757
747
|
|
|
758
748
|
if (args._subcommand) {
|
|
759
749
|
return mainSubcommand(args, { env, child, repo, project });
|
|
@@ -763,7 +753,7 @@ async function main(args, { env = process.env, runChild } = {}) {
|
|
|
763
753
|
|
|
764
754
|
// ── CLI entrypoint ──────────────────────────────────────────────────────
|
|
765
755
|
|
|
766
|
-
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env } = {}) {
|
|
756
|
+
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env, cwd = process.cwd() } = {}) {
|
|
767
757
|
let args;
|
|
768
758
|
try {
|
|
769
759
|
args = parseCliArgs(argv);
|
|
@@ -776,9 +766,13 @@ async function runCli(argv, { stdout = process.stdout, stderr = process.stderr,
|
|
|
776
766
|
stdout.write(USAGE);
|
|
777
767
|
return;
|
|
778
768
|
}
|
|
769
|
+
|
|
770
|
+
// Resolve the board from .devloops when --project is absent.
|
|
771
|
+
applyDevloopsBoard(args, cwd);
|
|
772
|
+
|
|
779
773
|
try {
|
|
780
774
|
const result = await main(args, { env });
|
|
781
|
-
|
|
775
|
+
process.exitCode = emitResult(result, { jq: args.jq, silent: args.silent, stdout, stderr });
|
|
782
776
|
} catch (err) {
|
|
783
777
|
stderr.write(JSON.stringify({ ok: false, error: err.message, code: err.code ?? "UNKNOWN" }) + "\n");
|
|
784
778
|
process.exitCode = classifyExitCode(err);
|
|
@@ -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)`,
|