dev-loops 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/.claude-plugin/plugin.json +1 -1
- package/.claude/agents/dev-loop.md +2 -1
- package/.claude/agents/review.md +2 -1
- package/.claude/commands/{auto.md → loop-auto.md} +1 -1
- package/.claude/commands/loop-continue.md +15 -0
- package/.claude/commands/loop-enqueue.md +22 -0
- package/.claude/commands/loop-grill.md +17 -0
- package/.claude/commands/{info.md → loop-info.md} +2 -2
- package/.claude/commands/loop-queue-status.md +24 -0
- package/.claude/commands/{start-spike.md → loop-start-spike.md} +1 -1
- package/.claude/commands/{start.md → loop-start.md} +1 -1
- package/.claude/commands/{status.md → loop-status.md} +1 -1
- package/.claude/hooks/_bash-command-classify.mjs +333 -29
- package/.claude/hooks/_hook-decisions.mjs +138 -15
- package/.claude/hooks/pre-tool-use-bash-gate.mjs +36 -15
- package/.claude/skills/copilot-pr-followup/SKILL.md +24 -12
- package/.claude/skills/dev-loop/SKILL.md +15 -11
- package/.claude/skills/docs/acceptance-criteria-verification.md +6 -2
- package/.claude/skills/docs/anti-patterns.md +2 -1
- package/.claude/skills/docs/artifact-authority-contract.md +22 -4
- package/.claude/skills/docs/copilot-loop-operations.md +1 -1
- package/.claude/skills/docs/cross-harness-regression-contract.md +60 -0
- package/.claude/skills/docs/issue-intake-procedure.md +1 -0
- package/.claude/skills/docs/local-planning-flow.md +1 -1
- package/.claude/skills/docs/local-planning-worked-example.md +1 -1
- package/.claude/skills/docs/merge-preconditions.md +17 -1
- package/.claude/skills/docs/plan-file-contract.md +1 -1
- package/.claude/skills/docs/public-dev-loop-contract.md +1 -1
- package/.claude/skills/docs/retrospective-checkpoint-contract.md +90 -76
- package/.claude/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/.claude/skills/docs/workflow-handoff-contract.md +39 -0
- package/.claude/skills/local-implementation/SKILL.md +25 -14
- package/.claude/skills/loop-grill/SKILL.md +163 -0
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +95 -0
- package/README.md +9 -9
- package/agents/dev-loop.agent.md +3 -2
- package/agents/developer.agent.md +1 -1
- package/agents/docs.agent.md +1 -1
- package/agents/fixer.agent.md +1 -1
- package/agents/quality.agent.md +1 -1
- package/agents/refiner.agent.md +1 -1
- package/agents/review.agent.md +3 -2
- package/cli/index.mjs +35 -42
- package/extension/README.md +4 -4
- package/extension/checks.ts +1 -5
- package/extension/harness-types.ts +1 -0
- package/extension/index.ts +6 -0
- package/extension/pi-extension-adapter.ts +2 -0
- package/extension/presentation.ts +7 -19
- package/package.json +9 -10
- package/scripts/github/capture-review-threads.mjs +2 -9
- package/scripts/github/comment-issue.mjs +2 -9
- package/scripts/github/create-label.mjs +133 -0
- package/scripts/github/detect-checkpoint-evidence.mjs +170 -14
- package/scripts/github/detect-linked-issue-pr.mjs +22 -6
- package/scripts/github/edit-pr.mjs +259 -0
- package/scripts/github/fetch-ci-logs.mjs +2 -9
- package/scripts/github/list-issues.mjs +2 -9
- package/scripts/github/manage-sub-issues.mjs +46 -10
- package/scripts/github/offer-human-handoff.mjs +8 -5
- package/scripts/github/post-gate-findings.mjs +14 -2
- package/scripts/github/probe-ci-status.mjs +8 -2
- package/scripts/github/probe-copilot-review.mjs +21 -14
- package/scripts/github/ready-for-review.mjs +26 -8
- package/scripts/github/reconcile-draft-gate.mjs +7 -3
- package/scripts/github/reply-resolve-review-thread.mjs +16 -5
- package/scripts/github/reply-resolve-review-threads.mjs +69 -7
- package/scripts/github/request-copilot-review.mjs +8 -2
- package/scripts/github/resolve-handoff-candidates.mjs +7 -3
- package/scripts/github/resolve-tracker-local-spec.mjs +9 -3
- package/scripts/github/stage-reviewer-draft.mjs +10 -2
- package/scripts/github/tick-verified-checkboxes.mjs +202 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +31 -10
- package/scripts/github/verify-fresh-review-context.mjs +150 -31
- package/scripts/github/view-pr.mjs +150 -0
- package/scripts/github/write-gate-context.mjs +248 -61
- package/scripts/github/write-gate-findings-log.mjs +75 -1
- package/scripts/lib/jq-output.mjs +18 -0
- package/scripts/loop/_post-convergence-change.mjs +211 -0
- package/scripts/loop/_pr-runner-coordination.mjs +70 -0
- package/scripts/loop/_repo-root-resolver.mjs +47 -0
- package/scripts/loop/build-handoff-envelope.mjs +14 -4
- package/scripts/loop/check-retro-tooling.mjs +14 -3
- package/scripts/loop/checkpoint-contract.mjs +7 -4
- package/scripts/loop/cleanup-worktree.mjs +12 -3
- package/scripts/loop/conductor-monitor.mjs +12 -18
- package/scripts/loop/copilot-pr-handoff.mjs +135 -14
- package/scripts/loop/debt-remediate.mjs +24 -12
- package/scripts/loop/detect-change-scope.mjs +36 -7
- package/scripts/loop/detect-copilot-loop-state.mjs +11 -5
- package/scripts/loop/detect-copilot-session-activity.mjs +7 -3
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +7 -3
- package/scripts/loop/detect-internal-only-pr.mjs +8 -2
- package/scripts/loop/detect-issue-refinement-artifact.mjs +6 -3
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +149 -66
- package/scripts/loop/detect-reviewer-loop-state.mjs +12 -2
- package/scripts/loop/detect-tracker-first-loop-state.mjs +9 -1
- package/scripts/loop/detect-tracker-pr-state.mjs +10 -3
- package/scripts/loop/ensure-worktree.mjs +8 -3
- package/scripts/loop/info.mjs +12 -4
- package/scripts/loop/inspect-run-viewer/constants.mjs +4 -18
- package/scripts/loop/inspect-run-viewer/vendor/mermaid.min.js +3405 -0
- package/scripts/loop/inspect-run-viewer-ci-changes.mjs +18 -6
- package/scripts/loop/inspect-run-viewer.mjs +67 -4
- package/scripts/loop/inspect-run.mjs +8 -2
- package/scripts/loop/outer-loop.mjs +8 -4
- package/scripts/loop/pr-runner-coordination.mjs +2 -9
- package/scripts/loop/pre-commit-branch-guard.mjs +16 -10
- package/scripts/loop/pre-flight-gate.mjs +9 -9
- package/scripts/loop/pre-pr-ready-gate.mjs +8 -4
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +13 -4
- package/scripts/loop/provision-worktree.mjs +74 -3
- package/scripts/loop/resolve-dev-loop-startup.mjs +76 -7
- package/scripts/loop/resolve-gate-dispatch.mjs +134 -0
- package/scripts/loop/resolve-pr-conflicts.mjs +14 -7
- package/scripts/loop/run-conductor-cycle.mjs +8 -2
- package/scripts/loop/run-queue.mjs +18 -9
- package/scripts/loop/run-refinement-audit.mjs +8 -9
- package/scripts/loop/run-watch-cycle.mjs +2 -9
- package/scripts/loop/sanctioned-commands.mjs +104 -0
- package/scripts/loop/steer-loop.mjs +29 -16
- package/scripts/loop/validate-pr-body-spec.mjs +207 -0
- package/scripts/loop/watch-initial-copilot-pr.mjs +8 -3
- package/scripts/pages/build-site.mjs +59 -8
- package/scripts/pages/build-state-atlas.mjs +441 -0
- package/scripts/projects/_resolve-project.mjs +148 -0
- package/scripts/projects/add-queue-item.mjs +60 -54
- package/scripts/projects/archive-done-items.mjs +49 -84
- package/scripts/projects/ensure-queue-board.mjs +10 -36
- package/scripts/projects/list-queue-items.mjs +116 -65
- package/scripts/projects/move-queue-item.mjs +28 -49
- package/scripts/projects/reconcile-queue.mjs +253 -0
- package/scripts/projects/reorder-queue-item.mjs +41 -47
- package/scripts/projects/resolve-active-board-item.mjs +108 -46
- package/scripts/projects/sync-item-status.mjs +15 -10
- package/scripts/refine/_refine-helpers.mjs +21 -5
- package/scripts/refine/exit-spike.mjs +18 -8
- package/scripts/refine/promote-plan.mjs +22 -16
- package/scripts/refine/prose-linkage-detector.mjs +1 -1
- package/scripts/refine/refine-plan-file.mjs +13 -4
- package/scripts/refine/refinement-completeness-checker.mjs +1 -1
- package/scripts/refine/scaffold-spike-file.mjs +4 -8
- package/scripts/refine/scope-boundary-cross-checker.mjs +1 -1
- package/scripts/refine/tree-integrity-validator.mjs +1 -1
- package/scripts/refine/validate-plan-file.mjs +1 -1
- package/scripts/refine/validate-spike-file.mjs +1 -1
- package/scripts/refine/verify.mjs +11 -6
- package/scripts/release/assert-core-dependency-version.mjs +123 -0
- package/scripts/release/extract-changelog-section.mjs +16 -2
- package/skills/copilot-pr-followup/SKILL.md +24 -12
- package/skills/dev-loop/SKILL.md +10 -6
- package/skills/docs/acceptance-criteria-verification.md +6 -2
- package/skills/docs/anti-patterns.md +2 -1
- package/skills/docs/artifact-authority-contract.md +22 -4
- package/skills/docs/copilot-loop-operations.md +1 -1
- package/skills/docs/cross-harness-regression-contract.md +60 -0
- package/skills/docs/issue-intake-procedure.md +1 -0
- package/skills/docs/local-planning-flow.md +1 -1
- package/skills/docs/local-planning-worked-example.md +1 -1
- package/skills/docs/merge-preconditions.md +17 -1
- package/skills/docs/plan-file-contract.md +1 -1
- package/skills/docs/public-dev-loop-contract.md +1 -1
- package/skills/docs/retrospective-checkpoint-contract.md +90 -76
- package/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/skills/docs/workflow-handoff-contract.md +39 -0
- package/skills/local-implementation/SKILL.md +25 -14
- package/skills/loop-grill/SKILL.md +165 -0
- package/.claude/commands/continue.md +0 -15
- package/scripts/loop/conductor.mjs +0 -233
- package/scripts/loop/detect-stale-runner.mjs +0 -265
- package/scripts/loop/pre-push-main-guard.mjs +0 -117
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { buildParseError, formatCliError, isCopilotLogin, isDirectCliRun, normalizeTimestamp } from "../_core-helpers.mjs";
|
|
2
|
+
import { buildParseError, formatCliError, isCopilotLogin, isDirectCliRun, normalizeTimestamp, parseJsonText } from "../_core-helpers.mjs";
|
|
3
3
|
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
4
|
+
import { detectPostConvergenceSignificantChange } from "./_post-convergence-change.mjs";
|
|
4
5
|
import { detectRepoSlug, parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
5
6
|
import { resolveRunId } from "@dev-loops/core/loop/run-context";
|
|
6
|
-
import path from "node:path";
|
|
7
7
|
import { loadDevLoopConfig, resolveRefinement } from "@dev-loops/core/config";
|
|
8
8
|
import { autoDetectSnapshot } from "./detect-copilot-loop-state.mjs";
|
|
9
9
|
import { performCopilotReviewRequest } from "../github/request-copilot-review.mjs";
|
|
10
10
|
import { detectInternalOnly as detectPrInternalOnly } from "./detect-internal-only-pr.mjs";
|
|
11
11
|
import { applyConfirmedReviewRequest, interpretLoopState, NEXT_ACTIONS, STATE, summarizeLoopInterpretation, TRANSITIONS } from "@dev-loops/core/loop/copilot-loop-state";
|
|
12
|
-
import { ensureAsyncRunnerOwnership } from "./_pr-runner-coordination.mjs";
|
|
12
|
+
import { ensureAsyncRunnerOwnership, releaseAsyncRunnerOwnership } from "./_pr-runner-coordination.mjs";
|
|
13
|
+
import { resolveRepoRoot } from "./_repo-root-resolver.mjs";
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
import {
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
enforceExternalHealthyWaitTimeout,
|
|
18
19
|
} from "@dev-loops/core/loop/timeout-policy";
|
|
19
20
|
import { parseArgs } from "node:util";
|
|
20
|
-
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
21
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
21
22
|
import {
|
|
22
23
|
DEFAULT_POLL_INTERVAL_MS,
|
|
23
24
|
COPILOT_REVIEW_WAIT_TIMEOUT_MS,
|
|
@@ -183,14 +184,7 @@ export function parseHandoffCliArgs(argv, { cwd = process.cwd() } = {}) {
|
|
|
183
184
|
options.watchStatus = watchStatus;
|
|
184
185
|
continue;
|
|
185
186
|
}
|
|
186
|
-
if (token
|
|
187
|
-
options.jq = requireTokenValue(token, parseError);
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
if (token.name === "silent") {
|
|
191
|
-
options.silent = true;
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
187
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
194
188
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
195
189
|
}
|
|
196
190
|
if (options.pr === undefined) {
|
|
@@ -300,12 +294,32 @@ export async function detectRecentHumanComments({ repo, pr, claimedAtMs }, { env
|
|
|
300
294
|
}
|
|
301
295
|
}
|
|
302
296
|
|
|
297
|
+
// Facts needed by the round-cap escape-hatch significant-change detector
|
|
298
|
+
// (#1103, #1126): the current head, the Copilot reviews (to find the last
|
|
299
|
+
// reviewed head), and the PR's changed files. Fetched only when the interpreter
|
|
300
|
+
// already resolved ROUND_CAP_CLEAN_FALLBACK, so this extra call is off the hot path.
|
|
301
|
+
async function fetchReopenCycleFacts({ repo, pr }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
302
|
+
const result = await runChild(
|
|
303
|
+
ghCommand,
|
|
304
|
+
["pr", "view", String(pr), "--repo", repo, "--json", "headRefOid,reviews,files"],
|
|
305
|
+
env,
|
|
306
|
+
);
|
|
307
|
+
if (result.code !== 0) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
return parseJsonText(result.stdout, { label: "gh pr view reopen-cycle facts" });
|
|
312
|
+
} catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
303
317
|
export async function runHandoff(options, { env = process.env, ghCommand = "gh" } = {}) {
|
|
304
318
|
const runnerOwnership = await ensureAsyncRunnerOwnership({
|
|
305
319
|
repo: options.repo,
|
|
306
320
|
pr: options.pr,
|
|
307
321
|
env,
|
|
308
|
-
cwd:
|
|
322
|
+
cwd: resolveRepoRoot(process.cwd()),
|
|
309
323
|
claimIfMissing: true,
|
|
310
324
|
});
|
|
311
325
|
if (!runnerOwnership.ok) {
|
|
@@ -337,7 +351,7 @@ export async function runHandoff(options, { env = process.env, ghCommand = "gh"
|
|
|
337
351
|
{ repo: options.repo, pr: options.pr },
|
|
338
352
|
{ env, ghCommand },
|
|
339
353
|
);
|
|
340
|
-
const config = await loadDevLoopConfig({ repoRoot:
|
|
354
|
+
const config = await loadDevLoopConfig({ repoRoot: resolveRepoRoot(process.cwd()) });
|
|
341
355
|
if (config.errors?.length > 0) {
|
|
342
356
|
console.error("[copilot-pr-handoff] config warnings:", JSON.stringify(config.errors));
|
|
343
357
|
}
|
|
@@ -358,6 +372,12 @@ export async function runHandoff(options, { env = process.env, ghCommand = "gh"
|
|
|
358
372
|
const TERMINAL_STATES = new Set([STATE.NO_PR, STATE.DONE, STATE.BLOCKED_NEEDS_USER_DECISION]);
|
|
359
373
|
const humanCommentUnavailable = humanCommentCheck.error && !humanCommentCheck.paused;
|
|
360
374
|
if ((humanCommentCheck.paused || humanCommentUnavailable) && !TERMINAL_STATES.has(interpretation.state)) {
|
|
375
|
+
const runnerRelease = await releaseAsyncRunnerOwnership({
|
|
376
|
+
repo: options.repo,
|
|
377
|
+
pr: options.pr,
|
|
378
|
+
env,
|
|
379
|
+
cwd: resolveRepoRoot(process.cwd()),
|
|
380
|
+
});
|
|
361
381
|
return {
|
|
362
382
|
ok: true,
|
|
363
383
|
action: "stop",
|
|
@@ -373,6 +393,7 @@ export async function runHandoff(options, { env = process.env, ghCommand = "gh"
|
|
|
373
393
|
terminal: true,
|
|
374
394
|
snapshot,
|
|
375
395
|
runnerOwnership,
|
|
396
|
+
...(runnerRelease.status !== "skipped_no_async_run_id" ? { runnerRelease } : {}),
|
|
376
397
|
humanCommentPause: {
|
|
377
398
|
reason: humanCommentCheck.paused ? "human_comment_detected" : "human_comment_check_unavailable",
|
|
378
399
|
error: humanCommentCheck.error || undefined,
|
|
@@ -438,6 +459,76 @@ export async function runHandoff(options, { env = process.env, ghCommand = "gh"
|
|
|
438
459
|
}
|
|
439
460
|
}
|
|
440
461
|
|
|
462
|
+
// In-flight-rerequest race (#1165): the interpreter routes to
|
|
463
|
+
// ROUND_CAP_CLEAN_FALLBACK even when a Copilot review is REQUESTED and pending
|
|
464
|
+
// on the current head, because at the cap it treats any assignment as a stale
|
|
465
|
+
// leftover (copilot-loop-state.mjs). An in-flight request is usually a fresh
|
|
466
|
+
// review genuinely coming (e.g. a force-rerequest for a significant
|
|
467
|
+
// post-convergence change) — but it can also be a stale at-cap assignment
|
|
468
|
+
// mislabeled as requested, as detect-copilot-loop-state.mjs's own fallback
|
|
469
|
+
// notes. Proceeding to pre_approval_gate on a real in-flight review would skip
|
|
470
|
+
// it — the exact gate-integrity gap #1126 closes — and
|
|
471
|
+
// detect-pr-gate-coordination-state gates pre-approval here. The reopen escape
|
|
472
|
+
// hatch below can only recover the wait verdict via a SECOND, fail-silent gh
|
|
473
|
+
// fetch (fetchReopenCycleFacts) whose failure/compare-miss silently drops back
|
|
474
|
+
// to "proceed", diverging from detect (which reused already-validated facts).
|
|
475
|
+
// Fail closed instead: while the in-flight evidence stands, wait for it to
|
|
476
|
+
// resolve rather than stop. If it never lands, a --watch-status refresh
|
|
477
|
+
// (which skips this branch) re-resolves to the clean fallback, so this never
|
|
478
|
+
// dead-ends the loop.
|
|
479
|
+
const reviewRequestInFlight = snapshot.copilotReviewRequestStatus === "requested"
|
|
480
|
+
|| snapshot.copilotReviewRequestStatus === "already-requested";
|
|
481
|
+
if (!internalOnlySkipCopilot
|
|
482
|
+
&& options.watchStatus === undefined
|
|
483
|
+
&& interpretation.state === STATE.ROUND_CAP_CLEAN_FALLBACK
|
|
484
|
+
&& reviewRequestInFlight) {
|
|
485
|
+
interpretation = {
|
|
486
|
+
...interpretation,
|
|
487
|
+
state: STATE.WAITING_FOR_COPILOT_REVIEW,
|
|
488
|
+
nextAction: NEXT_ACTIONS[STATE.WAITING_FOR_COPILOT_REVIEW],
|
|
489
|
+
allowedTransitions: [...(TRANSITIONS[STATE.WAITING_FOR_COPILOT_REVIEW] || [])],
|
|
490
|
+
roundCapCleanEligible: false,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Round-cap escape hatch (#1103, #1126): the interpreter resolves
|
|
495
|
+
// ROUND_CAP_CLEAN_FALLBACK (stop, no re-request) at the cap. But when a
|
|
496
|
+
// SIGNIFICANT post-convergence change (new product/test-logic since the last
|
|
497
|
+
// Copilot review — not doc/comment-only) has landed, a new Copilot cycle is
|
|
498
|
+
// owed. Reopen it here via the SAME shared detector detect-pr-gate-coordination-state
|
|
499
|
+
// uses, so the two agree: both offer a re-request iff (cap reached AND
|
|
500
|
+
// significant post-convergence change). A doc-only change stays at the clean
|
|
501
|
+
// fallback (stop), unchanged.
|
|
502
|
+
let reopenedCapCycle = false;
|
|
503
|
+
if (!internalOnlySkipCopilot
|
|
504
|
+
&& options.watchStatus === undefined
|
|
505
|
+
&& interpretation.state === STATE.ROUND_CAP_CLEAN_FALLBACK) {
|
|
506
|
+
const reopenFacts = await fetchReopenCycleFacts(options, { env, ghCommand });
|
|
507
|
+
const significant = await detectPostConvergenceSignificantChange(
|
|
508
|
+
{
|
|
509
|
+
repo: options.repo,
|
|
510
|
+
pr: options.pr,
|
|
511
|
+
currentHeadSha: typeof reopenFacts?.headRefOid === "string" ? reopenFacts.headRefOid.trim() : null,
|
|
512
|
+
reviews: reopenFacts?.reviews,
|
|
513
|
+
changedFiles: reopenFacts?.files,
|
|
514
|
+
roundCapReached: true,
|
|
515
|
+
regularCopilotRounds: (snapshot.copilotReviewRoundCount ?? 0) > 0,
|
|
516
|
+
},
|
|
517
|
+
{ env, ghCommand },
|
|
518
|
+
);
|
|
519
|
+
if (significant) {
|
|
520
|
+
reopenedCapCycle = true;
|
|
521
|
+
interpretation = {
|
|
522
|
+
...interpretation,
|
|
523
|
+
state: STATE.READY_TO_REREQUEST_REVIEW,
|
|
524
|
+
nextAction: NEXT_ACTIONS[STATE.READY_TO_REREQUEST_REVIEW],
|
|
525
|
+
allowedTransitions: [...(TRANSITIONS[STATE.READY_TO_REREQUEST_REVIEW] || [])],
|
|
526
|
+
autoRerequestEligible: true,
|
|
527
|
+
roundCapCleanEligible: false,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
441
532
|
let reviewRequestStatus;
|
|
442
533
|
const shouldRequestReview = !internalOnlySkipCopilot && options.watchStatus === undefined
|
|
443
534
|
&& (interpretation.state === STATE.PR_READY_NO_FEEDBACK
|
|
@@ -449,12 +540,31 @@ export async function runHandoff(options, { env = process.env, ghCommand = "gh"
|
|
|
449
540
|
repo: options.repo,
|
|
450
541
|
pr: options.pr,
|
|
451
542
|
sameHeadCleanConverged: interpretation.sameHeadCleanConverged,
|
|
543
|
+
// A reopened cap cycle was decided via the shared significant-change
|
|
544
|
+
// detector; tell the requester to honor it. performCopilotReviewRequest
|
|
545
|
+
// still refuses unless the head actually advanced past the last review
|
|
546
|
+
// (its hasNewCommits guard), so this cannot force an over-cap same-head request.
|
|
547
|
+
forceRerequestReview: reopenedCapCycle,
|
|
452
548
|
},
|
|
453
549
|
{ env, ghCommand },
|
|
454
550
|
);
|
|
455
551
|
reviewRequestStatus = requestResult.status;
|
|
456
552
|
snapshot = applyConfirmedReviewRequest(snapshot, reviewRequestStatus);
|
|
457
553
|
interpretation = interpretLoopState(snapshot, refinementConfig);
|
|
554
|
+
// The re-interpretation re-hits the round cap (rounds still >= max) and would
|
|
555
|
+
// flip a reopened cycle back to ROUND_CAP_CLEAN_FALLBACK. A confirmed request
|
|
556
|
+
// for a significant new change is a genuine new wait cycle, so map it to the
|
|
557
|
+
// honest WAITING_FOR_COPILOT_REVIEW state (what a below-cap re-request yields).
|
|
558
|
+
if (reopenedCapCycle
|
|
559
|
+
&& (reviewRequestStatus === "requested" || reviewRequestStatus === "already-requested")) {
|
|
560
|
+
interpretation = {
|
|
561
|
+
...interpretation,
|
|
562
|
+
state: STATE.WAITING_FOR_COPILOT_REVIEW,
|
|
563
|
+
nextAction: NEXT_ACTIONS[STATE.WAITING_FOR_COPILOT_REVIEW],
|
|
564
|
+
allowedTransitions: [...(TRANSITIONS[STATE.WAITING_FOR_COPILOT_REVIEW] || [])],
|
|
565
|
+
roundCapCleanEligible: false,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
458
568
|
}
|
|
459
569
|
const interpretationSummary = summarizeLoopInterpretation(interpretation, refinementConfig);
|
|
460
570
|
const effectiveReviewRequestStatus = reviewRequestStatus
|
|
@@ -517,6 +627,17 @@ export async function runHandoff(options, { env = process.env, ghCommand = "gh"
|
|
|
517
627
|
requestStatus: normalizedRequestStatus,
|
|
518
628
|
watchArgs: result.watchArgs,
|
|
519
629
|
});
|
|
630
|
+
if (result.terminal === true) {
|
|
631
|
+
const runnerRelease = await releaseAsyncRunnerOwnership({
|
|
632
|
+
repo: options.repo,
|
|
633
|
+
pr: options.pr,
|
|
634
|
+
env,
|
|
635
|
+
cwd: resolveRepoRoot(process.cwd()),
|
|
636
|
+
});
|
|
637
|
+
if (runnerRelease.status !== "skipped_no_async_run_id") {
|
|
638
|
+
result.runnerRelease = runnerRelease;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
520
641
|
return result;
|
|
521
642
|
}
|
|
522
643
|
export async function runCli(
|
|
@@ -24,6 +24,7 @@ import { DebtSignalSchema } from "@dev-loops/core/debt/signal";
|
|
|
24
24
|
import { clusterSignalsEnriched } from "@dev-loops/core/debt/cluster";
|
|
25
25
|
import { shapeFindings } from "@dev-loops/core/debt/shape";
|
|
26
26
|
import { createRemediationIssue } from "@dev-loops/core/debt/remediation-to-issue";
|
|
27
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
27
28
|
|
|
28
29
|
const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url));
|
|
29
30
|
|
|
@@ -45,9 +46,12 @@ Optional:
|
|
|
45
46
|
Output (stdout, JSON):
|
|
46
47
|
{ "ok": true, "signals": N, "findings": N, "remediationItems": N, "issues": [...], "summary": "..." }
|
|
47
48
|
|
|
49
|
+
${JQ_OUTPUT_USAGE}
|
|
50
|
+
|
|
48
51
|
Exit codes:
|
|
49
52
|
0 Success (all remediation issue creations succeeded)
|
|
50
|
-
1 Argument error, input validation failure, or issue creation failure
|
|
53
|
+
1 Argument error, input validation failure, or issue creation failure
|
|
54
|
+
2 Invalid --jq filter`.trim();
|
|
51
55
|
|
|
52
56
|
const parseError = buildParseError(USAGE);
|
|
53
57
|
|
|
@@ -160,6 +164,7 @@ export async function runCli(argv) {
|
|
|
160
164
|
input: { type: "string" },
|
|
161
165
|
repo: { type: "string" },
|
|
162
166
|
"dry-run": { type: "boolean" },
|
|
167
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
163
168
|
},
|
|
164
169
|
allowPositionals: true,
|
|
165
170
|
strict: false,
|
|
@@ -188,6 +193,7 @@ export async function runCli(argv) {
|
|
|
188
193
|
options.dryRun = true;
|
|
189
194
|
continue;
|
|
190
195
|
}
|
|
196
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
191
197
|
throw parseError(`Unknown flag: ${token.rawName}`);
|
|
192
198
|
}
|
|
193
199
|
|
|
@@ -196,6 +202,14 @@ export async function runCli(argv) {
|
|
|
196
202
|
return { exitCode: 0 };
|
|
197
203
|
}
|
|
198
204
|
|
|
205
|
+
// Every early-exit failure below reports via the shared jq/silent contract on
|
|
206
|
+
// stderr (this script's existing error-output convention); `ok:false` maps to
|
|
207
|
+
// the existing exit 1 by default, and an invalid --jq filter still fails
|
|
208
|
+
// closed with exit 2 instead of being swallowed by this fixed exit code.
|
|
209
|
+
const failEarly = (payload) => ({
|
|
210
|
+
exitCode: emitResult(payload, { jq: options.jq, silent: options.silent, stdout: process.stderr, stderr: process.stderr }),
|
|
211
|
+
});
|
|
212
|
+
|
|
199
213
|
if (!options.input) {
|
|
200
214
|
throw parseError("Missing required flag: --input <path>");
|
|
201
215
|
}
|
|
@@ -208,20 +222,20 @@ export async function runCli(argv) {
|
|
|
208
222
|
try {
|
|
209
223
|
rawInput = await readFile(inputPath, "utf-8");
|
|
210
224
|
} catch (err) {
|
|
211
|
-
return {
|
|
225
|
+
return failEarly({ ok: false, error: `Cannot read input file: ${inputPath}`, detail: err.message });
|
|
212
226
|
}
|
|
213
227
|
|
|
214
228
|
let signals;
|
|
215
229
|
try {
|
|
216
230
|
signals = JSON.parse(rawInput);
|
|
217
231
|
} catch (err) {
|
|
218
|
-
return {
|
|
232
|
+
return failEarly({ ok: false, error: "Input file is not valid JSON", detail: err.message });
|
|
219
233
|
}
|
|
220
234
|
|
|
221
235
|
// Validate signals
|
|
222
236
|
const validation = validateSignals(signals);
|
|
223
237
|
if (!validation.ok) {
|
|
224
|
-
return
|
|
238
|
+
return failEarly(validation);
|
|
225
239
|
}
|
|
226
240
|
|
|
227
241
|
// Resolve repo
|
|
@@ -230,14 +244,14 @@ export async function runCli(argv) {
|
|
|
230
244
|
try {
|
|
231
245
|
repo = parseRepoSlug(options.repo);
|
|
232
246
|
} catch {
|
|
233
|
-
return {
|
|
247
|
+
return failEarly({ ok: false, error: `Invalid repo slug: ${options.repo}` });
|
|
234
248
|
}
|
|
235
249
|
} else {
|
|
236
250
|
repo = detectRepo();
|
|
237
251
|
}
|
|
238
252
|
|
|
239
253
|
if (!repo) {
|
|
240
|
-
return {
|
|
254
|
+
return failEarly({ ok: false, error: "Cannot detect repository. Pass --repo <owner/name>." });
|
|
241
255
|
}
|
|
242
256
|
|
|
243
257
|
// Run pipeline: cluster → score → shape
|
|
@@ -298,8 +312,9 @@ export async function runCli(argv) {
|
|
|
298
312
|
report.repo = `${repo.owner}/${repo.name}`;
|
|
299
313
|
|
|
300
314
|
const outputTarget = report.ok ? process.stdout : process.stderr;
|
|
301
|
-
|
|
302
|
-
|
|
315
|
+
return {
|
|
316
|
+
exitCode: emitResult(report, { jq: options.jq, silent: options.silent, stdout: outputTarget, stderr: process.stderr }),
|
|
317
|
+
};
|
|
303
318
|
}
|
|
304
319
|
|
|
305
320
|
// ============================================================================
|
|
@@ -307,10 +322,7 @@ export async function runCli(argv) {
|
|
|
307
322
|
// ============================================================================
|
|
308
323
|
|
|
309
324
|
if (isDirectCliRun(import.meta.url)) {
|
|
310
|
-
runCli(process.argv.slice(2)).then(({ exitCode
|
|
311
|
-
if (output) {
|
|
312
|
-
process.stderr.write(JSON.stringify(output) + "\n");
|
|
313
|
-
}
|
|
325
|
+
runCli(process.argv.slice(2)).then(({ exitCode }) => {
|
|
314
326
|
process.exitCode = exitCode;
|
|
315
327
|
}).catch((err) => {
|
|
316
328
|
process.stderr.write(`${formatCliError(err)}\n`);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
5
6
|
|
|
6
7
|
const USAGE = `Usage: detect-change-scope.mjs [--base <ref>] [--head <ref>]
|
|
7
8
|
Detect change scope from git diff for light-mode eligibility.
|
|
@@ -9,9 +10,13 @@ Options:
|
|
|
9
10
|
--base <ref> Override base ref (default: HEAD~1)
|
|
10
11
|
--head <ref> Override head ref; ignored unless --base is also set
|
|
11
12
|
--help, -h Show this help
|
|
13
|
+
|
|
14
|
+
${JQ_OUTPUT_USAGE}
|
|
15
|
+
|
|
12
16
|
Exit codes:
|
|
13
17
|
0 Success
|
|
14
18
|
1 Error
|
|
19
|
+
2 Invalid --jq filter
|
|
15
20
|
`;
|
|
16
21
|
|
|
17
22
|
function parseCliArgs(argv) {
|
|
@@ -21,6 +26,7 @@ function parseCliArgs(argv) {
|
|
|
21
26
|
base: { type: "string" },
|
|
22
27
|
head: { type: "string" },
|
|
23
28
|
help: { type: "boolean", short: "h" },
|
|
29
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
24
30
|
},
|
|
25
31
|
allowPositionals: true,
|
|
26
32
|
strict: false,
|
|
@@ -43,7 +49,9 @@ function parseCliArgs(argv) {
|
|
|
43
49
|
}
|
|
44
50
|
if (token.name === "head") {
|
|
45
51
|
opts.head = token.value ?? null;
|
|
52
|
+
continue;
|
|
46
53
|
}
|
|
54
|
+
if (matchJqOutputToken(token, opts)) continue;
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
57
|
return opts;
|
|
@@ -88,6 +96,27 @@ function detectScope({ base, head } = {}) {
|
|
|
88
96
|
function isEligibleForLightMode(scope, threshold) {
|
|
89
97
|
return scope.filesChanged <= threshold.maxFiles && scope.linesChanged <= threshold.maxLines;
|
|
90
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Detect change scope for the merge-base diff between `base` and `head` (the
|
|
101
|
+
* three-dot `base...head` diff git resolves against merge-base(base, head)).
|
|
102
|
+
*
|
|
103
|
+
* Fails CLOSED: a missing base/head, or any git failure, returns `{ ok: false }`
|
|
104
|
+
* so callers that gate on scope (e.g. the light-mode pre-merge acceptance) reject
|
|
105
|
+
* rather than silently treating an unmeasurable diff as under threshold. Reuses
|
|
106
|
+
* the same `parseGitDiffStat` scope resolution as `detectScope`.
|
|
107
|
+
*/
|
|
108
|
+
function detectMergeBaseScope({ base, head, cwd } = {}) {
|
|
109
|
+
if (!base || !head) {
|
|
110
|
+
return { ok: false, filesChanged: 0, linesChanged: 0, error: "base and head are required for merge-base scope detection" };
|
|
111
|
+
}
|
|
112
|
+
let output;
|
|
113
|
+
try {
|
|
114
|
+
output = execFileSync("git", ["diff", "--stat", `${base}...${head}`], { encoding: "utf8", maxBuffer: 1_000_000, cwd: cwd || undefined });
|
|
115
|
+
} catch (err) {
|
|
116
|
+
return { ok: false, filesChanged: 0, linesChanged: 0, error: err instanceof Error ? err.message : String(err) };
|
|
117
|
+
}
|
|
118
|
+
return { ok: true, ...parseGitDiffStat(output) };
|
|
119
|
+
}
|
|
91
120
|
async function main() {
|
|
92
121
|
const opts = parseCliArgs(process.argv.slice(2));
|
|
93
122
|
const scope = detectScope(opts);
|
|
@@ -108,12 +137,12 @@ async function main() {
|
|
|
108
137
|
}
|
|
109
138
|
} catch {
|
|
110
139
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
140
|
+
// This tool always exits 0 on a parsed run (scope.ok:false only reflects a git
|
|
141
|
+
// diff failure inside the payload, never the process outcome) — force ok:true
|
|
142
|
+
// as emitResult's default so --jq/--silent compose without changing that.
|
|
143
|
+
process.exitCode = emitResult(
|
|
144
|
+
{ ...scope, eligibleForLightMode: eligible, threshold },
|
|
145
|
+
{ jq: opts.jq, silent: opts.silent, ok: true },
|
|
117
146
|
);
|
|
118
147
|
}
|
|
119
148
|
const isDirectRun =
|
|
@@ -124,4 +153,4 @@ if (isDirectRun) {
|
|
|
124
153
|
process.exitCode = 1;
|
|
125
154
|
});
|
|
126
155
|
}
|
|
127
|
-
export { detectScope, isEligibleForLightMode };
|
|
156
|
+
export { detectScope, detectMergeBaseScope, isEligibleForLightMode };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
3
|
import { parseArgs } from "node:util";
|
|
5
4
|
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
6
5
|
import {
|
|
@@ -28,6 +27,8 @@ import {
|
|
|
28
27
|
normalizeHeadScopedCommitStatus,
|
|
29
28
|
normalizeHeadScopedCiContract,
|
|
30
29
|
} from "@dev-loops/core/loop/copilot-ci-status";
|
|
30
|
+
import { resolveRepoRoot } from "./_repo-root-resolver.mjs";
|
|
31
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
31
32
|
const USAGE = `Usage:
|
|
32
33
|
detect-copilot-loop-state.mjs --repo <owner/name> --pr <number>
|
|
33
34
|
detect-copilot-loop-state.mjs --input <path>
|
|
@@ -53,9 +54,11 @@ Error output (stderr, JSON):
|
|
|
53
54
|
{ "ok": false, "error": "...", "usage": "..." }
|
|
54
55
|
gh/runtime failures:
|
|
55
56
|
{ "ok": false, "error": "..." }
|
|
57
|
+
${JQ_OUTPUT_USAGE}
|
|
56
58
|
Exit codes:
|
|
57
59
|
0 Success
|
|
58
|
-
1 Argument error, gh failure, or indeterminate state
|
|
60
|
+
1 Argument error, gh failure, or indeterminate state
|
|
61
|
+
2 Invalid --jq filter`.trim();
|
|
59
62
|
const VALID_OVERRIDE_STATUSES = new Set(["requested", "already-requested", "unavailable", "none", "failed"]);
|
|
60
63
|
const parseError = buildParseError(USAGE);
|
|
61
64
|
export function parseDetectCliArgs(argv) {
|
|
@@ -72,6 +75,7 @@ export function parseDetectCliArgs(argv) {
|
|
|
72
75
|
input: { type: "string" },
|
|
73
76
|
repo: { type: "string" },
|
|
74
77
|
pr: { type: "string" },
|
|
78
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
75
79
|
},
|
|
76
80
|
allowPositionals: true,
|
|
77
81
|
strict: false,
|
|
@@ -100,6 +104,7 @@ export function parseDetectCliArgs(argv) {
|
|
|
100
104
|
options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
|
|
101
105
|
continue;
|
|
102
106
|
}
|
|
107
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
103
108
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
104
109
|
}
|
|
105
110
|
if (options.inputPath !== undefined) {
|
|
@@ -419,6 +424,7 @@ export async function runCli(
|
|
|
419
424
|
argv = process.argv.slice(2),
|
|
420
425
|
{
|
|
421
426
|
stdout = process.stdout,
|
|
427
|
+
stderr = process.stderr,
|
|
422
428
|
env = process.env,
|
|
423
429
|
ghCommand = "gh",
|
|
424
430
|
} = {},
|
|
@@ -445,13 +451,13 @@ export async function runCli(
|
|
|
445
451
|
interpretationInput = snapshot;
|
|
446
452
|
}
|
|
447
453
|
let interpretation;
|
|
448
|
-
const config = await loadDevLoopConfig({ repoRoot:
|
|
454
|
+
const config = await loadDevLoopConfig({ repoRoot: resolveRepoRoot(process.cwd()) });
|
|
449
455
|
const refinementConfig = config.errors.length > 0
|
|
450
456
|
? resolveRefinement({ version: 1 })
|
|
451
457
|
: resolveRefinement(config.config);
|
|
452
458
|
interpretation = interpretLoopState(interpretationInput, refinementConfig);
|
|
453
459
|
const interpretationSummary = summarizeLoopInterpretation(interpretation);
|
|
454
|
-
|
|
460
|
+
process.exitCode = emitResult({
|
|
455
461
|
ok: true,
|
|
456
462
|
snapshot,
|
|
457
463
|
state: interpretation.state,
|
|
@@ -461,7 +467,7 @@ export async function runCli(
|
|
|
461
467
|
sameHeadCleanConverged: interpretation.sameHeadCleanConverged,
|
|
462
468
|
loopDisposition: interpretationSummary.loopDisposition,
|
|
463
469
|
terminal: interpretationSummary.terminal,
|
|
464
|
-
}
|
|
470
|
+
}, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
465
471
|
}
|
|
466
472
|
if (isDirectCliRun(import.meta.url)) {
|
|
467
473
|
runCli().catch((error) => {
|
|
@@ -3,6 +3,7 @@ import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "
|
|
|
3
3
|
import { parsePositiveInteger, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
4
4
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
6
7
|
const USAGE = `Usage: detect-copilot-session-activity.mjs --repo <owner/name> --branch <name> [--limit <number>]
|
|
7
8
|
Detect Copilot GitHub Actions session activity on a branch.
|
|
8
9
|
Required:
|
|
@@ -32,7 +33,8 @@ Error output (stderr, JSON):
|
|
|
32
33
|
Argument/usage errors:
|
|
33
34
|
{ "ok": false, "error": "...", "usage": "..." }
|
|
34
35
|
gh/runtime failures:
|
|
35
|
-
{ "ok": false, "error": "..." }
|
|
36
|
+
{ "ok": false, "error": "..." }
|
|
37
|
+
${JQ_OUTPUT_USAGE}`.trim();
|
|
36
38
|
const DEFAULT_LIMIT = 20;
|
|
37
39
|
const ACTIVE_RUN_STATUSES = new Set(["queued", "in_progress", "pending", "requested", "waiting"]);
|
|
38
40
|
const COPILOT_RUN_NAME_PATTERNS = Object.freeze([
|
|
@@ -55,6 +57,7 @@ export function parseDetectCopilotSessionActivityCliArgs(argv) {
|
|
|
55
57
|
repo: { type: "string" },
|
|
56
58
|
branch: { type: "string" },
|
|
57
59
|
limit: { type: "string" },
|
|
60
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
58
61
|
},
|
|
59
62
|
allowPositionals: true,
|
|
60
63
|
strict: false,
|
|
@@ -83,6 +86,7 @@ export function parseDetectCopilotSessionActivityCliArgs(argv) {
|
|
|
83
86
|
options.limit = parsePositiveInteger(requireTokenValue(token, parseError), "--limit", parseError);
|
|
84
87
|
continue;
|
|
85
88
|
}
|
|
89
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
86
90
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
87
91
|
}
|
|
88
92
|
if (options.repo === undefined || options.branch === undefined || options.branch.length === 0) {
|
|
@@ -185,7 +189,7 @@ export async function detectCopilotSessionActivity({ repo, branch, limit = DEFAU
|
|
|
185
189
|
}
|
|
186
190
|
export async function runCli(
|
|
187
191
|
argv = process.argv.slice(2),
|
|
188
|
-
{ stdout = process.stdout, env = process.env, ghCommand = "gh" } = {},
|
|
192
|
+
{ stdout = process.stdout, stderr = process.stderr, env = process.env, ghCommand = "gh" } = {},
|
|
189
193
|
) {
|
|
190
194
|
const options = parseDetectCopilotSessionActivityCliArgs(argv);
|
|
191
195
|
if (options.help) {
|
|
@@ -193,7 +197,7 @@ export async function runCli(
|
|
|
193
197
|
return;
|
|
194
198
|
}
|
|
195
199
|
const result = await detectCopilotSessionActivity(options, { env, ghCommand });
|
|
196
|
-
|
|
200
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
197
201
|
}
|
|
198
202
|
if (isDirectCliRun(import.meta.url)) {
|
|
199
203
|
runCli().catch((error) => {
|
|
@@ -5,6 +5,7 @@ import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
|
5
5
|
import { detectLinkedIssuePr } from "../github/detect-linked-issue-pr.mjs";
|
|
6
6
|
import { detectCopilotSessionActivity } from "./detect-copilot-session-activity.mjs";
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
8
9
|
const USAGE = `Usage: detect-initial-copilot-pr-state.mjs --repo <owner/name> --issue <number>
|
|
9
10
|
Detect whether an assigned issue is still on the bootstrap-only Copilot draft PR
|
|
10
11
|
or has moved into normal linked-PR follow-up.
|
|
@@ -43,7 +44,8 @@ Error output (stderr, JSON):
|
|
|
43
44
|
Argument/usage errors:
|
|
44
45
|
{ "ok": false, "error": "...", "usage": "..." }
|
|
45
46
|
gh/runtime failures:
|
|
46
|
-
{ "ok": false, "error": "..." }
|
|
47
|
+
{ "ok": false, "error": "..." }
|
|
48
|
+
${JQ_OUTPUT_USAGE}`.trim();
|
|
47
49
|
export const LINKED_PR_STATE = Object.freeze({
|
|
48
50
|
NO_LINKED_PR: "no_linked_pr",
|
|
49
51
|
PRIOR_LINKED_PR_CLOSED_UNMERGED: "prior_linked_pr_closed_unmerged",
|
|
@@ -91,6 +93,7 @@ export function parseDetectInitialCopilotPrStateCliArgs(argv) {
|
|
|
91
93
|
help: { type: "boolean", short: "h" },
|
|
92
94
|
repo: { type: "string" },
|
|
93
95
|
issue: { type: "string" },
|
|
96
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
94
97
|
},
|
|
95
98
|
allowPositionals: true,
|
|
96
99
|
strict: false,
|
|
@@ -115,6 +118,7 @@ export function parseDetectInitialCopilotPrStateCliArgs(argv) {
|
|
|
115
118
|
options.issue = parseIssueNumber(requireTokenValue(token, parseError), parseError);
|
|
116
119
|
continue;
|
|
117
120
|
}
|
|
121
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
118
122
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
119
123
|
}
|
|
120
124
|
if (options.repo === undefined || options.issue === undefined) {
|
|
@@ -313,7 +317,7 @@ export async function detectInitialCopilotPrState({ repo, issue }, { env = proce
|
|
|
313
317
|
}
|
|
314
318
|
export async function runCli(
|
|
315
319
|
argv = process.argv.slice(2),
|
|
316
|
-
{ stdout = process.stdout, env = process.env, ghCommand = "gh" } = {},
|
|
320
|
+
{ stdout = process.stdout, stderr = process.stderr, env = process.env, ghCommand = "gh" } = {},
|
|
317
321
|
) {
|
|
318
322
|
const options = parseDetectInitialCopilotPrStateCliArgs(argv);
|
|
319
323
|
if (options.help) {
|
|
@@ -324,7 +328,7 @@ export async function runCli(
|
|
|
324
328
|
{ repo: options.repo, issue: options.issue },
|
|
325
329
|
{ env, ghCommand },
|
|
326
330
|
);
|
|
327
|
-
|
|
331
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
328
332
|
}
|
|
329
333
|
if (isDirectCliRun(import.meta.url)) {
|
|
330
334
|
runCli().catch((error) => {
|
|
@@ -6,6 +6,7 @@ import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helper
|
|
|
6
6
|
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
7
7
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
9
10
|
|
|
10
11
|
const USAGE = `Usage: detect-internal-only-pr.mjs --repo <owner/name> --pr <number> [--config <path>]
|
|
11
12
|
Detect whether a PR only touches internal tooling files (scripts, docs, tests, config)
|
|
@@ -20,9 +21,11 @@ Optional:
|
|
|
20
21
|
Output (stdout, JSON):
|
|
21
22
|
{ "ok": true, "internalOnly": true|false, "files": ["path1", "path2", ...],
|
|
22
23
|
"reason": "...", "repo": "...", "pr": N }
|
|
24
|
+
${JQ_OUTPUT_USAGE}
|
|
23
25
|
Exit codes:
|
|
24
26
|
0 Success
|
|
25
|
-
1 Argument error or gh failure
|
|
27
|
+
1 Argument error or gh failure
|
|
28
|
+
2 Invalid --jq filter`.trim();
|
|
26
29
|
|
|
27
30
|
const parseError = buildParseError(USAGE);
|
|
28
31
|
|
|
@@ -134,6 +137,7 @@ export function parseCliArgs(argv) {
|
|
|
134
137
|
pr: { type: "string" },
|
|
135
138
|
config: { type: "string" },
|
|
136
139
|
"label-check": { type: "boolean" },
|
|
140
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
137
141
|
},
|
|
138
142
|
allowPositionals: true,
|
|
139
143
|
strict: false,
|
|
@@ -166,6 +170,7 @@ export function parseCliArgs(argv) {
|
|
|
166
170
|
options.labelCheck = true;
|
|
167
171
|
continue;
|
|
168
172
|
}
|
|
173
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
169
174
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
170
175
|
}
|
|
171
176
|
if (options.repo === undefined || options.pr === undefined) {
|
|
@@ -265,6 +270,7 @@ export async function runCli(
|
|
|
265
270
|
argv = process.argv.slice(2),
|
|
266
271
|
{
|
|
267
272
|
stdout = process.stdout,
|
|
273
|
+
stderr = process.stderr,
|
|
268
274
|
env = process.env,
|
|
269
275
|
ghCommand = "gh",
|
|
270
276
|
} = {},
|
|
@@ -275,7 +281,7 @@ export async function runCli(
|
|
|
275
281
|
return;
|
|
276
282
|
}
|
|
277
283
|
const result = await detectInternalOnly(options, { env, ghCommand });
|
|
278
|
-
|
|
284
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
279
285
|
}
|
|
280
286
|
|
|
281
287
|
if (isDirectCliRun(import.meta.url)) {
|