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
|
@@ -3,6 +3,7 @@ import { parseArgs } from "node:util";
|
|
|
3
3
|
import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
4
4
|
import { parsePositiveInteger, 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 USAGE = `Usage: manage-sub-issues.mjs <command> --repo <owner/name> --issue <number> [options]
|
|
7
8
|
Deterministic helper for reading, linking, ordering, and verifying GitHub sub-issue trees.
|
|
8
9
|
Commands:
|
|
@@ -37,7 +38,8 @@ Error output (stderr, JSON):
|
|
|
37
38
|
Argument/usage errors:
|
|
38
39
|
{ "ok": false, "error": "...", "usage": "..." }
|
|
39
40
|
gh/runtime failures:
|
|
40
|
-
{ "ok": false, "error": "..." }
|
|
41
|
+
{ "ok": false, "error": "..." }
|
|
42
|
+
${JQ_OUTPUT_USAGE}`.trim();
|
|
41
43
|
const parseError = buildParseError(USAGE);
|
|
42
44
|
function parseIssueList(value) {
|
|
43
45
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
@@ -87,6 +89,7 @@ export function parseManageSubIssuesCliArgs(argv) {
|
|
|
87
89
|
order: { type: "string" },
|
|
88
90
|
expected: { type: "string" },
|
|
89
91
|
ordered: { type: "boolean" },
|
|
92
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
90
93
|
},
|
|
91
94
|
allowPositionals: true,
|
|
92
95
|
strict: false,
|
|
@@ -127,6 +130,7 @@ export function parseManageSubIssuesCliArgs(argv) {
|
|
|
127
130
|
options.ordered = true;
|
|
128
131
|
continue;
|
|
129
132
|
}
|
|
133
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
130
134
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
131
135
|
}
|
|
132
136
|
if (options.repo === undefined || options.issue === undefined) {
|
|
@@ -224,15 +228,33 @@ function normalizeSubIssue(raw) {
|
|
|
224
228
|
return { id, number, title, state };
|
|
225
229
|
}
|
|
226
230
|
|
|
231
|
+
function extractEndpoint(args) {
|
|
232
|
+
return args.find((a) => typeof a === "string" && a.startsWith("repos/")) ?? "unknown endpoint";
|
|
233
|
+
}
|
|
234
|
+
function extractHttpStatus(output) {
|
|
235
|
+
const match = /^HTTP\/\S+\s+(\d{3})/m.exec(output ?? "");
|
|
236
|
+
return match ? match[1] : null;
|
|
237
|
+
}
|
|
227
238
|
/**
|
|
228
239
|
* Call gh api with optional JSON parsing.
|
|
229
240
|
* @param {boolean} [expectJson=true] - When false, skip JSON parsing (for empty-body responses like PATCH reorder).
|
|
230
241
|
*/
|
|
231
242
|
async function ghApi(ghCommand, args, env, expectJson = true) {
|
|
232
|
-
|
|
243
|
+
// Non-JSON calls (POST/PATCH with empty bodies) pass -i so a non-2xx response
|
|
244
|
+
// still surfaces its HTTP status even when the body is empty. JSON calls are
|
|
245
|
+
// left untouched since -i would break body parsing.
|
|
246
|
+
const captureStatus = !expectJson;
|
|
247
|
+
const apiArgs = captureStatus ? ["-i", ...args] : args;
|
|
248
|
+
const result = await runChild(ghCommand, ["api", ...apiArgs], env);
|
|
233
249
|
if (result.code !== 0) {
|
|
234
|
-
const
|
|
235
|
-
|
|
250
|
+
const endpoint = extractEndpoint(args);
|
|
251
|
+
const status = captureStatus ? extractHttpStatus(result.stdout) : null;
|
|
252
|
+
// "empty response body" only makes sense for the -i (captureStatus) path;
|
|
253
|
+
// for JSON/GET calls an empty stderr means fall back to the exit code.
|
|
254
|
+
const detail = result.stderr.trim()
|
|
255
|
+
|| (captureStatus ? "empty response body" : `exit code ${result.code}`);
|
|
256
|
+
const statusPart = status ? ` (HTTP ${status})` : "";
|
|
257
|
+
throw new Error(`gh api command failed${statusPart} for ${endpoint}: ${detail}`);
|
|
236
258
|
}
|
|
237
259
|
if (!expectJson) {
|
|
238
260
|
return null;
|
|
@@ -297,12 +319,26 @@ export async function runReorder({ repo, issue, order }, { env = process.env, gh
|
|
|
297
319
|
}
|
|
298
320
|
}
|
|
299
321
|
const reorderPath = buildSubIssueReorderPath(owner, name, issue);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
322
|
+
// GitHub's priority endpoint rejects after_id=0 (the "no predecessor" cursor)
|
|
323
|
+
// with an HTTP 500. To place the first requested item at the head, move it
|
|
324
|
+
// before the current head instead — or skip the call entirely if it's
|
|
325
|
+
// already there.
|
|
326
|
+
const currentHeadId = subIssues[0]?.id;
|
|
327
|
+
let afterId;
|
|
328
|
+
for (let index = 0; index < order.length; index += 1) {
|
|
329
|
+
const subIssueId = idByNumber.get(order[index]);
|
|
330
|
+
if (index === 0) {
|
|
331
|
+
afterId = subIssueId;
|
|
332
|
+
if (subIssueId === currentHeadId) {
|
|
333
|
+
continue; // already at the head; no call needed
|
|
334
|
+
}
|
|
335
|
+
const fieldArgs = ["-F", `sub_issue_id=${subIssueId}`, "-F", `before_id=${currentHeadId}`];
|
|
336
|
+
await ghApi(ghCommand, ["-X", "PATCH", reorderPath, ...fieldArgs], env, false);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
303
339
|
const fieldArgs = ["-F", `sub_issue_id=${subIssueId}`, "-F", `after_id=${afterId}`];
|
|
304
|
-
await ghApi(ghCommand, ["-X", "PATCH", reorderPath, ...fieldArgs], env, false);
|
|
305
340
|
afterId = subIssueId;
|
|
341
|
+
await ghApi(ghCommand, ["-X", "PATCH", reorderPath, ...fieldArgs], env, false);
|
|
306
342
|
}
|
|
307
343
|
return {
|
|
308
344
|
ok: true,
|
|
@@ -387,7 +423,7 @@ export async function runVerify(
|
|
|
387
423
|
}
|
|
388
424
|
export async function runCli(
|
|
389
425
|
argv = process.argv.slice(2),
|
|
390
|
-
{ stdout = process.stdout, env = process.env, ghCommand = "gh" } = {},
|
|
426
|
+
{ stdout = process.stdout, stderr = process.stderr, env = process.env, ghCommand = "gh" } = {},
|
|
391
427
|
) {
|
|
392
428
|
const options = parseManageSubIssuesCliArgs(argv);
|
|
393
429
|
if (options.help) {
|
|
@@ -405,7 +441,7 @@ export async function runCli(
|
|
|
405
441
|
} else if (command === "verify") {
|
|
406
442
|
result = await runVerify({ repo, issue, expected, ordered }, { env, ghCommand });
|
|
407
443
|
}
|
|
408
|
-
|
|
444
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
409
445
|
}
|
|
410
446
|
if (isDirectCliRun(import.meta.url)) {
|
|
411
447
|
runCli().catch((error) => {
|
|
@@ -4,6 +4,7 @@ import { parsePrNumber, runChild } from "../_cli-primitives.mjs";
|
|
|
4
4
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
5
5
|
import { loadDevLoopConfig } from "@dev-loops/core/config";
|
|
6
6
|
import { resolveHandoffCandidates } from "./resolve-handoff-candidates.mjs";
|
|
7
|
+
import { JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
7
8
|
|
|
8
9
|
const USAGE = `Usage: offer-human-handoff.mjs --repo <owner/name> --pr <number> [--assign <login>...] [--request-review <login>...] [--changed-files <csv>] [--pr-author <login>]
|
|
9
10
|
Human-handoff offer at the pre-approval / merge-handoff boundary (#920, Request B
|
|
@@ -28,9 +29,11 @@ Optional:
|
|
|
28
29
|
Output (stdout, JSON):
|
|
29
30
|
Offer mode: { "ok": true, "mode": "offer", "enabled", "candidates": [...], ... }
|
|
30
31
|
Apply mode: { "ok": true, "mode": "apply", "assigned": [...], "requestedReview": [...] }
|
|
32
|
+
${JQ_OUTPUT_USAGE}
|
|
31
33
|
Exit codes:
|
|
32
34
|
0 Success (including disabled no-op offer)
|
|
33
|
-
1 Argument error or gh failure
|
|
35
|
+
1 Argument error or gh failure
|
|
36
|
+
2 Invalid --jq filter`.trim();
|
|
34
37
|
|
|
35
38
|
const parseError = buildParseError(USAGE);
|
|
36
39
|
|
|
@@ -70,6 +73,8 @@ export function parseOfferCliArgs(argv) {
|
|
|
70
73
|
continue;
|
|
71
74
|
}
|
|
72
75
|
if (token === "--pr-author") { options.prAuthor = nextValue(args, ++i, "--pr-author").trim().replace(/^@/, ""); continue; }
|
|
76
|
+
if (token === "--jq") { options.jq = nextValue(args, ++i, "--jq"); continue; }
|
|
77
|
+
if (token === "--silent" || token === "-s") { options.silent = true; continue; }
|
|
73
78
|
throw parseError(`Unknown argument: ${token}`);
|
|
74
79
|
}
|
|
75
80
|
if (options.repo === undefined || options.pr === undefined) {
|
|
@@ -123,8 +128,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
123
128
|
{ repo: options.repo, pr: options.pr, assign: options.assign, requestReview: options.requestReview },
|
|
124
129
|
{ run, ghCommand },
|
|
125
130
|
);
|
|
126
|
-
|
|
127
|
-
return 0;
|
|
131
|
+
return emitResult(applied, { jq: options.jq, silent: options.silent });
|
|
128
132
|
} catch (error) {
|
|
129
133
|
process.stderr.write(`${formatCliError(error)}\n`);
|
|
130
134
|
return 1;
|
|
@@ -138,8 +142,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
138
142
|
{ repo: options.repo, pr: options.pr, changedFiles: options.changedFiles, prAuthor: options.prAuthor ?? null },
|
|
139
143
|
{ ...deps, config, repoRoot, run, ghCommand },
|
|
140
144
|
);
|
|
141
|
-
|
|
142
|
-
return 0;
|
|
145
|
+
return emitResult({ ...offer, mode: "offer" }, { jq: options.jq, silent: options.silent });
|
|
143
146
|
}
|
|
144
147
|
|
|
145
148
|
if (isDirectCliRun(import.meta.url)) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parsePrNumber, requireOptionValue, runChild } from "../_cli-primitives.mjs";
|
|
3
|
-
import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
3
|
+
import { formatCliError, isDirectCliRun, parseJsonText, sanitizeCopilotSummonTokens } from "../_core-helpers.mjs";
|
|
4
4
|
import { loadDevLoopConfig, resolveGatePostFindingsComments } from "@dev-loops/core/config";
|
|
5
5
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
6
|
+
import { JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
6
7
|
|
|
7
8
|
const USAGE = `Usage: post-gate-findings.mjs --repo <owner/name> --pr <number> --gate <draft_gate|pre_approval_gate> --head-sha <sha> --findings <json>
|
|
8
9
|
Post (or idempotently update) a visible, marker-tagged PR issue comment that lists the
|
|
@@ -23,9 +24,12 @@ Required:
|
|
|
23
24
|
([{severity, angle, summary, disposition?, files?}])
|
|
24
25
|
Output (stdout, JSON):
|
|
25
26
|
{ "ok": true, "action": "created"|"updated"|"noop"|"skipped", ... }
|
|
27
|
+
|
|
28
|
+
${JQ_OUTPUT_USAGE}
|
|
26
29
|
Exit codes:
|
|
27
30
|
0 Success
|
|
28
|
-
1 Argument error or gh failure
|
|
31
|
+
1 Argument error or gh failure
|
|
32
|
+
2 Invalid --jq filter`.trim();
|
|
29
33
|
|
|
30
34
|
const VALID_SEVERITIES = new Set(["must-fix", "worth-fixing-now", "defer"]);
|
|
31
35
|
// Severity ordering for grouped rendering (most-blocking first).
|
|
@@ -142,6 +146,14 @@ export function parsePostGateFindingsCliArgs(argv) {
|
|
|
142
146
|
options.findings = requireOptionValue(args, "--findings", parseError);
|
|
143
147
|
continue;
|
|
144
148
|
}
|
|
149
|
+
if (token === "--jq") {
|
|
150
|
+
options.jq = requireOptionValue(args, "--jq", parseError);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (token === "--silent" || token === "-s") {
|
|
154
|
+
options.silent = true;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
145
157
|
throw parseError(`Unknown argument: ${token}`);
|
|
146
158
|
}
|
|
147
159
|
const missing = ["repo", "pr", "gate", "headSha", "findings"]
|
|
@@ -203,7 +215,7 @@ export function renderFindingsCommentBody({ gate, headSha, findings }) {
|
|
|
203
215
|
];
|
|
204
216
|
if (findings.length === 0) {
|
|
205
217
|
lines.push("No findings. All review angles passed for this head.");
|
|
206
|
-
return lines.join("\n");
|
|
218
|
+
return sanitizeCopilotSummonTokens(lines.join("\n"));
|
|
207
219
|
}
|
|
208
220
|
const grouped = new Map();
|
|
209
221
|
for (const sev of SEVERITY_ORDER) {
|
|
@@ -249,7 +261,10 @@ export function renderFindingsCommentBody({ gate, headSha, findings }) {
|
|
|
249
261
|
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
250
262
|
lines.pop();
|
|
251
263
|
}
|
|
252
|
-
|
|
264
|
+
// Neutralize any bare @copilot/`/copilot`* tokens a finding summary quotes
|
|
265
|
+
// (e.g. an excerpt of the anti-summon rule itself) so this comment can never
|
|
266
|
+
// arm request-copilot-review.mjs's anti-summon guard.
|
|
267
|
+
return sanitizeCopilotSummonTokens(lines.join("\n"));
|
|
253
268
|
}
|
|
254
269
|
|
|
255
270
|
async function runGhJson(args, { env, ghCommand }) {
|
|
@@ -380,7 +395,7 @@ async function main() {
|
|
|
380
395
|
}
|
|
381
396
|
try {
|
|
382
397
|
const result = await postGateFindings(options);
|
|
383
|
-
process.
|
|
398
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent });
|
|
384
399
|
} catch (error) {
|
|
385
400
|
process.stderr.write(`${JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) })}\n`);
|
|
386
401
|
process.exitCode = 1;
|
|
@@ -4,6 +4,7 @@ import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helper
|
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
6
6
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
7
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
7
8
|
import {
|
|
8
9
|
summarizeHeadScopedCheckRunsSignal,
|
|
9
10
|
normalizeHeadScopedCommitStatus,
|
|
@@ -53,9 +54,11 @@ No-checks rule (grace, race-safe):
|
|
|
53
54
|
Diagnostic output (stderr):
|
|
54
55
|
{ "ok": true, "type": "watch_heartbeat", "elapsedMs": N, "totalBudgetMs": N, "poll": N, "maxPolls": N }
|
|
55
56
|
{ "ok": false, "error": "...", "usage"?: "..." }
|
|
57
|
+
${JQ_OUTPUT_USAGE}
|
|
56
58
|
Exit codes:
|
|
57
59
|
0 Success
|
|
58
|
-
1 Argument error or gh failure
|
|
60
|
+
1 Argument error or gh failure
|
|
61
|
+
2 Invalid --jq filter`.trim();
|
|
59
62
|
const parseError = buildParseError(USAGE);
|
|
60
63
|
|
|
61
64
|
export function parseCiWatchCliArgs(argv) {
|
|
@@ -67,6 +70,7 @@ export function parseCiWatchCliArgs(argv) {
|
|
|
67
70
|
pr: { type: "string" },
|
|
68
71
|
"timeout-ms": { type: "string" },
|
|
69
72
|
"poll-interval-ms": { type: "string" },
|
|
73
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
70
74
|
},
|
|
71
75
|
allowPositionals: true,
|
|
72
76
|
strict: false,
|
|
@@ -106,6 +110,7 @@ export function parseCiWatchCliArgs(argv) {
|
|
|
106
110
|
options.pollIntervalMs = parsePositiveMs(requireTokenValue(token, parseError), "--poll-interval-ms");
|
|
107
111
|
continue;
|
|
108
112
|
}
|
|
113
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
109
114
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
110
115
|
}
|
|
111
116
|
if (options.repo === undefined || options.pr === undefined) {
|
|
@@ -447,6 +452,7 @@ export async function runCli(
|
|
|
447
452
|
argv = process.argv.slice(2),
|
|
448
453
|
{
|
|
449
454
|
stdout = process.stdout,
|
|
455
|
+
stderr = process.stderr,
|
|
450
456
|
env = process.env,
|
|
451
457
|
ghCommand = "gh",
|
|
452
458
|
} = {},
|
|
@@ -457,7 +463,7 @@ export async function runCli(
|
|
|
457
463
|
return;
|
|
458
464
|
}
|
|
459
465
|
const result = await watchCiStatus(options, { env, ghCommand });
|
|
460
|
-
|
|
466
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
461
467
|
}
|
|
462
468
|
|
|
463
469
|
if (isDirectCliRun(import.meta.url)) {
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { setTimeout as delay } from "node:timers/promises";
|
|
3
3
|
import { buildParseError, formatCliError, isCopilotLogin, isDirectCliRun, parseJsonText, parseReviewThreads } from "../_core-helpers.mjs";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
|
-
import { parsePositiveInteger, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
6
|
-
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
5
|
+
import { parsePositiveInteger, parseNonNegativeInteger, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
6
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
7
7
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_POLL_INTERVAL_MS,
|
|
@@ -15,13 +15,18 @@ import {
|
|
|
15
15
|
const WATCH_HEARTBEAT_MS = 45_000; // 45 seconds
|
|
16
16
|
const REMOVED_FLAGS = new Set([
|
|
17
17
|
"--poll-interval-ms",
|
|
18
|
-
"--timeout-ms",
|
|
19
18
|
]);
|
|
20
|
-
const USAGE = `Usage: probe-copilot-review.mjs --repo <owner/name> --pr <number>
|
|
19
|
+
const USAGE = `Usage: probe-copilot-review.mjs --repo <owner/name> --pr <number> [--timeout-ms <n>]
|
|
21
20
|
Poll for fresh Copilot review activity on a GitHub pull request.
|
|
22
21
|
Required:
|
|
23
22
|
--repo <owner/name> Repository slug (e.g. owner/repo)
|
|
24
23
|
--pr <number> Pull request number
|
|
24
|
+
Options:
|
|
25
|
+
--timeout-ms <n> Total watch budget in ms (default ${COPILOT_REVIEW_WAIT_TIMEOUT_MS}, i.e. ${COPILOT_REVIEW_WAIT_TIMEOUT_MS / 60_000} min;
|
|
26
|
+
0 = single immediate check, no wait — returns "idle" if
|
|
27
|
+
no fresh activity). For a quick non-watch thread/state
|
|
28
|
+
read without entering the watch loop, pass 0 here or use
|
|
29
|
+
'dev-loops gate capture-threads'.
|
|
25
30
|
Output (stdout, JSON):
|
|
26
31
|
{ "ok": true, "status": "changed"|"timeout"|"idle", "repo": "...", "pr": N, "attempts": N,
|
|
27
32
|
"newComments": [...], "newReviews": [...], "newIssueComments": [...] }
|
|
@@ -32,10 +37,14 @@ ${JQ_OUTPUT_USAGE}
|
|
|
32
37
|
Activity statuses:
|
|
33
38
|
changed Fresh Copilot review activity found (check newComments/newReviews/newIssueComments)
|
|
34
39
|
timeout Watch period elapsed with no fresh Copilot activity
|
|
35
|
-
idle Zero-timeout single check found no change
|
|
40
|
+
idle Zero-timeout (--timeout-ms 0) single check found no change
|
|
36
41
|
Diagnostic output (stderr):
|
|
37
|
-
Progress/heartbeat (during watch):
|
|
42
|
+
Progress/heartbeat (during watch, --timeout-ms > 0):
|
|
38
43
|
{ "ok": true, "type": "watch_heartbeat", "elapsedMs": N, "totalBudgetMs": N, "poll": N, "maxPolls": N }
|
|
44
|
+
Heartbeat contract: watch-shaped runs (--timeout-ms > 0) emit watch_heartbeat lines to
|
|
45
|
+
stderr roughly every 45s as a liveness signal. Agents MUST NOT suppress stderr
|
|
46
|
+
(e.g. 2>/dev/null) on watch-shaped invocations — suppressing heartbeats makes a
|
|
47
|
+
legitimate watch look like a stall. Use --timeout-ms 0 for a non-watch single check.
|
|
39
48
|
Argument/usage errors:
|
|
40
49
|
{ "ok": false, "error": "...", "usage": "..." }
|
|
41
50
|
gh/runtime failures:
|
|
@@ -100,6 +109,7 @@ export function parseWatchCliArgs(argv) {
|
|
|
100
109
|
help: { type: "boolean", short: "h" },
|
|
101
110
|
repo: { type: "string" },
|
|
102
111
|
pr: { type: "string" },
|
|
112
|
+
"timeout-ms": { type: "string" },
|
|
103
113
|
concise: { type: "boolean" },
|
|
104
114
|
summary: { type: "boolean" },
|
|
105
115
|
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
@@ -140,18 +150,15 @@ export function parseWatchCliArgs(argv) {
|
|
|
140
150
|
options.pr = parsePositiveInteger(requireTokenValue(token, parseError), "--pr", parseError);
|
|
141
151
|
continue;
|
|
142
152
|
}
|
|
143
|
-
if (token.name === "
|
|
144
|
-
options.
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
if (token.name === "jq") {
|
|
148
|
-
options.jq = requireTokenValue(token, parseError);
|
|
153
|
+
if (token.name === "timeout-ms") {
|
|
154
|
+
options.timeoutMs = parseNonNegativeInteger(requireTokenValue(token, parseError), "--timeout-ms", parseError);
|
|
149
155
|
continue;
|
|
150
156
|
}
|
|
151
|
-
if (token.name === "
|
|
152
|
-
options.
|
|
157
|
+
if (token.name === "concise" || token.name === "summary") {
|
|
158
|
+
options.concise = true;
|
|
153
159
|
continue;
|
|
154
160
|
}
|
|
161
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
155
162
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
156
163
|
}
|
|
157
164
|
if (options.repo === undefined || options.pr === undefined) {
|
|
@@ -5,15 +5,17 @@ import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.m
|
|
|
5
5
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
6
6
|
import { loadDevLoopConfig, resolveGateConfig } from "@dev-loops/core/config";
|
|
7
7
|
import { findBlockingTitleMarkers } from "@dev-loops/core/loop/pr-title-markers";
|
|
8
|
+
import { syncBoardStatus as realSyncBoardStatus, loadStateColumnMap, LOGICAL_COLUMN } from "@dev-loops/core/loop/queue-board-sync";
|
|
9
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
8
10
|
|
|
9
|
-
const USAGE = `Usage: ready-for-review.mjs --repo <owner/name> --pr <number>\nWrapper around gh pr ready that enforces gate-evidence validation
|
|
11
|
+
const USAGE = `Usage: ready-for-review.mjs --repo <owner/name> --pr <number>\nWrapper around gh pr ready that enforces gate-evidence validation.\n\n${JQ_OUTPUT_USAGE}`;
|
|
10
12
|
const parseError = buildParseError(USAGE);
|
|
11
|
-
const PR_VIEW_QUERY = `query($owner:String!, $name:String!, $number:Int!) { repository(owner:$owner, name:$name) { pullRequest(number:$number) { id, isDraft, headRefOid, state, mergeStateStatus, title } } }`;
|
|
13
|
+
const PR_VIEW_QUERY = `query($owner:String!, $name:String!, $number:Int!) { repository(owner:$owner, name:$name) { pullRequest(number:$number) { id, isDraft, headRefOid, state, mergeStateStatus, title, closingIssuesReferences(first:10){ nodes{ number } } } } }`;
|
|
12
14
|
|
|
13
15
|
export function parseReadyForReviewCliArgs(argv) {
|
|
14
16
|
const { tokens } = parseArgs({
|
|
15
17
|
args: [...argv],
|
|
16
|
-
options: { help: { type: "boolean", short: "h" }, repo: { type: "string" }, pr: { type: "string" } },
|
|
18
|
+
options: { help: { type: "boolean", short: "h" }, repo: { type: "string" }, pr: { type: "string" }, ...JQ_OUTPUT_PARSE_OPTIONS },
|
|
17
19
|
allowPositionals: true,
|
|
18
20
|
strict: false,
|
|
19
21
|
tokens: true,
|
|
@@ -25,6 +27,7 @@ export function parseReadyForReviewCliArgs(argv) {
|
|
|
25
27
|
if (token.name === "help") { opts.help = true; return opts; }
|
|
26
28
|
if (token.name === "repo") { opts.repo = requireTokenValue(token, parseError).trim(); continue; }
|
|
27
29
|
if (token.name === "pr") { opts.pr = parsePrNumber(requireTokenValue(token, parseError), parseError); continue; }
|
|
30
|
+
if (matchJqOutputToken(token, opts, (t) => requireTokenValue(t, parseError))) continue;
|
|
28
31
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
29
32
|
}
|
|
30
33
|
if (!opts.repo || opts.pr === undefined) throw parseError("ready-for-review requires --repo and --pr");
|
|
@@ -43,7 +46,10 @@ async function fetchPrState({ repo, pr }, { env, ghCommand }) {
|
|
|
43
46
|
const r = await runGhJson(["api", "graphql", "-f", `query=${PR_VIEW_QUERY}`, "-f", `owner=${owner}`, "-f", `name=${name}`, "-F", `number=${pr}`], { env, ghCommand });
|
|
44
47
|
const d = r?.data?.repository?.pullRequest;
|
|
45
48
|
if (!d) throw new Error(`Could not fetch PR #${pr}`);
|
|
46
|
-
|
|
49
|
+
const closingIssues = (d.closingIssuesReferences?.nodes ?? [])
|
|
50
|
+
.map((n) => n?.number)
|
|
51
|
+
.filter((n) => Number.isInteger(n) && n > 0);
|
|
52
|
+
return { id: d.id, isDraft: d.isDraft === true, headRefOid: typeof d.headRefOid === "string" ? d.headRefOid.trim() : null, state: typeof d.state === "string" ? d.state.trim() : null, mergeStateStatus: typeof d.mergeStateStatus === "string" ? d.mergeStateStatus.trim() : null, title: typeof d.title === "string" ? d.title : null, closingIssues };
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
async function fetchCiStatus({ repo, pr }, { env, ghCommand }) {
|
|
@@ -73,7 +79,7 @@ async function fetchGateEvidence({ repo, pr, headSha }, { env, ghCommand }) {
|
|
|
73
79
|
return { draftGate: dg, draftGateMarker: dm, currentHeadClean: chc, cleanEvidenceExists: cee, effectiveHeadClean: chc || cphm };
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
export async function readyForReview(options, { env = process.env, ghCommand = "gh", repoRoot = process.cwd() } = {}) {
|
|
82
|
+
export async function readyForReview(options, { env = process.env, ghCommand = "gh", repoRoot = process.cwd(), syncBoardStatus = realSyncBoardStatus } = {}) {
|
|
77
83
|
const { config } = await loadDevLoopConfig({ repoRoot });
|
|
78
84
|
const draftGateConfig = resolveGateConfig(config, "draft");
|
|
79
85
|
const requireCi = draftGateConfig?.requireCi !== false;
|
|
@@ -89,15 +95,27 @@ export async function readyForReview(options, { env = process.env, ghCommand = "
|
|
|
89
95
|
if (!gate.effectiveHeadClean) { const mv = gate.draftGateMarker?.visible; const mh = gate.draftGateMarker?.headSha; throw new Error(mv && mh ? `PR #${options.pr} draft_gate marker does not match current head ${headSha.slice(0,7)}. Re-run draft gate.` : `PR #${options.pr} draft_gate marker is missing or incomplete on current head ${headSha.slice(0,7)}. Re-run draft gate.`); }
|
|
90
96
|
const readyResult = await runChild(ghCommand, ["pr", "ready", String(options.pr), "--repo", options.repo], env);
|
|
91
97
|
if (readyResult.code !== 0) throw new Error(`gh pr ready failed`);
|
|
92
|
-
|
|
98
|
+
// #1069: couple the In-Progress board move to the ready transition. Best-effort
|
|
99
|
+
// and NON-FATAL — a board failure must NEVER block or fail marking ready.
|
|
100
|
+
let boardSync;
|
|
101
|
+
try {
|
|
102
|
+
const inProgressColumn = loadStateColumnMap(repoRoot).columnNames[LOGICAL_COLUMN.IN_PROGRESS];
|
|
103
|
+
const targets = prState.closingIssues.length > 0 ? prState.closingIssues : [options.pr];
|
|
104
|
+
boardSync = [];
|
|
105
|
+
for (const target of targets) {
|
|
106
|
+
boardSync.push(await syncBoardStatus(options.repo, repoRoot, target, inProgressColumn, env, {}));
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
boardSync = [{ ok: true, skipped: true, reason: err?.message ?? "board sync failed" }];
|
|
110
|
+
}
|
|
111
|
+
return { ok: true, action: "marked_ready", repo: options.repo, pr: options.pr, headSha, draftGateSatisfied: gate.effectiveHeadClean, boardSync };
|
|
93
112
|
}
|
|
94
113
|
|
|
95
114
|
export async function main(argv = process.argv.slice(2), runtime = {}) {
|
|
96
115
|
const options = parseReadyForReviewCliArgs(argv);
|
|
97
116
|
if (options.help) { process.stdout.write(`${USAGE}\n`); return 0; }
|
|
98
117
|
const result = await readyForReview(options, runtime);
|
|
99
|
-
|
|
100
|
-
return 0;
|
|
118
|
+
return emitResult(result, { jq: options.jq, silent: options.silent });
|
|
101
119
|
}
|
|
102
120
|
|
|
103
121
|
if (isDirectCliRun(import.meta.url)) {
|
|
@@ -6,6 +6,7 @@ import { loadDevLoopConfig, resolveGateConfig } from "@dev-loops/core/config";
|
|
|
6
6
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
7
7
|
import { detectCheckpointEvidence } from "./detect-checkpoint-evidence.mjs";
|
|
8
8
|
import { upsertCheckpointVerdict } from "./upsert-checkpoint-verdict.mjs";
|
|
9
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
9
10
|
const USAGE = `Usage: reconcile-draft-gate.mjs --repo <owner/name> --pr <number>
|
|
10
11
|
Optional/manual recovery tool for an already non-draft PR when you want to
|
|
11
12
|
retroactively record clean \`draft_gate\` evidence.
|
|
@@ -32,14 +33,16 @@ Output (stdout, JSON):
|
|
|
32
33
|
Error output (stderr, JSON):
|
|
33
34
|
{ "ok": false, "error": "...", "usage": "..." }
|
|
34
35
|
{ "ok": false, "error": "..." }
|
|
36
|
+
${JQ_OUTPUT_USAGE}
|
|
35
37
|
Exit codes:
|
|
36
38
|
0 Success — PR was reconciled and gate evidence posted
|
|
37
|
-
1 Argument error, gh failure, or unrecoverable state
|
|
39
|
+
1 Argument error, gh failure, or unrecoverable state
|
|
40
|
+
2 Invalid --jq filter`.trim();
|
|
38
41
|
const parseError = buildParseError(USAGE);
|
|
39
42
|
export function parseReconcileDraftGateCliArgs(argv) {
|
|
40
43
|
const { tokens } = parseArgs({
|
|
41
44
|
args: [...argv],
|
|
42
|
-
options: { help: { type: "boolean", short: "h" }, repo: { type: "string" }, pr: { type: "string" } },
|
|
45
|
+
options: { help: { type: "boolean", short: "h" }, repo: { type: "string" }, pr: { type: "string" }, ...JQ_OUTPUT_PARSE_OPTIONS },
|
|
43
46
|
allowPositionals: true,
|
|
44
47
|
strict: false,
|
|
45
48
|
tokens: true,
|
|
@@ -68,6 +71,7 @@ export function parseReconcileDraftGateCliArgs(argv) {
|
|
|
68
71
|
options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
|
|
69
72
|
continue;
|
|
70
73
|
}
|
|
74
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
71
75
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
72
76
|
}
|
|
73
77
|
const missing = ["repo", "pr"].filter((key) => options[key] === undefined);
|
|
@@ -327,7 +331,7 @@ async function main() {
|
|
|
327
331
|
}
|
|
328
332
|
try {
|
|
329
333
|
const result = await reconcileDraftGate(options);
|
|
330
|
-
process.
|
|
334
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent });
|
|
331
335
|
} catch (error) {
|
|
332
336
|
process.stderr.write(
|
|
333
337
|
`${JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) })}\n`
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
replyAndMaybeResolve,
|
|
10
10
|
validateResolutionMessage,
|
|
11
11
|
} from "./_review-thread-mutations.mjs";
|
|
12
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
12
13
|
|
|
13
14
|
export { hasCommitShaReference } from "./_review-thread-mutations.mjs";
|
|
14
15
|
|
|
@@ -21,7 +22,9 @@ Required:
|
|
|
21
22
|
--pr <n> Pull request number
|
|
22
23
|
--comment-id <n> GraphQL databaseId of the comment to reply to
|
|
23
24
|
--thread-id <id> GraphQL node ID of the review thread
|
|
24
|
-
--body-file <path> Path to file containing the reply body text
|
|
25
|
+
--body-file <path> Path to file containing the reply body text
|
|
26
|
+
|
|
27
|
+
${JQ_OUTPUT_USAGE}`;
|
|
25
28
|
|
|
26
29
|
function parseError(message) {
|
|
27
30
|
return Object.assign(new Error(message), { usage: USAGE });
|
|
@@ -39,6 +42,7 @@ function parseCliArgs(argv) {
|
|
|
39
42
|
"thread-id": { type: "string" },
|
|
40
43
|
"body-file": { type: "string" },
|
|
41
44
|
help: { type: "boolean", short: "h" },
|
|
45
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
42
46
|
},
|
|
43
47
|
strict: true,
|
|
44
48
|
allowPositionals: false,
|
|
@@ -62,7 +66,15 @@ function parseCliArgs(argv) {
|
|
|
62
66
|
const pr = parsePositiveInteger(values.pr, "--pr", parseError);
|
|
63
67
|
const commentId = parsePositiveInteger(values["comment-id"], "--comment-id", parseError);
|
|
64
68
|
|
|
65
|
-
return {
|
|
69
|
+
return {
|
|
70
|
+
repo: repoSlug,
|
|
71
|
+
pr,
|
|
72
|
+
commentId,
|
|
73
|
+
threadId: values["thread-id"],
|
|
74
|
+
bodyFile: values["body-file"],
|
|
75
|
+
jq: values.jq,
|
|
76
|
+
silent: values.silent === true,
|
|
77
|
+
};
|
|
66
78
|
}
|
|
67
79
|
|
|
68
80
|
async function run(argv) {
|
|
@@ -82,11 +94,10 @@ async function run(argv) {
|
|
|
82
94
|
{ env: process.env, ghCommand: "gh" },
|
|
83
95
|
);
|
|
84
96
|
|
|
85
|
-
|
|
97
|
+
return emitResult({
|
|
86
98
|
ok: true, repo: repoSlug, pr, commentId, threadId,
|
|
87
99
|
replyId: result.replyId, replyUrl: result.replyUrl, resolved: true,
|
|
88
|
-
}
|
|
89
|
-
return 0;
|
|
100
|
+
}, { jq: parsed.jq, silent: parsed.silent });
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
if (isDirectCliRun(import.meta.url)) {
|