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
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
writeLinkedPrNumber,
|
|
18
18
|
PLAN_FILE_PROMOTE_ACTION,
|
|
19
19
|
} from "@dev-loops/core/loop/plan-file-promote-contract";
|
|
20
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
20
21
|
|
|
21
22
|
const CREATE_PR_PATH = fileURLToPath(new URL("../github/create-pr.mjs", import.meta.url));
|
|
22
23
|
|
|
@@ -42,9 +43,14 @@ Optional:
|
|
|
42
43
|
--branch <name> Branch name to commit the plan on (default: derived from the plan path)
|
|
43
44
|
--json Machine-readable JSON output
|
|
44
45
|
--help Show this help
|
|
46
|
+
|
|
47
|
+
${JQ_OUTPUT_USAGE}
|
|
48
|
+
(--jq/--silent only apply together with --json; the default text output is unaffected.)
|
|
49
|
+
|
|
45
50
|
Exit codes:
|
|
46
51
|
0 Promotion succeeded (or idempotent no-op for an already-linked plan)
|
|
47
|
-
1 Argument error, or fail-closed (plan not ready / git / PR-create failure); no issue ever created
|
|
52
|
+
1 Argument error, or fail-closed (plan not ready / git / PR-create failure); no issue ever created
|
|
53
|
+
2 Invalid --jq filter`.trim();
|
|
48
54
|
|
|
49
55
|
const parseError = buildParseError(USAGE);
|
|
50
56
|
|
|
@@ -57,6 +63,7 @@ export function parsePromotePlanCliArgs(argv) {
|
|
|
57
63
|
base: { type: "string" },
|
|
58
64
|
branch: { type: "string" },
|
|
59
65
|
json: { type: "boolean" },
|
|
66
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
60
67
|
},
|
|
61
68
|
allowPositionals: true,
|
|
62
69
|
strict: false,
|
|
@@ -88,6 +95,7 @@ export function parsePromotePlanCliArgs(argv) {
|
|
|
88
95
|
options.json = true;
|
|
89
96
|
continue;
|
|
90
97
|
}
|
|
98
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
91
99
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
92
100
|
}
|
|
93
101
|
if (typeof options.planFile !== "string" || options.planFile.trim().length === 0) {
|
|
@@ -123,11 +131,14 @@ function parsePrNumberFromGhOutput(text) {
|
|
|
123
131
|
return match ? Number.parseInt(match[1], 10) : null;
|
|
124
132
|
}
|
|
125
133
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
// Sets process.exitCode from `summary.ok` (0/1), or the shared --jq/--silent
|
|
135
|
+
// contract's exit code (0/1/2) when --json + --jq/--silent are in play.
|
|
136
|
+
function emit(stdout, options, summary, humanLines) {
|
|
137
|
+
if (options.json) {
|
|
138
|
+
process.exitCode = emitResult(summary, { jq: options.jq, silent: options.silent, stdout });
|
|
129
139
|
} else {
|
|
130
140
|
stdout.write(`${humanLines.join("\n")}\n`);
|
|
141
|
+
process.exitCode = summary.ok ? 0 : 1;
|
|
131
142
|
}
|
|
132
143
|
}
|
|
133
144
|
|
|
@@ -161,8 +172,7 @@ export async function runCli(argv = process.argv.slice(2), {
|
|
|
161
172
|
if (!decision.ok) {
|
|
162
173
|
// Fail closed: no git, no gh, no issue — surface the reason and stop.
|
|
163
174
|
const summary = { ok: false, reason: decision.reason, planFileIntakeState: decision.planFileIntakeState ?? null };
|
|
164
|
-
emit(stdout, options
|
|
165
|
-
process.exitCode = 1;
|
|
175
|
+
emit(stdout, options, summary, [`promote-plan: FAIL (${decision.reason})`]);
|
|
166
176
|
return summary;
|
|
167
177
|
}
|
|
168
178
|
|
|
@@ -174,7 +184,7 @@ export async function runCli(argv = process.argv.slice(2), {
|
|
|
174
184
|
planFile: planPath,
|
|
175
185
|
prNumber: decision.existingPrNumber,
|
|
176
186
|
};
|
|
177
|
-
emit(stdout, options
|
|
187
|
+
emit(stdout, options, summary, [
|
|
178
188
|
"promote-plan: PASS (already promoted)",
|
|
179
189
|
` plan: ${planPath}`,
|
|
180
190
|
` pr: #${decision.existingPrNumber} (existing; opened nothing)`,
|
|
@@ -207,8 +217,7 @@ export async function runCli(argv = process.argv.slice(2), {
|
|
|
207
217
|
|
|
208
218
|
const fail = (reason, detail) => {
|
|
209
219
|
const summary = { ok: false, reason, detail: detail ?? null, planFile: planPath };
|
|
210
|
-
emit(stdout, options
|
|
211
|
-
process.exitCode = 1;
|
|
220
|
+
emit(stdout, options, summary, [`promote-plan: FAIL (${reason})${detail ? `: ${detail}` : ""}`]);
|
|
212
221
|
return summary;
|
|
213
222
|
};
|
|
214
223
|
|
|
@@ -294,11 +303,10 @@ export async function runCli(argv = process.argv.slice(2), {
|
|
|
294
303
|
branch,
|
|
295
304
|
recovery: `gh pr create failed, so no PR was opened, but the plan is committed and branch ${branch} is pushed. Re-run promote-plan to recover: the commit step is idempotent (skips when nothing is staged) and the push is idempotent, so a clean re-run will retry opening the PR.`,
|
|
296
305
|
};
|
|
297
|
-
emit(stdout, options
|
|
306
|
+
emit(stdout, options, summary, [
|
|
298
307
|
`promote-plan: FAIL (pr_create_failed)${detail ? `: ${detail}` : ""}`,
|
|
299
308
|
` branch: ${branch} (committed and pushed; no PR opened — re-run promote-plan to recover, it is idempotent and will retry opening the PR)`,
|
|
300
309
|
]);
|
|
301
|
-
process.exitCode = 1;
|
|
302
310
|
return summary;
|
|
303
311
|
}
|
|
304
312
|
const prNumber = parsePrNumberFromGhOutput(prCreate.stdout);
|
|
@@ -315,11 +323,10 @@ export async function runCli(argv = process.argv.slice(2), {
|
|
|
315
323
|
branch,
|
|
316
324
|
recovery: `A draft PR was opened on branch ${branch} but its number could not be parsed from gh output. Find it with: gh pr list --head "${branch}" --json number,url. Then record the link with: node scripts/refine/promote-plan.mjs is idempotent only once the front-matter prNumber is set, so add it manually (prNumber: <n>) and commit ${planDocRelPath}.`,
|
|
317
325
|
};
|
|
318
|
-
emit(stdout, options
|
|
326
|
+
emit(stdout, options, summary, [
|
|
319
327
|
`promote-plan: FAIL (pr_number_unparseable)`,
|
|
320
328
|
` branch: ${branch} (a draft PR is open; its number could not be parsed — recover via 'gh pr list --head "${branch}"')`,
|
|
321
329
|
]);
|
|
322
|
-
process.exitCode = 1;
|
|
323
330
|
return summary;
|
|
324
331
|
}
|
|
325
332
|
|
|
@@ -340,11 +347,10 @@ export async function runCli(argv = process.argv.slice(2), {
|
|
|
340
347
|
prNumber,
|
|
341
348
|
recovery: `PR #${prNumber} is open but the plan->PR link commit failed. ${planDocRelPath} now carries prNumber: ${prNumber} in its front-matter; record the link with: git add -- "${planDocRelPath}" && git commit -m "docs(plan): link to PR #${prNumber}" && git push.`,
|
|
342
349
|
};
|
|
343
|
-
emit(stdout, options
|
|
350
|
+
emit(stdout, options, summary, [
|
|
344
351
|
`promote-plan: FAIL (${reason})${detail ? `: ${detail}` : ""}`,
|
|
345
352
|
` pr: #${prNumber} (open; plan->PR link NOT committed — recover manually)`,
|
|
346
353
|
]);
|
|
347
|
-
process.exitCode = 1;
|
|
348
354
|
return summary;
|
|
349
355
|
};
|
|
350
356
|
const linkedMarkdown = writeLinkedPrNumber(markdownText, prNumber);
|
|
@@ -370,7 +376,7 @@ export async function runCli(argv = process.argv.slice(2), {
|
|
|
370
376
|
branch,
|
|
371
377
|
prNumber,
|
|
372
378
|
};
|
|
373
|
-
emit(stdout, options
|
|
379
|
+
emit(stdout, options, summary, [
|
|
374
380
|
"promote-plan: PASS",
|
|
375
381
|
` plan: ${planPath}`,
|
|
376
382
|
` branch: ${branch}`,
|
|
@@ -80,7 +80,7 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
|
|
|
80
80
|
}
|
|
81
81
|
const tree = await loadTreeFromInput(options.input);
|
|
82
82
|
const result = runProseLinkageDetector(tree);
|
|
83
|
-
writeCheckerOutput(result, { stdout, json: options.json });
|
|
83
|
+
process.exitCode = writeCheckerOutput(result, { stdout, json: options.json, jq: options.jq, silent: options.silent });
|
|
84
84
|
return result;
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "@dev-loops/core/loop/plan-file-refine-contract";
|
|
14
14
|
import { PLAN_FILE_REFINEMENT_SECTIONS } from "@dev-loops/core/loop/plan-file-intake-contract";
|
|
15
15
|
import { classifyDocsGrillFinding } from "../loop/docs-grill-contract.mjs";
|
|
16
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
16
17
|
|
|
17
18
|
const USAGE = `Usage:
|
|
18
19
|
refine-plan-file.mjs --plan-file <path> --payload <path> [--json]
|
|
@@ -31,9 +32,14 @@ Required:
|
|
|
31
32
|
Optional:
|
|
32
33
|
--json Machine-readable JSON output
|
|
33
34
|
--help Show this help
|
|
35
|
+
|
|
36
|
+
${JQ_OUTPUT_USAGE}
|
|
37
|
+
(--jq/--silent only apply together with --json; the default text output is unaffected.)
|
|
38
|
+
|
|
34
39
|
Exit codes:
|
|
35
40
|
0 Refinement succeeded; plan written in place; stop for local human review
|
|
36
|
-
1 Argument error, or fail-closed refine/grill/ambiguous state (no write)
|
|
41
|
+
1 Argument error, or fail-closed refine/grill/ambiguous state (no write)
|
|
42
|
+
2 Invalid --jq filter`.trim();
|
|
37
43
|
|
|
38
44
|
const parseError = buildParseError(USAGE);
|
|
39
45
|
|
|
@@ -45,6 +51,7 @@ export function parseRefinePlanFileCliArgs(argv) {
|
|
|
45
51
|
"plan-file": { type: "string" },
|
|
46
52
|
payload: { type: "string" },
|
|
47
53
|
json: { type: "boolean" },
|
|
54
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
48
55
|
},
|
|
49
56
|
allowPositionals: true,
|
|
50
57
|
strict: false,
|
|
@@ -72,6 +79,7 @@ export function parseRefinePlanFileCliArgs(argv) {
|
|
|
72
79
|
options.json = true;
|
|
73
80
|
continue;
|
|
74
81
|
}
|
|
82
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
75
83
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
76
84
|
}
|
|
77
85
|
if (typeof options.planFile !== "string" || options.planFile.trim().length === 0) {
|
|
@@ -123,11 +131,12 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
|
|
|
123
131
|
if (!result.ok) {
|
|
124
132
|
// Fail closed: surface the reason, do not write, do not advance, do not promote.
|
|
125
133
|
if (options.json) {
|
|
126
|
-
|
|
134
|
+
const failurePayload = { ok: false, reason: result.reason, planFileIntakeState: result.planFileIntakeState ?? null };
|
|
135
|
+
process.exitCode = emitResult(failurePayload, { jq: options.jq, silent: options.silent, stdout });
|
|
127
136
|
} else {
|
|
128
137
|
stdout.write(`refine-plan-file: FAIL (${result.reason})\n`);
|
|
138
|
+
process.exitCode = 1;
|
|
129
139
|
}
|
|
130
|
-
process.exitCode = 1;
|
|
131
140
|
return result;
|
|
132
141
|
}
|
|
133
142
|
|
|
@@ -142,7 +151,7 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
|
|
|
142
151
|
grillDispositions: result.grillDispositions,
|
|
143
152
|
};
|
|
144
153
|
if (options.json) {
|
|
145
|
-
|
|
154
|
+
process.exitCode = emitResult(summary, { jq: options.jq, silent: options.silent, stdout });
|
|
146
155
|
} else {
|
|
147
156
|
stdout.write(
|
|
148
157
|
[
|
|
@@ -76,7 +76,7 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
|
|
|
76
76
|
}
|
|
77
77
|
const tree = await loadTreeFromInput(options.input);
|
|
78
78
|
const result = runRefinementCompletenessChecker(tree);
|
|
79
|
-
writeCheckerOutput(result, { stdout, json: options.json });
|
|
79
|
+
process.exitCode = writeCheckerOutput(result, { stdout, json: options.json, jq: options.jq, silent: options.silent });
|
|
80
80
|
return result;
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -14,7 +14,7 @@ import path from "node:path";
|
|
|
14
14
|
import { parseArgs } from "node:util";
|
|
15
15
|
|
|
16
16
|
import { formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
|
|
17
|
-
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
17
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
18
18
|
import { SPIKE_FILE_EXPLORATION_SECTIONS } from "./validate-spike-file.mjs";
|
|
19
19
|
|
|
20
20
|
// Placeholder bodies the operator fills in during the spike. Non-empty so the
|
|
@@ -113,14 +113,10 @@ function parseCliArgs(argv) {
|
|
|
113
113
|
case "out":
|
|
114
114
|
args.out = requireValue(token, "--out requires a value (path)");
|
|
115
115
|
break;
|
|
116
|
-
|
|
117
|
-
args
|
|
118
|
-
break;
|
|
119
|
-
case "silent":
|
|
120
|
-
args.silent = true;
|
|
121
|
-
break;
|
|
122
|
-
default:
|
|
116
|
+
default: {
|
|
117
|
+
if (matchJqOutputToken(token, args, (t) => requireValue(t, "--jq requires a filter"))) break;
|
|
123
118
|
throw parseError(`Unknown flag: ${token.rawName}`);
|
|
119
|
+
}
|
|
124
120
|
}
|
|
125
121
|
}
|
|
126
122
|
return args;
|
|
@@ -151,7 +151,7 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
|
|
|
151
151
|
}
|
|
152
152
|
const tree = await loadTreeFromInput(options.input);
|
|
153
153
|
const result = runScopeBoundaryCrossChecker(tree);
|
|
154
|
-
writeCheckerOutput(result, { stdout, json: options.json });
|
|
154
|
+
process.exitCode = writeCheckerOutput(result, { stdout, json: options.json, jq: options.jq, silent: options.silent });
|
|
155
155
|
return result;
|
|
156
156
|
}
|
|
157
157
|
|
|
@@ -199,7 +199,7 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
|
|
|
199
199
|
}
|
|
200
200
|
const tree = await loadTreeFromInput(options.input);
|
|
201
201
|
const result = runTreeIntegrityValidator(tree);
|
|
202
|
-
writeCheckerOutput(result, { stdout, json: options.json });
|
|
202
|
+
process.exitCode = writeCheckerOutput(result, { stdout, json: options.json, jq: options.jq, silent: options.silent });
|
|
203
203
|
return result;
|
|
204
204
|
}
|
|
205
205
|
|
|
@@ -52,7 +52,7 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
|
|
|
52
52
|
}
|
|
53
53
|
const markdownText = await readFile(options.input, "utf8");
|
|
54
54
|
const result = validatePlanFile(markdownText);
|
|
55
|
-
writeCheckerOutput(result, { stdout, json: options.json });
|
|
55
|
+
process.exitCode = writeCheckerOutput(result, { stdout, json: options.json, jq: options.jq, silent: options.silent });
|
|
56
56
|
return result;
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -75,7 +75,7 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
|
|
|
75
75
|
}
|
|
76
76
|
const markdownText = await readFile(options.input, "utf8");
|
|
77
77
|
const result = validateSpikeFile(markdownText);
|
|
78
|
-
writeCheckerOutput(result, { stdout, json: options.json });
|
|
78
|
+
process.exitCode = writeCheckerOutput(result, { stdout, json: options.json, jq: options.jq, silent: options.silent });
|
|
79
79
|
return result;
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -9,6 +9,7 @@ import { runProseLinkageDetector } from "./prose-linkage-detector.mjs";
|
|
|
9
9
|
import { runScopeBoundaryCrossChecker } from "./scope-boundary-cross-checker.mjs";
|
|
10
10
|
import { runRefinementCompletenessChecker } from "./refinement-completeness-checker.mjs";
|
|
11
11
|
import { runTreeIntegrityValidator } from "./tree-integrity-validator.mjs";
|
|
12
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
12
13
|
|
|
13
14
|
const USAGE = `Usage:
|
|
14
15
|
dev-loops refine verify --issue <number> [--repo <owner/name>] [--json]
|
|
@@ -26,7 +27,10 @@ Required (exactly one mode):
|
|
|
26
27
|
Optional:
|
|
27
28
|
--repo <owner/name> Repository slug for online mode
|
|
28
29
|
--json Machine-readable JSON output (default: human-readable summary)
|
|
29
|
-
--help Show this help
|
|
30
|
+
--help Show this help
|
|
31
|
+
|
|
32
|
+
${JQ_OUTPUT_USAGE}
|
|
33
|
+
(--jq/--silent only apply together with --json; the default text output is unaffected.)`.trim();
|
|
30
34
|
|
|
31
35
|
const parseError = buildParseError(USAGE);
|
|
32
36
|
|
|
@@ -39,6 +43,7 @@ export function parseRefineVerifyCliArgs(argv) {
|
|
|
39
43
|
repo: { type: "string" },
|
|
40
44
|
input: { type: "string" },
|
|
41
45
|
json: { type: "boolean" },
|
|
46
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
42
47
|
},
|
|
43
48
|
allowPositionals: true,
|
|
44
49
|
strict: false,
|
|
@@ -79,6 +84,7 @@ export function parseRefineVerifyCliArgs(argv) {
|
|
|
79
84
|
options.json = true;
|
|
80
85
|
continue;
|
|
81
86
|
}
|
|
87
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
82
88
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
83
89
|
}
|
|
84
90
|
|
|
@@ -177,13 +183,12 @@ export async function runCli(
|
|
|
177
183
|
};
|
|
178
184
|
|
|
179
185
|
if (options.json) {
|
|
180
|
-
|
|
186
|
+
process.exitCode = emitResult(payload, { jq: options.jq, silent: options.silent, stdout });
|
|
181
187
|
} else {
|
|
182
188
|
writeHumanOutput(result, tree, { stdout });
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
process.exitCode = 1;
|
|
189
|
+
if (!result.ok) {
|
|
190
|
+
process.exitCode = 1;
|
|
191
|
+
}
|
|
187
192
|
}
|
|
188
193
|
return payload;
|
|
189
194
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Release-time guard: fail closed when the published `dev-loops` package's
|
|
4
|
+
* `@dev-loops/core` dependency does not resolve to the same major.minor as the
|
|
5
|
+
* version being released. This is the check that would have caught #1033
|
|
6
|
+
* (`dev-loops@0.6.2` shipping `@dev-loops/core@^0.2.6`).
|
|
7
|
+
*
|
|
8
|
+
* Runs in release.yml BEFORE the GitHub Release is created, so a mismatched
|
|
9
|
+
* manifest never becomes a release (and never fires npm-publish). release.yml
|
|
10
|
+
* has no `npm ci`, so this script imports only `node:` builtins — a workspace
|
|
11
|
+
* import here would ERR_MODULE_NOT_FOUND and skip the guard entirely (same
|
|
12
|
+
* constraint as extract-changelog-section.mjs).
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* node scripts/release/assert-core-dependency-version.mjs [--release-version <v>] [--manifest <path>]
|
|
16
|
+
*
|
|
17
|
+
* --release-version defaults to the manifest `version`. Pass the tag-derived
|
|
18
|
+
* version in release.yml so a manifest that forgot its own bump is also caught.
|
|
19
|
+
* Exits 0 on lockstep, 1 on mismatch, 2 on usage/parse errors.
|
|
20
|
+
*/
|
|
21
|
+
import { realpathSync } from "node:fs";
|
|
22
|
+
import { readFile } from "node:fs/promises";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
|
|
25
|
+
const CORE_DEP = "@dev-loops/core";
|
|
26
|
+
|
|
27
|
+
function isDirectCliRun(importMetaUrl, argv1 = process.argv[1]) {
|
|
28
|
+
if (typeof argv1 !== "string" || argv1.length === 0) return false;
|
|
29
|
+
try {
|
|
30
|
+
return realpathSync(argv1) === realpathSync(fileURLToPath(importMetaUrl));
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extract the `major.minor` token from a semver version or npm range.
|
|
38
|
+
* Handles leading range operators (^, ~, >=, >, <=, <, =, v) and compound
|
|
39
|
+
* ranges by taking the first `<digits>.<digits>` occurrence.
|
|
40
|
+
* @param {string} spec
|
|
41
|
+
* @returns {string} e.g. "0.6"
|
|
42
|
+
*/
|
|
43
|
+
export function extractMajorMinor(spec) {
|
|
44
|
+
const match = String(spec).match(/(\d+)\.(\d+)/);
|
|
45
|
+
if (!match) {
|
|
46
|
+
throw new Error(`cannot parse a major.minor version from "${spec}"`);
|
|
47
|
+
}
|
|
48
|
+
return `${match[1]}.${match[2]}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Assert the `@dev-loops/core` dependency range is in lockstep (same
|
|
53
|
+
* major.minor) with the release version. Throws on mismatch (fail closed).
|
|
54
|
+
* @param {{releaseVersion: string, coreRange: string}} input
|
|
55
|
+
* @returns {{releaseVersion: string, coreRange: string, majorMinor: string}}
|
|
56
|
+
*/
|
|
57
|
+
export function assertCoreDependencyInLockstep({ releaseVersion, coreRange } = {}) {
|
|
58
|
+
if (!releaseVersion) throw new Error("releaseVersion is required");
|
|
59
|
+
if (!coreRange) {
|
|
60
|
+
throw new Error(`root package must declare a "${CORE_DEP}" dependency`);
|
|
61
|
+
}
|
|
62
|
+
const releaseMajorMinor = extractMajorMinor(releaseVersion);
|
|
63
|
+
const coreMajorMinor = extractMajorMinor(coreRange);
|
|
64
|
+
if (releaseMajorMinor !== coreMajorMinor) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`${CORE_DEP} dependency "${coreRange}" (major.minor ${coreMajorMinor}) does not match ` +
|
|
67
|
+
`the release version ${releaseVersion} (major.minor ${releaseMajorMinor}). ` +
|
|
68
|
+
`Bump the ${CORE_DEP} range to ^${releaseMajorMinor}.0 before releasing (root cause of #1033).`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return { releaseVersion, coreRange, majorMinor: releaseMajorMinor };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function usageError(message) {
|
|
75
|
+
const err = new Error(message);
|
|
76
|
+
err.usage = true;
|
|
77
|
+
return err;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseArgs(argv) {
|
|
81
|
+
const out = { manifest: "package.json", releaseVersion: null };
|
|
82
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
83
|
+
const arg = argv[i];
|
|
84
|
+
if (arg === "--release-version" || arg === "--manifest") {
|
|
85
|
+
const value = argv[++i];
|
|
86
|
+
// Fail closed like extract-changelog-section.mjs: a flag missing its value
|
|
87
|
+
// is a usage error (exit 2), not a silent fallback that defeats the guard.
|
|
88
|
+
if (value === undefined) throw usageError(`${arg} requires a value`);
|
|
89
|
+
if (arg === "--release-version") out.releaseVersion = value;
|
|
90
|
+
else out.manifest = value;
|
|
91
|
+
} else {
|
|
92
|
+
throw usageError(`unknown argument: ${arg}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function main(argv) {
|
|
99
|
+
const { manifest, releaseVersion } = parseArgs(argv);
|
|
100
|
+
// An unreadable or invalid manifest is a usage/parse error (exit 2), not a
|
|
101
|
+
// lockstep mismatch (exit 1) — matches the header contract and extract-changelog-section.mjs.
|
|
102
|
+
let pkg;
|
|
103
|
+
try {
|
|
104
|
+
pkg = JSON.parse(await readFile(manifest, "utf8"));
|
|
105
|
+
} catch (err) {
|
|
106
|
+
throw usageError(`cannot read or parse manifest "${manifest}": ${err.message}`);
|
|
107
|
+
}
|
|
108
|
+
const coreRange = pkg.dependencies?.[CORE_DEP];
|
|
109
|
+
const result = assertCoreDependencyInLockstep({
|
|
110
|
+
releaseVersion: releaseVersion ?? pkg.version,
|
|
111
|
+
coreRange,
|
|
112
|
+
});
|
|
113
|
+
process.stdout.write(
|
|
114
|
+
`${CORE_DEP} ${result.coreRange} is in lockstep with release ${result.releaseVersion} (major.minor ${result.majorMinor}).\n`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
119
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
120
|
+
process.stderr.write(`::error::${err.message}\n`);
|
|
121
|
+
process.exit(err.usage ? 2 : 1);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
// Inlined (was scripts/_core-helpers.mjs -> @dev-loops/core): this script runs
|
|
6
|
+
// in release.yml with no `npm ci`, so it must import only node: builtins.
|
|
7
|
+
// A workspace import here ERR_MODULE_NOT_FOUNDs and the Release never gets made.
|
|
8
|
+
function isDirectCliRun(importMetaUrl, argv1 = process.argv[1]) {
|
|
9
|
+
if (typeof argv1 !== "string" || argv1.length === 0) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return realpathSync(argv1) === realpathSync(fileURLToPath(importMetaUrl));
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
4
18
|
|
|
5
19
|
const USAGE = `Usage: extract-changelog-section.mjs --version <v> [--changelog <path>]
|
|
6
20
|
|
|
@@ -243,7 +243,8 @@ When unresolved feedback exists, use a narrow follow-up loop:
|
|
|
243
243
|
- resolve the review-round cap from config via `resolveRefinementConfig(config, "maxCopilotRounds")` from `@dev-loops/core/config`; default config ships `maxCopilotRounds: 5`
|
|
244
244
|
- **Opt out entirely:** `maxCopilotRounds: 0` disables the external Copilot review gate for the repo — the loop runs `draft_gate → pre_approval_gate` with the local harness only, never requesting or waiting on Copilot. Use this when the repo has no Copilot reviewer configured or prefers local-harness-only review.
|
|
245
245
|
- use the completed Copilot review-round count from `detect-copilot-loop-state.mjs` / `copilot-pr-handoff.mjs` as the current PR's review-round count
|
|
246
|
-
- if completed review rounds have reached the maximum (default: 5), do **not** re-request Copilot review
|
|
246
|
+
- if completed review rounds have reached the maximum (default: 5), do **not** re-request Copilot review within that concluded cycle
|
|
247
|
+
- if the loop already converged and then significant post-convergence changes land on a newer head (new/changed product or test logic, not doc/message/comment-only edits), treat that as a NEW cycle and re-request Copilot review when regular rounds are already > 0 (the prior cycle's cap does not suppress this new-cycle request)
|
|
247
248
|
- when the round limit is reached **and** the refreshed thread snapshot proves zero unresolved threads **and** current-head CI is green or credibly green, treat that clean state as eligible for `pre_approval_gate` fallback instead of deadlocking on another Copilot rerequest
|
|
248
249
|
- when using that fallback, add a short round-exhaustion note to the visible `pre_approval_gate` gate evidence so the PR records why no further Copilot rerequest occurred
|
|
249
250
|
- if the round cap is reached before the PR is thread-clean or before CI is green/credibly green, reply-resolve any remaining intentionally deferred threads with a short `deferred to follow-up` note, then stop and report that the Copilot round limit was reached
|
|
@@ -305,7 +306,7 @@ Do NOT use `gh pr comment`, `gh api`, or `gh pr review` for gate comments.
|
|
|
305
306
|
Both gates run this same checkpoint review chain. It is an **agent-orchestrated skill procedure** — a node script cannot spawn the per-angle reviewers, so the conductor agent drives the fan-out and uses the pure `@dev-loops/core/loop/gate-fanin` helpers only for consolidation, batching, and ledger mapping. The cost model is **build once, seed many**: the context-builder script builds ONE deterministic, neutral context bundle (diff + adjacent code) and each independent reviewer is seeded with that identical bundle verbatim instead of re-deriving it (work-dedup; no fork primitive, no Workflow tool). See [Gate Review Sub-Loop Contract](../../docs/gate-review-sub-loop-contract.md).
|
|
306
307
|
|
|
307
308
|
1. **Context (Phase 1) — build the neutral bundle ONCE:** build/read the gate-context artifact via `scripts/github/write-gate-context.mjs` (`buildGateContext` resolves the angle set through `resolveGateAnglesDynamic`; mandatory angles always survive). The artifact at `tmp/gate-context/<repo-slug>/pr-<N>/<gate>-<headSha>.json` carries the resolved angles + change scope + acceptance-criteria pointer. When the resolver was handed a diff with `diffOutput`, `buildGateContext` also persists the FULL diff to `tmp/gate-context/<repo-slug>/pr-<N>/<gate>-<headSha>.diff` and records `scope.diffPath` (relative) plus `scope.changedFiles` (full repo-relative paths). It ADDITIONALLY builds a deterministic, neutral `adjacentCode` bundle — each changed file plus its 1-hop import in/out-edges (callers/callees/imports), with size guards (skip lockfiles/generated/binary/minified; cap per-file bytes; truncate the long tail) recorded in a `stripped`/`truncated`/`missing` manifest. This single bundle is the build-once seed; reviewers do not re-derive the diff + adjacent code.
|
|
308
|
-
2. **Fan-out (Phase 2) — seed each reviewer with the bundle verbatim:** plan batches with `planFanoutBatches(angles,
|
|
309
|
+
2. **Fan-out (Phase 2) — seed each reviewer with the bundle verbatim:** plan batches with `planFanoutBatches(angles, cap)` using the `gates.maxFanoutReviewers` cap (default 8). Spawn one scoped `review` agent per resolved angle (plain Agent tool), parallel up to the cap, batching any overflow sequentially. When `degraded` is true, record the sequential degradation in the gate evidence. Reviewers are independent: each is seeded with the IDENTICAL neutral bundle (the gate-context artifact's `scope.diffPath` + `adjacentCode`) verbatim plus its single angle, and never inherits the conductor agent's conversation or opinions. Each reviewer follows the [review agent scoped angle-review mode](../../agents/review.agent.md): fresh context, single angle, read-only, writing its per-angle findings artifact to `tmp/gate-reviews/<repo-slug>/pr-<N>/<gate>-<headSha>/<angle>.json`. Brief each reviewer to use the provided bundle as its base and to review like an external code reviewer hunting real bugs: read the FULL diff (from `scope.diffPath`, or `git diff` when null) and the bundled adjacent code (callers, callees, imports) rather than re-deriving them, then review adversarially for concrete defects (edge cases, input validation, numeric coercion incl. NaN/Infinity/floats/negatives, null/undefined, boundary conditions, mismatched caller/callee contracts, dedup/identity bugs) with `file:line` + the failing scenario — not process nits like "no test exists". Reviewers are read-only and MAY widen scope (open adjacent repo files beyond the bundle) only when their angle genuinely needs more; that widening is recorded in the optional `contextWidened` field on each per-angle findings artifact.
|
|
309
310
|
3. **Fan-in (Phase 3):** consolidate the per-angle artifacts via `consolidateFanin({ angleResults, blockCleanOnFindingSeverities })` (blocking severities from `resolveGateConfig(config, <configKey>).blockCleanOnFindingSeverities`, where `<configKey>` is the config key for the gate — `draft` for `draft_gate`, `preApproval` for `pre_approval_gate` — not the `draft_gate|pre_approval_gate` artifact name). It yields the gate `verdict` (`clean` when no blocking-severity finding remains, `findings_present` when one does, `blocked` when any per-angle artifact is malformed/missing). Map consolidated findings with `toFindingsLogShape(...)` and write the disposition ledger via `write-gate-findings-log.mjs` **before** the visible comment. The ledger is written regardless of any comment opt-out. Then, when `resolveGatePostFindingsComments(config)` is true (default; opt-out via `gates.postFindingsComments: false`), post the consolidated findings as a visible, marker-tagged PR comment via `node <resolved-skill-scripts>/github/post-gate-findings.mjs --repo <owner/name> --pr <number> --gate <gate> --head-sha <current_head_sha> --findings '<toFindingsLogShape JSON>'` so the findings are auditable and Copilot/humans are aware of them. The helper is idempotent per gate (exactly one comment per gate, updated in place on each run; the reviewed head is shown in the body) and posts a brief "no findings" note when the consolidated set is empty. When `postFindingsComments` is false the helper no-ops with a `skipped` result and you skip this post step; the ledger still records the findings.
|
|
310
311
|
4. **Verdict (Phase 4):** post the verdict with the [Gate comment command](#mandatory-gate-comment-command-contract) using `--execution-mode fanout_fanin` and `--findings-json <path>`. Write that JSON from the collected per-angle results (the same per-angle `{angle, verdict, findings}` artifacts at `tmp/gate-reviews/<repo-slug>/pr-<N>/<gate>-<headSha>/<angle>.json` that fed `consolidateFanin` in Phase 3; the flat `toFindingsLogShape` output is also accepted and grouped by `.angle`) so the verdict comment renders the structured per-angle breakdown instead of a wall-of-text summary. Do NOT use `--findings-summary` for a fan-out gate — that is the inline_single_agent fallback.
|
|
311
312
|
5. **Retry (Phase 5):** on blocking findings, drive each internal fan-out finding through the SAME fix → reply-with-resolving-commit → resolve loop used for external Copilot review comments (Step 7 above): fix on the branch, push the resolving commit, then re-run only the `findings_present` angles from the previous pass (the context-builder and fan-in always re-run); repeat until the consolidated verdict is `clean` for the current head SHA. Internal fan-out findings and external review comments are handled identically by that loop.
|
|
@@ -322,11 +323,7 @@ The canonical checkpoint verdict comment contract is [Gate Review Comment Contra
|
|
|
322
323
|
- **CI prerequisite:** resolve the draft gate config first (`resolveGateConfig(config, "draft")`). When `requireCi=true` (default), wait for green current-head CI before entering `draft_gate`. When `requireCi=false`, the draft gate may proceed without green CI. This draft-only override does **not** relax `pre_approval_gate`; final approval and merge readiness still require green current-head CI.
|
|
323
324
|
- **Pass criteria:** all configured draft gate angles pass; all findings at severities in `blockCleanOnFindingSeverities` are addressed; validation passes; no unrelated files are included.
|
|
324
325
|
- **Next step after passing:** mark the PR ready for review.
|
|
325
|
-
- **Board status sync (
|
|
326
|
-
```sh
|
|
327
|
-
node <resolved-skill-scripts>/projects/sync-item-status.mjs --repo <owner/name> --item <linked-issue> --to-column "In Progress"
|
|
328
|
-
```
|
|
329
|
-
Best-effort and NON-FATAL: it uses local `gh` auth (no CI/PAT), exits 0 when the board is not configured / the item is not on the board / the API fails, and never blocks marking the PR ready.
|
|
326
|
+
- **Board status sync (built-in, after ready-for-review):** the In-Progress board move is now performed automatically as a deterministic tail of `ready-for-review.mjs` — marking the PR ready couples the board move to the ready transition (#1069), so no separate `sync-item-status` step is needed. It stays best-effort and NON-FATAL: it uses local `gh` auth (no CI/PAT), exits 0 when the board is not configured / the item is not on the board / the API fails, and never blocks marking the PR ready.
|
|
330
327
|
- **Non-substitution rule:** a clean `draft_gate` comment only authorizes the draft → ready-for-review transition for that head SHA. It does **not** satisfy `pre_approval_gate`, final-approval readiness, or merge-ready requirements.
|
|
331
328
|
- **Required PR comment:** post a visible checkpoint verdict comment using the mandatory [Gate comment command](#mandatory-gate-comment-command-contract). Keep validation reporting concise: include command names with pass/fail status. Do **not** paste raw passing test output into the visible gate comment. If you include a failing validation excerpt, keep it focused and truncate it to a deterministic retained-prefix length before posting the comment. See [Gate Review Comment Contract](../../docs/gate-review-comment-contract.md). Do not run `gh pr ready` unless a visible `clean` `draft_gate` checkpoint verdict comment exists for the current head SHA. A checkpoint verdict comment for an older head SHA does not satisfy this requirement for the current head. If findings exist, the PR stays draft and needs fixes before retry. If fixes advance the head SHA while still draft, post a new checkpoint verdict comment for the new head. If the checkpoint verdict comment cannot be posted, fail closed and do not run `gh pr ready`.
|
|
332
329
|
|
|
@@ -340,7 +337,7 @@ This is the default pre-approval gate for this workflow boundary. The canonical
|
|
|
340
337
|
- **Review angles:** resolved at runtime from config via `resolveGateAngles(config, "preApproval")` from `@dev-loops/core/config`. Default config enables all configured pre-approval gate angle families; consumer repos may opt out individual angles via `excludeAngles`.
|
|
341
338
|
- **Persona mapping:** each angle resolves to a reviewer persona via `resolveReviewerRole(config, angle)` from `@dev-loops/core/config`. Include this prompt in each reviewer's briefing so the reviewer knows exactly what to look for.
|
|
342
339
|
- **Pass criteria:** the sub-loop completes with verdict `clean`; all configured angles pass; if parallel execution is impractical, still run all configured lenses and explicitly record the limitation.
|
|
343
|
-
- **Acceptance criteria verification:** follow the canonical procedure in [Acceptance Criteria Verification](../docs/acceptance-criteria-verification.md) before posting the `pre_approval_gate` comment.
|
|
340
|
+
- **Acceptance criteria verification:** follow the canonical procedure in [Acceptance Criteria Verification](../docs/acceptance-criteria-verification.md) before posting the `pre_approval_gate` comment. After a clean verification this also ticks the verified PR-body checkboxes via `scripts/github/tick-verified-checkboxes.mjs`, so the merged PR shows checked AC.
|
|
344
341
|
- **Next step after passing:** continue the Step 7 flow and then proceed to the human approval checkpoint below.
|
|
345
342
|
- **Non-substitution rule:** a clean `pre_approval_gate` comment is separate from `draft_gate` evidence. It governs final-approval readiness for that head SHA; it does **not** replace the required `draft_gate` evidence for leaving draft.
|
|
346
343
|
- **Required PR comment:** post a visible checkpoint verdict comment using the mandatory [Gate comment command](#mandatory-gate-comment-command-contract). Keep validation reporting concise: include command names with pass/fail status. Do **not** paste raw passing test output into the visible gate comment. If you include a failing validation excerpt, keep it focused and truncate it to a deterministic retained-prefix length before posting the comment. Do not declare final-approval readiness unless a visible `clean` `pre_approval_gate` checkpoint verdict comment exists for the current head SHA. Final-approval readiness must not rely only on local or hidden artifacts; the visible PR comment is the required auditable evidence. If the checkpoint verdict comment cannot be posted, fail closed and do not declare final-approval readiness. A checkpoint verdict comment for an older head SHA does not satisfy this requirement for the current head. If findings exist, follow-up fixes are required before final approval. The `pre_approval_gate` procedure must be entered and completed (visible comment posted) before any merge-ready or approval-ready declaration. Skipping the gate is not recoverable by asserting convergence. If fixes advance the head SHA, post a new checkpoint verdict comment for the new head.
|
|
@@ -350,14 +347,14 @@ This is the default pre-approval gate for this workflow boundary. The canonical
|
|
|
350
347
|
Before any merge-ready or final-approval claim, run `detect-pr-gate-coordination-state.mjs` for the current PR. If it reports `gateBoundary=conflict_resolution` or `mergeStateStatus` is conflicted, stop the normal gate path immediately and use this recovery flow:
|
|
351
348
|
|
|
352
349
|
1. fetch fresh `origin/main`, confirm the current PR head SHA, and summarize the conflict scope from `mergeStateStatus` plus any reported `conflictFiles`
|
|
353
|
-
2. ask for explicit authorization before any
|
|
354
|
-
3. after authorization, reconcile locally on the PR branch; default to
|
|
350
|
+
2. ask for explicit authorization before any merge commit or other branch-state-changing reconciliation command
|
|
351
|
+
3. after authorization, reconcile locally on the PR branch; default to a merge commit (`git merge origin/main`) per the behind-branch integration policy in [Local Implementation Skill](../local-implementation/SKILL.md#branch--review--merge-policy), unless the operator explicitly chooses another conflict-resolution command
|
|
355
352
|
4. auto-resolve simple conflicts when the correct fix is mechanical and clearly in scope; report complex conflicts explicitly and fix them manually only for in-scope files
|
|
356
353
|
5. rerun the smallest honest local validation for the touched conflict slice
|
|
357
354
|
6. rerun `detect-pr-gate-coordination-state.mjs` for the new head
|
|
358
355
|
7. because the head changed, rerun `pre_approval_gate` for the new head before any approval-ready or merge-ready claim
|
|
359
356
|
8. wait for current-head CI again before retrying merge evaluation
|
|
360
|
-
9. if the chosen reconciliation rewrote branch history (
|
|
357
|
+
9. if the chosen reconciliation rewrote branch history (rebase only — merge commits push as a normal fast-forward), ask for explicit authorization before `git push --force-with-lease` (`--force-with-lease` only, never bare `--force`), then continue the loop on the updated head
|
|
361
358
|
|
|
362
359
|
`mergeStateStatus: CLEAN` alone is not enough to resume approval or merge claims. The existing merge-ready preconditions still apply: zero unresolved review threads, a clean current-head `pre_approval_gate`, and green current-head CI.
|
|
363
360
|
|
|
@@ -384,6 +381,21 @@ node <resolved-skill-scripts>/github/detect-checkpoint-evidence.mjs \
|
|
|
384
381
|
|
|
385
382
|
This helper is always-on: it uses `gh api` to fetch visible PR issue comments and fails closed unless both required gate comments exist: a clean `draft_gate` comment for the one-time draft boundary and a clean current-head `pre_approval_gate` comment. Do not run `gh pr merge` if this command exits non-zero. There is no opt-out flag. Resolved threads, green CI, clean Copilot rereview, or local notes do not substitute for this successful helper output. If a final approval or merge boundary sees `gh pr merge` without a same-boundary successful check, treat that as a workflow violation and stop.
|
|
386
383
|
|
|
384
|
+
### Stale runner-coordination lock held by a completed run
|
|
385
|
+
|
|
386
|
+
The pre-merge gate evidence check fails closed on the PR's runner-coordination claim (`.pi/runner-coordination/<owner>/<name>/pr-<n>.json`): a fresh merge re-dispatch (new run id) is refused with `ownership_lost`, or `stale_runner` once the claim ages past the max-age window.
|
|
387
|
+
|
|
388
|
+
The auto-loop now releases its claim best-effort when a run reaches a terminal stop (clean-converged, blocked, or done — including the stop at the human approval checkpoint), so a merge-authorized re-dispatch normally inherits a cleared claim and proceeds. The release is non-fatal: it never blocks the stop, and it never clears a claim owned by a genuinely active competing run.
|
|
389
|
+
|
|
390
|
+
If a stale claim still blocks the merge because the completing run could not release (crash, killed process, or a pre-#1109 run), the sanctioned recovery for a lock held by a COMPLETED run is an explicit takeover by the merge run:
|
|
391
|
+
|
|
392
|
+
```sh
|
|
393
|
+
node <resolved-skill-scripts>/loop/pr-runner-coordination.mjs takeover \
|
|
394
|
+
--repo <owner/name> --pr <number>
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
`takeover` seizes ownership for the current run id and records the displaced run under `previousRun`. Only take over when the prior owner is genuinely completed/dead. A genuinely active (non-stale) run must still be allowed to block — do not take over to race a live run.
|
|
398
|
+
|
|
387
399
|
### Mandatory post-merge retrospective checkpoint write
|
|
388
400
|
|
|
389
401
|
After a merge succeeds (or an explicit retrospective skip is authorized), write the durable retrospective checkpoint before exiting the subagent session:
|
|
@@ -410,7 +422,7 @@ node <resolved-skill-scripts>/projects/archive-done-items.mjs --repo <owner/name
|
|
|
410
422
|
|
|
411
423
|
Board and threshold resolve from `.devloops` (`queue.projectNumber`/`queue.boardTitle`, `queue.archiveOlderThanDays`, default 7d), using local `gh` auth — no CI, cron, or PAT. This step is best-effort and NON-FATAL: ignore any failure and never let it block the merge or the retrospective.
|
|
412
424
|
|
|
413
|
-
|
|
425
|
+
The deterministic mechanism that converges merged→Done is `dev-loops queue reconcile` — idempotent, run best-effort at loop startup — so the merge lands on the "Done" column without a remembered manual step. The manual sync below is now an optional fallback (e.g. to converge immediately without waiting for the next startup reconcile):
|
|
414
426
|
|
|
415
427
|
```sh
|
|
416
428
|
node <resolved-skill-scripts>/projects/sync-item-status.mjs --repo <owner/name> --item <linked-issue> --to-column "Done"
|