dev-loops 0.7.2 → 0.8.0
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 +1 -1
- package/.claude/agents/refiner.md +2 -2
- package/.claude/agents/review.md +9 -12
- package/.claude/commands/loop-continue.md +2 -1
- package/.claude/commands/loop-enqueue.md +8 -1
- package/.claude/commands/loop-info.md +1 -1
- package/.claude/hooks/_bash-command-classify.mjs +7 -6
- package/.claude/hooks/_hook-decisions.mjs +5 -4
- package/.claude/skills/copilot-pr-followup/SKILL.md +5 -5
- package/.claude/skills/dev-loop/SKILL.md +7 -7
- package/.claude/skills/docs/acceptance-criteria-verification.md +12 -4
- package/.claude/skills/docs/anti-patterns.md +3 -3
- package/.claude/skills/docs/artifact-authority-contract.md +6 -4
- package/.claude/skills/docs/confirmation-rules.md +1 -1
- package/.claude/skills/docs/copilot-loop-operations.md +4 -2
- package/.claude/skills/docs/issue-intake-procedure.md +12 -3
- package/.claude/skills/docs/merge-preconditions.md +5 -3
- package/.claude/skills/docs/pr-lifecycle-contract.md +1 -1
- package/.claude/skills/docs/public-dev-loop-contract.md +2 -1
- package/.claude/skills/docs/retrospective-checkpoint-contract.md +9 -6
- package/.claude/skills/docs/validation-policy.md +1 -1
- package/.claude/skills/local-implementation/SKILL.md +3 -3
- package/.claude/skills/loop-grill/SKILL.md +34 -14
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +30 -0
- package/README.md +95 -189
- package/agents/refiner.agent.md +2 -2
- package/agents/review.agent.md +9 -12
- package/extension/README.md +5 -4
- package/package.json +5 -4
- package/scripts/docs/validate-state-machine-conformance.mjs +78 -1
- package/scripts/github/_gate-names.mjs +5 -0
- package/scripts/github/capture-review-threads.mjs +2 -2
- package/scripts/github/detect-checkpoint-evidence.mjs +7 -7
- package/scripts/github/edit-issue.mjs +259 -0
- package/scripts/github/probe-ci-status.mjs +18 -0
- package/scripts/github/probe-copilot-review.mjs +24 -3
- package/scripts/github/reconcile-draft-gate.mjs +13 -13
- package/scripts/github/request-copilot-review.mjs +17 -16
- package/scripts/github/upsert-checkpoint-verdict.mjs +25 -23
- package/scripts/github/verify-briefing-prefixes.mjs +224 -33
- package/scripts/github/write-gate-context.mjs +8 -4
- package/scripts/loop/_post-convergence-change.mjs +2 -2
- package/scripts/loop/_pr-runner-coordination.mjs +112 -13
- package/scripts/loop/check-retro-tooling.mjs +14 -9
- package/scripts/loop/copilot-pr-handoff.mjs +18 -14
- package/scripts/loop/detect-copilot-loop-state.mjs +11 -11
- package/scripts/loop/detect-internal-only-pr.mjs +6 -6
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +117 -15
- package/scripts/loop/detect-refinement-grill-state.mjs +136 -0
- package/scripts/loop/run-watch-cycle.mjs +42 -7
- package/scripts/loop/sanctioned-commands.mjs +1 -0
- package/scripts/pages/build-state-atlas.mjs +15 -0
- package/scripts/projects/add-queue-item.mjs +87 -4
- package/skills/copilot-pr-followup/SKILL.md +5 -5
- package/skills/dev-loop/SKILL.md +2 -2
- package/skills/docs/acceptance-criteria-verification.md +12 -4
- package/skills/docs/anti-patterns.md +3 -3
- package/skills/docs/artifact-authority-contract.md +6 -4
- package/skills/docs/confirmation-rules.md +1 -1
- package/skills/docs/copilot-loop-operations.md +4 -2
- package/skills/docs/issue-intake-procedure.md +12 -3
- package/skills/docs/merge-preconditions.md +5 -3
- package/skills/docs/pr-lifecycle-contract.md +1 -1
- package/skills/docs/public-dev-loop-contract.md +2 -1
- package/skills/docs/required-rules.json +17 -1
- package/skills/docs/retrospective-checkpoint-contract.md +9 -6
- package/skills/docs/validation-policy.md +1 -1
- package/skills/local-implementation/SKILL.md +3 -3
- package/skills/loop-grill/SKILL.md +38 -17
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { buildParseError, formatCliError, isCopilotLogin, isDirectCliRun, normalizeTimestamp, parseJsonText } from "../_core-helpers.mjs";
|
|
3
|
-
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
3
|
+
import { parsePrNumber, requireTokenValue, runChild as defaultRunChild } from "../_cli-primitives.mjs";
|
|
4
4
|
import { detectPostConvergenceSignificantChange } from "./_post-convergence-change.mjs";
|
|
5
5
|
import { detectRepoSlug, parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
6
6
|
import { resolveRunId } from "@dev-loops/core/loop/run-context";
|
|
@@ -227,7 +227,7 @@ export function parseHandoffCliArgs(argv, { cwd = process.cwd() } = {}) {
|
|
|
227
227
|
* loop should pause for operator review.
|
|
228
228
|
* Returns { paused: true, humanComments: [...] } when human comments need attention.
|
|
229
229
|
*/
|
|
230
|
-
export async function detectRecentHumanComments({ repo, pr, claimedAtMs }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
230
|
+
export async function detectRecentHumanComments({ repo, pr, claimedAtMs }, { env = process.env, ghCommand = "gh", runChild = defaultRunChild } = {}) {
|
|
231
231
|
try {
|
|
232
232
|
const result = await runChild(
|
|
233
233
|
ghCommand,
|
|
@@ -312,7 +312,7 @@ export async function detectRecentHumanComments({ repo, pr, claimedAtMs }, { env
|
|
|
312
312
|
// (#1103, #1126): the current head, the Copilot reviews (to find the last
|
|
313
313
|
// reviewed head), and the PR's changed files. Fetched only when the interpreter
|
|
314
314
|
// already resolved ROUND_CAP_CLEAN_FALLBACK, so this extra call is off the hot path.
|
|
315
|
-
async function fetchReopenCycleFacts({ repo, pr }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
315
|
+
async function fetchReopenCycleFacts({ repo, pr }, { env = process.env, ghCommand = "gh", runChild = defaultRunChild } = {}) {
|
|
316
316
|
const result = await runChild(
|
|
317
317
|
ghCommand,
|
|
318
318
|
["pr", "view", String(pr), "--repo", repo, "--json", "headRefOid,reviews,files"],
|
|
@@ -328,12 +328,16 @@ async function fetchReopenCycleFacts({ repo, pr }, { env = process.env, ghComman
|
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
export async function runHandoff(options, { env = process.env, ghCommand = "gh" } = {}) {
|
|
331
|
+
export async function runHandoff(options, { env = process.env, ghCommand = "gh", runChild = defaultRunChild, repoRoot } = {}) {
|
|
332
|
+
// Single resolved repo root for config + runner-coordination reads/writes.
|
|
333
|
+
// Defaults to the checkout's git toplevel (production); an injected repoRoot
|
|
334
|
+
// keeps the whole cascade hermetic when driven in-process.
|
|
335
|
+
const resolvedRepoRoot = repoRoot ?? resolveRepoRoot(process.cwd());
|
|
332
336
|
const runnerOwnership = await ensureAsyncRunnerOwnership({
|
|
333
337
|
repo: options.repo,
|
|
334
338
|
pr: options.pr,
|
|
335
339
|
env,
|
|
336
|
-
cwd:
|
|
340
|
+
cwd: resolvedRepoRoot,
|
|
337
341
|
claimIfMissing: true,
|
|
338
342
|
});
|
|
339
343
|
if (!runnerOwnership.ok) {
|
|
@@ -363,9 +367,9 @@ export async function runHandoff(options, { env = process.env, ghCommand = "gh"
|
|
|
363
367
|
}
|
|
364
368
|
let snapshot = await autoDetectSnapshot(
|
|
365
369
|
{ repo: options.repo, pr: options.pr },
|
|
366
|
-
{ env, ghCommand },
|
|
370
|
+
{ env, ghCommand, runChild },
|
|
367
371
|
);
|
|
368
|
-
const config = await loadDevLoopConfig({ repoRoot:
|
|
372
|
+
const config = await loadDevLoopConfig({ repoRoot: resolvedRepoRoot });
|
|
369
373
|
if (config.errors?.length > 0) {
|
|
370
374
|
console.error("[copilot-pr-handoff] config warnings:", JSON.stringify(config.errors));
|
|
371
375
|
}
|
|
@@ -389,7 +393,7 @@ export async function runHandoff(options, { env = process.env, ghCommand = "gh"
|
|
|
389
393
|
if (resolveRunId(env)) {
|
|
390
394
|
humanCommentCheck = await detectRecentHumanComments(
|
|
391
395
|
{ repo: options.repo, pr: options.pr, claimedAtMs: runnerOwnership?.activeRun?.claimedAt ? new Date(runnerOwnership.activeRun.claimedAt).getTime() : undefined },
|
|
392
|
-
{ env, ghCommand },
|
|
396
|
+
{ env, ghCommand, runChild },
|
|
393
397
|
);
|
|
394
398
|
}
|
|
395
399
|
const TERMINAL_STATES = new Set([STATE.NO_PR, STATE.DONE, STATE.BLOCKED_NEEDS_USER_DECISION]);
|
|
@@ -399,7 +403,7 @@ export async function runHandoff(options, { env = process.env, ghCommand = "gh"
|
|
|
399
403
|
repo: options.repo,
|
|
400
404
|
pr: options.pr,
|
|
401
405
|
env,
|
|
402
|
-
cwd:
|
|
406
|
+
cwd: resolvedRepoRoot,
|
|
403
407
|
});
|
|
404
408
|
return {
|
|
405
409
|
ok: true,
|
|
@@ -467,7 +471,7 @@ export async function runHandoff(options, { env = process.env, ghCommand = "gh"
|
|
|
467
471
|
interpretation.state === STATE.READY_TO_REREQUEST_REVIEW)
|
|
468
472
|
) {
|
|
469
473
|
try {
|
|
470
|
-
const internalCheck = await detectPrInternalOnly(options, { env, ghCommand });
|
|
474
|
+
const internalCheck = await detectPrInternalOnly(options, { env, ghCommand, runChild });
|
|
471
475
|
if (internalCheck.ok && internalCheck.internalOnly) {
|
|
472
476
|
internalOnlySkipCopilot = true;
|
|
473
477
|
interpretation = {
|
|
@@ -526,7 +530,7 @@ export async function runHandoff(options, { env = process.env, ghCommand = "gh"
|
|
|
526
530
|
if (!internalOnlySkipCopilot
|
|
527
531
|
&& options.watchStatus === undefined
|
|
528
532
|
&& interpretation.state === STATE.ROUND_CAP_CLEAN_FALLBACK) {
|
|
529
|
-
const reopenFacts = await fetchReopenCycleFacts(options, { env, ghCommand });
|
|
533
|
+
const reopenFacts = await fetchReopenCycleFacts(options, { env, ghCommand, runChild });
|
|
530
534
|
const significant = await detectPostConvergenceSignificantChange(
|
|
531
535
|
{
|
|
532
536
|
repo: options.repo,
|
|
@@ -537,7 +541,7 @@ export async function runHandoff(options, { env = process.env, ghCommand = "gh"
|
|
|
537
541
|
roundCapReached: true,
|
|
538
542
|
regularCopilotRounds: (snapshot.copilotReviewRoundCount ?? 0) > 0,
|
|
539
543
|
},
|
|
540
|
-
{ env, ghCommand },
|
|
544
|
+
{ env, ghCommand, runChild },
|
|
541
545
|
);
|
|
542
546
|
if (significant) {
|
|
543
547
|
reopenedCapCycle = true;
|
|
@@ -573,7 +577,7 @@ export async function runHandoff(options, { env = process.env, ghCommand = "gh"
|
|
|
573
577
|
// backstop must never permit rounds the interpreter already forbids.
|
|
574
578
|
lightweight: options.lightweight,
|
|
575
579
|
},
|
|
576
|
-
{ env, ghCommand },
|
|
580
|
+
{ env, ghCommand, runChild },
|
|
577
581
|
);
|
|
578
582
|
reviewRequestStatus = requestResult.status;
|
|
579
583
|
snapshot = applyConfirmedReviewRequest(snapshot, reviewRequestStatus);
|
|
@@ -659,7 +663,7 @@ export async function runHandoff(options, { env = process.env, ghCommand = "gh"
|
|
|
659
663
|
repo: options.repo,
|
|
660
664
|
pr: options.pr,
|
|
661
665
|
env,
|
|
662
|
-
cwd:
|
|
666
|
+
cwd: resolvedRepoRoot,
|
|
663
667
|
});
|
|
664
668
|
if (runnerRelease.status !== "skipped_no_async_run_id") {
|
|
665
669
|
result.runnerRelease = runnerRelease;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
|
-
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
4
|
+
import { parsePrNumber, requireTokenValue, runChild as defaultRunChild } from "../_cli-primitives.mjs";
|
|
5
5
|
import {
|
|
6
6
|
buildParseError,
|
|
7
7
|
formatCliError,
|
|
@@ -140,7 +140,7 @@ export function parseDetectCliArgs(argv) {
|
|
|
140
140
|
}
|
|
141
141
|
return options;
|
|
142
142
|
}
|
|
143
|
-
async function fetchPrView({ repo, pr }, { env, ghCommand }) {
|
|
143
|
+
async function fetchPrView({ repo, pr }, { env, ghCommand, runChild = defaultRunChild }) {
|
|
144
144
|
const result = await runChild(
|
|
145
145
|
ghCommand,
|
|
146
146
|
["pr", "view", String(pr), "--repo", repo, "--json", "headRefOid,isDraft,state,number,reviews,statusCheckRollup"],
|
|
@@ -161,7 +161,7 @@ async function fetchPrView({ repo, pr }, { env, ghCommand }) {
|
|
|
161
161
|
}
|
|
162
162
|
return payload;
|
|
163
163
|
}
|
|
164
|
-
async function fetchCopilotRequested({ repo, pr }, { env, ghCommand }) {
|
|
164
|
+
async function fetchCopilotRequested({ repo, pr }, { env, ghCommand, runChild = defaultRunChild }) {
|
|
165
165
|
const result = await runChild(
|
|
166
166
|
ghCommand,
|
|
167
167
|
["api", `repos/${repo}/pulls/${pr}/requested_reviewers`],
|
|
@@ -180,7 +180,7 @@ async function fetchCopilotRequested({ repo, pr }, { env, ghCommand }) {
|
|
|
180
180
|
const users = Array.isArray(payload?.users) ? payload.users : [];
|
|
181
181
|
return users.some((user) => isCopilotLogin(user?.login));
|
|
182
182
|
}
|
|
183
|
-
async function fetchLatestCopilotReviewRequestAt({ repo, pr }, { env, ghCommand }) {
|
|
183
|
+
async function fetchLatestCopilotReviewRequestAt({ repo, pr }, { env, ghCommand, runChild = defaultRunChild }) {
|
|
184
184
|
const result = await runChild(
|
|
185
185
|
ghCommand,
|
|
186
186
|
["api", `repos/${repo}/issues/${pr}/timeline`, "--paginate", "--jq",
|
|
@@ -212,7 +212,7 @@ function extractPrVisibleCheckNames(statusCheckRollup) {
|
|
|
212
212
|
.filter((name) => typeof name === "string" && name.length > 0);
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
async function fetchCurrentHeadCiEvidence({ repo, headSha, prVisibleCheckNames }, { env, ghCommand }) {
|
|
215
|
+
async function fetchCurrentHeadCiEvidence({ repo, headSha, prVisibleCheckNames }, { env, ghCommand, runChild = defaultRunChild }) {
|
|
216
216
|
const [checkRunsResult, statusesResult] = await Promise.all([
|
|
217
217
|
runChild(
|
|
218
218
|
ghCommand,
|
|
@@ -328,8 +328,8 @@ function hasSubmittedCopilotReviewOffCurrentHead(reviewSummary, currentHeadSha)
|
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
|
|
331
|
-
export async function autoDetectSnapshot({ repo, pr, reviewRequestStatusOverride, localValidationHeadSha, draftGateResetAtMs }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
332
|
-
const prData = await fetchPrView({ repo, pr }, { env, ghCommand });
|
|
331
|
+
export async function autoDetectSnapshot({ repo, pr, reviewRequestStatusOverride, localValidationHeadSha, draftGateResetAtMs }, { env = process.env, ghCommand = "gh", runChild = defaultRunChild } = {}) {
|
|
332
|
+
const prData = await fetchPrView({ repo, pr }, { env, ghCommand, runChild });
|
|
333
333
|
if (prData === null) {
|
|
334
334
|
return normalizeSnapshot({ prExists: false });
|
|
335
335
|
}
|
|
@@ -355,13 +355,13 @@ export async function autoDetectSnapshot({ repo, pr, reviewRequestStatusOverride
|
|
|
355
355
|
} else if (reviewSummary.hasPendingReviewOnCurrentHead) {
|
|
356
356
|
copilotReviewRequestStatus = "requested";
|
|
357
357
|
} else {
|
|
358
|
-
const copilotRequested = await fetchCopilotRequested({ repo, pr }, { env, ghCommand });
|
|
358
|
+
const copilotRequested = await fetchCopilotRequested({ repo, pr }, { env, ghCommand, runChild });
|
|
359
359
|
if (!copilotRequested) {
|
|
360
360
|
copilotReviewRequestStatus = "none";
|
|
361
361
|
} else if (!reviewSummary.hasSubmittedReviewOnCurrentHead) {
|
|
362
362
|
copilotReviewRequestStatus = "requested";
|
|
363
363
|
} else {
|
|
364
|
-
const latestRequestAt = await fetchLatestCopilotReviewRequestAt({ repo, pr }, { env, ghCommand });
|
|
364
|
+
const latestRequestAt = await fetchLatestCopilotReviewRequestAt({ repo, pr }, { env, ghCommand, runChild });
|
|
365
365
|
const latestReviewAt = reviewSummary.latestSubmittedReviewOnCurrentHeadAt;
|
|
366
366
|
if (latestRequestAt !== null && latestReviewAt !== null && latestRequestAt > latestReviewAt) {
|
|
367
367
|
copilotReviewRequestStatus = "requested";
|
|
@@ -376,7 +376,7 @@ export async function autoDetectSnapshot({ repo, pr, reviewRequestStatusOverride
|
|
|
376
376
|
let actionableThreadCount = 0;
|
|
377
377
|
let lastCopilotRoundMaxSignal = null;
|
|
378
378
|
try {
|
|
379
|
-
const threadsPayload = await fetchGithubReviewThreadsPayload({ repo, pr }, { env, ghCommand });
|
|
379
|
+
const threadsPayload = await fetchGithubReviewThreadsPayload({ repo, pr }, { env, ghCommand, runChild });
|
|
380
380
|
const parsed = parseReviewThreads(threadsPayload);
|
|
381
381
|
unresolvedThreadCount = parsed.summary.unresolvedThreads;
|
|
382
382
|
actionableThreadCount = parsed.summary.actionableThreads;
|
|
@@ -400,7 +400,7 @@ export async function autoDetectSnapshot({ repo, pr, reviewRequestStatusOverride
|
|
|
400
400
|
let failureDetails = [];
|
|
401
401
|
let excludedFailureDetails = [];
|
|
402
402
|
if (shouldRefreshCurrentHeadCi) {
|
|
403
|
-
const refreshed = await fetchCurrentHeadCiEvidence({ repo, headSha: prHeadSha, prVisibleCheckNames }, { env, ghCommand });
|
|
403
|
+
const refreshed = await fetchCurrentHeadCiEvidence({ repo, headSha: prHeadSha, prVisibleCheckNames }, { env, ghCommand, runChild });
|
|
404
404
|
currentHeadCiStatus = refreshed?.status ?? "none";
|
|
405
405
|
failureDetails = refreshed?.failureDetails ?? [];
|
|
406
406
|
excludedFailureDetails = refreshed?.excludedFailureDetails ?? [];
|
|
@@ -3,7 +3,7 @@ import { statSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
5
|
import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
|
|
6
|
-
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
6
|
+
import { parsePrNumber, requireTokenValue, runChild as defaultRunChild } from "../_cli-primitives.mjs";
|
|
7
7
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
9
|
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
@@ -184,7 +184,7 @@ export function parseCliArgs(argv) {
|
|
|
184
184
|
return options;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
async function fetchPrFiles({ repo, pr }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
187
|
+
async function fetchPrFiles({ repo, pr }, { env = process.env, ghCommand = "gh", runChild = defaultRunChild } = {}) {
|
|
188
188
|
const result = await runChild(
|
|
189
189
|
ghCommand,
|
|
190
190
|
["pr", "view", String(pr), "--repo", repo, "--json", "files", "--jq", ".files[].path"],
|
|
@@ -198,7 +198,7 @@ async function fetchPrFiles({ repo, pr }, { env = process.env, ghCommand = "gh"
|
|
|
198
198
|
return paths;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
async function fetchPrLabels({ repo, pr }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
201
|
+
async function fetchPrLabels({ repo, pr }, { env = process.env, ghCommand = "gh", runChild = defaultRunChild } = {}) {
|
|
202
202
|
const result = await runChild(
|
|
203
203
|
ghCommand,
|
|
204
204
|
["pr", "view", String(pr), "--repo", repo, "--json", "labels", "--jq", ".labels[].name"],
|
|
@@ -219,10 +219,10 @@ async function fetchPrLabels({ repo, pr }, { env = process.env, ghCommand = "gh"
|
|
|
219
219
|
* - If ANY changed file doesn't match any pattern → internalOnly=false
|
|
220
220
|
* - No blacklist needed — a non-matching file is consumer-facing by definition.
|
|
221
221
|
*/
|
|
222
|
-
export async function detectInternalOnly(options, { env = process.env, ghCommand = "gh" } = {}) {
|
|
222
|
+
export async function detectInternalOnly(options, { env = process.env, ghCommand = "gh", runChild = defaultRunChild } = {}) {
|
|
223
223
|
const patterns = loadInternalPathPatterns(options.config);
|
|
224
224
|
const matchers = buildPatternMatchers(patterns);
|
|
225
|
-
const files = await fetchPrFiles(options, { env, ghCommand });
|
|
225
|
+
const files = await fetchPrFiles(options, { env, ghCommand, runChild });
|
|
226
226
|
|
|
227
227
|
if (files.length === 0) {
|
|
228
228
|
return {
|
|
@@ -250,7 +250,7 @@ export async function detectInternalOnly(options, { env = process.env, ghCommand
|
|
|
250
250
|
|
|
251
251
|
// Check for explicit internal_only label if requested (confirmation only)
|
|
252
252
|
if (options.labelCheck) {
|
|
253
|
-
const labels = await fetchPrLabels(options, { env, ghCommand });
|
|
253
|
+
const labels = await fetchPrLabels(options, { env, ghCommand, runChild });
|
|
254
254
|
if (labels.includes("internal_only")) {
|
|
255
255
|
// Label confirms — path check already passed
|
|
256
256
|
}
|
|
@@ -10,12 +10,13 @@ import {
|
|
|
10
10
|
resolveDraftGateRoundResetMs,
|
|
11
11
|
summarizeCopilotReviews,
|
|
12
12
|
} from "../_core-helpers.mjs";
|
|
13
|
-
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
13
|
+
import { parsePrNumber, requireTokenValue, runChild as defaultRunChild } from "../_cli-primitives.mjs";
|
|
14
14
|
import { loadDevLoopConfig, resolveEffectiveCopilotRoundCap, resolveGateConfig, resolveRefinement, resolveRefinementConfig, resolveWorkflowConfig } from "@dev-loops/core/config";
|
|
15
15
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
16
16
|
import { buildSnapshotFromPrFacts, interpretLoopState, isCopilotRoundCapReached, summarizeLoopInterpretation } from "@dev-loops/core/loop/copilot-loop-state";
|
|
17
|
-
import { evaluatePrGateCoordination, PR_CHECKPOINT, PR_CHECKPOINT_ACTION } from "@dev-loops/core/loop/pr-gate-coordination";
|
|
17
|
+
import { evaluatePrGateCoordination, PR_CHECKPOINT, PR_CHECKPOINT_ACTION, REFINEMENT_ARTIFACT_SPEC_SOURCE } from "@dev-loops/core/loop/pr-gate-coordination";
|
|
18
18
|
import { shouldGuardCopilotReviewRequest } from "@dev-loops/core/loop/pr-gate-coordination";
|
|
19
|
+
import { PLAN_FILE_PROMOTION_DOC_PATH_PATTERN } from "@dev-loops/core/loop/plan-file-promote-contract";
|
|
19
20
|
import { UI_E2E_CHECK_NAMES } from "@dev-loops/core/loop/ui-e2e-scoping";
|
|
20
21
|
import { fetchGithubReviewThreadsPayload } from "../github/capture-review-threads.mjs";
|
|
21
22
|
import { detectPostConvergenceSignificantChange } from "./_post-convergence-change.mjs";
|
|
@@ -67,11 +68,19 @@ Output (stdout, JSON):
|
|
|
67
68
|
"headSha": null,
|
|
68
69
|
"verdict": null
|
|
69
70
|
},
|
|
71
|
+
"refinementArtifact": {
|
|
72
|
+
"status": "present",
|
|
73
|
+
"specSource": "linked_issue",
|
|
74
|
+
"reason": "...",
|
|
75
|
+
"finding": null
|
|
76
|
+
},
|
|
70
77
|
"allowedNextActions": ["resolve_merge_conflicts"],
|
|
71
78
|
"forbiddenActions": ["run_pre_approval_gate", "declare_merge_ready"],
|
|
72
79
|
"nextAction": "resolve_merge_conflicts",
|
|
73
80
|
"reason": "..."
|
|
74
81
|
}
|
|
82
|
+
(refinementArtifact.planDocPath is present only when specSource is
|
|
83
|
+
"plan_file"; the key is omitted entirely for "linked_issue" and "pr_body")
|
|
75
84
|
Error output (stderr, JSON):
|
|
76
85
|
{ "ok": false, "error": "...", "usage": "..." }
|
|
77
86
|
{ "ok": false, "error": "..." }
|
|
@@ -167,7 +176,7 @@ export function parseGitStatusConflictFiles(text) {
|
|
|
167
176
|
}
|
|
168
177
|
return conflictFiles;
|
|
169
178
|
}
|
|
170
|
-
async function fetchRequestedReviewers({ repo, pr }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
179
|
+
async function fetchRequestedReviewers({ repo, pr }, { env = process.env, ghCommand = "gh", runChild = defaultRunChild } = {}) {
|
|
171
180
|
const result = await runChild(
|
|
172
181
|
ghCommand,
|
|
173
182
|
["api", `repos/${repo}/pulls/${pr}/requested_reviewers`],
|
|
@@ -179,7 +188,7 @@ async function fetchRequestedReviewers({ repo, pr }, { env = process.env, ghComm
|
|
|
179
188
|
}
|
|
180
189
|
return parseRequestedReviewersPayload(result.stdout);
|
|
181
190
|
}
|
|
182
|
-
async function fetchPrFacts({ repo, pr }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
191
|
+
async function fetchPrFacts({ repo, pr }, { env = process.env, ghCommand = "gh", runChild = defaultRunChild } = {}) {
|
|
183
192
|
const result = await runChild(
|
|
184
193
|
ghCommand,
|
|
185
194
|
["pr", "view", String(pr), "--repo", repo, "--json", "number,state,isDraft,headRefOid,mergeable,mergeStateStatus,body,title,closingIssuesReferences,reviews,statusCheckRollup,files"],
|
|
@@ -202,18 +211,19 @@ export async function fetchPrFactsWithSettledMergeable(
|
|
|
202
211
|
{
|
|
203
212
|
env = process.env,
|
|
204
213
|
ghCommand = "gh",
|
|
214
|
+
runChild = defaultRunChild,
|
|
205
215
|
maxPolls = 3,
|
|
206
216
|
pollDelayMs = 1500,
|
|
207
217
|
sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
208
218
|
fetch = fetchPrFacts,
|
|
209
219
|
} = {},
|
|
210
220
|
) {
|
|
211
|
-
let prData = await fetch(options, { env, ghCommand });
|
|
221
|
+
let prData = await fetch(options, { env, ghCommand, runChild });
|
|
212
222
|
let polls = 0;
|
|
213
223
|
while (String(prData?.mergeable || "").toUpperCase() === "UNKNOWN" && polls < maxPolls) {
|
|
214
224
|
polls += 1;
|
|
215
225
|
await sleep(pollDelayMs);
|
|
216
|
-
prData = await fetch(options, { env, ghCommand });
|
|
226
|
+
prData = await fetch(options, { env, ghCommand, runChild });
|
|
217
227
|
}
|
|
218
228
|
return prData;
|
|
219
229
|
}
|
|
@@ -271,7 +281,7 @@ export function resolveLinkedIssuesFromPr(prData) {
|
|
|
271
281
|
return dedupe(matches.map((m) => Number((/(\d+)/.exec(m) || [])[1])));
|
|
272
282
|
}
|
|
273
283
|
|
|
274
|
-
async function fetchIssueBody({ repo, issue }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
284
|
+
async function fetchIssueBody({ repo, issue }, { env = process.env, ghCommand = "gh", runChild = defaultRunChild } = {}) {
|
|
275
285
|
const result = await runChild(
|
|
276
286
|
ghCommand,
|
|
277
287
|
["issue", "view", String(issue), "--repo", repo, "--json", "body"],
|
|
@@ -287,18 +297,107 @@ async function fetchIssueBody({ repo, issue }, { env = process.env, ghCommand =
|
|
|
287
297
|
return null;
|
|
288
298
|
}
|
|
289
299
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
300
|
+
// Plan-file promotion PRs (P4, `buildPromotionPrBody`) carry no linked issue by
|
|
301
|
+
// design; the PR body instead names the committed plan doc as the spec-of-record
|
|
302
|
+
// verbatim (marker owned by plan-file-promote-contract.mjs). Matching that
|
|
303
|
+
// literal marker is how the refinement check tells a promoted plan-file PR
|
|
304
|
+
// apart from an ordinary issue-less body.
|
|
305
|
+
|
|
306
|
+
// Resolve the refinement artifact for a draft PR with zero linked issues,
|
|
307
|
+
// directly from its own body. The sanctioned three-origin model
|
|
308
|
+
// (artifact-authority-contract.md) backs a draft PR with a linked issue, a
|
|
309
|
+
// promoted plan file, or the PR body itself as the spec-of-record; only the
|
|
310
|
+
// first fetches an issue body, so this never needs a network call.
|
|
311
|
+
// `specSource` here (linked_issue|pr_body|plan_file) is a distinct enum from
|
|
312
|
+
// handoff-envelope.mjs's CANONICAL_SPEC_SOURCE (phase_doc|pr_body): same field
|
|
313
|
+
// name, different object, different value space — never conflate the two.
|
|
314
|
+
async function resolveNoIssueRefinementArtifact(body) {
|
|
315
|
+
const { detectIssueRefinementArtifact, validatePrBodySpec } = await import("@dev-loops/core/loop/issue-refinement-artifact");
|
|
316
|
+
const planDocMatch = PLAN_FILE_PROMOTION_DOC_PATH_PATTERN.exec(body);
|
|
317
|
+
if (planDocMatch) {
|
|
318
|
+
const planDocPath = planDocMatch[1].trim();
|
|
319
|
+
// The marker sentence alone is spoofable (any body can paste it): require the
|
|
320
|
+
// body to still carry real AC/DoD checklist content, the same invariant
|
|
321
|
+
// `buildPromotionPrBody` actually embeds, before trusting the plan-file claim.
|
|
322
|
+
// Deliberately narrower than `artifact.hasACs`: that flag also turns true on a
|
|
323
|
+
// bare linked-refinement-doc mention (no checklist items at all), which is the
|
|
324
|
+
// right OR for a linked ISSUE body but not for a plan-file promotion PR body —
|
|
325
|
+
// a plan-file claim must be backed by actual AC/DoD checklist items, not just
|
|
326
|
+
// another doc mention next to the marker.
|
|
327
|
+
// Plan-file promotion is issue-less by design (`buildPromotionPrBody`
|
|
328
|
+
// neutralizes closing keywords): any closing reference the body carries —
|
|
329
|
+
// including the cross-repo `owner/repo#N` form validatePrBodySpec also
|
|
330
|
+
// accepts — means the body would auto-close an issue it doesn't actually
|
|
331
|
+
// back. Reuse validatePrBodySpec's closing-reference extraction
|
|
332
|
+
// (issueLess mode's `closesIssues`) instead of a second regex.
|
|
333
|
+
const closesIssues = validatePrBodySpec({ body, issueLess: true }).closesIssues;
|
|
334
|
+
if (closesIssues.length > 0) {
|
|
294
335
|
return {
|
|
295
336
|
status: "missing",
|
|
296
337
|
linkedIssue: null,
|
|
297
338
|
linkedIssues: [],
|
|
298
|
-
|
|
339
|
+
specSource: REFINEMENT_ARTIFACT_SPEC_SOURCE.PLAN_FILE,
|
|
340
|
+
planDocPath,
|
|
341
|
+
reason: `PR body names the promoted plan doc \`${planDocPath}\` as the spec-of-record but also carries a GitHub closing reference (${closesIssues.map((n) => `#${n}`).join(", ")}); plan-file promotion is issue-less by design and must not carry a reference that would auto-close an issue it doesn't back.`,
|
|
299
342
|
finding: "missing_refinement_artifact",
|
|
300
343
|
};
|
|
301
344
|
}
|
|
345
|
+
const artifact = detectIssueRefinementArtifact({ body });
|
|
346
|
+
if (artifact.acItems.length > 0 || artifact.dodItems.length > 0) {
|
|
347
|
+
return {
|
|
348
|
+
status: "present",
|
|
349
|
+
linkedIssue: null,
|
|
350
|
+
linkedIssues: [],
|
|
351
|
+
specSource: REFINEMENT_ARTIFACT_SPEC_SOURCE.PLAN_FILE,
|
|
352
|
+
planDocPath,
|
|
353
|
+
acItems: artifact.acItems,
|
|
354
|
+
dodItems: artifact.dodItems,
|
|
355
|
+
sections: artifact.sections,
|
|
356
|
+
reason: `Refinement artifact present via the promoted plan doc \`${planDocPath}\` the PR body carries as the spec-of-record (plan-file promotion; no linked issue required).`,
|
|
357
|
+
finding: null,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
status: "missing",
|
|
362
|
+
linkedIssue: null,
|
|
363
|
+
linkedIssues: [],
|
|
364
|
+
specSource: REFINEMENT_ARTIFACT_SPEC_SOURCE.PLAN_FILE,
|
|
365
|
+
planDocPath,
|
|
366
|
+
reason: `PR body names the promoted plan doc \`${planDocPath}\` as the spec-of-record but carries no Acceptance criteria / DoD checklist content (${artifact.reason}); a bare marker sentence cannot satisfy the refinement check.`,
|
|
367
|
+
finding: "missing_refinement_artifact",
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
const specResult = validatePrBodySpec({ body, issueLess: true });
|
|
371
|
+
if (specResult.ok) {
|
|
372
|
+
return {
|
|
373
|
+
status: "present",
|
|
374
|
+
linkedIssue: null,
|
|
375
|
+
linkedIssues: [],
|
|
376
|
+
specSource: REFINEMENT_ARTIFACT_SPEC_SOURCE.PR_BODY,
|
|
377
|
+
acItems: specResult.acItems,
|
|
378
|
+
dodItems: specResult.dodItems,
|
|
379
|
+
sections: specResult.sections,
|
|
380
|
+
reason: "Refinement artifact present via the PR body itself (issue-less lightweight PR-body-as-spec; validate-pr-body-spec --no-issue clean).",
|
|
381
|
+
finding: null,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
status: "missing",
|
|
386
|
+
linkedIssue: null,
|
|
387
|
+
linkedIssues: [],
|
|
388
|
+
specSource: REFINEMENT_ARTIFACT_SPEC_SOURCE.PR_BODY,
|
|
389
|
+
reason: `PR body fails the issue-less lightweight spec-of-record validation (validate-pr-body-spec --no-issue: ${specResult.errors.map((e) => e.code).join(", ")}).`,
|
|
390
|
+
finding: "missing_refinement_artifact",
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export async function loadRefinementArtifact({ repo, prData, prDraft, prClosed, prMerged }, { env = process.env, ghCommand = "gh", runChild = defaultRunChild } = {}) {
|
|
395
|
+
const linkedIssues = resolveLinkedIssuesFromPr(prData);
|
|
396
|
+
if (linkedIssues.length === 0) {
|
|
397
|
+
if (prDraft) {
|
|
398
|
+
const body = typeof prData?.body === "string" ? prData.body : "";
|
|
399
|
+
return resolveNoIssueRefinementArtifact(body);
|
|
400
|
+
}
|
|
302
401
|
return {
|
|
303
402
|
status: "unknown",
|
|
304
403
|
linkedIssue: null,
|
|
@@ -320,7 +419,7 @@ export async function loadRefinementArtifact({ repo, prData, prDraft, prClosed,
|
|
|
320
419
|
// is refined if AT LEAST ONE linked issue carries a refinement artifact.
|
|
321
420
|
const evaluated = [];
|
|
322
421
|
for (const issue of linkedIssues) {
|
|
323
|
-
const body = await fetchIssueBody({ repo, issue }, { env, ghCommand });
|
|
422
|
+
const body = await fetchIssueBody({ repo, issue }, { env, ghCommand, runChild });
|
|
324
423
|
if (body === null) {
|
|
325
424
|
evaluated.push({ issue, artifact: null });
|
|
326
425
|
continue;
|
|
@@ -339,6 +438,7 @@ export async function loadRefinementArtifact({ repo, prData, prDraft, prClosed,
|
|
|
339
438
|
status: "present",
|
|
340
439
|
linkedIssue: firstPresent.issue,
|
|
341
440
|
linkedIssues,
|
|
441
|
+
specSource: REFINEMENT_ARTIFACT_SPEC_SOURCE.LINKED_ISSUE,
|
|
342
442
|
refinedIssues,
|
|
343
443
|
source: a.source,
|
|
344
444
|
acItems: a.acItems,
|
|
@@ -366,6 +466,7 @@ export async function loadRefinementArtifact({ repo, prData, prDraft, prClosed,
|
|
|
366
466
|
status: "missing",
|
|
367
467
|
linkedIssue: firstEvaluated.issue,
|
|
368
468
|
linkedIssues,
|
|
469
|
+
specSource: REFINEMENT_ARTIFACT_SPEC_SOURCE.LINKED_ISSUE,
|
|
369
470
|
refinedIssues,
|
|
370
471
|
reason: `Failed to fetch body for linked issue(s) ${scopeLabel}; draft gate cannot verify a refinement artifact, treating as missing.`,
|
|
371
472
|
finding: "missing_refinement_artifact",
|
|
@@ -389,6 +490,7 @@ export async function loadRefinementArtifact({ repo, prData, prDraft, prClosed,
|
|
|
389
490
|
status: "missing",
|
|
390
491
|
linkedIssue: firstFetched.issue,
|
|
391
492
|
linkedIssues,
|
|
493
|
+
specSource: REFINEMENT_ARTIFACT_SPEC_SOURCE.LINKED_ISSUE,
|
|
392
494
|
refinedIssues,
|
|
393
495
|
source: first.source,
|
|
394
496
|
acItems: first.acItems,
|
|
@@ -402,7 +504,7 @@ export async function loadRefinementArtifact({ repo, prData, prDraft, prClosed,
|
|
|
402
504
|
_onlyEnforcedWhenDraft: prDraft === true,
|
|
403
505
|
};
|
|
404
506
|
}
|
|
405
|
-
async function fetchLocalConflictFiles({ env = process.env, gitCommand = "git" } = {}) {
|
|
507
|
+
async function fetchLocalConflictFiles({ env = process.env, gitCommand = "git", runChild = defaultRunChild } = {}) {
|
|
406
508
|
let result;
|
|
407
509
|
try {
|
|
408
510
|
result = await runChild(
|
|
@@ -516,7 +618,7 @@ export async function loadPrGateCoordinationContext(options, runtime = {}) {
|
|
|
516
618
|
};
|
|
517
619
|
}
|
|
518
620
|
|
|
519
|
-
async function fetchCopilotEverFormallyRequested({ repo, pr }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
621
|
+
async function fetchCopilotEverFormallyRequested({ repo, pr }, { env = process.env, ghCommand = "gh", runChild = defaultRunChild } = {}) {
|
|
520
622
|
const result = await runChild(
|
|
521
623
|
ghCommand,
|
|
522
624
|
["api", `repos/${repo}/issues/${pr}/timeline`, "--paginate", "--jq",
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { requireTokenValue } from "../_cli-primitives.mjs";
|
|
5
|
+
import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
6
|
+
import {
|
|
7
|
+
interpretRefinementGrillState,
|
|
8
|
+
normalizeGrillSnapshot,
|
|
9
|
+
} from "@dev-loops/core/loop/refinement-grill-state";
|
|
10
|
+
import { detectIssueRefinementArtifact } from "@dev-loops/core/loop/issue-refinement-artifact";
|
|
11
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
12
|
+
|
|
13
|
+
const HELP = `Usage: detect-refinement-grill-state.mjs [--input <path> | --body-file <path> [--surface issue|pr|plan]]
|
|
14
|
+
Detect refinement/grill sub-loop state.
|
|
15
|
+
Modes (choose exactly one):
|
|
16
|
+
--input <path> Interpret a JSON grill snapshot from file
|
|
17
|
+
--body-file <path> Seed the deterministic already-refined / zero-iteration
|
|
18
|
+
snapshot from a markdown body on disk
|
|
19
|
+
Options (body-file mode only):
|
|
20
|
+
--surface issue|pr|plan Surface the body belongs to (default: issue)
|
|
21
|
+
|
|
22
|
+
${JQ_OUTPUT_USAGE}
|
|
23
|
+
|
|
24
|
+
Exit codes:
|
|
25
|
+
0 Success
|
|
26
|
+
1 Error
|
|
27
|
+
2 Invalid --jq filter
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
const VALID_SURFACES = new Set(["issue", "pr", "plan"]);
|
|
31
|
+
|
|
32
|
+
export function parseDetectGrillCliArgs(argv) {
|
|
33
|
+
const { tokens } = parseArgs({
|
|
34
|
+
args: [...argv],
|
|
35
|
+
options: {
|
|
36
|
+
help: { type: "boolean", short: "h" },
|
|
37
|
+
input: { type: "string" },
|
|
38
|
+
"body-file": { type: "string" },
|
|
39
|
+
surface: { type: "string" },
|
|
40
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
41
|
+
},
|
|
42
|
+
allowPositionals: true,
|
|
43
|
+
strict: false,
|
|
44
|
+
tokens: true,
|
|
45
|
+
});
|
|
46
|
+
const options = {
|
|
47
|
+
inputPath: undefined,
|
|
48
|
+
bodyFilePath: undefined,
|
|
49
|
+
surface: undefined,
|
|
50
|
+
help: false,
|
|
51
|
+
};
|
|
52
|
+
for (const token of tokens) {
|
|
53
|
+
if (token.kind === "positional") {
|
|
54
|
+
throw new Error(`Unknown argument: ${token.value}`);
|
|
55
|
+
}
|
|
56
|
+
if (token.kind !== "option") {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (token.name === "help") {
|
|
60
|
+
options.help = true;
|
|
61
|
+
return options;
|
|
62
|
+
}
|
|
63
|
+
if (token.name === "input") {
|
|
64
|
+
options.inputPath = requireTokenValue(token);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (token.name === "body-file") {
|
|
68
|
+
options.bodyFilePath = requireTokenValue(token);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (token.name === "surface") {
|
|
72
|
+
const surface = requireTokenValue(token).trim();
|
|
73
|
+
if (!VALID_SURFACES.has(surface)) {
|
|
74
|
+
throw new Error("--surface must be one of: issue, pr, plan");
|
|
75
|
+
}
|
|
76
|
+
options.surface = surface;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t))) continue;
|
|
80
|
+
throw new Error(`Unknown argument: ${token.rawName}`);
|
|
81
|
+
}
|
|
82
|
+
const hasInput = options.inputPath !== undefined;
|
|
83
|
+
const hasBodyFile = options.bodyFilePath !== undefined;
|
|
84
|
+
if (hasInput === hasBodyFile) {
|
|
85
|
+
throw new Error("Provide exactly one input source: --input <path> or --body-file <path>");
|
|
86
|
+
}
|
|
87
|
+
if (hasInput && options.surface !== undefined) {
|
|
88
|
+
throw new Error("--surface applies only to --body-file mode");
|
|
89
|
+
}
|
|
90
|
+
return options;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function runCli(
|
|
94
|
+
argv = process.argv.slice(2),
|
|
95
|
+
{ stdout = process.stdout, stderr = process.stderr } = {},
|
|
96
|
+
) {
|
|
97
|
+
const options = parseDetectGrillCliArgs(argv);
|
|
98
|
+
if (options.help) { stdout.write(HELP); return; }
|
|
99
|
+
|
|
100
|
+
let snapshot;
|
|
101
|
+
if (options.inputPath) {
|
|
102
|
+
const text = await readFile(options.inputPath, "utf8");
|
|
103
|
+
snapshot = normalizeGrillSnapshot(parseJsonText(text));
|
|
104
|
+
} else {
|
|
105
|
+
const body = await readFile(options.bodyFilePath, "utf8");
|
|
106
|
+
const surface = options.surface ?? "issue";
|
|
107
|
+
// Deterministic seed for the already-refined / zero-iteration path only: the full
|
|
108
|
+
// semantic gap detection (scope/actor/decision) is the agent-layer bounded input consumed
|
|
109
|
+
// at await_answers. This body-file mode computes ONLY the deterministic AC-presence signal
|
|
110
|
+
// via the single is-it-refined source of truth, detectIssueRefinementArtifact.
|
|
111
|
+
const artifact = detectIssueRefinementArtifact({ body });
|
|
112
|
+
snapshot = normalizeGrillSnapshot({
|
|
113
|
+
loaded: true,
|
|
114
|
+
detectRan: true,
|
|
115
|
+
surface,
|
|
116
|
+
openGapCount: artifact.finding ? 1 : 0,
|
|
117
|
+
unresolvedGapCount: 0,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const interpretation = interpretRefinementGrillState(snapshot);
|
|
122
|
+
process.exitCode = emitResult(
|
|
123
|
+
{
|
|
124
|
+
ok: true,
|
|
125
|
+
snapshot,
|
|
126
|
+
state: interpretation.state,
|
|
127
|
+
allowedTransitions: interpretation.allowedTransitions,
|
|
128
|
+
nextAction: interpretation.nextAction,
|
|
129
|
+
},
|
|
130
|
+
{ jq: options.jq, silent: options.silent, stdout, stderr },
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
135
|
+
runCli().catch((error) => { process.stderr.write(`${formatCliError(error)}\n`); process.exitCode = 1; });
|
|
136
|
+
}
|