dev-loops 0.6.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/.claude-plugin/plugin.json +1 -1
- package/.claude/agents/dev-loop.md +8 -7
- package/.claude/agents/fixer.md +1 -0
- package/.claude/agents/refiner.md +2 -2
- package/.claude/agents/review.md +4 -3
- package/.claude/commands/{auto.md → loop-auto.md} +1 -1
- package/.claude/commands/loop-continue.md +15 -0
- package/.claude/commands/loop-enqueue.md +22 -0
- package/.claude/commands/loop-grill.md +17 -0
- package/.claude/commands/{info.md → loop-info.md} +2 -2
- package/.claude/commands/loop-queue-status.md +24 -0
- package/.claude/commands/{start-spike.md → loop-start-spike.md} +2 -2
- package/.claude/commands/{start.md → loop-start.md} +1 -1
- package/.claude/commands/{status.md → loop-status.md} +1 -1
- package/.claude/hooks/_bash-command-classify.mjs +333 -29
- package/.claude/hooks/_hook-decisions.mjs +138 -15
- package/.claude/hooks/pre-tool-use-bash-gate.mjs +36 -15
- package/.claude/skills/copilot-pr-followup/SKILL.md +60 -34
- package/.claude/skills/dev-loop/SKILL.md +16 -12
- package/.claude/skills/docs/acceptance-criteria-verification.md +8 -3
- package/.claude/skills/docs/anti-patterns.md +5 -3
- package/.claude/skills/docs/artifact-authority-contract.md +30 -11
- package/.claude/skills/docs/confirmation-rules.md +1 -0
- package/.claude/skills/docs/contract-style-guide.md +37 -0
- package/.claude/skills/docs/copilot-loop-operations.md +19 -15
- package/.claude/skills/docs/cross-harness-regression-contract.md +60 -0
- package/.claude/skills/docs/debt-remediation-contract.md +1 -1
- package/.claude/skills/docs/epic-tree-refinement-procedure.md +25 -22
- package/.claude/skills/docs/issue-intake-procedure.md +26 -19
- package/.claude/skills/docs/local-planning-flow.md +1 -1
- package/.claude/skills/docs/local-planning-worked-example.md +1 -1
- package/.claude/skills/docs/merge-preconditions.md +30 -12
- package/.claude/skills/docs/plan-file-contract.md +1 -1
- package/.claude/skills/docs/pr-lifecycle-contract.md +44 -35
- package/.claude/skills/docs/public-dev-loop-contract.md +60 -44
- package/.claude/skills/docs/retrospective-checkpoint-contract.md +106 -79
- package/.claude/skills/docs/spike-mode-contract.md +14 -7
- package/.claude/skills/docs/stop-conditions.md +30 -15
- package/.claude/skills/docs/tracker-first-loop-state.md +13 -8
- package/.claude/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/.claude/skills/docs/validation-policy.md +4 -5
- package/.claude/skills/docs/workflow-handoff-contract.md +39 -0
- package/.claude/skills/local-implementation/SKILL.md +62 -145
- package/.claude/skills/loop-grill/SKILL.md +163 -0
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +101 -0
- package/README.md +9 -9
- package/agents/dev-loop.agent.md +11 -10
- package/agents/developer.agent.md +1 -1
- package/agents/docs.agent.md +1 -1
- package/agents/fixer.agent.md +2 -1
- package/agents/quality.agent.md +1 -1
- package/agents/refiner.agent.md +3 -3
- package/agents/review.agent.md +5 -4
- package/cli/index.mjs +35 -42
- package/extension/README.md +4 -4
- package/extension/checks.ts +1 -5
- package/extension/harness-types.ts +1 -0
- package/extension/index.ts +6 -0
- package/extension/pi-extension-adapter.ts +2 -0
- package/extension/presentation.ts +7 -19
- package/package.json +12 -12
- package/scripts/_core-helpers.mjs +2 -0
- package/scripts/claude/headless-dev-loop.mjs +1 -1
- package/scripts/docs/validate-rule-ownership.mjs +442 -0
- package/scripts/docs/validate-state-machine-conformance.mjs +1092 -0
- package/scripts/github/_review-thread-mutations.mjs +5 -2
- package/scripts/github/capture-review-threads.mjs +2 -9
- package/scripts/github/comment-issue.mjs +2 -9
- package/scripts/github/create-label.mjs +133 -0
- package/scripts/github/create-pr.mjs +119 -10
- package/scripts/github/detect-checkpoint-evidence.mjs +259 -18
- package/scripts/github/detect-linked-issue-pr.mjs +22 -6
- package/scripts/github/edit-pr.mjs +259 -0
- package/scripts/github/fetch-ci-logs.mjs +2 -9
- package/scripts/github/list-issues.mjs +2 -9
- package/scripts/github/list-review-threads.mjs +277 -0
- package/scripts/github/manage-sub-issues.mjs +46 -10
- package/scripts/github/offer-human-handoff.mjs +8 -5
- package/scripts/github/post-gate-findings.mjs +20 -5
- package/scripts/github/probe-ci-status.mjs +8 -2
- package/scripts/github/probe-copilot-review.mjs +21 -14
- package/scripts/github/ready-for-review.mjs +26 -8
- package/scripts/github/reconcile-draft-gate.mjs +7 -3
- package/scripts/github/reply-resolve-review-thread.mjs +16 -5
- package/scripts/github/reply-resolve-review-threads.mjs +69 -7
- package/scripts/github/request-copilot-review.mjs +98 -26
- package/scripts/github/resolve-handoff-candidates.mjs +7 -3
- package/scripts/github/resolve-tracker-local-spec.mjs +9 -3
- package/scripts/github/stage-reviewer-draft.mjs +10 -2
- package/scripts/github/tick-verified-checkboxes.mjs +202 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +105 -15
- package/scripts/github/verify-briefing-prefixes.mjs +191 -0
- package/scripts/github/verify-fresh-review-context.mjs +226 -32
- package/scripts/github/view-pr.mjs +150 -0
- package/scripts/github/wait-pr-checks.mjs +171 -0
- package/scripts/github/write-gate-context.mjs +458 -65
- package/scripts/github/write-gate-findings-log.mjs +128 -2
- package/scripts/lib/jq-output.mjs +18 -0
- package/scripts/loop/_post-convergence-change.mjs +211 -0
- package/scripts/loop/_pr-runner-coordination.mjs +70 -0
- package/scripts/loop/_repo-root-resolver.mjs +47 -0
- package/scripts/loop/build-handoff-envelope.mjs +14 -4
- package/scripts/loop/check-retro-tooling.mjs +14 -3
- package/scripts/loop/checkpoint-contract.mjs +7 -4
- package/scripts/loop/cleanup-worktree.mjs +12 -3
- package/scripts/loop/conductor-monitor.mjs +12 -18
- package/scripts/loop/copilot-pr-handoff.mjs +162 -14
- package/scripts/loop/debt-remediate.mjs +24 -12
- package/scripts/loop/detect-change-scope.mjs +38 -9
- package/scripts/loop/detect-copilot-loop-state.mjs +34 -8
- package/scripts/loop/detect-copilot-session-activity.mjs +7 -3
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +7 -3
- package/scripts/loop/detect-internal-only-pr.mjs +8 -2
- package/scripts/loop/detect-issue-refinement-artifact.mjs +6 -3
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +184 -69
- package/scripts/loop/detect-reviewer-loop-state.mjs +12 -2
- package/scripts/loop/detect-tracker-first-loop-state.mjs +9 -1
- package/scripts/loop/detect-tracker-pr-state.mjs +10 -3
- package/scripts/loop/ensure-worktree.mjs +8 -3
- package/scripts/loop/info.mjs +12 -4
- package/scripts/loop/inspect-run-viewer/constants.mjs +4 -18
- package/scripts/loop/inspect-run-viewer/vendor/mermaid.min.js +3405 -0
- package/scripts/loop/inspect-run-viewer-ci-changes.mjs +18 -6
- package/scripts/loop/inspect-run-viewer.mjs +67 -4
- package/scripts/loop/inspect-run.mjs +8 -2
- package/scripts/loop/outer-loop.mjs +8 -4
- package/scripts/loop/pr-runner-coordination.mjs +2 -9
- package/scripts/loop/pre-commit-branch-guard.mjs +16 -10
- package/scripts/loop/pre-flight-gate.mjs +9 -9
- package/scripts/loop/pre-pr-ready-gate.mjs +8 -4
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +13 -4
- package/scripts/loop/provision-worktree.mjs +74 -3
- package/scripts/loop/resolve-dev-loop-startup.mjs +216 -10
- package/scripts/loop/resolve-gate-dispatch.mjs +134 -0
- package/scripts/loop/resolve-pr-conflicts.mjs +14 -7
- package/scripts/loop/run-conductor-cycle.mjs +8 -2
- package/scripts/loop/run-queue.mjs +18 -9
- package/scripts/loop/run-refinement-audit.mjs +8 -9
- package/scripts/loop/run-watch-cycle.mjs +2 -9
- package/scripts/loop/sanctioned-commands.mjs +106 -0
- package/scripts/loop/steer-loop.mjs +29 -16
- package/scripts/loop/validate-pr-body-spec.mjs +223 -0
- package/scripts/loop/watch-initial-copilot-pr.mjs +8 -3
- package/scripts/pages/build-site.mjs +59 -8
- package/scripts/pages/build-state-atlas.mjs +443 -0
- package/scripts/projects/_resolve-project.mjs +1 -0
- package/scripts/projects/add-queue-item.mjs +60 -54
- package/scripts/projects/archive-done-items.mjs +49 -84
- package/scripts/projects/ensure-queue-board.mjs +10 -36
- package/scripts/projects/list-queue-items.mjs +59 -382
- package/scripts/projects/move-queue-item.mjs +21 -449
- package/scripts/projects/reconcile-queue.mjs +253 -0
- package/scripts/projects/reorder-queue-item.mjs +43 -68
- package/scripts/projects/resolve-active-board-item.mjs +108 -46
- package/scripts/projects/sync-item-status.mjs +15 -10
- package/scripts/refine/_refine-helpers.mjs +21 -5
- package/scripts/refine/exit-spike.mjs +18 -8
- package/scripts/refine/promote-plan.mjs +22 -16
- package/scripts/refine/prose-linkage-detector.mjs +1 -1
- package/scripts/refine/refine-plan-file.mjs +13 -4
- package/scripts/refine/refinement-completeness-checker.mjs +1 -1
- package/scripts/refine/scaffold-spike-file.mjs +4 -8
- package/scripts/refine/scope-boundary-cross-checker.mjs +1 -1
- package/scripts/refine/tree-integrity-validator.mjs +1 -1
- package/scripts/refine/validate-plan-file.mjs +1 -1
- package/scripts/refine/validate-spike-file.mjs +1 -1
- package/scripts/refine/verify.mjs +11 -6
- package/scripts/release/assert-core-dependency-version.mjs +123 -0
- package/scripts/release/extract-changelog-section.mjs +16 -2
- package/skills/copilot-pr-followup/SKILL.md +60 -34
- package/skills/dev-loop/SKILL.md +11 -7
- package/skills/docs/acceptance-criteria-verification.md +8 -3
- package/skills/docs/anti-patterns.md +5 -3
- package/skills/docs/artifact-authority-contract.md +30 -11
- package/skills/docs/confirmation-rules.md +1 -0
- package/skills/docs/contract-style-guide.md +37 -0
- package/skills/docs/copilot-loop-operations.md +19 -15
- package/skills/docs/cross-harness-regression-contract.md +60 -0
- package/skills/docs/debt-remediation-contract.md +1 -1
- package/skills/docs/epic-tree-refinement-procedure.md +25 -22
- package/skills/docs/issue-intake-procedure.md +26 -19
- package/skills/docs/local-planning-flow.md +1 -1
- package/skills/docs/local-planning-worked-example.md +1 -1
- package/skills/docs/merge-preconditions.md +30 -12
- package/skills/docs/plan-file-contract.md +1 -1
- package/skills/docs/pr-lifecycle-contract.md +44 -35
- package/skills/docs/public-dev-loop-contract.md +60 -44
- package/skills/docs/required-rules.json +149 -0
- package/skills/docs/retrospective-checkpoint-contract.md +106 -79
- package/skills/docs/spike-mode-contract.md +14 -7
- package/skills/docs/stop-conditions.md +30 -15
- package/skills/docs/tracker-first-loop-state.md +13 -8
- package/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/skills/docs/validation-policy.md +4 -5
- package/skills/docs/workflow-handoff-contract.md +39 -0
- package/skills/local-implementation/SKILL.md +62 -145
- package/skills/loop-grill/SKILL.md +165 -0
- package/.claude/commands/continue.md +0 -15
- package/scripts/docs/validate-no-duplicate-rules.mjs +0 -250
- package/scripts/loop/conductor.mjs +0 -233
- package/scripts/loop/detect-stale-runner.mjs +0 -265
- package/scripts/loop/pre-push-main-guard.mjs +0 -117
|
@@ -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
|
|
|
@@ -71,6 +71,8 @@ Max watch timeout: **30 minutes** (from `policy-constants.mjs` COPILOT_REVIEW_WA
|
|
|
71
71
|
```sh
|
|
72
72
|
node <resolved-skill-scripts>/github/request-copilot-review.mjs --help
|
|
73
73
|
node <resolved-skill-scripts>/github/probe-copilot-review.mjs --help
|
|
74
|
+
node <resolved-skill-scripts>/github/list-review-threads.mjs --help
|
|
75
|
+
node <resolved-skill-scripts>/github/wait-pr-checks.mjs --help
|
|
74
76
|
node <resolved-skill-scripts>/loop/detect-copilot-loop-state.mjs --help
|
|
75
77
|
```
|
|
76
78
|
|
|
@@ -94,7 +96,7 @@ Verify all material claims against source, tests, configuration, and CI.
|
|
|
94
96
|
When this skill refers to helper paths such as `scripts/...` or `docs/...`, resolve them from the actual skill installation layout you are running, not from the active target repository checkout.
|
|
95
97
|
|
|
96
98
|
Use this rule:
|
|
97
|
-
- if the skill is installed as a normalized standalone copy, the required bundled contract docs live under the shared `../docs/` directory next to the installed skill directories
|
|
99
|
+
- if the skill is installed as a normalized standalone copy, the required bundled contract docs live under the shared `../docs/` directory next to the installed skill directories. <!-- rule: ASSET-PATH-INSTALLED-NO-ASSUME --> `ASSET-PATH-INSTALLED-NO-ASSUME`: Agents MUST NOT assume helper scripts are bundled unless that installed layout actually contains them.
|
|
98
100
|
- if you are working in the `dev-loops` source repository, this skill file lives under `skills/copilot-pr-followup/`, so source-repo helper scripts live two levels up at `../../scripts/`, while required bundled contract docs live one level up at `../docs/`
|
|
99
101
|
- when in doubt, resolve helper paths relative to this [skill file](./SKILL.md) first, then verify the target file exists before running it
|
|
100
102
|
|
|
@@ -105,7 +107,7 @@ Required bundled runtime contract docs for installed copies of this skill:
|
|
|
105
107
|
- [Copilot Loop Operations](../docs/copilot-loop-operations.md)
|
|
106
108
|
|
|
107
109
|
Read those bundled `../docs/` files from the installed skill layout instead of assuming the source repository checkout is present. If any required bundled contract doc is missing from the installed skill layout, treat that as a packaging/installer bug.
|
|
108
|
-
|
|
110
|
+
<!-- rule: ASSET-PATH-SOURCE-NO-REPO-LOCAL --> `ASSET-PATH-SOURCE-NO-REPO-LOCAL`: Agents MUST NOT assume `scripts/...` is repo-local to the target codebase they are operating on.
|
|
109
111
|
|
|
110
112
|
## Authority and safety rules
|
|
111
113
|
|
|
@@ -123,16 +125,19 @@ Inspect: PR body/title (must satisfy [PR description contract](../docs/copilot-l
|
|
|
123
125
|
|
|
124
126
|
At the issue-assignment seam, use `detect-initial-copilot-pr-state.mjs` and keep waiting when `waiting_for_initial_copilot_implementation`.
|
|
125
127
|
|
|
126
|
-
|
|
128
|
+
<!-- rule: COPILOT-FOLLOWUP-REQUEST-HELPER-ONLY -->
|
|
129
|
+
`COPILOT-FOLLOWUP-REQUEST-HELPER-ONLY`: Copilot review requests MUST go through `request-copilot-review.mjs` (see [Operational cookbook](#operational-cookbook)); the agent MUST NOT request Copilot by posting literal `/copilot` or `/copilot re-review` PR comments, and MUST NOT rely solely on `gh pr view --json reviewRequests` to confirm a request. After draft→ready or fix push, explicitly decide whether another pass is desired; if yes, ensure green/credibly green posture first.
|
|
127
130
|
|
|
128
|
-
|
|
131
|
+
<!-- rule: COPILOT-FOLLOWUP-REQUEST-BRANCHING -->
|
|
132
|
+
`COPILOT-FOLLOWUP-REQUEST-BRANCHING`: The agent MUST branch on the `request-copilot-review.mjs` machine-readable result exactly as follows, and MUST NOT treat an attempted request as equivalent to a confirmed request:
|
|
129
133
|
- `requested`: if another Copilot pass is actually desired, immediately re-baseline with `detect-copilot-loop-state.mjs` and follow its `nextAction` (enter persistent wait only through `dev-loops loop watch-cycle` or `gh run watch`)
|
|
130
134
|
- `already-requested`: apply the same detector-first rebasing and wait branching as `requested`
|
|
131
135
|
- `suppressed_same_head_clean`: report clean-converged state and stop unless `--force-rerequest-review` bypass is intentionally authorized
|
|
132
136
|
- `unavailable`: report the limitation and stop
|
|
137
|
+
- `blocked_by_copilot_comment`: no request was placed. Delete the violating `violationCommentIds` (or confirm they only quote the rule inside a code span/fenced block, which does not arm the guard) and re-run; do NOT treat this as a placed request and do NOT enter a wait seam
|
|
133
138
|
- non-zero / unexpected failure: stop and report error
|
|
134
139
|
|
|
135
|
-
|
|
140
|
+
Branch on `status`, never on `ok`/exit-code truthiness alone: `ok: true` means the helper ran without error, not that a review was placed. Under `--silent`, the exit code is 0 only for `requested`; every other status (including `blocked_by_copilot_comment`) exits non-zero.
|
|
136
141
|
|
|
137
142
|
### Re-attachment guard (check for existing loop state first)
|
|
138
143
|
|
|
@@ -166,7 +171,8 @@ The outer-loop checkpoint is the canonical re-attachment artifact.
|
|
|
166
171
|
|
|
167
172
|
Start every wait seam with a detector refresh: `detect-copilot-loop-state.mjs --repo <owner/name> --pr <number>`.
|
|
168
173
|
|
|
169
|
-
|
|
174
|
+
<!-- rule: COPILOT-FOLLOWUP-WAIT-TOOLS -->
|
|
175
|
+
`COPILOT-FOLLOWUP-WAIT-TOOLS`: The agent MUST wait only through allowed deterministic tools: `detect-copilot-loop-state.mjs` (one-shot), `dev-loops loop watch-cycle` (persistent), `copilot-pr-handoff.mjs --watch-status` (refresh after timeout/idle), `dev-loops loop watch-ci --repo <owner/name> --pr <number>` (provider-agnostic CI: CircleCI / Actions / external commit-status), `scripts/github/wait-pr-checks.mjs --repo <owner/name> --pr <number>` (same provider-agnostic CI wait, with a direct 0/1/2 process-exit-code contract for shell/scripted callers instead of a JSON `status` field), `gh run watch <run-id> --repo <owner/name>` (Actions-only fallback when the run id is known); otherwise exit and resume later from a fresh detector call.
|
|
170
176
|
|
|
171
177
|
Practical rules: do not poll manually. `waiting_for_copilot_review` → `run-watch-cycle.mjs` or report-and-resume. `waiting_for_ci` with pending/none CI → `dev-loops loop watch-ci --repo <owner/name> --pr <number>` (provider-agnostic; covers CircleCI / Actions / external commit-status) or report-and-resume; `gh run watch <run-id>` is an Actions-only fallback. `dev-loops loop watch-cycle` also auto-routes a `waiting_for_ci` boundary to this CI watcher. Bounded CI exception: zero current-head suites + previous-head green + local `npm run verify` passed → rerun detector with `--local-validation-head-sha` for `crediblyGreen` promotion. `ciStatus=failure` → stop/fix, never wait.
|
|
172
178
|
|
|
@@ -181,6 +187,7 @@ Preferred approach:
|
|
|
181
187
|
<!-- /pi-only -->
|
|
182
188
|
- dispatch fix findings to the `fixer` agent; do not run inline fix passes in-watcher
|
|
183
189
|
- do not report completion while unresolved Copilot feedback remains
|
|
190
|
+
- once a watch/probe settles, do not parse its raw output: re-read via `detect-copilot-loop-state.mjs --repo <owner/name> --pr <number>` and `list-review-threads.mjs --repo <owner/name> --pr <number> --unresolved-only` — inline interpreters are barred by `OPS-NO-INLINE-INTERPRETER` in [Copilot loop operations](../docs/copilot-loop-operations.md)
|
|
184
191
|
|
|
185
192
|
### Canonical async dispatch wording
|
|
186
193
|
|
|
@@ -209,6 +216,7 @@ This step covers four responsibilities: the draft gate right before `gh pr ready
|
|
|
209
216
|
When unresolved feedback exists, use a narrow follow-up loop:
|
|
210
217
|
|
|
211
218
|
1. inspect unresolved comments/threads and failing checks
|
|
219
|
+
- enumerate unresolved threads (with the thread/comment ids the reply-resolve helpers below need) via `scripts/github/list-review-threads.mjs --repo <owner/name> --pr <number> --unresolved-only` rather than a hand-written `gh api graphql` query
|
|
212
220
|
2. before the first local file write in each fixer pass on a Copilot-assigned PR, run `node <resolved-skill-scripts>/loop/pre-write-remote-freshness-guard.mjs --branch <headRefName>` as a required fail-closed guard
|
|
213
221
|
- source `<headRefName>` from authoritative PR state (`headRefName`), not from a local branch guess
|
|
214
222
|
- if the guard exits non-zero (`remote_ahead`), stop writing locally, reconcile to the refreshed remote head, then restart the fixer pass
|
|
@@ -222,7 +230,7 @@ When unresolved feedback exists, use a narrow follow-up loop:
|
|
|
222
230
|
- source `<headRefName>` from authoritative PR state (`headRefName`), not from a local branch guess
|
|
223
231
|
- if the guard exits non-zero (`branch_mismatch`), stop and realign to the expected branch before staging or committing
|
|
224
232
|
7. if files changed, push the resolving commit before any thread reply claims the fix is present
|
|
225
|
-
8.
|
|
233
|
+
8. <!-- rule: COPILOT-FOLLOWUP-REPLY-RESOLVE-HELPER --> `COPILOT-FOLLOWUP-REPLY-RESOLVE-HELPER`: When a comment or thread is actually addressed, the agent MUST reply on GitHub with a short resolution note that references the resolving commit SHA or commit URL when applicable, using the deterministic helpers below rather than ad hoc thread mutations:
|
|
226
234
|
- for one thread, must use the deterministic helper `reply-resolve-review-thread.mjs` from the resolved skill scripts directory
|
|
227
235
|
- when the same bounded resolution note applies to multiple matching unresolved threads, use `reply-resolve-review-threads.mjs` instead of ad hoc inline `gh api` / `gh api graphql` mutations
|
|
228
236
|
- when using the single-thread helper, pair `--comment-id` and `--thread-id` from the same fresh PR thread snapshot rather than mixing ids across review rounds
|
|
@@ -239,11 +247,13 @@ When unresolved feedback exists, use a narrow follow-up loop:
|
|
|
239
247
|
- do not stop at a local fix if GitHub-side reply/resolve is authorized
|
|
240
248
|
11. after completing reply/resolve for a pass, verify zero unresolved threads remain via `dev-loops gate capture-threads` before proceeding
|
|
241
249
|
- if the refreshed snapshot reports unresolved threads, re-enter the reply/resolve loop for the missed threads
|
|
242
|
-
12. only after GitHub-side reply/resolve work is done for the addressed threads and the refreshed thread snapshot proves zero unresolved threads remain
|
|
243
|
-
- resolve the review-round cap from config via `resolveRefinementConfig(config, "maxCopilotRounds")` from `@dev-loops/core/config`; default config ships `maxCopilotRounds: 5`
|
|
250
|
+
12. <!-- rule: COPILOT-FOLLOWUP-ROUND-CAP --> `COPILOT-FOLLOWUP-ROUND-CAP`: The agent MUST decide whether another Copilot pass is desired, applying the round-cap/signal-gating rules below, only after GitHub-side reply/resolve work is done for the addressed threads and the refreshed thread snapshot proves zero unresolved threads remain
|
|
251
|
+
- resolve the review-round cap from config via `resolveRefinementConfig(config, "maxCopilotRounds")` from `@dev-loops/core/config`; default config ships `maxCopilotRounds: 5`. For a light-dispatched PR, resolve `resolveEffectiveCopilotRoundCap(config, { lightweight: true })` instead — `min(localImplementation.lightMode.maxCopilotRounds ?? 1, maxCopilotRounds)` (default lightweight cap: 1) — see the [Artifact Authority Contract](../docs/artifact-authority-contract.md) lightweight section (issue #1210)
|
|
252
|
+
- for a light-dispatched PR, pass `--lightweight` on every round-cap-consuming helper invocation — `detect-copilot-loop-state.mjs`, `copilot-pr-handoff.mjs`, `detect-pr-gate-coordination-state.mjs`, `request-copilot-review.mjs`, and `upsert-checkpoint-verdict.mjs` — otherwise those tools resolve the full-PR cap and the composed lightweight cap is never enforced
|
|
244
253
|
- **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
254
|
- 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
|
|
255
|
+
- if completed review rounds have reached the maximum (default: 5), do **not** re-request Copilot review within that concluded cycle
|
|
256
|
+
- 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
257
|
- 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
258
|
- 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
259
|
- 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
|
|
@@ -262,7 +272,10 @@ Do not treat `fix applied locally` as the end of the loop when the workflow also
|
|
|
262
272
|
|
|
263
273
|
### Mandatory gate-comment command contract
|
|
264
274
|
|
|
265
|
-
|
|
275
|
+
<!-- rule: COPILOT-FOLLOWUP-GATE-COMMENT-CANONICAL -->
|
|
276
|
+
`COPILOT-FOLLOWUP-GATE-COMMENT-CANONICAL`: For every `draft_gate` or `pre_approval_gate` comment, agents MUST run `upsert-checkpoint-verdict.mjs` and MUST NOT use `gh pr comment`, `gh api`, or `gh pr review` for gate comments.
|
|
277
|
+
|
|
278
|
+
Per `COPILOT-FOLLOWUP-GATE-COMMENT-CANONICAL` above, run:
|
|
266
279
|
|
|
267
280
|
For a gate that ran via the fan-out/fan-in sub-loop, pass the structured per-angle review results via `--findings-json` (NOT the wall-of-text `--findings-summary`). The operator/loop writes that JSON file from the collected per-angle results — the same per-angle `{angle, verdict, findings}` objects the fan-out reviewers wrote to `tmp/gate-reviews/<repo-slug>/pr-<N>/<gate>-<headSha>/<angle>.json` that feed `consolidateFanin`. The helper renders a readable per-angle breakdown and derives the single-line `**Findings summary:**` digest itself:
|
|
268
281
|
|
|
@@ -296,19 +309,19 @@ node <resolved-skill-scripts>/github/upsert-checkpoint-verdict.mjs \
|
|
|
296
309
|
|
|
297
310
|
`--execution-mode <fanout_fanin|inline_single_agent>` records how the gate review ran (default `inline_single_agent`). When the gate did not run via the fan-out/fan-in sub-loop ([Gate Review Sub-Loop Contract](../../docs/gate-review-sub-loop-contract.md)), you MUST pass `--execution-mode inline_single_agent --inline-reason "<why>"` — silent inline runs are no longer allowed: inline mode requires a non-empty `--inline-reason` and emits a stderr warning. Because inline is the default mode, a bare call with neither flag now fails with an argument error, so always pass `--execution-mode` explicitly (and `--inline-reason` for inline). The recorded `executionMode` is surfaced by `detect-checkpoint-evidence.mjs` and gated by `gates.requireFanoutEvidence`.
|
|
298
311
|
|
|
299
|
-
Do NOT use `gh pr comment`, `gh api`, or `gh pr review` for gate comments.
|
|
300
|
-
|
|
301
312
|
`--force --force-reason` on `upsert-checkpoint-verdict.mjs` is a narrow operator-authorized CI override for the helper itself, not the default gate path. Use it only when the helper refuses gate entry solely because the current head is `blocked_needs_user_decision` with `ciStatus="failure"`, and only after the user explicitly authorizes ignoring that current-head CI failure for this one gate-comment upsert. It does **not** bypass stale-head checks, unresolved-thread / unsettled-review refusal, non-draft `draft_gate` refusal, merge conflicts, or other legality checks.
|
|
302
313
|
|
|
303
314
|
### Gate fan-out/fan-in procedure (agent-orchestrated)
|
|
304
315
|
|
|
305
|
-
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.
|
|
316
|
+
Both gates run this same checkpoint review chain, owned end-to-end by [Gate Review Sub-Loop Contract](../../docs/gate-review-sub-loop-contract.md) (`GATE-EXEC-BUILD-ONCE-SEED`, `GATE-EXEC-BRIEFING-PREFIX`, `GATE-EXEC-SEPARATE-CHAINS`, `GATE-EXEC-POST-BEFORE-FIX`, `GATE-EXEC-REGATE-MANDATORY`, `GATE-EXEC-LIGHT-ESCALATION`); this section owns only this skill's dispatch of that 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.
|
|
306
317
|
|
|
307
|
-
1. **Context (Phase 1)
|
|
308
|
-
2. **Fan-out (Phase 2)
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
318
|
+
1. **Context (Phase 1):** build/read the gate-context artifact via `scripts/github/write-gate-context.mjs` (`buildGateContext` resolves the angle set through `resolveGateAnglesDynamic`; mandatory angles always survive). Consume the resulting `tmp/gate-context/<repo-slug>/pr-<N>/<gate>-<headSha>.json` artifact: the FULL diff at `scope.diffPath` plus the top-level adjacent-code bundle (`adjacentCode`) are the build-once seed every reviewer uses verbatim — see `GATE-EXEC-BUILD-ONCE-SEED` for the bundle contract; do not re-derive them per reviewer.
|
|
319
|
+
2. **Fan-out (Phase 2):** 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, sequential for overflow (record any degradation in the gate evidence). Each reviewer follows [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`.
|
|
320
|
+
<!-- rule: COPILOT-FOLLOWUP-ADVERSARIAL-BRIEFING -->
|
|
321
|
+
`COPILOT-FOLLOWUP-ADVERSARIAL-BRIEFING`: Each reviewer MUST be briefed 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 MAY widen scope (open adjacent repo files beyond the bundle) only when their angle genuinely needs more, recording that in the optional `contextWidened` field on their findings artifact. The LAYOUT of this briefing (invariant block first, this adversarial angle prompt last) and the `--prefix-hash`/`--prefix-file` sentinel recording are owned by `GATE-EXEC-BRIEFING-PREFIX` — not restated here.
|
|
322
|
+
3. **Fan-in (Phase 3):** before consolidating, run `scripts/github/verify-briefing-prefixes.mjs --head-sha <current_head_sha>` and stop the pass on a fail-closed result (`GATE-EXEC-BRIEFING-PREFIX` owns this check). Then consolidate via `consolidateFanin({ angleResults, blockCleanOnFindingSeverities })` (blocking severities from `resolveGateConfig(config, <configKey>).blockCleanOnFindingSeverities` — `draft` or `preApproval`, not the `<gate>` artifact name). Map with `toFindingsLogShape(...)` and write the disposition ledger via `write-gate-findings-log.mjs` before the visible comment (`GATE-EXEC-DISPOSITION-LEDGER`, `GATE-EXEC-POST-BEFORE-FIX` own that ordering and the ledger's opt-out-proof durability). When `resolveGatePostFindingsComments(config)` is true (default), post 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>'`.
|
|
323
|
+
4. **Verdict (Phase 4):** post via the [Gate comment command](#mandatory-gate-comment-command-contract) using `--execution-mode fanout_fanin` and `--findings-json <path>` built from the same per-angle artifacts consolidated in Phase 3 — that flag's accepted shapes are owned by the Gate comment command section above; do not restate them here.
|
|
324
|
+
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); re-run only the `findings_present` angles from the previous pass (context-builder and fan-in always re-run); repeat until the consolidated verdict is `clean` for the current head SHA (`GATE-EXEC-REGATE-MANDATORY`).
|
|
312
325
|
|
|
313
326
|
### Draft gate contract (before marking PR ready for review)
|
|
314
327
|
|
|
@@ -322,13 +335,9 @@ The canonical checkpoint verdict comment contract is [Gate Review Comment Contra
|
|
|
322
335
|
- **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
336
|
- **Pass criteria:** all configured draft gate angles pass; all findings at severities in `blockCleanOnFindingSeverities` are addressed; validation passes; no unrelated files are included.
|
|
324
337
|
- **Next step after passing:** mark the PR ready for review.
|
|
325
|
-
- **Board status sync (
|
|
326
|
-
|
|
327
|
-
|
|
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.
|
|
330
|
-
- **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
|
-
- **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`.
|
|
338
|
+
- **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.
|
|
339
|
+
- **Non-substitution rule:** a clean `draft_gate` comment only authorizes the draft → ready-for-review transition for that head SHA; cross-gate non-substitution is owned by `GATE-COMMENT-NON-SUBSTITUTION` in [Gate Review Comment Contract](../../docs/gate-review-comment-contract.md). This skill does not restate that rule.
|
|
340
|
+
- **Required PR comment:** post a visible checkpoint verdict comment using the mandatory [Gate comment command](#mandatory-gate-comment-command-contract). Comment field content and validation-reporting format are owned by `GATE-COMMENT-VALIDATION-REPORTING`; the draft-boundary comment requirement is owned by `GATE-COMMENT-DRAFT-REQUIREMENTS`; posting-failure fail-closed behavior is owned by `GATE-COMMENT-FAIL-CLOSED` — all in [Gate Review Comment Contract](../../docs/gate-review-comment-contract.md). This skill does not restate those field/format/fail-closed rules.
|
|
332
341
|
|
|
333
342
|
### Pre-approval gate contract
|
|
334
343
|
|
|
@@ -339,25 +348,26 @@ This is the default pre-approval gate for this workflow boundary. The canonical
|
|
|
339
348
|
- **Execution directive:** run the [Gate fan-out/fan-in procedure](#gate-fan-outfan-in-procedure-agent-orchestrated) (the agent-orchestrated chain defined in [Gate Review Sub-Loop Contract](../../docs/gate-review-sub-loop-contract.md)) with the pre-approval gate inspection angles resolved from config. The `acceptance-criteria` angle is mandatory for this gate (see `.devloops` `gates.preApproval.mandatoryAngles`) and always survives dynamic resolution. Retry rule: in subsequent cycles, only re-run reviewers that produced `findings_present` in the previous pass.
|
|
340
349
|
- **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
350
|
- **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
|
-
- **Pass criteria:** the sub-loop completes with verdict `clean`; all configured angles pass
|
|
343
|
-
- **Acceptance criteria verification:** follow the canonical procedure in [Acceptance Criteria Verification](../docs/acceptance-criteria-verification.md) before posting the `pre_approval_gate` comment.
|
|
351
|
+
- **Pass criteria:** the sub-loop completes with verdict `clean`; all configured angles pass, following the sequential-fallback rule owned by `GATE-EXEC-FANOUT-SEQUENTIAL-FALLBACK` in [Gate Review Sub-Loop Contract](../../docs/gate-review-sub-loop-contract.md).
|
|
352
|
+
- **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
353
|
- **Next step after passing:** continue the Step 7 flow and then proceed to the human approval checkpoint below.
|
|
345
|
-
- **Non-substitution rule:** a clean `pre_approval_gate` comment is separate from `draft_gate` evidence
|
|
346
|
-
- **Required PR comment:** post a visible checkpoint verdict comment using the mandatory [Gate comment command](#mandatory-gate-comment-command-contract).
|
|
354
|
+
- **Non-substitution rule:** a clean `pre_approval_gate` comment governs final-approval readiness for that head SHA and is separate from `draft_gate` evidence; cross-gate non-substitution is owned by `GATE-COMMENT-NON-SUBSTITUTION` in [Gate Review Comment Contract](../../docs/gate-review-comment-contract.md). This skill does not restate that rule.
|
|
355
|
+
- **Required PR comment:** post a visible checkpoint verdict comment using the mandatory [Gate comment command](#mandatory-gate-comment-command-contract). Comment field content and validation-reporting format are owned by `GATE-COMMENT-VALIDATION-REPORTING`; the pre-approval-boundary comment requirement is owned by `GATE-COMMENT-PREAPPROVAL-REQUIREMENTS`; posting-failure fail-closed behavior is owned by `GATE-COMMENT-FAIL-CLOSED` — all in [Gate Review Comment Contract](../../docs/gate-review-comment-contract.md). This skill does not restate those field/format/fail-closed rules.
|
|
356
|
+
- <!-- rule: GATE-SKIP-NOT-RECOVERABLE-BY-CONVERGENCE --> `GATE-SKIP-NOT-RECOVERABLE-BY-CONVERGENCE`: Skipping the gate MUST NOT be treated as recoverable by asserting convergence.
|
|
347
357
|
|
|
348
358
|
### Conflict-resolution gate
|
|
349
359
|
|
|
350
360
|
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
361
|
|
|
352
362
|
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
|
|
363
|
+
2. ask for explicit authorization before any merge commit or other branch-state-changing reconciliation command
|
|
364
|
+
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
365
|
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
366
|
5. rerun the smallest honest local validation for the touched conflict slice
|
|
357
367
|
6. rerun `detect-pr-gate-coordination-state.mjs` for the new head
|
|
358
368
|
7. because the head changed, rerun `pre_approval_gate` for the new head before any approval-ready or merge-ready claim
|
|
359
369
|
8. wait for current-head CI again before retrying merge evaluation
|
|
360
|
-
9. if the chosen reconciliation rewrote branch history (
|
|
370
|
+
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
371
|
|
|
362
372
|
`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
373
|
|
|
@@ -384,6 +394,21 @@ node <resolved-skill-scripts>/github/detect-checkpoint-evidence.mjs \
|
|
|
384
394
|
|
|
385
395
|
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
396
|
|
|
397
|
+
### Stale runner-coordination lock held by a completed run
|
|
398
|
+
|
|
399
|
+
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.
|
|
400
|
+
|
|
401
|
+
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.
|
|
402
|
+
|
|
403
|
+
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:
|
|
404
|
+
|
|
405
|
+
```sh
|
|
406
|
+
node <resolved-skill-scripts>/loop/pr-runner-coordination.mjs takeover \
|
|
407
|
+
--repo <owner/name> --pr <number>
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
`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.
|
|
411
|
+
|
|
387
412
|
### Mandatory post-merge retrospective checkpoint write
|
|
388
413
|
|
|
389
414
|
After a merge succeeds (or an explicit retrospective skip is authorized), write the durable retrospective checkpoint before exiting the subagent session:
|
|
@@ -410,7 +435,7 @@ node <resolved-skill-scripts>/projects/archive-done-items.mjs --repo <owner/name
|
|
|
410
435
|
|
|
411
436
|
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
437
|
|
|
413
|
-
|
|
438
|
+
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
439
|
|
|
415
440
|
```sh
|
|
416
441
|
node <resolved-skill-scripts>/projects/sync-item-status.mjs --repo <owner/name> --item <linked-issue> --to-column "Done"
|
|
@@ -434,6 +459,7 @@ Follow [Stop Conditions](../docs/stop-conditions.md). Genuine stops: `blocked` s
|
|
|
434
459
|
|
|
435
460
|
See [Anti-patterns](../docs/anti-patterns.md). Key repo-specific additions:
|
|
436
461
|
- Use `reply-resolve-review-thread.mjs` / `reply-resolve-review-threads.mjs` helpers instead of ad hoc `gh api`/`gh api graphql` thread-mutation commands. Do NOT use `gh pr comment`, `gh api`, or `gh pr review` for gate comments (use `upsert-checkpoint-verdict.mjs`).
|
|
462
|
+
- Use `list-review-threads.mjs` and `wait-pr-checks.mjs` instead of ad hoc `gh api graphql` review-thread queries or `gh pr checks` shell-pipe polling loops.
|
|
437
463
|
- Do not declare merge-ready without visible `pre_approval_gate` comment on current head SHA. Do not declare merge-ready based solely on `mergeable_state: clean` + CI green without gate evidence. CI green + resolved threads alone is insufficient.
|
|
438
464
|
- Do not blind-run `gh pr merge`/`gh pr update-branch`/unapproved rebase when conflicted. Do not dispatch async dev-loop tasks that omit the pre-approval gate requirement.
|
|
439
465
|
- Do not assume generated wiki is authoritative over code or CI.
|