dev-loops 0.7.1 → 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 +7 -7
- package/.claude/agents/fixer.md +1 -0
- package/.claude/agents/refiner.md +2 -2
- package/.claude/agents/review.md +2 -2
- package/.claude/commands/loop-enqueue.md +1 -1
- package/.claude/commands/loop-info.md +1 -1
- package/.claude/commands/loop-start-spike.md +1 -1
- package/.claude/skills/copilot-pr-followup/SKILL.md +37 -23
- package/.claude/skills/dev-loop/SKILL.md +6 -6
- package/.claude/skills/docs/acceptance-criteria-verification.md +3 -2
- package/.claude/skills/docs/anti-patterns.md +3 -2
- package/.claude/skills/docs/artifact-authority-contract.md +12 -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 +18 -14
- package/.claude/skills/docs/cross-harness-regression-contract.md +2 -2
- 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 +25 -19
- package/.claude/skills/docs/merge-preconditions.md +13 -11
- package/.claude/skills/docs/pr-lifecycle-contract.md +44 -35
- package/.claude/skills/docs/public-dev-loop-contract.md +59 -43
- package/.claude/skills/docs/retrospective-checkpoint-contract.md +18 -5
- 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/validation-policy.md +4 -5
- package/.claude/skills/local-implementation/SKILL.md +48 -142
- package/CHANGELOG.md +6 -0
- package/agents/dev-loop.agent.md +8 -8
- package/agents/fixer.agent.md +1 -0
- package/agents/refiner.agent.md +2 -2
- package/agents/review.agent.md +2 -2
- package/package.json +5 -4
- 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/create-pr.mjs +119 -10
- package/scripts/github/detect-checkpoint-evidence.mjs +102 -17
- package/scripts/github/list-review-threads.mjs +277 -0
- package/scripts/github/post-gate-findings.mjs +6 -3
- package/scripts/github/request-copilot-review.mjs +92 -26
- package/scripts/github/upsert-checkpoint-verdict.mjs +74 -5
- package/scripts/github/verify-briefing-prefixes.mjs +191 -0
- package/scripts/github/verify-fresh-review-context.mjs +78 -3
- package/scripts/github/wait-pr-checks.mjs +171 -0
- package/scripts/github/write-gate-context.mjs +213 -7
- package/scripts/github/write-gate-findings-log.mjs +54 -2
- package/scripts/loop/copilot-pr-handoff.mjs +29 -2
- package/scripts/loop/detect-change-scope.mjs +2 -2
- package/scripts/loop/detect-copilot-loop-state.mjs +23 -3
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +35 -3
- package/scripts/loop/resolve-dev-loop-startup.mjs +145 -8
- package/scripts/loop/sanctioned-commands.mjs +2 -0
- package/scripts/loop/validate-pr-body-spec.mjs +21 -5
- package/scripts/pages/build-state-atlas.mjs +50 -48
- package/scripts/projects/_resolve-project.mjs +1 -148
- package/scripts/projects/list-queue-items.mjs +3 -377
- package/scripts/projects/move-queue-item.mjs +3 -410
- package/scripts/projects/reorder-queue-item.mjs +3 -22
- package/skills/copilot-pr-followup/SKILL.md +37 -23
- package/skills/dev-loop/SKILL.md +1 -1
- package/skills/docs/acceptance-criteria-verification.md +3 -2
- package/skills/docs/anti-patterns.md +3 -2
- package/skills/docs/artifact-authority-contract.md +12 -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 +18 -14
- package/skills/docs/cross-harness-regression-contract.md +2 -2
- 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 +25 -19
- package/skills/docs/merge-preconditions.md +13 -11
- package/skills/docs/pr-lifecycle-contract.md +44 -35
- package/skills/docs/public-dev-loop-contract.md +59 -43
- package/skills/docs/required-rules.json +149 -0
- package/skills/docs/retrospective-checkpoint-contract.md +18 -5
- 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/validation-policy.md +4 -5
- package/skills/local-implementation/SKILL.md +48 -142
- package/scripts/docs/validate-no-duplicate-rules.mjs +0 -250
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* <tmpRoot>/gate-context/<repo-slug>/pr-<N>/<gate>-<headSha>.json
|
|
30
30
|
*/
|
|
31
31
|
import { execFileSync } from "node:child_process";
|
|
32
|
+
import { createHash } from "node:crypto";
|
|
32
33
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
33
34
|
import path from "node:path";
|
|
34
35
|
import { parseArgs } from "node:util";
|
|
@@ -117,8 +118,10 @@ Optional:
|
|
|
117
118
|
--branch <name> Source branch name
|
|
118
119
|
--touched-files <json> JSON array of changed file path strings (separate from the diff-derived scope.changedFiles)
|
|
119
120
|
--base <ref> Git ref to diff against (git diff <ref>...HEAD); populates scope.diffPath, scope.changedFiles, and adjacentCode (the full build-once bundle). Without it, the CLI emits an explicit thin briefing (scope.diffSource="none") — see docs/gate-review-sub-loop-contract.md.
|
|
120
|
-
--acceptance-criteria <ptr> Pointer to acceptance criteria (issue ref, doc path, URL)
|
|
121
|
+
--acceptance-criteria <ptr> Pointer to acceptance criteria (issue ref, doc path, URL); also used as the linked-issue label in the rendered briefing prefix
|
|
121
122
|
--validation-posture <text> Short description of the validation posture
|
|
123
|
+
--pr-body <text> PR description text, inlined into the rendered briefing prefix
|
|
124
|
+
--issue-body <text> Linked-issue body text, inlined into the briefing prefix under --acceptance-criteria's label; omitted entirely when absent
|
|
122
125
|
--tmp-root <path> Root tmp directory (default: tmp/)
|
|
123
126
|
|
|
124
127
|
${JQ_OUTPUT_USAGE}
|
|
@@ -231,6 +234,8 @@ export function parseWriteGateContextCliArgs(argv) {
|
|
|
231
234
|
base: { type: "string" },
|
|
232
235
|
"acceptance-criteria": { type: "string" },
|
|
233
236
|
"validation-posture": { type: "string" },
|
|
237
|
+
"pr-body": { type: "string" },
|
|
238
|
+
"issue-body": { type: "string" },
|
|
234
239
|
"tmp-root": { type: "string" },
|
|
235
240
|
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
236
241
|
},
|
|
@@ -250,6 +255,8 @@ export function parseWriteGateContextCliArgs(argv) {
|
|
|
250
255
|
base: null,
|
|
251
256
|
acceptanceCriteria: null,
|
|
252
257
|
validationPosture: null,
|
|
258
|
+
prBody: null,
|
|
259
|
+
issueBody: null,
|
|
253
260
|
tmpRoot: "tmp",
|
|
254
261
|
};
|
|
255
262
|
for (const token of tokens) {
|
|
@@ -312,6 +319,14 @@ export function parseWriteGateContextCliArgs(argv) {
|
|
|
312
319
|
options.validationPosture = requireTokenValue(token, parseError).trim();
|
|
313
320
|
continue;
|
|
314
321
|
}
|
|
322
|
+
if (token.name === "pr-body") {
|
|
323
|
+
options.prBody = requireTokenValue(token, parseError);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (token.name === "issue-body") {
|
|
327
|
+
options.issueBody = requireTokenValue(token, parseError);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
315
330
|
if (token.name === "tmp-root") {
|
|
316
331
|
options.tmpRoot = requireTokenValue(token, parseError).trim();
|
|
317
332
|
continue;
|
|
@@ -426,6 +441,137 @@ export function buildGateDiffPath({ repo, pr, gate, headSha, tmpRoot = "tmp" })
|
|
|
426
441
|
return path.join(tmpRoot, "gate-context", repoSlug, `pr-${safePr}`, `${safeGate}-${safeSha}.diff`);
|
|
427
442
|
}
|
|
428
443
|
|
|
444
|
+
/**
|
|
445
|
+
* Build the deterministic path for the rendered invariant briefing prefix
|
|
446
|
+
* (GATE-EXEC-BRIEFING-PREFIX): the byte-identical block every per-angle
|
|
447
|
+
* reviewer of this gate pass is seeded with, before their angle-specific
|
|
448
|
+
* suffix. Mirrors buildGateContextPath/buildGateDiffPath. Exported so the
|
|
449
|
+
* fan-out reviewers and `verify-fresh-review-context.mjs --prefix-file` agree
|
|
450
|
+
* on the path with the context-builder.
|
|
451
|
+
*
|
|
452
|
+
* @param {object} input
|
|
453
|
+
* @param {string} input.repo — owner/name
|
|
454
|
+
* @param {number|string} input.pr
|
|
455
|
+
* @param {string} input.gate — draft_gate | pre_approval_gate
|
|
456
|
+
* @param {string} input.headSha
|
|
457
|
+
* @param {string} [input.tmpRoot] — default "tmp"
|
|
458
|
+
* @returns {string} relative briefing-prefix path
|
|
459
|
+
*/
|
|
460
|
+
export function buildGateBriefingPrefixPath({ repo, pr, gate, headSha, tmpRoot = "tmp" }) {
|
|
461
|
+
const repoSlug = repoSlugFor(repo);
|
|
462
|
+
const { pr: safePr, gate: safeGate, headSha: safeSha } = validatePathSegments({ pr, gate, headSha });
|
|
463
|
+
return path.join(tmpRoot, "gate-context", repoSlug, `pr-${safePr}`, `${safeGate}-${safeSha}.briefing-prefix.txt`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Size cap (bytes) above which the rendered briefing prefix falls back to
|
|
468
|
+
* pointer mode for the diff section (a scope.diffPath reference when present,
|
|
469
|
+
* else an explicit unavailable-pointer disclosure) instead of inlining the
|
|
470
|
+
* diff text in a fenced block. No `gates.*` config knob exists for this
|
|
471
|
+
* yet — a named constant is the right size for a single fixed threshold;
|
|
472
|
+
* promote to config only if a real need for tuning it emerges.
|
|
473
|
+
*/
|
|
474
|
+
export const BRIEFING_PREFIX_INLINE_DIFF_CAP_BYTES = 200 * 1024;
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Render the invariant briefing-prefix text (GATE-EXEC-BRIEFING-PREFIX):
|
|
478
|
+
* header (repo/PR/head/gate/worktree + the mandatory verify-fresh-review-context.mjs
|
|
479
|
+
* instruction), PR body, linked-issue body (when present), the full diff at the
|
|
480
|
+
* reviewed head (inlined up to `capBytes`, else a pointer to `diffPath`), and a
|
|
481
|
+
* changed-files/adjacent-code summary — in that fixed order. Pure and
|
|
482
|
+
* deterministic: identical input always renders identical bytes, so two builds
|
|
483
|
+
* at the same head produce a byte-identical prefix (the fan-out's shared-prefix
|
|
484
|
+
* requirement).
|
|
485
|
+
*
|
|
486
|
+
* @param {object} input
|
|
487
|
+
* @param {string} input.repo
|
|
488
|
+
* @param {number|string} input.pr
|
|
489
|
+
* @param {string} input.gate
|
|
490
|
+
* @param {string} input.headSha
|
|
491
|
+
* @param {string} input.worktreeRoot — absolute path reviewers run in
|
|
492
|
+
* @param {string} input.contextPath — the sibling JSON artifact path
|
|
493
|
+
* @param {string} input.briefingPrefixPath — this rendered file's own path
|
|
494
|
+
* @param {string|null} [input.prBody]
|
|
495
|
+
* @param {string|null} [input.issueRef] — label for the linked-issue section (e.g. "#877")
|
|
496
|
+
* @param {string|null} [input.issueBody] — omit the whole section when null/empty
|
|
497
|
+
* @param {string|null} [input.diffOutput] — full diff text, when captured
|
|
498
|
+
* @param {string|null} [input.diffPath] — persisted `.diff` pointer (pointer-mode fallback)
|
|
499
|
+
* @param {string[]} [input.changedFiles]
|
|
500
|
+
* @param {object|null} [input.adjacentCode] — buildAdjacentBundle output
|
|
501
|
+
* @param {number} [input.capBytes] — default BRIEFING_PREFIX_INLINE_DIFF_CAP_BYTES
|
|
502
|
+
* @returns {{ text: string, prefixMode: "inline"|"pointer", diffBytes: number }}
|
|
503
|
+
*/
|
|
504
|
+
export function renderBriefingPrefix({
|
|
505
|
+
repo, pr, gate, headSha, worktreeRoot, contextPath, briefingPrefixPath,
|
|
506
|
+
prBody = null, issueRef = null, issueBody = null,
|
|
507
|
+
diffOutput = null, diffPath = null, changedFiles = [], adjacentCode = null,
|
|
508
|
+
capBytes = BRIEFING_PREFIX_INLINE_DIFF_CAP_BYTES,
|
|
509
|
+
}) {
|
|
510
|
+
const hasDiffText = typeof diffOutput === "string" && diffOutput.length > 0;
|
|
511
|
+
const diffBytes = hasDiffText ? Buffer.byteLength(diffOutput, "utf8") : 0;
|
|
512
|
+
const prefixMode = hasDiffText && diffBytes > capBytes ? "pointer" : "inline";
|
|
513
|
+
|
|
514
|
+
const lines = [];
|
|
515
|
+
lines.push("# Gate Review Briefing — invariant prefix (GATE-EXEC-BRIEFING-PREFIX)");
|
|
516
|
+
lines.push("");
|
|
517
|
+
lines.push(`repo: ${repo}`);
|
|
518
|
+
lines.push(`pr: #${pr}`);
|
|
519
|
+
lines.push(`gate: ${gate}`);
|
|
520
|
+
lines.push(`head: ${headSha}`);
|
|
521
|
+
lines.push(`worktree: ${worktreeRoot}`);
|
|
522
|
+
lines.push(`prefixMode: ${prefixMode}`);
|
|
523
|
+
lines.push("");
|
|
524
|
+
lines.push(
|
|
525
|
+
`Mandatory: before doing any angle-specific work, run \`node scripts/github/verify-fresh-review-context.mjs --scope <your-angle> --context-path ${contextPath} --prefix-file ${briefingPrefixPath}\`. Refuse to proceed on contamination or a missing artifact.`,
|
|
526
|
+
);
|
|
527
|
+
lines.push("");
|
|
528
|
+
lines.push("## PR body");
|
|
529
|
+
lines.push("");
|
|
530
|
+
lines.push(typeof prBody === "string" && prBody.trim().length > 0 ? prBody.trim() : "(no PR body provided)");
|
|
531
|
+
lines.push("");
|
|
532
|
+
const hasIssueBody = typeof issueBody === "string" && issueBody.trim().length > 0;
|
|
533
|
+
if (hasIssueBody) {
|
|
534
|
+
lines.push(`## Linked issue${issueRef ? ` ${issueRef}` : ""}`);
|
|
535
|
+
lines.push("");
|
|
536
|
+
lines.push(issueBody.trim());
|
|
537
|
+
lines.push("");
|
|
538
|
+
}
|
|
539
|
+
lines.push(`## Diff at reviewed head (${headSha})`);
|
|
540
|
+
lines.push("");
|
|
541
|
+
if (!hasDiffText) {
|
|
542
|
+
lines.push("(no diff text captured for this bundle)");
|
|
543
|
+
} else if (prefixMode === "inline") {
|
|
544
|
+
lines.push("```diff");
|
|
545
|
+
lines.push(diffOutput.endsWith("\n") ? diffOutput.slice(0, -1) : diffOutput);
|
|
546
|
+
lines.push("```");
|
|
547
|
+
} else {
|
|
548
|
+
lines.push(
|
|
549
|
+
`Diff exceeds the ${capBytes}-byte inline cap (${diffBytes} bytes) — pointer mode. Read the full diff from:`,
|
|
550
|
+
);
|
|
551
|
+
lines.push(` ${diffPath ?? "(diff pointer unavailable — re-derive with git diff)"}`);
|
|
552
|
+
}
|
|
553
|
+
lines.push("");
|
|
554
|
+
lines.push("## Changed files + adjacent-code summary");
|
|
555
|
+
lines.push("");
|
|
556
|
+
const files = Array.isArray(changedFiles) ? changedFiles : [];
|
|
557
|
+
lines.push(`Changed files (${files.length}):`);
|
|
558
|
+
for (const f of files) lines.push(`- ${f}`);
|
|
559
|
+
const adjacentFiles = adjacentCode && Array.isArray(adjacentCode.files)
|
|
560
|
+
? adjacentCode.files.filter((f) => f.role !== "changed")
|
|
561
|
+
: [];
|
|
562
|
+
if (adjacentCode) {
|
|
563
|
+
lines.push(`Adjacent files (${adjacentFiles.length}):`);
|
|
564
|
+
for (const f of adjacentFiles) lines.push(`- ${f.path} (${f.role})`);
|
|
565
|
+
lines.push(
|
|
566
|
+
`Stripped: ${adjacentCode.stripped?.length ?? 0}, Truncated: ${adjacentCode.truncated?.length ?? 0}, Missing: ${adjacentCode.missing?.length ?? 0}`,
|
|
567
|
+
);
|
|
568
|
+
} else {
|
|
569
|
+
lines.push("Adjacent files (0): (no adjacent-code bundle for this briefing)");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return { text: lines.join("\n") + "\n", prefixMode, diffBytes };
|
|
573
|
+
}
|
|
574
|
+
|
|
429
575
|
/**
|
|
430
576
|
* Parse `git diff --name-status` output into full repo-relative changed file
|
|
431
577
|
* paths. Handles rename/copy entries (R100 old new, C75 old new) by recording
|
|
@@ -500,6 +646,12 @@ export function buildGateContextArtifact(options) {
|
|
|
500
646
|
if (options.adjacentCode && typeof options.adjacentCode === "object") {
|
|
501
647
|
artifact.adjacentCode = options.adjacentCode;
|
|
502
648
|
}
|
|
649
|
+
// Whether the rendered briefing prefix inlined the reviewed-head diff (in a
|
|
650
|
+
// fenced block) or fell back to the diffPath pointer (size cap). Only set
|
|
651
|
+
// when a caller actually rendered a prefix (writeGateContext does, always).
|
|
652
|
+
if (typeof options.prefixMode === "string" && options.prefixMode.length > 0) {
|
|
653
|
+
artifact.prefixMode = options.prefixMode;
|
|
654
|
+
}
|
|
503
655
|
return artifact;
|
|
504
656
|
}
|
|
505
657
|
|
|
@@ -520,7 +672,7 @@ export function buildGateContextArtifact(options) {
|
|
|
520
672
|
* @param {string} input.tmpRoot
|
|
521
673
|
* @param {number} [input.maxFileBytes]
|
|
522
674
|
* @param {{ repoRoot: string }} opts
|
|
523
|
-
* @returns {Promise<{ diffPath: string|null, changedFiles: string[], adjacentCode: object|null }>}
|
|
675
|
+
* @returns {Promise<{ diffPath: string|null, changedFiles: string[], adjacentCode: object|null, diffOutput: string|null }>}
|
|
524
676
|
*/
|
|
525
677
|
async function resolveDiffScope({ diff, repo, pr, gate, headSha, tmpRoot, maxFileBytes }, { repoRoot }) {
|
|
526
678
|
const diffOutput = diff?.diffOutput;
|
|
@@ -562,7 +714,7 @@ async function resolveDiffScope({ diff, repo, pr, gate, headSha, tmpRoot, maxFil
|
|
|
562
714
|
}
|
|
563
715
|
}
|
|
564
716
|
|
|
565
|
-
return { diffPath, changedFiles, adjacentCode };
|
|
717
|
+
return { diffPath, changedFiles, adjacentCode, diffOutput: typeof diffOutput === "string" ? diffOutput : null };
|
|
566
718
|
}
|
|
567
719
|
|
|
568
720
|
/**
|
|
@@ -647,6 +799,19 @@ export function captureDiffFromBase(base, { repoRoot, maxBuffer = 64 * 1024 * 10
|
|
|
647
799
|
return { nameStatusOutput, diffOutput };
|
|
648
800
|
}
|
|
649
801
|
|
|
802
|
+
/**
|
|
803
|
+
* Write both the JSON context artifact AND its sibling rendered briefing
|
|
804
|
+
* prefix (GATE-EXEC-BRIEFING-PREFIX): the byte-identical invariant block every
|
|
805
|
+
* per-angle reviewer of this gate pass is seeded with. The prefix's
|
|
806
|
+
* `prefixMode` (inline|pointer, from the diff-size cap) is recorded on the
|
|
807
|
+
* JSON artifact so both files stay in sync.
|
|
808
|
+
*
|
|
809
|
+
* @param {object} options — parsed CLI options shape, optionally carrying
|
|
810
|
+
* `diffOutput`, `prBody`, `issueBody` (all null-safe; a caller with none of
|
|
811
|
+
* these still gets a valid — if thin — prefix).
|
|
812
|
+
* @param {{ repoRoot?: string }} [runtime]
|
|
813
|
+
* @returns {Promise<{ ok: boolean, path: string, artifact: object, prefixPath: string, prefixHash: string, prefixMode: "inline"|"pointer" }>}
|
|
814
|
+
*/
|
|
650
815
|
export async function writeGateContext(options, { repoRoot = process.cwd() } = {}) {
|
|
651
816
|
const contextPath = buildGateContextPath({
|
|
652
817
|
repo: options.repo,
|
|
@@ -655,14 +820,49 @@ export async function writeGateContext(options, { repoRoot = process.cwd() } = {
|
|
|
655
820
|
headSha: options.headSha,
|
|
656
821
|
tmpRoot: options.tmpRoot || "tmp",
|
|
657
822
|
});
|
|
823
|
+
const briefingPrefixPath = buildGateBriefingPrefixPath({
|
|
824
|
+
repo: options.repo,
|
|
825
|
+
pr: options.pr,
|
|
826
|
+
gate: options.gate,
|
|
827
|
+
headSha: options.headSha,
|
|
828
|
+
tmpRoot: options.tmpRoot || "tmp",
|
|
829
|
+
});
|
|
830
|
+
const rendered = renderBriefingPrefix({
|
|
831
|
+
repo: options.repo,
|
|
832
|
+
pr: options.pr,
|
|
833
|
+
gate: options.gate,
|
|
834
|
+
headSha: options.headSha,
|
|
835
|
+
worktreeRoot: path.resolve(repoRoot),
|
|
836
|
+
contextPath,
|
|
837
|
+
briefingPrefixPath,
|
|
838
|
+
prBody: options.prBody ?? null,
|
|
839
|
+
issueRef: options.acceptanceCriteria ?? null,
|
|
840
|
+
issueBody: options.issueBody ?? null,
|
|
841
|
+
diffOutput: options.diffOutput ?? null,
|
|
842
|
+
diffPath: options.diffPath ?? null,
|
|
843
|
+
changedFiles: options.changedFiles ?? [],
|
|
844
|
+
adjacentCode: options.adjacentCode ?? null,
|
|
845
|
+
});
|
|
846
|
+
|
|
658
847
|
const fullPath = path.resolve(repoRoot, contextPath);
|
|
659
848
|
const artifact = {
|
|
660
|
-
...buildGateContextArtifact(options),
|
|
849
|
+
...buildGateContextArtifact({ ...options, prefixMode: rendered.prefixMode }),
|
|
661
850
|
loggedAt: new Date().toISOString(),
|
|
662
851
|
};
|
|
852
|
+
// Write ORDER matters: the sibling briefing prefix goes first and the JSON
|
|
853
|
+
// artifact last, so the artifact's existence is the completion marker for
|
|
854
|
+
// the whole set. Downstream consumers (readGateContext, the reviewers'
|
|
855
|
+
// --context-path guard) key on the JSON — a prefix-write failure must not
|
|
856
|
+
// leave a complete-looking artifact pointing at a missing prefix file.
|
|
857
|
+
const fullPrefixPath = path.resolve(repoRoot, briefingPrefixPath);
|
|
858
|
+
await mkdir(path.dirname(fullPrefixPath), { recursive: true });
|
|
859
|
+
await writeFile(fullPrefixPath, rendered.text, "utf8");
|
|
860
|
+
const prefixHash = createHash("sha256").update(rendered.text, "utf8").digest("hex");
|
|
861
|
+
|
|
663
862
|
await mkdir(path.dirname(fullPath), { recursive: true });
|
|
664
863
|
await writeFile(fullPath, JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
665
|
-
|
|
864
|
+
|
|
865
|
+
return { ok: true, path: contextPath, artifact, prefixPath: briefingPrefixPath, prefixHash, prefixMode: rendered.prefixMode };
|
|
666
866
|
}
|
|
667
867
|
|
|
668
868
|
/**
|
|
@@ -685,10 +885,12 @@ export async function writeGateContext(options, { repoRoot = process.cwd() } = {
|
|
|
685
885
|
* @param {string[]} [input.touchedFiles]
|
|
686
886
|
* @param {string|null} [input.acceptanceCriteria]
|
|
687
887
|
* @param {string|null} [input.validationPosture]
|
|
888
|
+
* @param {string|null} [input.prBody] — PR description text, inlined into the rendered briefing prefix
|
|
889
|
+
* @param {string|null} [input.issueBody] — linked-issue body text, inlined under `acceptanceCriteria`'s label; omitted when absent
|
|
688
890
|
* @param {number} [input.maxFileBytes] — per-file cap for the adjacent-code bundle (default DEFAULT_MAX_FILE_BYTES)
|
|
689
891
|
* @param {string} [input.tmpRoot]
|
|
690
892
|
* @param {{ repoRoot?: string }} [opts]
|
|
691
|
-
* @returns {Promise<{ ok: boolean, path: string, artifact: object, resolver: object }>}
|
|
893
|
+
* @returns {Promise<{ ok: boolean, path: string, artifact: object, prefixPath: string, prefixHash: string, prefixMode: "inline"|"pointer", resolver: object }>}
|
|
692
894
|
*
|
|
693
895
|
* The artifact additionally carries a deterministic, neutral `adjacentCode`
|
|
694
896
|
* bundle (#895) when changed files are present: 1-hop import in/out-edges of the
|
|
@@ -707,7 +909,7 @@ export async function buildGateContext(input, { repoRoot = process.cwd() } = {})
|
|
|
707
909
|
// Diff-derived scope: persisted FULL diff (scope.diffPath), parsed
|
|
708
910
|
// scope.changedFiles, and the neutral adjacentCode bundle, all built ONCE by
|
|
709
911
|
// the shared resolveDiffScope helper (also used by the CLI --base path, #1140).
|
|
710
|
-
const { diffPath, changedFiles, adjacentCode } = await resolveDiffScope(
|
|
912
|
+
const { diffPath, changedFiles, adjacentCode, diffOutput } = await resolveDiffScope(
|
|
711
913
|
{ diff: input.diff, repo: input.repo, pr: input.pr, gate: input.gate, headSha: input.headSha, tmpRoot, maxFileBytes: input.maxFileBytes },
|
|
712
914
|
{ repoRoot },
|
|
713
915
|
);
|
|
@@ -724,9 +926,12 @@ export async function buildGateContext(input, { repoRoot = process.cwd() } = {})
|
|
|
724
926
|
touchedFiles: input.touchedFiles ?? [],
|
|
725
927
|
changedFiles,
|
|
726
928
|
diffPath,
|
|
929
|
+
diffOutput,
|
|
727
930
|
adjacentCode,
|
|
728
931
|
acceptanceCriteria: input.acceptanceCriteria ?? null,
|
|
729
932
|
validationPosture: input.validationPosture ?? null,
|
|
933
|
+
prBody: input.prBody ?? null,
|
|
934
|
+
issueBody: input.issueBody ?? null,
|
|
730
935
|
tmpRoot,
|
|
731
936
|
},
|
|
732
937
|
{ repoRoot },
|
|
@@ -800,6 +1005,7 @@ export async function main(argv = process.argv.slice(2), { repoRoot = process.cw
|
|
|
800
1005
|
options.changedFiles = scope.changedFiles;
|
|
801
1006
|
options.diffPath = scope.diffPath;
|
|
802
1007
|
options.adjacentCode = scope.adjacentCode;
|
|
1008
|
+
options.diffOutput = scope.diffOutput;
|
|
803
1009
|
options.diffSource = "base";
|
|
804
1010
|
} else {
|
|
805
1011
|
process.stderr.write("[write-gate-context] warning: no --base given; emitting a THIN briefing (scope.diffPath=null, scope.changedFiles=[], no adjacentCode). Pass --base <ref> for the full build-once bundle.\n");
|
|
@@ -5,7 +5,8 @@ import { parseArgs } from "node:util";
|
|
|
5
5
|
import { parsePrNumber, requireTokenValue } from "../_cli-primitives.mjs";
|
|
6
6
|
import { formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
|
|
7
7
|
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
8
|
-
import { provenanceConsistencyError } from "@dev-loops/core/loop/gate-fanin";
|
|
8
|
+
import { checkFanoutAngleCoverage, provenanceConsistencyError } from "@dev-loops/core/loop/gate-fanin";
|
|
9
|
+
import { loadDevLoopConfig, resolveGateAngleContract, resolveRejectForeignAngles } from "@dev-loops/core/config";
|
|
9
10
|
const USAGE = `Usage: write-gate-findings-log.mjs --repo <owner/name> --pr <number> --gate <draft_gate|pre_approval_gate> --head-sha <sha> --verdict <clean|findings_present|blocked> --findings <json> [--tmp-root <path>]
|
|
10
11
|
Write a durable <gate>-<headSha>.json log under deterministic tmp/ paths.
|
|
11
12
|
Required:
|
|
@@ -30,6 +31,7 @@ function normalizeGate(value) {
|
|
|
30
31
|
const normalized = String(value).trim().toLowerCase();
|
|
31
32
|
return gates.has(normalized) ? normalized : null;
|
|
32
33
|
}
|
|
34
|
+
const GATE_CONFIG_KEY = { draft_gate: "draft", pre_approval_gate: "preApproval" };
|
|
33
35
|
function normalizeVerdict(value) {
|
|
34
36
|
const verdicts = new Set(["clean", "findings_present", "blocked"]);
|
|
35
37
|
const normalized = String(value).trim().toLowerCase();
|
|
@@ -148,6 +150,42 @@ export function parseProvenanceJson(raw) {
|
|
|
148
150
|
}
|
|
149
151
|
return normalized;
|
|
150
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Validate recorded provenance.perAngle against the gate's configured angle
|
|
155
|
+
* contract (mandatoryAngles + pool, resolved from .devloops/defaults). A
|
|
156
|
+
* missing mandatory angle always fails the write. A foreign (out-of-pool)
|
|
157
|
+
* angle fails the write unless `gates.rejectForeignAngles: false`, in which
|
|
158
|
+
* case it is returned as a warning instead. Throws a `parseError`-shaped Error
|
|
159
|
+
* (matching this module's other validation failures) on a fail-closed
|
|
160
|
+
* rejection.
|
|
161
|
+
*
|
|
162
|
+
* @param {{ perAngle: Array<{ angle: string }> }} provenance
|
|
163
|
+
* @param {"draft_gate"|"pre_approval_gate"} gate
|
|
164
|
+
* @param {{ repoRoot?: string }} [options]
|
|
165
|
+
* @returns {Promise<{ warning: string|null }>}
|
|
166
|
+
*/
|
|
167
|
+
export async function checkProvenanceAngleCoverage(provenance, gate, { repoRoot = process.cwd() } = {}) {
|
|
168
|
+
const { config } = await loadDevLoopConfig({ repoRoot });
|
|
169
|
+
const gateKey = GATE_CONFIG_KEY[gate];
|
|
170
|
+
const { mandatoryAngles, pool } = resolveGateAngleContract(config, gateKey);
|
|
171
|
+
const { missingMandatory, foreignAngles } = checkFanoutAngleCoverage(provenance.perAngle, {
|
|
172
|
+
mandatoryAngles,
|
|
173
|
+
pool,
|
|
174
|
+
});
|
|
175
|
+
if (missingMandatory.length > 0) {
|
|
176
|
+
throw parseError(
|
|
177
|
+
`--provenance.perAngle is missing mandatory angle(s) for ${gate}: ${missingMandatory.join(", ")} (configured in gates.${gateKey}.mandatoryAngles)`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (foreignAngles.length > 0) {
|
|
181
|
+
const message = `--provenance.perAngle names angle(s) outside the configured pool for ${gate}: ${foreignAngles.join(", ")}`;
|
|
182
|
+
if (resolveRejectForeignAngles(config)) {
|
|
183
|
+
throw parseError(`${message} (add them to gates.${gateKey}.angles, or set gates.rejectForeignAngles: false to warn instead of fail)`);
|
|
184
|
+
}
|
|
185
|
+
return { warning: `${message} (gates.rejectForeignAngles is false; recorded as a warning)` };
|
|
186
|
+
}
|
|
187
|
+
return { warning: null };
|
|
188
|
+
}
|
|
151
189
|
export function parseWriteGateFindingsLogCliArgs(argv) {
|
|
152
190
|
const { tokens } = parseArgs({
|
|
153
191
|
args: [...argv],
|
|
@@ -250,6 +288,12 @@ export function buildLogPath({ repo, pr, gate, headSha, tmpRoot }) {
|
|
|
250
288
|
export async function writeGateFindingsLog(options, { repoRoot = process.cwd() } = {}) {
|
|
251
289
|
const findings = parseFindingsJson(options.findings);
|
|
252
290
|
const provenance = options.provenance === undefined ? undefined : parseProvenanceJson(options.provenance);
|
|
291
|
+
// Angle-coverage enforcement (fail-closed on missing mandatory angles / foreign
|
|
292
|
+
// angles) only applies when provenance is actually recorded — provenance
|
|
293
|
+
// remains optional and additive (inline_single_agent writes never carry it).
|
|
294
|
+
const angleCoverage = provenance !== undefined
|
|
295
|
+
? await checkProvenanceAngleCoverage(provenance, options.gate, { repoRoot })
|
|
296
|
+
: { warning: null };
|
|
253
297
|
const logPath = buildLogPath({
|
|
254
298
|
repo: options.repo,
|
|
255
299
|
pr: options.pr,
|
|
@@ -276,7 +320,9 @@ export async function writeGateFindingsLog(options, { repoRoot = process.cwd() }
|
|
|
276
320
|
}
|
|
277
321
|
await mkdir(path.dirname(fullPath), { recursive: true });
|
|
278
322
|
await writeFile(fullPath, JSON.stringify(log, null, 2) + "\n", "utf8");
|
|
279
|
-
return
|
|
323
|
+
return angleCoverage.warning
|
|
324
|
+
? { ok: true, path: logPath, log, warning: angleCoverage.warning }
|
|
325
|
+
: { ok: true, path: logPath, log };
|
|
280
326
|
}
|
|
281
327
|
async function main() {
|
|
282
328
|
let options;
|
|
@@ -293,6 +339,12 @@ async function main() {
|
|
|
293
339
|
}
|
|
294
340
|
try {
|
|
295
341
|
const result = await writeGateFindingsLog(options);
|
|
342
|
+
// rejectForeignAngles: false is WARNING mode, not silence — surface the
|
|
343
|
+
// angle-coverage warning on stderr too (the JSON result carries it as
|
|
344
|
+
// `warning`). Suppressed under --silent.
|
|
345
|
+
if (result.warning && !options.silent) {
|
|
346
|
+
process.stderr.write(`WARNING: ${result.warning}\n`);
|
|
347
|
+
}
|
|
296
348
|
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent });
|
|
297
349
|
} catch (error) {
|
|
298
350
|
process.stderr.write(JSON.stringify({
|
|
@@ -4,11 +4,12 @@ import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.m
|
|
|
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";
|
|
7
|
-
import { loadDevLoopConfig, resolveRefinement } from "@dev-loops/core/config";
|
|
7
|
+
import { loadDevLoopConfig, resolveEffectiveCopilotRoundCap, 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 { PR_LIFECYCLE_STATE } from "@dev-loops/core/loop/pr-lifecycle";
|
|
12
13
|
import { ensureAsyncRunnerOwnership, releaseAsyncRunnerOwnership } from "./_pr-runner-coordination.mjs";
|
|
13
14
|
import { resolveRepoRoot } from "./_repo-root-resolver.mjs";
|
|
14
15
|
|
|
@@ -38,6 +39,13 @@ Optional:
|
|
|
38
39
|
--watch-status <status> Refresh deterministic loop state after a prior
|
|
39
40
|
watcher result (changed|timeout|idle). This mode
|
|
40
41
|
never requests review; it only re-detects state.
|
|
42
|
+
--lightweight This PR is light-dispatched (#1210): compose the
|
|
43
|
+
Copilot round cap with localImplementation.lightMode.
|
|
44
|
+
maxCopilotRounds (default 1) via
|
|
45
|
+
min(lightMode.maxCopilotRounds, refinement.maxCopilotRounds)
|
|
46
|
+
instead of using refinement.maxCopilotRounds alone.
|
|
47
|
+
refinement.maxCopilotRounds: 0 still disables Copilot
|
|
48
|
+
rounds even with --lightweight.
|
|
41
49
|
Output (stdout, JSON):
|
|
42
50
|
{ "ok": true, "action": "watch"|"fix"|"stop", "state": "...",
|
|
43
51
|
"allowedTransitions": [...], "nextAction": "...", "snapshot": {...},
|
|
@@ -100,7 +108,7 @@ function summarizeRequestWatchContract({
|
|
|
100
108
|
|| interpretation.state === STATE.READY_TO_REREQUEST_REVIEW
|
|
101
109
|
&& interpretation.sameHeadCleanConverged !== true
|
|
102
110
|
) {
|
|
103
|
-
routingState =
|
|
111
|
+
routingState = PR_LIFECYCLE_STATE.READY_STATE_NEEDS_COPILOT_REQUEST;
|
|
104
112
|
} else if (interpretation.state === STATE.INTERNAL_TOOLING_DIRECT_GATE) {
|
|
105
113
|
routingState = "internal_tooling_skip_copilot";
|
|
106
114
|
}
|
|
@@ -138,6 +146,7 @@ export function parseHandoffCliArgs(argv, { cwd = process.cwd() } = {}) {
|
|
|
138
146
|
repo: undefined,
|
|
139
147
|
pr: undefined,
|
|
140
148
|
watchStatus: undefined,
|
|
149
|
+
lightweight: false,
|
|
141
150
|
jq: undefined,
|
|
142
151
|
silent: false,
|
|
143
152
|
};
|
|
@@ -148,6 +157,7 @@ export function parseHandoffCliArgs(argv, { cwd = process.cwd() } = {}) {
|
|
|
148
157
|
repo: { type: "string" },
|
|
149
158
|
pr: { type: "string" },
|
|
150
159
|
"watch-status": { type: "string" },
|
|
160
|
+
lightweight: { type: "boolean" },
|
|
151
161
|
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
152
162
|
},
|
|
153
163
|
allowPositionals: true,
|
|
@@ -184,6 +194,10 @@ export function parseHandoffCliArgs(argv, { cwd = process.cwd() } = {}) {
|
|
|
184
194
|
options.watchStatus = watchStatus;
|
|
185
195
|
continue;
|
|
186
196
|
}
|
|
197
|
+
if (token.name === "lightweight") {
|
|
198
|
+
options.lightweight = true;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
187
201
|
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
188
202
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
189
203
|
}
|
|
@@ -358,6 +372,15 @@ export async function runHandoff(options, { env = process.env, ghCommand = "gh"
|
|
|
358
372
|
const refinementConfig = config.errors?.length > 0
|
|
359
373
|
? resolveRefinement({ version: 1 })
|
|
360
374
|
: resolveRefinement(config.config);
|
|
375
|
+
if (options.lightweight) {
|
|
376
|
+
// Compose (not replace) the round cap for light-dispatched PRs (#1210):
|
|
377
|
+
// min(lightMode.maxCopilotRounds ?? 1, refinement.maxCopilotRounds), so
|
|
378
|
+
// maxCopilotRounds: 0 still disables Copilot rounds everywhere.
|
|
379
|
+
refinementConfig.maxCopilotRounds = resolveEffectiveCopilotRoundCap(
|
|
380
|
+
config.errors?.length > 0 ? { version: 1 } : config.config,
|
|
381
|
+
{ lightweight: true },
|
|
382
|
+
);
|
|
383
|
+
}
|
|
361
384
|
let interpretation = interpretLoopState(snapshot, refinementConfig);
|
|
362
385
|
|
|
363
386
|
// Check for human comments since last subagent action
|
|
@@ -545,6 +568,10 @@ export async function runHandoff(options, { env = process.env, ghCommand = "gh"
|
|
|
545
568
|
// still refuses unless the head actually advanced past the last review
|
|
546
569
|
// (its hasNewCommits guard), so this cannot force an over-cap same-head request.
|
|
547
570
|
forceRerequestReview: reopenedCapCycle,
|
|
571
|
+
// Thread the light-dispatch signal so the requester enforces the same
|
|
572
|
+
// composed round cap this handoff resolved (#1210) — the enforcement
|
|
573
|
+
// backstop must never permit rounds the interpreter already forbids.
|
|
574
|
+
lightweight: options.lightweight,
|
|
548
575
|
},
|
|
549
576
|
{ env, ghCommand },
|
|
550
577
|
);
|
|
@@ -75,7 +75,7 @@ export function parseGitDiffStat(output) {
|
|
|
75
75
|
}
|
|
76
76
|
return { filesChanged: fileCount, linesChanged: insertions + deletions };
|
|
77
77
|
}
|
|
78
|
-
function detectScope({ base, head } = {}) {
|
|
78
|
+
function detectScope({ base, head, cwd } = {}) {
|
|
79
79
|
let diffArgs = ["diff", "--stat"];
|
|
80
80
|
if (base && head) {
|
|
81
81
|
diffArgs.push(`${base}..${head}`);
|
|
@@ -86,7 +86,7 @@ function detectScope({ base, head } = {}) {
|
|
|
86
86
|
}
|
|
87
87
|
let output;
|
|
88
88
|
try {
|
|
89
|
-
output = execFileSync("git", diffArgs, { encoding: "utf8", maxBuffer: 1_000_000 });
|
|
89
|
+
output = execFileSync("git", diffArgs, { encoding: "utf8", maxBuffer: 1_000_000, cwd: cwd || undefined });
|
|
90
90
|
} catch (err) {
|
|
91
91
|
return { ok: false, filesChanged: 0, linesChanged: 0, error: err instanceof Error ? err.message : String(err) };
|
|
92
92
|
}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "../_core-helpers.mjs";
|
|
15
15
|
import { fetchGithubReviewThreadsPayload } from "../github/capture-review-threads.mjs";
|
|
16
16
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
17
|
-
import { loadDevLoopConfig, resolveRefinement } from "@dev-loops/core/config";
|
|
17
|
+
import { loadDevLoopConfig, resolveEffectiveCopilotRoundCap, resolveRefinement } from "@dev-loops/core/config";
|
|
18
18
|
import {
|
|
19
19
|
buildSnapshotFromPrFacts,
|
|
20
20
|
interpretLoopState,
|
|
@@ -43,8 +43,13 @@ Required (auto-detect mode):
|
|
|
43
43
|
--pr <number> Pull request number
|
|
44
44
|
Required (snapshot mode):
|
|
45
45
|
--input <path> Path to snapshot JSON file
|
|
46
|
-
Optional (
|
|
47
|
-
|
|
46
|
+
Optional (either mode):
|
|
47
|
+
--lightweight This PR is light-dispatched (#1210): compose the Copilot
|
|
48
|
+
round cap with localImplementation.lightMode.maxCopilotRounds
|
|
49
|
+
(default 1) via min(lightMode.maxCopilotRounds,
|
|
50
|
+
refinement.maxCopilotRounds) instead of using
|
|
51
|
+
refinement.maxCopilotRounds alone. refinement.maxCopilotRounds:
|
|
52
|
+
0 still disables Copilot rounds even with --lightweight.
|
|
48
53
|
Output (stdout, JSON):
|
|
49
54
|
{ "ok": true, "snapshot": {..., "copilotReviewRoundCount": N}, "state": "...", "allowedTransitions": [...], "nextAction": "...",
|
|
50
55
|
"autoRerequestEligible": true|false, "sameHeadCleanConverged": true|false,
|
|
@@ -67,6 +72,7 @@ export function parseDetectCliArgs(argv) {
|
|
|
67
72
|
inputPath: undefined,
|
|
68
73
|
repo: undefined,
|
|
69
74
|
pr: undefined,
|
|
75
|
+
lightweight: false,
|
|
70
76
|
};
|
|
71
77
|
const { tokens } = parseArgs({
|
|
72
78
|
args: [...argv],
|
|
@@ -75,6 +81,7 @@ export function parseDetectCliArgs(argv) {
|
|
|
75
81
|
input: { type: "string" },
|
|
76
82
|
repo: { type: "string" },
|
|
77
83
|
pr: { type: "string" },
|
|
84
|
+
lightweight: { type: "boolean" },
|
|
78
85
|
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
79
86
|
},
|
|
80
87
|
allowPositionals: true,
|
|
@@ -104,6 +111,10 @@ export function parseDetectCliArgs(argv) {
|
|
|
104
111
|
options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
|
|
105
112
|
continue;
|
|
106
113
|
}
|
|
114
|
+
if (token.name === "lightweight") {
|
|
115
|
+
options.lightweight = true;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
107
118
|
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
108
119
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
109
120
|
}
|
|
@@ -455,6 +466,15 @@ export async function runCli(
|
|
|
455
466
|
const refinementConfig = config.errors.length > 0
|
|
456
467
|
? resolveRefinement({ version: 1 })
|
|
457
468
|
: resolveRefinement(config.config);
|
|
469
|
+
if (options.lightweight) {
|
|
470
|
+
// Compose (not replace) the round cap for light-dispatched PRs (#1210):
|
|
471
|
+
// min(lightMode.maxCopilotRounds ?? 1, refinement.maxCopilotRounds), so
|
|
472
|
+
// maxCopilotRounds: 0 still disables Copilot rounds everywhere.
|
|
473
|
+
refinementConfig.maxCopilotRounds = resolveEffectiveCopilotRoundCap(
|
|
474
|
+
config.errors.length > 0 ? { version: 1 } : config.config,
|
|
475
|
+
{ lightweight: true },
|
|
476
|
+
);
|
|
477
|
+
}
|
|
458
478
|
interpretation = interpretLoopState(interpretationInput, refinementConfig);
|
|
459
479
|
const interpretationSummary = summarizeLoopInterpretation(interpretation);
|
|
460
480
|
process.exitCode = emitResult({
|