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
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
replyAndMaybeResolve,
|
|
13
13
|
validateResolutionMessage,
|
|
14
14
|
} from "./_review-thread-mutations.mjs";
|
|
15
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
15
16
|
const USAGE = `Usage: reply-resolve-review-threads.mjs --repo <owner/name> --pr <number> [--author <login>] [--message <text>] [--resolve]
|
|
16
17
|
Reply to all matching unresolved review threads on one PR and optionally resolve them.
|
|
17
18
|
Required:
|
|
@@ -30,9 +31,11 @@ Error output (stderr, JSON):
|
|
|
30
31
|
{ "ok": false, "error": "...", "usage": "..." }
|
|
31
32
|
Runtime/gh failures:
|
|
32
33
|
{ "ok": false, "error": "...", "partialProgress"?: { ... } }
|
|
34
|
+
${JQ_OUTPUT_USAGE}
|
|
33
35
|
Exit codes:
|
|
34
36
|
0 Success
|
|
35
|
-
1 Argument error or gh/runtime failure
|
|
37
|
+
1 Argument error or gh/runtime failure
|
|
38
|
+
2 Invalid --jq filter`.trim();
|
|
36
39
|
const parseError = buildParseError(USAGE);
|
|
37
40
|
export function parseReplyResolveThreadsCliArgs(argv) {
|
|
38
41
|
const { tokens } = parseArgs({
|
|
@@ -44,6 +47,7 @@ export function parseReplyResolveThreadsCliArgs(argv) {
|
|
|
44
47
|
author: { type: "string" },
|
|
45
48
|
message: { type: "string" },
|
|
46
49
|
resolve: { type: "boolean" },
|
|
50
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
47
51
|
},
|
|
48
52
|
allowPositionals: true,
|
|
49
53
|
strict: false,
|
|
@@ -88,6 +92,7 @@ export function parseReplyResolveThreadsCliArgs(argv) {
|
|
|
88
92
|
options.resolve = true;
|
|
89
93
|
continue;
|
|
90
94
|
}
|
|
95
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
91
96
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
92
97
|
}
|
|
93
98
|
if (options.repo === undefined || options.pr === undefined) {
|
|
@@ -111,6 +116,62 @@ async function readStdinText(stdin) {
|
|
|
111
116
|
}
|
|
112
117
|
return text;
|
|
113
118
|
}
|
|
119
|
+
// When --message is set, stdin is read only to detect a conflicting second
|
|
120
|
+
// message source. A detached/idle pipe never sends EOF, so an unbounded read
|
|
121
|
+
// hangs forever and the process never exits (issue #1012). Resolve as soon as
|
|
122
|
+
// any NON-WHITESPACE byte arrives (a conflicting body is detected the instant
|
|
123
|
+
// real content appears — no need to wait for EOF); keep buffering while only
|
|
124
|
+
// whitespace has arrived (a leading newline is not yet a conflict and more may
|
|
125
|
+
// follow); resolve on natural EOF; and time out on a silent/idle pipe. Either
|
|
126
|
+
// way the stdin handle is released so the event loop can drain and the tool
|
|
127
|
+
// always terminates.
|
|
128
|
+
const CONFLICT_STDIN_TIMEOUT_MS = 500;
|
|
129
|
+
function readStdinConflictProbe(stdin, timeoutMs) {
|
|
130
|
+
return new Promise((resolve) => {
|
|
131
|
+
let settled = false;
|
|
132
|
+
let timer;
|
|
133
|
+
const cleanup = () => {
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
stdin.off?.("data", onData);
|
|
136
|
+
stdin.off?.("end", onEnd);
|
|
137
|
+
stdin.off?.("error", onEnd);
|
|
138
|
+
// Release the handle: an abandoned reader on a still-open pipe would
|
|
139
|
+
// otherwise keep the event loop alive and re-introduce the hang.
|
|
140
|
+
stdin.pause?.();
|
|
141
|
+
if (typeof stdin.unref === "function") {
|
|
142
|
+
stdin.unref();
|
|
143
|
+
} else {
|
|
144
|
+
stdin.destroy?.();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const finish = (value) => {
|
|
148
|
+
if (settled) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
settled = true;
|
|
152
|
+
cleanup();
|
|
153
|
+
resolve(value);
|
|
154
|
+
};
|
|
155
|
+
// Resolve early only once non-whitespace content is seen (a real conflict);
|
|
156
|
+
// '' or whitespace-only on clean EOF is not a conflict; undefined only on
|
|
157
|
+
// timeout (idle pipe) so the caller proceeds with --message.
|
|
158
|
+
let text = "";
|
|
159
|
+
const onData = (chunk) => {
|
|
160
|
+
text += String(chunk);
|
|
161
|
+
if (text.trim().length > 0) {
|
|
162
|
+
finish(text);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const onEnd = () => finish(text);
|
|
166
|
+
timer = setTimeout(() => finish(undefined), timeoutMs);
|
|
167
|
+
timer.unref?.();
|
|
168
|
+
stdin.setEncoding?.("utf8");
|
|
169
|
+
stdin.on?.("data", onData);
|
|
170
|
+
stdin.on?.("end", onEnd);
|
|
171
|
+
stdin.on?.("error", onEnd);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
114
175
|
async function resolveMessageInput(options, { stdin = process.stdin } = {}) {
|
|
115
176
|
if (typeof options.message === "string") {
|
|
116
177
|
if (stdin.isTTY) {
|
|
@@ -119,8 +180,8 @@ async function resolveMessageInput(options, { stdin = process.stdin } = {}) {
|
|
|
119
180
|
}
|
|
120
181
|
return options.message;
|
|
121
182
|
}
|
|
122
|
-
const stdinText = await
|
|
123
|
-
if (stdinText.trim().length > 0) {
|
|
183
|
+
const stdinText = await readStdinConflictProbe(stdin, CONFLICT_STDIN_TIMEOUT_MS);
|
|
184
|
+
if (typeof stdinText === "string" && stdinText.trim().length > 0) {
|
|
124
185
|
throw parseError("Choose exactly one message source: --message <text> or stdin");
|
|
125
186
|
}
|
|
126
187
|
if (options.message.trim().length === 0) {
|
|
@@ -241,6 +302,7 @@ export async function runCli(
|
|
|
241
302
|
{
|
|
242
303
|
stdin = process.stdin,
|
|
243
304
|
stdout = process.stdout,
|
|
305
|
+
stderr = process.stderr,
|
|
244
306
|
env = process.env,
|
|
245
307
|
ghCommand = "gh",
|
|
246
308
|
} = {},
|
|
@@ -258,7 +320,7 @@ export async function runCli(
|
|
|
258
320
|
);
|
|
259
321
|
const { matchedTargets, skippedThreadCount } = planBatchReplyTargets(parsed, options.author);
|
|
260
322
|
if (matchedTargets.length === 0) {
|
|
261
|
-
|
|
323
|
+
process.exitCode = emitResult(createSuccessPayload({
|
|
262
324
|
repo: options.repo,
|
|
263
325
|
pr: options.pr,
|
|
264
326
|
author: options.author,
|
|
@@ -268,7 +330,7 @@ export async function runCli(
|
|
|
268
330
|
resolvedThreadCount: 0,
|
|
269
331
|
skippedThreadCount,
|
|
270
332
|
results: [],
|
|
271
|
-
})
|
|
333
|
+
}), { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
272
334
|
return;
|
|
273
335
|
}
|
|
274
336
|
const results = [];
|
|
@@ -328,7 +390,7 @@ export async function runCli(
|
|
|
328
390
|
}
|
|
329
391
|
const repliedThreadCount = results.length;
|
|
330
392
|
const resolvedThreadCount = results.filter((entry) => entry.resolved).length;
|
|
331
|
-
|
|
393
|
+
process.exitCode = emitResult(createSuccessPayload({
|
|
332
394
|
repo: options.repo,
|
|
333
395
|
pr: options.pr,
|
|
334
396
|
author: options.author,
|
|
@@ -338,7 +400,7 @@ export async function runCli(
|
|
|
338
400
|
resolvedThreadCount,
|
|
339
401
|
skippedThreadCount,
|
|
340
402
|
results,
|
|
341
|
-
})
|
|
403
|
+
}), { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
342
404
|
}
|
|
343
405
|
if (isDirectCliRun(import.meta.url)) {
|
|
344
406
|
runCli().catch((error) => {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
3
|
import {
|
|
4
4
|
buildParseError,
|
|
5
|
+
containsBareCopilotSummon,
|
|
5
6
|
formatCliError,
|
|
6
7
|
isCopilotLogin,
|
|
7
8
|
isDirectCliRun,
|
|
@@ -14,7 +15,8 @@ import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.m
|
|
|
14
15
|
import { fetchGithubReviewThreadsPayload } from "./capture-review-threads.mjs";
|
|
15
16
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
16
17
|
import { buildSnapshotFromPrFacts, interpretLoopState } from "@dev-loops/core/loop/copilot-loop-state";
|
|
17
|
-
import { loadDevLoopConfig, resolveRefinement } from "@dev-loops/core/config";
|
|
18
|
+
import { loadDevLoopConfig, resolveEffectiveCopilotRoundCap, resolveRefinement } from "@dev-loops/core/config";
|
|
19
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
18
20
|
const BLOCKED_BY_COPILOT_COMMENT_STATUS = "blocked_by_copilot_comment";
|
|
19
21
|
const SUPPRESSED_SAME_HEAD_CLEAN_STATUS = "suppressed_same_head_clean";
|
|
20
22
|
const ROUND_CAP_REACHED_STATUS = "round_cap_reached";
|
|
@@ -29,13 +31,19 @@ Optional:
|
|
|
29
31
|
--force-rerequest-review Bypass the round cap when new commits exist since
|
|
30
32
|
the last Copilot review. Refused when the PR head
|
|
31
33
|
has not changed since the last review.
|
|
34
|
+
--lightweight This PR is light-dispatched (#1210): enforce the
|
|
35
|
+
composed round cap min(localImplementation.lightMode.
|
|
36
|
+
maxCopilotRounds ?? 1, refinement.maxCopilotRounds)
|
|
37
|
+
instead of refinement.maxCopilotRounds alone.
|
|
32
38
|
Debug:
|
|
33
39
|
DEVLOOPS_DEBUG=1 Emit stderr traces when best-effort same-head clean
|
|
34
40
|
convergence detection falls back to unsuppressed behavior
|
|
35
41
|
Output (stdout, JSON):
|
|
36
42
|
{ "ok": true, "status": "requested"|"already-requested"|"unavailable"|"suppressed_same_head_clean"|"blocked_by_copilot_comment"|"round_cap_reached"|"no_changes_since_last_review"|"suppressed_draft",
|
|
37
43
|
"repo": "...", "pr": N, "reviewer": "Copilot", "detail"?: "...",
|
|
38
|
-
"sameHeadCleanConverged"?: true, "violationCommentIds"?: [N], "completedRounds"?: N, "maxRounds"?: N
|
|
44
|
+
"sameHeadCleanConverged"?: true, "violationCommentIds"?: [N], "completedRounds"?: N, "maxRounds"?: N,
|
|
45
|
+
"configWarning"?: "..." (present only when --lightweight and dev-loop config failed to load/validate;
|
|
46
|
+
the lightweight default cap of 1 was applied instead of the full-PR default) }
|
|
39
47
|
Request statuses:
|
|
40
48
|
requested Copilot review was successfully requested
|
|
41
49
|
already-requested Copilot review was already observably in progress; no new request needed
|
|
@@ -50,9 +58,22 @@ Error output (stderr, JSON):
|
|
|
50
58
|
{ "ok": false, "error": "...", "usage": "..." }
|
|
51
59
|
gh/runtime failures:
|
|
52
60
|
{ "ok": false, "error": "..." }
|
|
61
|
+
${JQ_OUTPUT_USAGE}
|
|
62
|
+
Status contract: "ok": true means the helper ran without error, NOT that a
|
|
63
|
+
review was placed. Callers MUST branch on "status", never on "ok" truthiness
|
|
64
|
+
alone — every non-"requested" status (including blocked_by_copilot_comment)
|
|
65
|
+
is a caller-must-branch outcome, not a silent success.
|
|
66
|
+
--silent exit code: 0 only when status is "requested" (a new request was just
|
|
67
|
+
placed this run); non-zero for every other status, including
|
|
68
|
+
already-requested/suppressed_same_head_clean/unavailable/blocked_by_copilot_comment/
|
|
69
|
+
round_cap_reached/no_changes_since_last_review/suppressed_draft. Without
|
|
70
|
+
--silent the JSON body always prints regardless of status. --jq combined with
|
|
71
|
+
--silent keeps the shared jq-stream truthiness semantics (exit reflects the
|
|
72
|
+
filtered value) and is exempt from the status-based rule above.
|
|
53
73
|
Exit codes:
|
|
54
|
-
0 Success (including unavailable)
|
|
55
|
-
1 Argument error
|
|
74
|
+
0 Success (including unavailable); with --silent, only when status is "requested"
|
|
75
|
+
1 Argument error, gh failure, or (--silent) any non-"requested" status
|
|
76
|
+
2 Invalid --jq filter`.trim();
|
|
56
77
|
const parseError = buildParseError(USAGE);
|
|
57
78
|
export function parseRequestCliArgs(argv) {
|
|
58
79
|
const { tokens } = parseArgs({
|
|
@@ -60,8 +81,10 @@ export function parseRequestCliArgs(argv) {
|
|
|
60
81
|
options: {
|
|
61
82
|
help: { type: "boolean", short: "h" },
|
|
62
83
|
"force-rerequest-review": { type: "boolean" },
|
|
84
|
+
lightweight: { type: "boolean" },
|
|
63
85
|
repo: { type: "string" },
|
|
64
86
|
pr: { type: "string" },
|
|
87
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
65
88
|
},
|
|
66
89
|
allowPositionals: true,
|
|
67
90
|
strict: false,
|
|
@@ -72,6 +95,7 @@ export function parseRequestCliArgs(argv) {
|
|
|
72
95
|
repo: undefined,
|
|
73
96
|
pr: undefined,
|
|
74
97
|
forceRerequestReview: false,
|
|
98
|
+
lightweight: false,
|
|
75
99
|
};
|
|
76
100
|
for (const token of tokens) {
|
|
77
101
|
if (token.kind === "positional") {
|
|
@@ -88,6 +112,10 @@ export function parseRequestCliArgs(argv) {
|
|
|
88
112
|
options.forceRerequestReview = true;
|
|
89
113
|
continue;
|
|
90
114
|
}
|
|
115
|
+
if (token.name === "lightweight") {
|
|
116
|
+
options.lightweight = true;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
91
119
|
if (token.name === "repo") {
|
|
92
120
|
options.repo = requireTokenValue(token, parseError).trim();
|
|
93
121
|
continue;
|
|
@@ -96,6 +124,7 @@ export function parseRequestCliArgs(argv) {
|
|
|
96
124
|
options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
|
|
97
125
|
continue;
|
|
98
126
|
}
|
|
127
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
99
128
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
100
129
|
}
|
|
101
130
|
if (options.repo === undefined || options.pr === undefined) {
|
|
@@ -437,7 +466,9 @@ export async function checkForCopilotComments({ repo, pr }, { env = process.env,
|
|
|
437
466
|
if (isCopilotLogin(author)) {
|
|
438
467
|
continue;
|
|
439
468
|
}
|
|
440
|
-
|
|
469
|
+
// Exempt bare-text occurrences inside inline code spans/fenced blocks: a
|
|
470
|
+
// gate-evidence comment legitimately quotes the anti-summon rule itself.
|
|
471
|
+
if (containsBareCopilotSummon(body)) {
|
|
441
472
|
violationCommentIds.push(comment.id);
|
|
442
473
|
}
|
|
443
474
|
}
|
|
@@ -474,16 +505,47 @@ export async function performCopilotReviewRequest(options, { env = process.env,
|
|
|
474
505
|
}
|
|
475
506
|
let refinementConfig = { maxCopilotRounds: 5 };
|
|
476
507
|
let maxRounds = 5; // Built-in default; overridden by config when loadable
|
|
508
|
+
// Lightweight fallback when config is unreadable/invalid: fail toward the
|
|
509
|
+
// SAFE (smaller) lightweight cap instead of silently inheriting the
|
|
510
|
+
// full-PR default of 5 above, which would let a light-dispatched PR run
|
|
511
|
+
// far more review rounds than intended whenever the config can't be read.
|
|
512
|
+
const LIGHTWEIGHT_DEFAULT_CAP = 1;
|
|
513
|
+
let configWarning = null;
|
|
477
514
|
try {
|
|
478
515
|
const { config, errors } = await loadDevLoopConfig();
|
|
479
516
|
if (!errors || errors.length === 0) {
|
|
480
517
|
refinementConfig = resolveRefinement(config);
|
|
481
|
-
|
|
482
|
-
|
|
518
|
+
// Light-dispatched PRs (#1210) enforce the COMPOSED cap —
|
|
519
|
+
// min(lightMode.maxCopilotRounds ?? 1, refinement.maxCopilotRounds) — so
|
|
520
|
+
// this enforcement backstop cannot permit rounds beyond the lightweight cap.
|
|
521
|
+
const effectiveCap = options.lightweight
|
|
522
|
+
? resolveEffectiveCopilotRoundCap(config, { lightweight: true })
|
|
523
|
+
: refinementConfig.maxCopilotRounds;
|
|
524
|
+
// >= 0 (not > 0): maxCopilotRounds: 0 is documented as "disable Copilot
|
|
525
|
+
// rounds"; it must be honored as an immediate refusal, not silently
|
|
526
|
+
// ignored in favor of the built-in default of 5.
|
|
527
|
+
if (Number.isFinite(effectiveCap) && effectiveCap >= 0) {
|
|
528
|
+
maxRounds = effectiveCap;
|
|
483
529
|
}
|
|
530
|
+
if (options.lightweight) {
|
|
531
|
+
refinementConfig = { ...refinementConfig, maxCopilotRounds: effectiveCap };
|
|
532
|
+
}
|
|
533
|
+
} else if (options.lightweight) {
|
|
534
|
+
maxRounds = LIGHTWEIGHT_DEFAULT_CAP;
|
|
535
|
+
refinementConfig = { ...refinementConfig, maxCopilotRounds: LIGHTWEIGHT_DEFAULT_CAP };
|
|
536
|
+
configWarning = `dev-loop config could not be validated; using the lightweight default cap of ${LIGHTWEIGHT_DEFAULT_CAP} instead of the full-PR default. errors=${JSON.stringify(errors)}`;
|
|
537
|
+
}
|
|
538
|
+
} catch (err) {
|
|
539
|
+
if (options.lightweight) {
|
|
540
|
+
maxRounds = LIGHTWEIGHT_DEFAULT_CAP;
|
|
541
|
+
refinementConfig = { ...refinementConfig, maxCopilotRounds: LIGHTWEIGHT_DEFAULT_CAP };
|
|
542
|
+
configWarning = `dev-loop config could not be loaded; using the lightweight default cap of ${LIGHTWEIGHT_DEFAULT_CAP} instead of the full-PR default. error=${err instanceof Error ? err.message : String(err)}`;
|
|
484
543
|
}
|
|
485
|
-
} catch {
|
|
486
544
|
}
|
|
545
|
+
// Every remaining return in this function is config-dependent (round-cap
|
|
546
|
+
// decisions, the request itself); surface a config-load fallback on all of
|
|
547
|
+
// them rather than just the path a given test happens to exercise.
|
|
548
|
+
const withConfigWarning = (result) => (configWarning ? { ...result, configWarning } : result);
|
|
487
549
|
// Reconcile the completed-round count with detect-pr-gate-coordination-state (#896):
|
|
488
550
|
// when the raw count has reached the cap, re-derive it with the draft-gate round
|
|
489
551
|
// reset applied. A clean draft_gate re-pass on an earlier head resets the count, so
|
|
@@ -506,7 +568,7 @@ export async function performCopilotReviewRequest(options, { env = process.env,
|
|
|
506
568
|
refinementConfig,
|
|
507
569
|
);
|
|
508
570
|
if (!roundCapAutoRerequest.eligible) {
|
|
509
|
-
return {
|
|
571
|
+
return withConfigWarning({
|
|
510
572
|
ok: true,
|
|
511
573
|
status: ROUND_CAP_REACHED_STATUS,
|
|
512
574
|
repo: options.repo,
|
|
@@ -515,7 +577,7 @@ export async function performCopilotReviewRequest(options, { env = process.env,
|
|
|
515
577
|
completedRounds,
|
|
516
578
|
maxRounds,
|
|
517
579
|
detail: `Round cap of ${maxRounds} reached with ${completedRounds} completed rounds. No further re-requests will be made.`,
|
|
518
|
-
};
|
|
580
|
+
});
|
|
519
581
|
}
|
|
520
582
|
}
|
|
521
583
|
// --force-rerequest-review: only bypass when there are new commits since the last review
|
|
@@ -526,7 +588,7 @@ export async function performCopilotReviewRequest(options, { env = process.env,
|
|
|
526
588
|
const canCompare = currentHeadSha !== null && lastReviewSha !== null;
|
|
527
589
|
const hasNewCommits = canCompare && currentHeadSha !== lastReviewSha;
|
|
528
590
|
if (!canCompare) {
|
|
529
|
-
return {
|
|
591
|
+
return withConfigWarning({
|
|
530
592
|
ok: true,
|
|
531
593
|
status: ROUND_CAP_REACHED_STATUS,
|
|
532
594
|
repo: options.repo,
|
|
@@ -535,10 +597,10 @@ export async function performCopilotReviewRequest(options, { env = process.env,
|
|
|
535
597
|
detail: `Round cap of ${maxRounds} reached with ${completedRounds} completed rounds. --force-rerequest-review was supplied but commit SHA data is unavailable, so change-since-last-review could not be evaluated.`,
|
|
536
598
|
completedRounds,
|
|
537
599
|
maxRounds,
|
|
538
|
-
};
|
|
600
|
+
});
|
|
539
601
|
}
|
|
540
602
|
if (!hasNewCommits) {
|
|
541
|
-
return {
|
|
603
|
+
return withConfigWarning({
|
|
542
604
|
ok: true,
|
|
543
605
|
status: NO_CHANGES_SINCE_LAST_REVIEW_STATUS,
|
|
544
606
|
repo: options.repo,
|
|
@@ -547,7 +609,7 @@ export async function performCopilotReviewRequest(options, { env = process.env,
|
|
|
547
609
|
detail: "No changes since last Copilot review. --force-rerequest-review requires new commits on the PR head.",
|
|
548
610
|
completedRounds,
|
|
549
611
|
maxRounds,
|
|
550
|
-
};
|
|
612
|
+
});
|
|
551
613
|
}
|
|
552
614
|
// Has new commits — bypass the round cap and proceed with the request
|
|
553
615
|
}
|
|
@@ -557,7 +619,7 @@ export async function performCopilotReviewRequest(options, { env = process.env,
|
|
|
557
619
|
before,
|
|
558
620
|
);
|
|
559
621
|
if (sameHeadCleanConverged) {
|
|
560
|
-
return {
|
|
622
|
+
return withConfigWarning({
|
|
561
623
|
ok: true,
|
|
562
624
|
status: SUPPRESSED_SAME_HEAD_CLEAN_STATUS,
|
|
563
625
|
repo: options.repo,
|
|
@@ -565,35 +627,35 @@ export async function performCopilotReviewRequest(options, { env = process.env,
|
|
|
565
627
|
reviewer: "Copilot",
|
|
566
628
|
sameHeadCleanConverged: true,
|
|
567
629
|
detail: "Current head already has a clean submitted Copilot review; same-head clean-convergence suppression is always enforced.",
|
|
568
|
-
};
|
|
630
|
+
});
|
|
569
631
|
}
|
|
570
632
|
if (before.requested || before.hasPendingReviewOnCurrentHead) {
|
|
571
|
-
return {
|
|
633
|
+
return withConfigWarning({
|
|
572
634
|
ok: true,
|
|
573
635
|
status: "already-requested",
|
|
574
636
|
repo: options.repo,
|
|
575
637
|
pr: options.pr,
|
|
576
638
|
reviewer: "Copilot",
|
|
577
|
-
};
|
|
639
|
+
});
|
|
578
640
|
}
|
|
579
641
|
const requestResult = await requestCopilotReview(options, { env, ghCommand });
|
|
580
642
|
if (requestResult.status === "unavailable") {
|
|
581
643
|
const after = await fetchCopilotReviewState(options, { env, ghCommand });
|
|
582
644
|
if (after.requested || after.hasPendingReviewOnCurrentHead || after.hasSubmittedReviewOnCurrentHead) {
|
|
583
|
-
return {
|
|
645
|
+
return withConfigWarning({
|
|
584
646
|
ok: true,
|
|
585
647
|
status: "already-requested",
|
|
586
648
|
repo: options.repo,
|
|
587
649
|
pr: options.pr,
|
|
588
650
|
reviewer: "Copilot",
|
|
589
|
-
};
|
|
651
|
+
});
|
|
590
652
|
}
|
|
591
|
-
return {
|
|
653
|
+
return withConfigWarning({
|
|
592
654
|
...requestResult,
|
|
593
|
-
};
|
|
655
|
+
});
|
|
594
656
|
}
|
|
595
657
|
if (requestResult.status === "already-requested") {
|
|
596
|
-
return requestResult;
|
|
658
|
+
return withConfigWarning(requestResult);
|
|
597
659
|
}
|
|
598
660
|
const after = await fetchCopilotReviewState(options, { env, ghCommand });
|
|
599
661
|
const reviewCountIncreased = after.copilotReviewIds.length > before.copilotReviewIds.length;
|
|
@@ -601,14 +663,15 @@ export async function performCopilotReviewRequest(options, { env = process.env,
|
|
|
601
663
|
if (!reviewNowObservablyInProgress) {
|
|
602
664
|
throw new Error("Copilot review request did not appear in requested reviewers or fresh/in-progress Copilot reviews after gh pr edit");
|
|
603
665
|
}
|
|
604
|
-
return {
|
|
666
|
+
return withConfigWarning({
|
|
605
667
|
...requestResult,
|
|
606
|
-
};
|
|
668
|
+
});
|
|
607
669
|
}
|
|
608
670
|
export async function runCli(
|
|
609
671
|
argv = process.argv.slice(2),
|
|
610
672
|
{
|
|
611
673
|
stdout = process.stdout,
|
|
674
|
+
stderr = process.stderr,
|
|
612
675
|
env = process.env,
|
|
613
676
|
ghCommand = "gh",
|
|
614
677
|
} = {},
|
|
@@ -619,7 +682,16 @@ export async function runCli(
|
|
|
619
682
|
return;
|
|
620
683
|
}
|
|
621
684
|
const result = await performCopilotReviewRequest(options, { env, ghCommand });
|
|
622
|
-
|
|
685
|
+
// Honest status under --silent: `ok: true` reports "the helper ran without
|
|
686
|
+
// error", not "a review was placed" — a caller checking only exit-code
|
|
687
|
+
// truthiness must NOT read a non-`requested` status (blocked_by_copilot_comment,
|
|
688
|
+
// round_cap_reached, etc.) as a placed request. --silent therefore answers
|
|
689
|
+
// "was a request just placed" specifically: exit 0 only for `requested`,
|
|
690
|
+
// non-zero for every other status. Non-silent output is unaffected — the full
|
|
691
|
+
// JSON body (with `ok: true`) still prints for every documented status; the
|
|
692
|
+
// caller MUST branch on `.status`, not `.ok`.
|
|
693
|
+
const silentOk = options.silent ? result.status === "requested" : undefined;
|
|
694
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr, ok: silentOk });
|
|
623
695
|
}
|
|
624
696
|
if (isDirectCliRun(import.meta.url)) {
|
|
625
697
|
runCli().catch((error) => {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
loadDevLoopConfig,
|
|
10
10
|
resolveHumanHandoffConfig,
|
|
11
11
|
} from "@dev-loops/core/config";
|
|
12
|
+
import { JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
12
13
|
|
|
13
14
|
const USAGE = `Usage: resolve-handoff-candidates.mjs --repo <owner/name> --pr <number> [--changed-files <a,b,c>] [--pr-author <login>]
|
|
14
15
|
Resolve an ordered, deduped list of human-handoff reviewer/assignee candidates
|
|
@@ -39,9 +40,11 @@ Optional:
|
|
|
39
40
|
Output (stdout, JSON):
|
|
40
41
|
{ "ok": true, "enabled": bool, "candidates": [{ "login", "source", "isTeam"?, "paths"? }],
|
|
41
42
|
"changedFiles": [...], "sources": [...], "warnings": [...] }
|
|
43
|
+
${JQ_OUTPUT_USAGE}
|
|
42
44
|
Exit codes:
|
|
43
45
|
0 Success (including disabled no-op and fail-soft per-source skips)
|
|
44
|
-
1 Argument error
|
|
46
|
+
1 Argument error
|
|
47
|
+
2 Invalid --jq filter`.trim();
|
|
45
48
|
|
|
46
49
|
const parseError = buildParseError(USAGE);
|
|
47
50
|
|
|
@@ -74,6 +77,8 @@ export function parseResolveCandidatesCliArgs(argv) {
|
|
|
74
77
|
continue;
|
|
75
78
|
}
|
|
76
79
|
if (token === "--pr-author") { options.prAuthor = nextValue(args, ++i, "--pr-author").trim().replace(/^@/, ""); continue; }
|
|
80
|
+
if (token === "--jq") { options.jq = nextValue(args, ++i, "--jq"); continue; }
|
|
81
|
+
if (token === "--silent" || token === "-s") { options.silent = true; continue; }
|
|
77
82
|
throw parseError(`Unknown argument: ${token}`);
|
|
78
83
|
}
|
|
79
84
|
if (options.repo === undefined || options.pr === undefined) {
|
|
@@ -403,8 +408,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
403
408
|
{ repo: options.repo, pr: options.pr, changedFiles: options.changedFiles, prAuthor: options.prAuthor ?? null },
|
|
404
409
|
{ ...deps, config, repoRoot },
|
|
405
410
|
);
|
|
406
|
-
|
|
407
|
-
return 0;
|
|
411
|
+
return emitResult(result, { jq: options.jq, silent: options.silent });
|
|
408
412
|
}
|
|
409
413
|
|
|
410
414
|
if (isDirectCliRun(import.meta.url)) {
|
|
@@ -3,6 +3,7 @@ import { parseArgs } from "node:util";
|
|
|
3
3
|
import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
4
4
|
import { parseIssueNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
5
5
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
6
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
6
7
|
const ISSUE_JSON_FIELDS = "number,title,body,url,state";
|
|
7
8
|
const USAGE = `Usage: resolve-tracker-local-spec.mjs (--repo <owner/name> --issue <number> | --issue-url <github-issue-url>)
|
|
8
9
|
Resolve the canonical tracker-backed local spec bundle from one GitHub issue reference.
|
|
@@ -30,7 +31,8 @@ Error output (stderr, JSON):
|
|
|
30
31
|
Argument/usage errors:
|
|
31
32
|
{ "ok": false, "error": "...", "usage": "..." }
|
|
32
33
|
gh/runtime failures:
|
|
33
|
-
{ "ok": false, "error": "..." }
|
|
34
|
+
{ "ok": false, "error": "..." }
|
|
35
|
+
${JQ_OUTPUT_USAGE}`.trim();
|
|
34
36
|
const parseError = buildParseError(USAGE);
|
|
35
37
|
export function parseGitHubIssueUrl(value) {
|
|
36
38
|
let parsedUrl;
|
|
@@ -68,6 +70,7 @@ export function parseResolveTrackerLocalSpecCliArgs(argv) {
|
|
|
68
70
|
repo: { type: "string" },
|
|
69
71
|
issue: { type: "string" },
|
|
70
72
|
"issue-url": { type: "string" },
|
|
73
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
71
74
|
},
|
|
72
75
|
allowPositionals: true,
|
|
73
76
|
strict: false,
|
|
@@ -102,6 +105,7 @@ export function parseResolveTrackerLocalSpecCliArgs(argv) {
|
|
|
102
105
|
options.issueUrl = requireTokenValue(token, parseError).trim();
|
|
103
106
|
continue;
|
|
104
107
|
}
|
|
108
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
105
109
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
106
110
|
}
|
|
107
111
|
const usingIssueUrl = typeof options.issueUrl === "string";
|
|
@@ -119,6 +123,8 @@ export function parseResolveTrackerLocalSpecCliArgs(argv) {
|
|
|
119
123
|
repo,
|
|
120
124
|
issue,
|
|
121
125
|
issueUrl: options.issueUrl,
|
|
126
|
+
...(options.jq !== undefined ? { jq: options.jq } : {}),
|
|
127
|
+
...(options.silent !== undefined ? { silent: options.silent } : {}),
|
|
122
128
|
};
|
|
123
129
|
}
|
|
124
130
|
try {
|
|
@@ -201,7 +207,7 @@ export async function resolveTrackerLocalSpec(
|
|
|
201
207
|
}
|
|
202
208
|
export async function runCli(
|
|
203
209
|
argv = process.argv.slice(2),
|
|
204
|
-
{ stdout = process.stdout, env = process.env, ghCommand = "gh" } = {},
|
|
210
|
+
{ stdout = process.stdout, stderr = process.stderr, env = process.env, ghCommand = "gh" } = {},
|
|
205
211
|
) {
|
|
206
212
|
const options = parseResolveTrackerLocalSpecCliArgs(argv);
|
|
207
213
|
if (options.help) {
|
|
@@ -212,7 +218,7 @@ export async function runCli(
|
|
|
212
218
|
{ repo: options.repo, issue: options.issue },
|
|
213
219
|
{ env, ghCommand },
|
|
214
220
|
);
|
|
215
|
-
|
|
221
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
216
222
|
}
|
|
217
223
|
if (isDirectCliRun(import.meta.url)) {
|
|
218
224
|
runCli().catch((error) => {
|
|
@@ -7,6 +7,7 @@ import { parseArgs } from "node:util";
|
|
|
7
7
|
import { parsePositiveInteger, requireTokenValue } from "../_cli-primitives.mjs";
|
|
8
8
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
9
9
|
import { buildDraftReviewPayload } from "@dev-loops/core/loop/reviewer-loop-state";
|
|
10
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
10
11
|
const HELP = `Usage: stage-reviewer-draft.mjs --repo <owner/name> --pr <number> --review-file <path> [--local-state-output <path>]
|
|
11
12
|
Stage a pending draft review on a GitHub pull request.
|
|
12
13
|
Options:
|
|
@@ -15,9 +16,13 @@ Options:
|
|
|
15
16
|
--review-file <path> Path to JSON file containing review payload (required)
|
|
16
17
|
--local-state-output <path> Path to write local state snapshot (optional)
|
|
17
18
|
--help, -h Show this help
|
|
19
|
+
|
|
20
|
+
${JQ_OUTPUT_USAGE}
|
|
21
|
+
|
|
18
22
|
Exit codes:
|
|
19
23
|
0 Success
|
|
20
24
|
1 Error
|
|
25
|
+
2 Invalid --jq filter
|
|
21
26
|
`;
|
|
22
27
|
export function parseStageDraftCliArgs(argv) {
|
|
23
28
|
const { tokens } = parseArgs({
|
|
@@ -28,6 +33,7 @@ export function parseStageDraftCliArgs(argv) {
|
|
|
28
33
|
pr: { type: "string" },
|
|
29
34
|
"review-file": { type: "string" },
|
|
30
35
|
"local-state-output": { type: "string" },
|
|
36
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
31
37
|
},
|
|
32
38
|
allowPositionals: true,
|
|
33
39
|
strict: false,
|
|
@@ -67,6 +73,7 @@ export function parseStageDraftCliArgs(argv) {
|
|
|
67
73
|
options.localStateOutput = requireTokenValue(token);
|
|
68
74
|
continue;
|
|
69
75
|
}
|
|
76
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t))) continue;
|
|
70
77
|
throw new Error(`Unknown argument: ${token.rawName}`);
|
|
71
78
|
}
|
|
72
79
|
if (!options.repo || !options.pr || !options.reviewFile) {
|
|
@@ -169,6 +176,7 @@ export async function runCli(
|
|
|
169
176
|
argv = process.argv.slice(2),
|
|
170
177
|
{
|
|
171
178
|
stdout = process.stdout,
|
|
179
|
+
stderr = process.stderr,
|
|
172
180
|
env = process.env,
|
|
173
181
|
ghCommand = "gh",
|
|
174
182
|
} = {},
|
|
@@ -190,7 +198,7 @@ export async function runCli(
|
|
|
190
198
|
await postDraftReview({ repo: options.repo, pr: options.pr, reviewPayload }, { env, ghCommand }),
|
|
191
199
|
);
|
|
192
200
|
const localStatePath = await writeLocalState(options.localStateOutput, draftReview);
|
|
193
|
-
|
|
201
|
+
process.exitCode = emitResult({
|
|
194
202
|
ok: true,
|
|
195
203
|
repo: options.repo,
|
|
196
204
|
pr: options.pr,
|
|
@@ -199,7 +207,7 @@ export async function runCli(
|
|
|
199
207
|
reviewState: draftReview.state,
|
|
200
208
|
commitSha: draftReview.commitSha,
|
|
201
209
|
localStatePath,
|
|
202
|
-
}
|
|
210
|
+
}, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
203
211
|
}
|
|
204
212
|
if (isDirectCliRun(import.meta.url)) {
|
|
205
213
|
runCli().catch((error) => {
|