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
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
|
|
5
5
|
import { parsePositiveInteger, requireTokenValue, runCommand } from "../_cli-primitives.mjs";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
7
8
|
const DEFAULT_MAX_LINES = 1000;
|
|
8
9
|
const DEFAULT_DUPLICATE_WINDOW_LINES = 4;
|
|
9
10
|
const DEFAULT_BRANCH_THRESHOLD = 25;
|
|
@@ -35,7 +36,9 @@ Success output (stdout, JSON):
|
|
|
35
36
|
}
|
|
36
37
|
Failure behavior (stderr, JSON, exit 1):
|
|
37
38
|
- malformed arguments, blank paths, invalid thresholds, and zero auditable files fail closed
|
|
38
|
-
- findings are not a process failure; findings still return exit 0
|
|
39
|
+
- findings are not a process failure; findings still return exit 0
|
|
40
|
+
|
|
41
|
+
${JQ_OUTPUT_USAGE}`.trim();
|
|
39
42
|
const parseError = buildParseError(USAGE);
|
|
40
43
|
const BRANCH_TOKEN_PATTERN = /\b(?:if|else|switch|case|for|while|catch|finally|break|continue)\b|&&|\|\||\?(?![?.])/gu;
|
|
41
44
|
const PRIORITY_ORDER = new Map([
|
|
@@ -67,6 +70,7 @@ export function parseRefinementAuditCliArgs(argv) {
|
|
|
67
70
|
"branch-threshold": { type: "string" },
|
|
68
71
|
"thin-wrapper-max-lines": { type: "string" },
|
|
69
72
|
output: { type: "string" },
|
|
73
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
70
74
|
},
|
|
71
75
|
allowPositionals: true,
|
|
72
76
|
strict: false,
|
|
@@ -127,6 +131,7 @@ export function parseRefinementAuditCliArgs(argv) {
|
|
|
127
131
|
options.output = requireTokenValue(token, parseError, { flagPattern: /^-/u });
|
|
128
132
|
continue;
|
|
129
133
|
}
|
|
134
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
130
135
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
131
136
|
}
|
|
132
137
|
if (options.paths !== undefined && options.pathsFile !== undefined) {
|
|
@@ -512,22 +517,16 @@ export async function runCli(
|
|
|
512
517
|
fullRepoScan: false,
|
|
513
518
|
},
|
|
514
519
|
};
|
|
515
|
-
const serializedPayload = `${JSON.stringify(payload)}\n`;
|
|
516
520
|
if (typeof options.output === "string") {
|
|
517
521
|
const outputPath = path.resolve(cwd, options.output);
|
|
518
522
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
519
|
-
await writeFile(outputPath,
|
|
523
|
+
await writeFile(outputPath, `${JSON.stringify(payload)}\n`, "utf8");
|
|
520
524
|
}
|
|
521
|
-
|
|
525
|
+
process.exitCode = emitResult(payload, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
522
526
|
return payload;
|
|
523
527
|
}
|
|
524
528
|
if (isDirectCliRun(import.meta.url)) {
|
|
525
529
|
runCli()
|
|
526
|
-
.then((result) => {
|
|
527
|
-
if (result?.ok === false) {
|
|
528
|
-
process.exitCode = 1;
|
|
529
|
-
}
|
|
530
|
-
})
|
|
531
530
|
.catch((error) => {
|
|
532
531
|
process.stderr.write(`${formatCliError(error)}\n`);
|
|
533
532
|
process.exitCode = 1;
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
EXTERNAL_HEALTHY_WAIT_TIMEOUT_POLICY,
|
|
16
16
|
enforceExternalHealthyWaitTimeout,
|
|
17
17
|
} from "@dev-loops/core/loop/timeout-policy";
|
|
18
|
-
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
18
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
19
19
|
const REMOVED_FLAGS = new Set([
|
|
20
20
|
"--force-rerequest-review",
|
|
21
21
|
"--probe-only",
|
|
@@ -254,14 +254,7 @@ export function parseWatchCycleCliArgs(argv) {
|
|
|
254
254
|
options.concise = true;
|
|
255
255
|
continue;
|
|
256
256
|
}
|
|
257
|
-
if (token
|
|
258
|
-
options.jq = requireTokenValue(token, parseError);
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
if (token.name === "silent") {
|
|
262
|
-
options.silent = true;
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
257
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
265
258
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
266
259
|
}
|
|
267
260
|
if (options.repo === undefined || options.pr === undefined) {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical sanctioned operation → wrapper command map (issue #1081).
|
|
3
|
+
*
|
|
4
|
+
* SINGLE SOURCE OF TRUTH for "which wrapper does a dev-loop subagent use for
|
|
5
|
+
* which GitHub/loop operation". Previously this knowledge was re-derived per
|
|
6
|
+
* handoff and scattered across SKILL.md / copilot-loop-operations.md /
|
|
7
|
+
* pr-lifecycle-contract.md, so subagents burned cycles rediscovering e.g.
|
|
8
|
+
* `gh pr ready` → scripts/github/ready-for-review.mjs.
|
|
9
|
+
*
|
|
10
|
+
* This lives in the CONSUMER layer (scripts/), NOT in @dev-loops/core, because
|
|
11
|
+
* the values are THIS repo's `scripts/...` wrapper paths. Core stays
|
|
12
|
+
* consumer-agnostic: it defines the envelope SHAPE and carries whatever
|
|
13
|
+
* `sanctionedCommands` object the consumer supplies. The
|
|
14
|
+
* `loop build-envelope` CLI injects this map into every emitted envelope so a
|
|
15
|
+
* spawned subagent receives it verbatim by DEFAULT, regardless of who wrote
|
|
16
|
+
* the handoff.
|
|
17
|
+
*
|
|
18
|
+
* Wrapper paths are repo-root-relative. A contract test
|
|
19
|
+
* (test/contracts/sanctioned-commands-exist.test.mjs) asserts every mapped
|
|
20
|
+
* wrapper exists on disk and fails closed if one is renamed/removed.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const SANCTIONED_COMMANDS = Object.freeze({
|
|
24
|
+
// Read-only lookups. Each value is the sanctioned wrapper; several also
|
|
25
|
+
// accept `loop info` as an equivalent aggregate read (noted in the doc).
|
|
26
|
+
reads: Object.freeze({
|
|
27
|
+
"pr-facts": "scripts/github/view-pr.mjs",
|
|
28
|
+
"ci-status": "scripts/github/probe-ci-status.mjs",
|
|
29
|
+
"ci-wait": "scripts/github/wait-pr-checks.mjs",
|
|
30
|
+
"ci-logs": "scripts/github/fetch-ci-logs.mjs",
|
|
31
|
+
"issue-list": "scripts/github/list-issues.mjs",
|
|
32
|
+
"copilot-review-state": "scripts/github/probe-copilot-review.mjs",
|
|
33
|
+
"review-threads": "scripts/github/list-review-threads.mjs",
|
|
34
|
+
"gate-coordination": "scripts/loop/detect-pr-gate-coordination-state.mjs",
|
|
35
|
+
}),
|
|
36
|
+
|
|
37
|
+
// Metadata edits.
|
|
38
|
+
edits: Object.freeze({
|
|
39
|
+
"pr-body-title-assignee-milestone": "scripts/github/edit-pr.mjs",
|
|
40
|
+
"issue-comment": "scripts/github/comment-issue.mjs",
|
|
41
|
+
}),
|
|
42
|
+
|
|
43
|
+
// Lifecycle mutations a subagent MAY perform (state transitions on the PR /
|
|
44
|
+
// review). Board status transitions are deliberately NOT here — they are
|
|
45
|
+
// orchestratorOwned (see below): in the current batch model the orchestrator
|
|
46
|
+
// owns board moves, and a subagent's In-Progress move rides ready-for-review.mjs
|
|
47
|
+
// (#1069), so there is no standalone subagent-sanctioned board-sync op.
|
|
48
|
+
lifecycle: Object.freeze({
|
|
49
|
+
"ready-for-review": "scripts/github/ready-for-review.mjs",
|
|
50
|
+
"pr-create": "scripts/github/create-pr.mjs",
|
|
51
|
+
"copilot-request": "scripts/github/request-copilot-review.mjs",
|
|
52
|
+
"reply-resolve-thread": "scripts/github/reply-resolve-review-thread.mjs",
|
|
53
|
+
"reply-resolve-threads": "scripts/github/reply-resolve-review-threads.mjs",
|
|
54
|
+
"gate-verdict-comment": "scripts/github/upsert-checkpoint-verdict.mjs",
|
|
55
|
+
}),
|
|
56
|
+
|
|
57
|
+
// Never allowed for any operation above — the sanctioned wrapper is mandatory.
|
|
58
|
+
forbidden: Object.freeze([
|
|
59
|
+
"gh pr view",
|
|
60
|
+
"gh pr checks",
|
|
61
|
+
"gh pr edit",
|
|
62
|
+
"node -e",
|
|
63
|
+
"node --input-type=module -e",
|
|
64
|
+
"node -p",
|
|
65
|
+
"python -c",
|
|
66
|
+
"python3 -c",
|
|
67
|
+
"tailing subagent transcripts",
|
|
68
|
+
"sleep-poll loops",
|
|
69
|
+
]),
|
|
70
|
+
|
|
71
|
+
// Orchestrator-owned: a spawned dev-loop subagent must NEVER do these. Board
|
|
72
|
+
// status transitions live here (not in `lifecycle`) — the orchestrator owns
|
|
73
|
+
// move-queue-item/sync-item-status in the current batch model.
|
|
74
|
+
orchestratorOwned: Object.freeze([
|
|
75
|
+
"gh pr merge",
|
|
76
|
+
"board status transitions (move-queue-item / sync-item-status.mjs; current batch model)",
|
|
77
|
+
]),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// The wrapper-path-bearing groups are the object-valued groups (reads/edits/
|
|
81
|
+
// lifecycle); `forbidden`/`orchestratorOwned` are string arrays, not path maps.
|
|
82
|
+
const PATH_GROUP_KEYS = Object.freeze(
|
|
83
|
+
Object.keys(SANCTIONED_COMMANDS).filter(
|
|
84
|
+
(k) => !Array.isArray(SANCTIONED_COMMANDS[k]),
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* EVERY value named across the path-bearing groups — with NO shape filtering,
|
|
90
|
+
* so a malformed/typo'd entry is RETURNED (and then fails the contract test's
|
|
91
|
+
* shape+existence assertions) rather than being silently skipped. Auto-includes
|
|
92
|
+
* any future path-bearing group. This is what keeps the map from drifting off
|
|
93
|
+
* disk (#1081). For a well-formed map every element is a repo-root-relative
|
|
94
|
+
* `scripts/*.mjs` string; the no-filtering contract means a malformed map can
|
|
95
|
+
* yield a non-string here, which the contract test is designed to catch.
|
|
96
|
+
* @returns {unknown[]} wrapper-path values (strings for a well-formed map)
|
|
97
|
+
*/
|
|
98
|
+
export function listSanctionedWrapperPaths() {
|
|
99
|
+
const out = [];
|
|
100
|
+
for (const key of PATH_GROUP_KEYS) {
|
|
101
|
+
for (const value of Object.values(SANCTIONED_COMMANDS[key])) {
|
|
102
|
+
out.push(value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { formatCliError } from "../_core-helpers.mjs";
|
|
35
35
|
import { requireTokenValue as readSharedTokenValue } from "../_cli-primitives.mjs";
|
|
36
36
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
37
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
37
38
|
const SUBMIT_USAGE = `Usage:
|
|
38
39
|
steer-loop.mjs submit --repo <owner/name> --pr <number>
|
|
39
40
|
--kind stop_at_next_safe_gate --directive <text> --seq <n>
|
|
@@ -61,7 +62,9 @@ Optional:
|
|
|
61
62
|
Output (stdout, JSON):
|
|
62
63
|
{ "ok": true, "acknowledgement": { ... }, "result": { ... }, "steeringState": { ... } }
|
|
63
64
|
Error output (stderr, JSON):
|
|
64
|
-
{ "ok": false, "error": "...", "usage": "..." }
|
|
65
|
+
{ "ok": false, "error": "...", "usage": "..." }
|
|
66
|
+
|
|
67
|
+
${JQ_OUTPUT_USAGE}`.trim();
|
|
65
68
|
const STATUS_USAGE = `Usage:
|
|
66
69
|
steer-loop.mjs status --run-id <id> [--state-file <path>]
|
|
67
70
|
steer-loop.mjs status --repo <owner/name> --pr <number> [--state-file <path>]
|
|
@@ -75,7 +78,9 @@ Optional:
|
|
|
75
78
|
Output (stdout, JSON):
|
|
76
79
|
{ "ok": true, "status": { ... } }
|
|
77
80
|
Error output (stderr, JSON):
|
|
78
|
-
{ "ok": false, "error": "...", "usage": "..." }
|
|
81
|
+
{ "ok": false, "error": "...", "usage": "..." }
|
|
82
|
+
|
|
83
|
+
${JQ_OUTPUT_USAGE}`.trim();
|
|
79
84
|
const PROMOTE_USAGE = `Usage:
|
|
80
85
|
steer-loop.mjs promote --run-id <id> --loop-state <state> [--state-file <path>]
|
|
81
86
|
steer-loop.mjs promote --repo <owner/name> --pr <number>
|
|
@@ -93,7 +98,9 @@ Optional:
|
|
|
93
98
|
Output (stdout, JSON):
|
|
94
99
|
{ "ok": true, "promotedCount": <n>, "promoted": [ ... ], "steeringState": { ... } }
|
|
95
100
|
Error output (stderr, JSON):
|
|
96
|
-
{ "ok": false, "error": "...", "usage": "..." }
|
|
101
|
+
{ "ok": false, "error": "...", "usage": "..." }
|
|
102
|
+
|
|
103
|
+
${JQ_OUTPUT_USAGE}`.trim();
|
|
97
104
|
const TOP_USAGE = `Usage:
|
|
98
105
|
steer-loop.mjs <subcommand> [options]
|
|
99
106
|
Subcommands:
|
|
@@ -171,6 +178,7 @@ export function parseSubmitCliArgs(argv) {
|
|
|
171
178
|
"event-id": { type: "string" },
|
|
172
179
|
"copilot-input": { type: "string" },
|
|
173
180
|
"reviewer-input": { type: "string" },
|
|
181
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
174
182
|
},
|
|
175
183
|
allowPositionals: true,
|
|
176
184
|
strict: false,
|
|
@@ -250,6 +258,7 @@ export function parseSubmitCliArgs(argv) {
|
|
|
250
258
|
options.reviewerInputPath = readRequiredOptionValue(token, SUBMIT_USAGE);
|
|
251
259
|
continue;
|
|
252
260
|
}
|
|
261
|
+
if (matchJqOutputToken(token, options, (t) => readRequiredOptionValue(t, SUBMIT_USAGE))) continue;
|
|
253
262
|
throw usageError(`Unknown argument: ${token.rawName}`, SUBMIT_USAGE);
|
|
254
263
|
}
|
|
255
264
|
if (!options.help) {
|
|
@@ -290,6 +299,7 @@ export function parseStatusCliArgs(argv) {
|
|
|
290
299
|
repo: { type: "string" },
|
|
291
300
|
pr: { type: "string" },
|
|
292
301
|
"state-file": { type: "string" },
|
|
302
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
293
303
|
},
|
|
294
304
|
allowPositionals: true,
|
|
295
305
|
strict: false,
|
|
@@ -324,6 +334,7 @@ export function parseStatusCliArgs(argv) {
|
|
|
324
334
|
options.stateFile = readRequiredOptionValue(token, STATUS_USAGE);
|
|
325
335
|
continue;
|
|
326
336
|
}
|
|
337
|
+
if (matchJqOutputToken(token, options, (t) => readRequiredOptionValue(t, STATUS_USAGE))) continue;
|
|
327
338
|
throw usageError(`Unknown argument: ${token.rawName}`, STATUS_USAGE);
|
|
328
339
|
}
|
|
329
340
|
if (!options.help) {
|
|
@@ -357,6 +368,7 @@ export function parsePromoteCliArgs(argv) {
|
|
|
357
368
|
pr: { type: "string" },
|
|
358
369
|
"state-file": { type: "string" },
|
|
359
370
|
"loop-state": { type: "string" },
|
|
371
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
360
372
|
},
|
|
361
373
|
allowPositionals: true,
|
|
362
374
|
strict: false,
|
|
@@ -399,6 +411,7 @@ export function parsePromoteCliArgs(argv) {
|
|
|
399
411
|
options.loopState = val;
|
|
400
412
|
continue;
|
|
401
413
|
}
|
|
414
|
+
if (matchJqOutputToken(token, options, (t) => readRequiredOptionValue(t, PROMOTE_USAGE))) continue;
|
|
402
415
|
throw usageError(`Unknown argument: ${token.rawName}`, PROMOTE_USAGE);
|
|
403
416
|
}
|
|
404
417
|
if (!options.help) {
|
|
@@ -651,7 +664,7 @@ function rejectUnsteerableInspection(inspection, { runId, eventId, seq, directiv
|
|
|
651
664
|
}
|
|
652
665
|
export async function runSubmit(
|
|
653
666
|
argv = [],
|
|
654
|
-
{ stdout = process.stdout, cwd = process.cwd(), env = process.env, ghCommand = "gh" } = {},
|
|
667
|
+
{ stdout = process.stdout, stderr = process.stderr, cwd = process.cwd(), env = process.env, ghCommand = "gh" } = {},
|
|
655
668
|
) {
|
|
656
669
|
const options = parseSubmitCliArgs(argv);
|
|
657
670
|
if (options.help) {
|
|
@@ -793,12 +806,12 @@ export async function runSubmit(
|
|
|
793
806
|
} catch {
|
|
794
807
|
steeringState = createSteeringState(runId, target);
|
|
795
808
|
}
|
|
796
|
-
|
|
809
|
+
process.exitCode = emitResult({
|
|
797
810
|
ok: true,
|
|
798
811
|
acknowledgement: validationRejection.acknowledgement,
|
|
799
812
|
result: validationRejection.result,
|
|
800
813
|
steeringState,
|
|
801
|
-
}
|
|
814
|
+
}, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
802
815
|
return;
|
|
803
816
|
}
|
|
804
817
|
const { steeringState: newState, result } = await withStateFileLock(stateFilePath, async () => {
|
|
@@ -828,9 +841,9 @@ export async function runSubmit(
|
|
|
828
841
|
safePointCategory,
|
|
829
842
|
readbackPath,
|
|
830
843
|
});
|
|
831
|
-
|
|
844
|
+
process.exitCode = emitResult({ ok: true, acknowledgement, result, steeringState: newState }, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
832
845
|
}
|
|
833
|
-
export async function runStatus(argv = [], { stdout = process.stdout, cwd = process.cwd() } = {}) {
|
|
846
|
+
export async function runStatus(argv = [], { stdout = process.stdout, stderr = process.stderr, cwd = process.cwd() } = {}) {
|
|
834
847
|
const options = parseStatusCliArgs(argv);
|
|
835
848
|
if (options.help) {
|
|
836
849
|
stdout.write(`${STATUS_USAGE}\n`);
|
|
@@ -843,9 +856,9 @@ export async function runStatus(argv = [], { stdout = process.stdout, cwd = proc
|
|
|
843
856
|
const stateFilePath = options.stateFile ?? (target ? defaultStateFilePathForTarget(target, cwd) : defaultStateFilePath(runId, cwd));
|
|
844
857
|
const steeringState = await loadOrCreateSteeringState(stateFilePath, runId, target);
|
|
845
858
|
const status = getSteeringStatus(steeringState);
|
|
846
|
-
|
|
859
|
+
process.exitCode = emitResult({ ok: true, status }, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
847
860
|
}
|
|
848
|
-
export async function runPromote(argv = [], { stdout = process.stdout, cwd = process.cwd() } = {}) {
|
|
861
|
+
export async function runPromote(argv = [], { stdout = process.stdout, stderr = process.stderr, cwd = process.cwd() } = {}) {
|
|
849
862
|
const options = parsePromoteCliArgs(argv);
|
|
850
863
|
if (options.help) {
|
|
851
864
|
stdout.write(`${PROMOTE_USAGE}\n`);
|
|
@@ -864,16 +877,16 @@ export async function runPromote(argv = [], { stdout = process.stdout, cwd = pro
|
|
|
864
877
|
}
|
|
865
878
|
return nextState;
|
|
866
879
|
});
|
|
867
|
-
|
|
880
|
+
process.exitCode = emitResult({
|
|
868
881
|
ok: true,
|
|
869
882
|
promotedCount: promotedState.promoted.length,
|
|
870
883
|
promoted: promotedState.promoted,
|
|
871
884
|
steeringState: promotedState.steeringState,
|
|
872
|
-
}
|
|
885
|
+
}, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
873
886
|
}
|
|
874
887
|
export async function runCli(
|
|
875
888
|
argv = process.argv.slice(2),
|
|
876
|
-
{ stdout = process.stdout, cwd = process.cwd(), env = process.env, ghCommand = "gh" } = {},
|
|
889
|
+
{ stdout = process.stdout, stderr = process.stderr, cwd = process.cwd(), env = process.env, ghCommand = "gh" } = {},
|
|
877
890
|
) {
|
|
878
891
|
const [subcommand, ...rest] = argv;
|
|
879
892
|
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
@@ -881,13 +894,13 @@ export async function runCli(
|
|
|
881
894
|
return;
|
|
882
895
|
}
|
|
883
896
|
if (subcommand === "submit") {
|
|
884
|
-
return runSubmit(rest, { stdout, cwd, env, ghCommand });
|
|
897
|
+
return runSubmit(rest, { stdout, stderr, cwd, env, ghCommand });
|
|
885
898
|
}
|
|
886
899
|
if (subcommand === "promote") {
|
|
887
|
-
return runPromote(rest, { stdout, cwd });
|
|
900
|
+
return runPromote(rest, { stdout, stderr, cwd });
|
|
888
901
|
}
|
|
889
902
|
if (subcommand === "status") {
|
|
890
|
-
return runStatus(rest, { stdout, cwd });
|
|
903
|
+
return runStatus(rest, { stdout, stderr, cwd });
|
|
891
904
|
}
|
|
892
905
|
const error = usageError(`Unknown subcommand: ${subcommand}`, TOP_USAGE);
|
|
893
906
|
throw error;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validate that a PR body carries the invariants required to serve as the
|
|
4
|
+
* lightweight spec-of-record (issue #1025): Objective/why, in-scope, explicit
|
|
5
|
+
* non-goals, testable Acceptance criteria, Definition of done, and Open
|
|
6
|
+
* questions/risks.
|
|
7
|
+
*
|
|
8
|
+
* Thin CLI wrapper (parallel to detect-issue-refinement-artifact.mjs): fetches
|
|
9
|
+
* the PR body via `gh pr view <n> --json body` and runs the shared pure
|
|
10
|
+
* validator `validatePrBodySpec` (reuses issue-refinement-artifact markdown
|
|
11
|
+
* logic — no parallel validator). Fails closed (non-zero + per-section reason)
|
|
12
|
+
* when any invariant is missing.
|
|
13
|
+
*/
|
|
14
|
+
import { readFile } from "node:fs/promises";
|
|
15
|
+
import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
16
|
+
import { requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
17
|
+
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
18
|
+
import { parseArgs } from "node:util";
|
|
19
|
+
import { validatePrBodySpec } from "@dev-loops/core/loop/issue-refinement-artifact";
|
|
20
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
21
|
+
|
|
22
|
+
const USAGE = `Usage:
|
|
23
|
+
validate-pr-body-spec.mjs --repo <owner/name> --pr <number> [--expected-issue <n>]
|
|
24
|
+
validate-pr-body-spec.mjs --input <path> [--expected-issue <n>]
|
|
25
|
+
Validate that a PR body carries the lightweight spec-of-record invariants
|
|
26
|
+
(Objective/why, In scope, Explicit non-goals, testable Acceptance criteria,
|
|
27
|
+
Definition of done, Open questions/risks, and — unless --no-issue is given —
|
|
28
|
+
a GitHub closing-keyword issue reference such as \`Closes #123\`).
|
|
29
|
+
Required (exactly one):
|
|
30
|
+
--repo <owner/name> --pr <number> Fetch the PR body via gh and validate it
|
|
31
|
+
--input <path> Path to a JSON file with { "body": "..." }
|
|
32
|
+
(optional "repo" and "pr" fields are echoed
|
|
33
|
+
back in the result when present)
|
|
34
|
+
Optional:
|
|
35
|
+
--expected-issue <n> Positive integer; the referenced closing
|
|
36
|
+
issue(s) must include this number, else
|
|
37
|
+
"closes_wrong_issue". Mutually exclusive
|
|
38
|
+
with --no-issue.
|
|
39
|
+
--no-issue Explicit issue-less mode (#1210): the PR
|
|
40
|
+
is the sole artifact, so a closing-issue
|
|
41
|
+
reference is FORBIDDEN rather than
|
|
42
|
+
required — one present fails closed under
|
|
43
|
+
"unexpected_closing_issue_reference".
|
|
44
|
+
Mutually exclusive with --expected-issue.
|
|
45
|
+
Success output (stdout, JSON):
|
|
46
|
+
{
|
|
47
|
+
"ok": true | false,
|
|
48
|
+
"checker": "validate-pr-body-spec",
|
|
49
|
+
"repo": "owner/name" | null,
|
|
50
|
+
"pr": 123 | null,
|
|
51
|
+
"errors": [ { "code": "missing_...", "message": "..." } ],
|
|
52
|
+
"sections": [...],
|
|
53
|
+
"acItems": [...],
|
|
54
|
+
"dodItems": [...],
|
|
55
|
+
"closesIssues": [...]
|
|
56
|
+
}
|
|
57
|
+
Exit: 0 when every invariant is present; 1 (fail closed) when any is missing.
|
|
58
|
+
Error output (stderr, JSON):
|
|
59
|
+
{ "ok": false, "error": "...", "usage": "..." }
|
|
60
|
+
${JQ_OUTPUT_USAGE}`.trim();
|
|
61
|
+
|
|
62
|
+
const parseError = buildParseError(USAGE);
|
|
63
|
+
|
|
64
|
+
export function parseValidatePrBodySpecCliArgs(argv) {
|
|
65
|
+
const options = {
|
|
66
|
+
help: false,
|
|
67
|
+
repo: undefined,
|
|
68
|
+
pr: undefined,
|
|
69
|
+
input: undefined,
|
|
70
|
+
expectedIssue: undefined,
|
|
71
|
+
noIssue: false,
|
|
72
|
+
};
|
|
73
|
+
const { tokens } = parseArgs({
|
|
74
|
+
args: [...argv],
|
|
75
|
+
options: {
|
|
76
|
+
help: { type: "boolean", short: "h" },
|
|
77
|
+
repo: { type: "string" },
|
|
78
|
+
pr: { type: "string" },
|
|
79
|
+
input: { type: "string" },
|
|
80
|
+
"expected-issue": { type: "string" },
|
|
81
|
+
"no-issue": { type: "boolean" },
|
|
82
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
83
|
+
},
|
|
84
|
+
allowPositionals: true,
|
|
85
|
+
strict: false,
|
|
86
|
+
tokens: true,
|
|
87
|
+
});
|
|
88
|
+
for (const token of tokens) {
|
|
89
|
+
if (token.kind === "positional") {
|
|
90
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
91
|
+
}
|
|
92
|
+
if (token.kind !== "option") {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (token.name === "help") {
|
|
96
|
+
options.help = true;
|
|
97
|
+
return options;
|
|
98
|
+
}
|
|
99
|
+
if (token.name === "repo") {
|
|
100
|
+
options.repo = requireTokenValue(token, parseError).trim();
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (token.name === "pr") {
|
|
104
|
+
const value = requireTokenValue(token, parseError);
|
|
105
|
+
if (!/^\d+$/.test(value) || Number(value) === 0) {
|
|
106
|
+
throw parseError("--pr must be a positive integer");
|
|
107
|
+
}
|
|
108
|
+
options.pr = Number(value);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (token.name === "input") {
|
|
112
|
+
options.input = requireTokenValue(token, parseError).trim();
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (token.name === "expected-issue") {
|
|
116
|
+
const value = requireTokenValue(token, parseError);
|
|
117
|
+
if (!/^\d+$/.test(value) || Number(value) === 0) {
|
|
118
|
+
throw parseError("--expected-issue must be a positive integer");
|
|
119
|
+
}
|
|
120
|
+
options.expectedIssue = Number(value);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (token.name === "no-issue") {
|
|
124
|
+
options.noIssue = true;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
128
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
129
|
+
}
|
|
130
|
+
if (options.help) {
|
|
131
|
+
return options;
|
|
132
|
+
}
|
|
133
|
+
if (options.noIssue && Number.isInteger(options.expectedIssue)) {
|
|
134
|
+
throw parseError("--no-issue is mutually exclusive with --expected-issue; provide exactly one issue-linkage mode");
|
|
135
|
+
}
|
|
136
|
+
const hasInput = typeof options.input === "string" && options.input.length > 0;
|
|
137
|
+
const hasAnyRemote = (typeof options.repo === "string" && options.repo.length > 0) || Number.isInteger(options.pr);
|
|
138
|
+
const hasRemotePair = typeof options.repo === "string" && options.repo.length > 0 && Number.isInteger(options.pr);
|
|
139
|
+
// --input and the remote mode are mutually exclusive input sources: a stray
|
|
140
|
+
// --repo/--pr alongside --input must fail closed, not be silently ignored.
|
|
141
|
+
if (hasInput && hasAnyRemote) {
|
|
142
|
+
throw parseError("--input is mutually exclusive with --repo/--pr; provide exactly one input mode");
|
|
143
|
+
}
|
|
144
|
+
if (hasInput === hasRemotePair) {
|
|
145
|
+
throw parseError("Provide exactly one of --input <path> or --repo <owner/name> --pr <number>");
|
|
146
|
+
}
|
|
147
|
+
return options;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function fetchPrBody({ repo, pr }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
151
|
+
const result = await runChild(
|
|
152
|
+
ghCommand,
|
|
153
|
+
["pr", "view", String(pr), "--repo", repo, "--json", "body"],
|
|
154
|
+
env,
|
|
155
|
+
);
|
|
156
|
+
if (result.code !== 0) {
|
|
157
|
+
const detail = result.stderr.trim() || `exit code ${result.code}`;
|
|
158
|
+
throw new Error(`gh command failed: ${detail}`);
|
|
159
|
+
}
|
|
160
|
+
const payload = parseJsonText(result.stdout, { label: "gh pr view" });
|
|
161
|
+
if (!payload || typeof payload !== "object") {
|
|
162
|
+
throw new Error("Invalid gh pr view payload: missing body");
|
|
163
|
+
}
|
|
164
|
+
return typeof payload.body === "string" ? payload.body : "";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function loadInputPayload(inputPath) {
|
|
168
|
+
const text = await readFile(inputPath, "utf8");
|
|
169
|
+
const payload = parseJsonText(text, { label: `input file ${inputPath}` });
|
|
170
|
+
if (!payload || typeof payload !== "object") {
|
|
171
|
+
throw new Error(`Input file ${inputPath} must be a JSON object`);
|
|
172
|
+
}
|
|
173
|
+
return payload;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function validatePrBodySpecFromOptions(options, { env = process.env, ghCommand = "gh" } = {}) {
|
|
177
|
+
if (typeof options.input === "string" && options.input.length > 0) {
|
|
178
|
+
const payload = await loadInputPayload(options.input);
|
|
179
|
+
const body = typeof payload.body === "string" ? payload.body : "";
|
|
180
|
+
const repo = typeof payload.repo === "string" ? payload.repo : options.repo ?? null;
|
|
181
|
+
const pr = Number.isInteger(payload.pr) ? payload.pr : options.pr ?? null;
|
|
182
|
+
return { ...validatePrBodySpec({ body, expectedIssue: options.expectedIssue ?? null, issueLess: options.noIssue }), repo, pr };
|
|
183
|
+
}
|
|
184
|
+
parseRepoSlug(options.repo);
|
|
185
|
+
const body = await fetchPrBody({ repo: options.repo, pr: options.pr }, { env, ghCommand });
|
|
186
|
+
return {
|
|
187
|
+
...validatePrBodySpec({ body, expectedIssue: options.expectedIssue ?? null, issueLess: options.noIssue }),
|
|
188
|
+
repo: options.repo,
|
|
189
|
+
pr: options.pr,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function runCli(
|
|
194
|
+
argv = process.argv.slice(2),
|
|
195
|
+
{ stdout = process.stdout, stderr = process.stderr, env = process.env, ghCommand = "gh" } = {},
|
|
196
|
+
) {
|
|
197
|
+
let options;
|
|
198
|
+
try {
|
|
199
|
+
options = parseValidatePrBodySpecCliArgs(argv);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
stderr.write(`${formatCliError(error, { usage: USAGE })}\n`);
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
if (options.help) {
|
|
205
|
+
stdout.write(`${USAGE}\n`);
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
const result = await validatePrBodySpecFromOptions(options, { env, ghCommand });
|
|
210
|
+
// Fail closed: a body missing any invariant maps result.ok=false to exit 1.
|
|
211
|
+
return emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
212
|
+
} catch (error) {
|
|
213
|
+
stderr.write(`${formatCliError(error)}\n`);
|
|
214
|
+
return 1;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
219
|
+
const code = await runCli();
|
|
220
|
+
if (code !== 0) {
|
|
221
|
+
process.exitCode = code;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -5,6 +5,7 @@ import { parseArgs } from "node:util";
|
|
|
5
5
|
import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
|
|
6
6
|
import { parseIssueNumber, requireTokenValue } from "../_cli-primitives.mjs";
|
|
7
7
|
import { 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
|
import { detectInitialCopilotPrState, LINKED_PR_STATE } from "./detect-initial-copilot-pr-state.mjs";
|
|
9
10
|
import { enforcePersistentInternalWaitTimeout } from "@dev-loops/core/loop/timeout-policy";
|
|
10
11
|
import {
|
|
@@ -45,9 +46,11 @@ Error output (stderr, JSON):
|
|
|
45
46
|
{ "ok": false, "error": "...", "usage": "..." }
|
|
46
47
|
gh/runtime failures:
|
|
47
48
|
{ "ok": false, "error": "..." }
|
|
49
|
+
${JQ_OUTPUT_USAGE}
|
|
48
50
|
Exit codes:
|
|
49
51
|
0 Success (ready_for_followup, timed_out, and prior_linked_pr_closed_unmerged are all ok:true)
|
|
50
|
-
1 Argument error or gh failure
|
|
52
|
+
1 Argument error or gh failure
|
|
53
|
+
2 Invalid --jq filter`.trim();
|
|
51
54
|
const parseError = buildParseError(USAGE);
|
|
52
55
|
function rejectRemovedFlag(token) {
|
|
53
56
|
throw parseError(
|
|
@@ -108,6 +111,7 @@ export function parseWatchInitialCopilotPrCliArgs(argv) {
|
|
|
108
111
|
help: { type: "boolean", short: "h" },
|
|
109
112
|
repo: { type: "string" },
|
|
110
113
|
issue: { type: "string" },
|
|
114
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
111
115
|
},
|
|
112
116
|
allowPositionals: true,
|
|
113
117
|
strict: false,
|
|
@@ -135,6 +139,7 @@ export function parseWatchInitialCopilotPrCliArgs(argv) {
|
|
|
135
139
|
options.issue = parseIssueNumber(requireTokenValue(token, parseError), parseError);
|
|
136
140
|
continue;
|
|
137
141
|
}
|
|
142
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
138
143
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
139
144
|
}
|
|
140
145
|
if (options.repo === undefined || options.issue === undefined) {
|
|
@@ -251,7 +256,7 @@ export async function watchInitialCopilotPr(
|
|
|
251
256
|
}
|
|
252
257
|
export async function runCli(
|
|
253
258
|
argv = process.argv.slice(2),
|
|
254
|
-
{ stdout = process.stdout, env = process.env, ghCommand = "gh" } = {},
|
|
259
|
+
{ stdout = process.stdout, stderr = process.stderr, env = process.env, ghCommand = "gh" } = {},
|
|
255
260
|
) {
|
|
256
261
|
const options = parseWatchInitialCopilotPrCliArgs(argv);
|
|
257
262
|
if (options.help) {
|
|
@@ -259,7 +264,7 @@ export async function runCli(
|
|
|
259
264
|
return;
|
|
260
265
|
}
|
|
261
266
|
const result = await watchInitialCopilotPr(options, { env, ghCommand });
|
|
262
|
-
|
|
267
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
263
268
|
}
|
|
264
269
|
if (isDirectCliRun(import.meta.url)) {
|
|
265
270
|
runCli().catch((error) => {
|