dev-loops 0.2.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/.claude-plugin/plugin.json +1 -1
- package/.claude/agents/dev-loop.md +2 -1
- package/.claude/agents/developer.md +1 -0
- package/.claude/agents/fixer.md +1 -0
- package/.claude/agents/review.md +30 -0
- package/.claude/skills/copilot-pr-followup/SKILL.md +57 -3
- package/.claude/skills/dev-loop/SKILL.md +5 -5
- package/.claude/skills/docs/anti-patterns.md +2 -0
- package/.claude/skills/docs/copilot-loop-operations.md +2 -2
- package/.claude/skills/local-implementation/SKILL.md +17 -3
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +60 -0
- package/agents/dev-loop.agent.md +5 -1
- package/agents/developer.agent.md +1 -0
- package/agents/fixer.agent.md +1 -0
- package/agents/review.agent.md +30 -0
- package/cli/index.mjs +42 -7
- package/package.json +2 -2
- package/scripts/README.md +6 -5
- package/scripts/_cli-primitives.mjs +2 -0
- package/scripts/_core-helpers.mjs +1 -0
- package/scripts/claude/generate-claude-assets.mjs +12 -2
- package/scripts/claude/headless-dev-loop.mjs +53 -13
- package/scripts/claude/headless-info-smoke.mjs +45 -11
- package/scripts/docs/validate-links.mjs +20 -11
- package/scripts/github/build-adjacent-bundle.mjs +448 -0
- package/scripts/github/capture-review-threads.mjs +32 -14
- package/scripts/github/{create-draft-pr.mjs → create-pr.mjs} +28 -12
- package/scripts/github/detect-checkpoint-evidence.mjs +123 -15
- package/scripts/github/detect-linked-issue-pr.mjs +22 -10
- package/scripts/github/manage-sub-issues.mjs +37 -16
- package/scripts/github/post-gate-findings.mjs +392 -0
- package/scripts/github/probe-copilot-review.mjs +24 -12
- package/scripts/github/ready-for-review.mjs +17 -8
- package/scripts/github/reconcile-draft-gate.mjs +24 -12
- package/scripts/github/reply-resolve-review-threads.mjs +34 -15
- package/scripts/github/request-copilot-review.mjs +97 -19
- package/scripts/github/resolve-tracker-local-spec.mjs +29 -12
- package/scripts/github/stage-reviewer-draft.mjs +32 -14
- package/scripts/github/upsert-checkpoint-verdict.mjs +646 -41
- package/scripts/github/verify-fresh-review-context.mjs +12 -1
- package/scripts/github/write-gate-context.mjs +634 -0
- package/scripts/github/write-gate-findings-log.mjs +42 -21
- package/scripts/loop/build-handoff-envelope.mjs +32 -14
- package/scripts/loop/conductor-monitor.mjs +25 -9
- package/scripts/loop/conductor.mjs +31 -12
- package/scripts/loop/copilot-pr-handoff.mjs +31 -14
- package/scripts/loop/debt-remediate.mjs +28 -11
- package/scripts/loop/detect-change-scope.mjs +36 -11
- package/scripts/loop/detect-copilot-loop-state.mjs +29 -12
- package/scripts/loop/detect-copilot-session-activity.mjs +29 -12
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +26 -10
- package/scripts/loop/detect-internal-only-pr.mjs +31 -13
- package/scripts/loop/detect-issue-refinement-artifact.mjs +29 -12
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +56 -28
- package/scripts/loop/detect-reviewer-loop-state.mjs +35 -16
- package/scripts/loop/detect-stale-runner.mjs +32 -14
- package/scripts/loop/detect-tracker-first-loop-state.mjs +38 -11
- package/scripts/loop/detect-tracker-pr-state.mjs +23 -8
- package/scripts/loop/info.mjs +28 -10
- package/scripts/loop/inspect-run-viewer/cli.mjs +44 -21
- package/scripts/loop/inspect-run.mjs +35 -16
- package/scripts/loop/outer-loop.mjs +35 -16
- package/scripts/loop/pr-runner-coordination.mjs +31 -12
- package/scripts/loop/pre-commit-branch-guard.mjs +26 -9
- package/scripts/loop/pre-flight-gate.mjs +25 -9
- package/scripts/loop/pre-pr-ready-gate.mjs +24 -8
- package/scripts/loop/pre-push-main-guard.mjs +19 -5
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +23 -7
- package/scripts/loop/resolve-dev-loop-startup.mjs +29 -12
- package/scripts/loop/run-conductor-cycle.mjs +23 -8
- package/scripts/loop/run-queue.mjs +87 -15
- package/scripts/loop/run-refinement-audit.mjs +44 -22
- package/scripts/loop/run-watch-cycle.mjs +28 -12
- package/scripts/loop/steer-loop.mjs +122 -62
- package/scripts/loop/watch-initial-copilot-pr.mjs +28 -12
- package/scripts/projects/add-queue-item.mjs +60 -43
- package/scripts/projects/archive-done-items.mjs +506 -0
- package/scripts/projects/ensure-queue-board.mjs +65 -64
- package/scripts/projects/list-queue-items.mjs +57 -56
- package/scripts/projects/move-queue-item.mjs +123 -124
- package/scripts/projects/reorder-queue-item.mjs +384 -108
- package/scripts/projects/sync-item-status.mjs +198 -0
- package/scripts/refine/_refine-helpers.mjs +21 -9
- package/scripts/refine/verify.mjs +31 -13
- package/skills/copilot-pr-followup/SKILL.md +57 -3
- package/skills/dev-loop/SKILL.md +9 -5
- package/skills/dev-loop/scripts/log-bash-exit-1.mjs +2 -2
- package/skills/dev-loop/scripts/phase-files.mjs +2 -2
- package/skills/docs/anti-patterns.md +2 -0
- package/skills/docs/copilot-loop-operations.md +2 -2
- package/skills/local-implementation/SKILL.md +17 -3
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
3
|
-
import { parsePositiveInteger,
|
|
3
|
+
import { parsePositiveInteger, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
4
4
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
5
6
|
const USAGE = `Usage: detect-copilot-session-activity.mjs --repo <owner/name> --branch <name> [--limit <number>]
|
|
6
7
|
Detect Copilot GitHub Actions session activity on a branch.
|
|
7
8
|
Required:
|
|
@@ -41,32 +42,48 @@ const COPILOT_RUN_NAME_PATTERNS = Object.freeze([
|
|
|
41
42
|
]);
|
|
42
43
|
const parseError = buildParseError(USAGE);
|
|
43
44
|
export function parseDetectCopilotSessionActivityCliArgs(argv) {
|
|
44
|
-
const args = [...argv];
|
|
45
45
|
const options = {
|
|
46
46
|
help: false,
|
|
47
47
|
repo: undefined,
|
|
48
48
|
branch: undefined,
|
|
49
49
|
limit: DEFAULT_LIMIT,
|
|
50
50
|
};
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
const { tokens } = parseArgs({
|
|
52
|
+
args: [...argv],
|
|
53
|
+
options: {
|
|
54
|
+
help: { type: "boolean", short: "h" },
|
|
55
|
+
repo: { type: "string" },
|
|
56
|
+
branch: { type: "string" },
|
|
57
|
+
limit: { type: "string" },
|
|
58
|
+
},
|
|
59
|
+
allowPositionals: true,
|
|
60
|
+
strict: false,
|
|
61
|
+
tokens: true,
|
|
62
|
+
});
|
|
63
|
+
for (const token of tokens) {
|
|
64
|
+
if (token.kind === "positional") {
|
|
65
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
66
|
+
}
|
|
67
|
+
if (token.kind !== "option") {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (token.name === "help") {
|
|
54
71
|
options.help = true;
|
|
55
72
|
return options;
|
|
56
73
|
}
|
|
57
|
-
if (token === "
|
|
58
|
-
options.repo =
|
|
74
|
+
if (token.name === "repo") {
|
|
75
|
+
options.repo = requireTokenValue(token, parseError).trim();
|
|
59
76
|
continue;
|
|
60
77
|
}
|
|
61
|
-
if (token === "
|
|
62
|
-
options.branch =
|
|
78
|
+
if (token.name === "branch") {
|
|
79
|
+
options.branch = requireTokenValue(token, parseError).trim();
|
|
63
80
|
continue;
|
|
64
81
|
}
|
|
65
|
-
if (token === "
|
|
66
|
-
options.limit = parsePositiveInteger(
|
|
82
|
+
if (token.name === "limit") {
|
|
83
|
+
options.limit = parsePositiveInteger(requireTokenValue(token, parseError), "--limit", parseError);
|
|
67
84
|
continue;
|
|
68
85
|
}
|
|
69
|
-
throw parseError(`Unknown argument: ${token}`);
|
|
86
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
70
87
|
}
|
|
71
88
|
if (options.repo === undefined || options.branch === undefined || options.branch.length === 0) {
|
|
72
89
|
throw parseError("detect-copilot-session-activity requires both --repo <owner/name> and --branch <name>");
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
3
|
-
import { parseIssueNumber,
|
|
3
|
+
import { parseIssueNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
4
4
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
5
5
|
import { detectLinkedIssuePr } from "../github/detect-linked-issue-pr.mjs";
|
|
6
6
|
import { detectCopilotSessionActivity } from "./detect-copilot-session-activity.mjs";
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
7
8
|
const USAGE = `Usage: detect-initial-copilot-pr-state.mjs --repo <owner/name> --issue <number>
|
|
8
9
|
Detect whether an assigned issue is still on the bootstrap-only Copilot draft PR
|
|
9
10
|
or has moved into normal linked-PR follow-up.
|
|
@@ -79,27 +80,42 @@ const INITIAL_COPILOT_PR_FACTS_QUERY = [
|
|
|
79
80
|
].join("\n");
|
|
80
81
|
const parseError = buildParseError(USAGE);
|
|
81
82
|
export function parseDetectInitialCopilotPrStateCliArgs(argv) {
|
|
82
|
-
const args = [...argv];
|
|
83
83
|
const options = {
|
|
84
84
|
help: false,
|
|
85
85
|
repo: undefined,
|
|
86
86
|
issue: undefined,
|
|
87
87
|
};
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
const { tokens } = parseArgs({
|
|
89
|
+
args: [...argv],
|
|
90
|
+
options: {
|
|
91
|
+
help: { type: "boolean", short: "h" },
|
|
92
|
+
repo: { type: "string" },
|
|
93
|
+
issue: { type: "string" },
|
|
94
|
+
},
|
|
95
|
+
allowPositionals: true,
|
|
96
|
+
strict: false,
|
|
97
|
+
tokens: true,
|
|
98
|
+
});
|
|
99
|
+
for (const token of tokens) {
|
|
100
|
+
if (token.kind === "positional") {
|
|
101
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
102
|
+
}
|
|
103
|
+
if (token.kind !== "option") {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (token.name === "help") {
|
|
91
107
|
options.help = true;
|
|
92
108
|
return options;
|
|
93
109
|
}
|
|
94
|
-
if (token === "
|
|
95
|
-
options.repo =
|
|
110
|
+
if (token.name === "repo") {
|
|
111
|
+
options.repo = requireTokenValue(token, parseError).trim();
|
|
96
112
|
continue;
|
|
97
113
|
}
|
|
98
|
-
if (token === "
|
|
99
|
-
options.issue = parseIssueNumber(
|
|
114
|
+
if (token.name === "issue") {
|
|
115
|
+
options.issue = parseIssueNumber(requireTokenValue(token, parseError), parseError);
|
|
100
116
|
continue;
|
|
101
117
|
}
|
|
102
|
-
throw parseError(`Unknown argument: ${token}`);
|
|
118
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
103
119
|
}
|
|
104
120
|
if (options.repo === undefined || options.issue === undefined) {
|
|
105
121
|
throw parseError("detect-initial-copilot-pr-state requires both --repo <owner/name> and --issue <number>");
|
|
@@ -3,8 +3,9 @@ import { statSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
5
|
import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
|
|
6
|
-
import { parsePrNumber,
|
|
6
|
+
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
7
7
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
8
|
+
import { parseArgs } from "node:util";
|
|
8
9
|
|
|
9
10
|
const USAGE = `Usage: detect-internal-only-pr.mjs --repo <owner/name> --pr <number> [--config <path>]
|
|
10
11
|
Detect whether a PR only touches internal tooling files (scripts, docs, tests, config)
|
|
@@ -118,7 +119,6 @@ function buildPatternMatchers(patterns) {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
export function parseCliArgs(argv) {
|
|
121
|
-
const args = [...argv];
|
|
122
122
|
const options = {
|
|
123
123
|
help: false,
|
|
124
124
|
repo: undefined,
|
|
@@ -126,29 +126,47 @@ export function parseCliArgs(argv) {
|
|
|
126
126
|
config: undefined,
|
|
127
127
|
labelCheck: false,
|
|
128
128
|
};
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
const { tokens } = parseArgs({
|
|
130
|
+
args: [...argv],
|
|
131
|
+
options: {
|
|
132
|
+
help: { type: "boolean", short: "h" },
|
|
133
|
+
repo: { type: "string" },
|
|
134
|
+
pr: { type: "string" },
|
|
135
|
+
config: { type: "string" },
|
|
136
|
+
"label-check": { type: "boolean" },
|
|
137
|
+
},
|
|
138
|
+
allowPositionals: true,
|
|
139
|
+
strict: false,
|
|
140
|
+
tokens: true,
|
|
141
|
+
});
|
|
142
|
+
for (const token of tokens) {
|
|
143
|
+
if (token.kind === "positional") {
|
|
144
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
145
|
+
}
|
|
146
|
+
if (token.kind !== "option") {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (token.name === "help") {
|
|
132
150
|
options.help = true;
|
|
133
151
|
return options;
|
|
134
152
|
}
|
|
135
|
-
if (token === "
|
|
136
|
-
options.repo =
|
|
153
|
+
if (token.name === "repo") {
|
|
154
|
+
options.repo = requireTokenValue(token, parseError).trim();
|
|
137
155
|
continue;
|
|
138
156
|
}
|
|
139
|
-
if (token === "
|
|
140
|
-
options.pr = parsePrNumber(
|
|
157
|
+
if (token.name === "pr") {
|
|
158
|
+
options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
|
|
141
159
|
continue;
|
|
142
160
|
}
|
|
143
|
-
if (token === "
|
|
144
|
-
options.config =
|
|
161
|
+
if (token.name === "config") {
|
|
162
|
+
options.config = requireTokenValue(token, parseError).trim();
|
|
145
163
|
continue;
|
|
146
164
|
}
|
|
147
|
-
if (token === "
|
|
165
|
+
if (token.name === "label-check") {
|
|
148
166
|
options.labelCheck = true;
|
|
149
167
|
continue;
|
|
150
168
|
}
|
|
151
|
-
throw parseError(`Unknown argument: ${token}`);
|
|
169
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
152
170
|
}
|
|
153
171
|
if (options.repo === undefined || options.pr === undefined) {
|
|
154
172
|
throw parseError("detect-internal-only-pr requires both --repo <owner/name> and --pr <number>");
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
5
5
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
6
7
|
import {
|
|
7
8
|
detectIssueRefinementArtifact,
|
|
8
9
|
REFINEMENT_SOURCE,
|
|
@@ -33,36 +34,52 @@ Error output (stderr, JSON):
|
|
|
33
34
|
{ "ok": false, "error": "...", "usage": "..." }`.trim();
|
|
34
35
|
const parseError = buildParseError(USAGE);
|
|
35
36
|
export function parseDetectIssueRefinementArtifactCliArgs(argv) {
|
|
36
|
-
const args = [...argv];
|
|
37
37
|
const options = {
|
|
38
38
|
help: false,
|
|
39
39
|
repo: undefined,
|
|
40
40
|
issue: undefined,
|
|
41
41
|
input: undefined,
|
|
42
42
|
};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
const { tokens } = parseArgs({
|
|
44
|
+
args: [...argv],
|
|
45
|
+
options: {
|
|
46
|
+
help: { type: "boolean", short: "h" },
|
|
47
|
+
repo: { type: "string" },
|
|
48
|
+
issue: { type: "string" },
|
|
49
|
+
input: { type: "string" },
|
|
50
|
+
},
|
|
51
|
+
allowPositionals: true,
|
|
52
|
+
strict: false,
|
|
53
|
+
tokens: true,
|
|
54
|
+
});
|
|
55
|
+
for (const token of tokens) {
|
|
56
|
+
if (token.kind === "positional") {
|
|
57
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
58
|
+
}
|
|
59
|
+
if (token.kind !== "option") {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (token.name === "help") {
|
|
46
63
|
options.help = true;
|
|
47
64
|
return options;
|
|
48
65
|
}
|
|
49
|
-
if (token === "
|
|
50
|
-
options.repo =
|
|
66
|
+
if (token.name === "repo") {
|
|
67
|
+
options.repo = requireTokenValue(token, parseError).trim();
|
|
51
68
|
continue;
|
|
52
69
|
}
|
|
53
|
-
if (token === "
|
|
54
|
-
const value =
|
|
70
|
+
if (token.name === "issue") {
|
|
71
|
+
const value = requireTokenValue(token, parseError);
|
|
55
72
|
if (!/^\d+$/.test(value) || Number(value) === 0) {
|
|
56
73
|
throw parseError("--issue must be a positive integer");
|
|
57
74
|
}
|
|
58
75
|
options.issue = Number(value);
|
|
59
76
|
continue;
|
|
60
77
|
}
|
|
61
|
-
if (token === "
|
|
62
|
-
options.input =
|
|
78
|
+
if (token.name === "input") {
|
|
79
|
+
options.input = requireTokenValue(token, parseError).trim();
|
|
63
80
|
continue;
|
|
64
81
|
}
|
|
65
|
-
throw parseError(`Unknown argument: ${token}`);
|
|
82
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
66
83
|
}
|
|
67
84
|
const hasInput = typeof options.input === "string" && options.input.length > 0;
|
|
68
85
|
const hasRemote = typeof options.repo === "string" && options.repo.length > 0 && Number.isInteger(options.issue);
|
|
@@ -6,19 +6,20 @@ import {
|
|
|
6
6
|
formatCliError,
|
|
7
7
|
isCopilotLogin,
|
|
8
8
|
isDirectCliRun,
|
|
9
|
-
normalizeTimestamp,
|
|
10
9
|
parseJsonText,
|
|
11
10
|
parseReviewThreads,
|
|
11
|
+
resolveDraftGateRoundResetMs,
|
|
12
12
|
summarizeCopilotReviews,
|
|
13
13
|
} from "../_core-helpers.mjs";
|
|
14
|
-
import { parsePrNumber,
|
|
15
|
-
import { loadDevLoopConfig, resolveGateConfig, resolveRefinementConfig, resolveWorkflowConfig } from "@dev-loops/core/config";
|
|
14
|
+
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
15
|
+
import { loadDevLoopConfig, resolveGateConfig, resolveRefinement, resolveRefinementConfig, resolveWorkflowConfig } from "@dev-loops/core/config";
|
|
16
16
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
17
17
|
import { buildSnapshotFromPrFacts, interpretLoopState, summarizeLoopInterpretation } from "@dev-loops/core/loop/copilot-loop-state";
|
|
18
18
|
import { evaluatePrGateCoordination, PR_CHECKPOINT, PR_CHECKPOINT_ACTION } from "@dev-loops/core/loop/pr-gate-coordination";
|
|
19
19
|
import { shouldGuardCopilotReviewRequest } from "@dev-loops/core/loop/pr-gate-coordination";
|
|
20
20
|
import { fetchGithubReviewThreadsPayload } from "../github/capture-review-threads.mjs";
|
|
21
21
|
import { detectCheckpointEvidence } from "../github/detect-checkpoint-evidence.mjs";
|
|
22
|
+
import { parseArgs } from "node:util";
|
|
22
23
|
const UNMERGED_GIT_STATUS_CODES = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
|
|
23
24
|
const USAGE = `Usage: detect-pr-gate-coordination-state.mjs --repo <owner/name> --pr <number>
|
|
24
25
|
Determine which PR gate/transition is legal next for a pull request.
|
|
@@ -70,27 +71,42 @@ Exit codes:
|
|
|
70
71
|
1 Argument error or gh/runtime failure`.trim();
|
|
71
72
|
const parseError = buildParseError(USAGE);
|
|
72
73
|
export function parseDetectPrGateCoordinationCliArgs(argv) {
|
|
73
|
-
const args = [...argv];
|
|
74
74
|
const options = {
|
|
75
75
|
help: false,
|
|
76
76
|
repo: undefined,
|
|
77
77
|
pr: undefined,
|
|
78
78
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
const { tokens } = parseArgs({
|
|
80
|
+
args: [...argv],
|
|
81
|
+
options: {
|
|
82
|
+
help: { type: "boolean", short: "h" },
|
|
83
|
+
repo: { type: "string" },
|
|
84
|
+
pr: { type: "string" },
|
|
85
|
+
},
|
|
86
|
+
allowPositionals: true,
|
|
87
|
+
strict: false,
|
|
88
|
+
tokens: true,
|
|
89
|
+
});
|
|
90
|
+
for (const token of tokens) {
|
|
91
|
+
if (token.kind === "positional") {
|
|
92
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
93
|
+
}
|
|
94
|
+
if (token.kind !== "option") {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (token.name === "help") {
|
|
82
98
|
options.help = true;
|
|
83
99
|
return options;
|
|
84
100
|
}
|
|
85
|
-
if (token === "
|
|
86
|
-
options.repo =
|
|
101
|
+
if (token.name === "repo") {
|
|
102
|
+
options.repo = requireTokenValue(token, parseError).trim();
|
|
87
103
|
continue;
|
|
88
104
|
}
|
|
89
|
-
if (token === "
|
|
90
|
-
options.pr = parsePrNumber(
|
|
105
|
+
if (token.name === "pr") {
|
|
106
|
+
options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
|
|
91
107
|
continue;
|
|
92
108
|
}
|
|
93
|
-
throw parseError(`Unknown argument: ${token}`);
|
|
109
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
94
110
|
}
|
|
95
111
|
if (options.repo === undefined || options.pr === undefined) {
|
|
96
112
|
throw parseError("detect-pr-gate-coordination-state requires both --repo <owner/name> and --pr <number>");
|
|
@@ -287,18 +303,13 @@ export async function loadPrGateCoordinationContext(options, runtime = {}) {
|
|
|
287
303
|
const gateEvidence = await detectCheckpointEvidence(options, runtime);
|
|
288
304
|
// When draft gate was re-passed on a different head, use its timestamp
|
|
289
305
|
// to reset the Copilot round count — only reviews after the re-pass count.
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
&& typeof draftGateHeadSha === "string"
|
|
298
|
-
&& !draftGateOnCurrentHead
|
|
299
|
-
&& typeof gateEvidence.draftGate?.updatedAt === "string"
|
|
300
|
-
? normalizeTimestamp(gateEvidence.draftGate.updatedAt)
|
|
301
|
-
: null;
|
|
306
|
+
// Shared with request-copilot-review so both scripts compute the same
|
|
307
|
+
// completed round count / cap (#896). Prefix matching for the head SHA lets
|
|
308
|
+
// shortened SHAs (7+) from gate comments match the full headRefOid.
|
|
309
|
+
const draftGateResetAtMs = resolveDraftGateRoundResetMs({
|
|
310
|
+
draftGate: gateEvidence.draftGate,
|
|
311
|
+
currentHeadSha,
|
|
312
|
+
});
|
|
302
313
|
const reviewSummary = summarizeCopilotReviews(prData?.reviews, { headSha: currentHeadSha, draftGateResetAtMs });
|
|
303
314
|
const reviewRequestStatus = requestedReviewers.requested
|
|
304
315
|
? "requested"
|
|
@@ -322,8 +333,19 @@ export async function loadPrGateCoordinationContext(options, runtime = {}) {
|
|
|
322
333
|
if (gateEvidence.currentHeadSha !== currentHeadSha) {
|
|
323
334
|
throw new Error(`PR head changed while loading gate coordination facts for ${options.repo}#${options.pr}; refuse to evaluate mixed-head gate state.`);
|
|
324
335
|
}
|
|
325
|
-
|
|
326
|
-
|
|
336
|
+
// Resolve the refinement config (round cap, low-signal heuristic) and feed it to
|
|
337
|
+
// the interpreter. Without it, the interpreter cannot see maxCopilotRounds and so
|
|
338
|
+
// never resolves ROUND_CAP_CLEAN_FALLBACK — a post-cap clean head would fall to
|
|
339
|
+
// READY_TO_REREQUEST_REVIEW, dead-ending the loop at the round cap (#896). This
|
|
340
|
+
// keeps the gate-coordination interpretation consistent with the standalone
|
|
341
|
+
// detect-copilot-loop-state path and with request-copilot-review's cap logic.
|
|
342
|
+
const interpreterRepoRoot = runtime.repoRoot ?? process.cwd();
|
|
343
|
+
const interpreterConfigResult = await loadDevLoopConfig({ repoRoot: interpreterRepoRoot });
|
|
344
|
+
const interpreterRefinementConfig = (Array.isArray(interpreterConfigResult.errors) && interpreterConfigResult.errors.length > 0)
|
|
345
|
+
? resolveRefinement({ version: 1 })
|
|
346
|
+
: resolveRefinement(interpreterConfigResult.config ?? { version: 1 });
|
|
347
|
+
const interpretation = interpretLoopState(snapshot, interpreterRefinementConfig);
|
|
348
|
+
const disposition = summarizeLoopInterpretation(interpretation, interpreterRefinementConfig);
|
|
327
349
|
const mergeStateStatus = typeof prData?.mergeStateStatus === "string" && prData.mergeStateStatus.trim().length > 0
|
|
328
350
|
? prData.mergeStateStatus.trim().toUpperCase()
|
|
329
351
|
: null;
|
|
@@ -346,6 +368,7 @@ export async function loadPrGateCoordinationContext(options, runtime = {}) {
|
|
|
346
368
|
interpretation,
|
|
347
369
|
disposition,
|
|
348
370
|
refinementArtifact,
|
|
371
|
+
refinementConfig: interpreterRefinementConfig,
|
|
349
372
|
};
|
|
350
373
|
}
|
|
351
374
|
|
|
@@ -413,9 +436,13 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
|
|
|
413
436
|
&& typeof (context.snapshot?.copilotReviewRoundCount) === "number"
|
|
414
437
|
&& context.snapshot?.copilotReviewRoundCount >= maxCopilotRounds;
|
|
415
438
|
const sameHeadCleanConverged = context.interpretation?.sameHeadCleanConverged ?? false;
|
|
439
|
+
// Round-cap clean fallback (#896): the interpreter resolved a clean post-cap head
|
|
440
|
+
// (zero unresolved threads + green CI) that Copilot will not re-review. The formal
|
|
441
|
+
// request guard must not fire here — pre_approval_gate reviews the post-cap head.
|
|
442
|
+
const roundCapCleanFallback = context.interpretation?.roundCapCleanEligible ?? false;
|
|
416
443
|
const copilotReviewEverFormallyRequested = copilotReviewRequestStatus === "none"
|
|
417
444
|
&& guardBoundaries.has(result.gateBoundary)
|
|
418
|
-
&& !(roundCapReached && sameHeadCleanConverged)
|
|
445
|
+
&& !(roundCapReached && (sameHeadCleanConverged || roundCapCleanFallback))
|
|
419
446
|
? await fetchCopilotEverFormallyRequested(
|
|
420
447
|
{ repo: context.repo, pr: context.pr },
|
|
421
448
|
runtime,
|
|
@@ -426,7 +453,8 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
|
|
|
426
453
|
copilotReviewRoundCount: context.snapshot?.copilotReviewRoundCount ?? 0,
|
|
427
454
|
copilotReviewEverFormallyRequested,
|
|
428
455
|
maxCopilotRounds,
|
|
429
|
-
sameHeadCleanConverged
|
|
456
|
+
sameHeadCleanConverged,
|
|
457
|
+
roundCapCleanFallback,
|
|
430
458
|
gateBoundary: result.gateBoundary,
|
|
431
459
|
})) {
|
|
432
460
|
result.gateBoundary = PR_CHECKPOINT.POST_DRAFT_EXTERNAL_REVIEW;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import {
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
4
5
|
import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
5
6
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
6
7
|
import {
|
|
@@ -26,7 +27,20 @@ function parseBool(value, flag) {
|
|
|
26
27
|
throw new Error(`${flag} must be true or false`);
|
|
27
28
|
}
|
|
28
29
|
export function parseDetectReviewerCliArgs(argv) {
|
|
29
|
-
const
|
|
30
|
+
const { tokens } = parseArgs({
|
|
31
|
+
args: [...argv],
|
|
32
|
+
options: {
|
|
33
|
+
help: { type: "boolean", short: "h" },
|
|
34
|
+
input: { type: "string" },
|
|
35
|
+
repo: { type: "string" },
|
|
36
|
+
pr: { type: "string" },
|
|
37
|
+
"review-requested": { type: "string" },
|
|
38
|
+
"local-state": { type: "string" },
|
|
39
|
+
},
|
|
40
|
+
allowPositionals: true,
|
|
41
|
+
strict: false,
|
|
42
|
+
tokens: true,
|
|
43
|
+
});
|
|
30
44
|
const options = {
|
|
31
45
|
inputPath: undefined,
|
|
32
46
|
repo: undefined,
|
|
@@ -35,36 +49,41 @@ export function parseDetectReviewerCliArgs(argv) {
|
|
|
35
49
|
localStatePath: undefined,
|
|
36
50
|
help: false,
|
|
37
51
|
};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
for (const token of tokens) {
|
|
53
|
+
if (token.kind === "positional") {
|
|
54
|
+
throw new Error(`Unknown argument: ${token.value}`);
|
|
55
|
+
}
|
|
56
|
+
if (token.kind !== "option") {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (token.name === "help") {
|
|
41
60
|
options.help = true;
|
|
42
61
|
return options;
|
|
43
62
|
}
|
|
44
|
-
if (token === "
|
|
45
|
-
options.inputPath =
|
|
63
|
+
if (token.name === "input") {
|
|
64
|
+
options.inputPath = requireTokenValue(token);
|
|
46
65
|
continue;
|
|
47
66
|
}
|
|
48
|
-
if (token === "
|
|
49
|
-
options.repo =
|
|
67
|
+
if (token.name === "repo") {
|
|
68
|
+
options.repo = requireTokenValue(token).trim();
|
|
50
69
|
continue;
|
|
51
70
|
}
|
|
52
|
-
if (token === "
|
|
53
|
-
options.pr = parsePrNumber(
|
|
71
|
+
if (token.name === "pr") {
|
|
72
|
+
options.pr = parsePrNumber(requireTokenValue(token));
|
|
54
73
|
continue;
|
|
55
74
|
}
|
|
56
|
-
if (token === "
|
|
75
|
+
if (token.name === "review-requested") {
|
|
57
76
|
options.reviewRequestedOverride = parseBool(
|
|
58
|
-
|
|
77
|
+
requireTokenValue(token),
|
|
59
78
|
"--review-requested",
|
|
60
79
|
);
|
|
61
80
|
continue;
|
|
62
81
|
}
|
|
63
|
-
if (token === "
|
|
64
|
-
options.localStatePath =
|
|
82
|
+
if (token.name === "local-state") {
|
|
83
|
+
options.localStatePath = requireTokenValue(token);
|
|
65
84
|
continue;
|
|
66
85
|
}
|
|
67
|
-
throw new Error(`Unknown argument: ${token}`);
|
|
86
|
+
throw new Error(`Unknown argument: ${token.rawName}`);
|
|
68
87
|
}
|
|
69
88
|
if (options.inputPath !== undefined) {
|
|
70
89
|
if (options.repo !== undefined || options.pr !== undefined) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
|
|
4
|
-
import { parsePrNumber,
|
|
4
|
+
import { parsePrNumber, requireTokenValue } from "../_cli-primitives.mjs";
|
|
5
5
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
6
6
|
import { resolveRunId as resolveEnvRunId } from "@dev-loops/core/loop/run-context";
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
7
8
|
import {
|
|
8
9
|
detectStaleRunner,
|
|
9
10
|
STALE_RUNNER_ERROR,
|
|
@@ -50,7 +51,6 @@ Exit codes:
|
|
|
50
51
|
1 Argument error or stale/exit-signal condition detected`.trim();
|
|
51
52
|
const parseError = buildParseError(USAGE);
|
|
52
53
|
function parseCliArgs(argv) {
|
|
53
|
-
const args = [...argv];
|
|
54
54
|
const options = {
|
|
55
55
|
help: false,
|
|
56
56
|
repo: undefined,
|
|
@@ -58,22 +58,40 @@ function parseCliArgs(argv) {
|
|
|
58
58
|
staleRunnerMaxAgeMs: undefined,
|
|
59
59
|
runId: undefined,
|
|
60
60
|
};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
const { tokens } = parseArgs({
|
|
62
|
+
args: [...argv],
|
|
63
|
+
options: {
|
|
64
|
+
help: { type: "boolean", short: "h" },
|
|
65
|
+
repo: { type: "string" },
|
|
66
|
+
pr: { type: "string" },
|
|
67
|
+
"stale-runner-max-age-ms": { type: "string" },
|
|
68
|
+
"run-id": { type: "string" },
|
|
69
|
+
},
|
|
70
|
+
allowPositionals: true,
|
|
71
|
+
strict: false,
|
|
72
|
+
tokens: true,
|
|
73
|
+
});
|
|
74
|
+
for (const token of tokens) {
|
|
75
|
+
if (token.kind === "positional") {
|
|
76
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
77
|
+
}
|
|
78
|
+
if (token.kind !== "option") {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (token.name === "help") {
|
|
64
82
|
options.help = true;
|
|
65
83
|
return options;
|
|
66
84
|
}
|
|
67
|
-
if (token === "
|
|
68
|
-
options.repo =
|
|
85
|
+
if (token.name === "repo") {
|
|
86
|
+
options.repo = requireTokenValue(token, parseError).trim();
|
|
69
87
|
continue;
|
|
70
88
|
}
|
|
71
|
-
if (token === "
|
|
72
|
-
options.pr = parsePrNumber(
|
|
89
|
+
if (token.name === "pr") {
|
|
90
|
+
options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
|
|
73
91
|
continue;
|
|
74
92
|
}
|
|
75
|
-
if (token === "
|
|
76
|
-
const raw =
|
|
93
|
+
if (token.name === "stale-runner-max-age-ms") {
|
|
94
|
+
const raw = requireTokenValue(token, parseError).trim();
|
|
77
95
|
const parsed = Number(raw);
|
|
78
96
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
79
97
|
throw parseError(`--stale-runner-max-age-ms must be a positive integer (ms), got: ${raw}`);
|
|
@@ -81,11 +99,11 @@ function parseCliArgs(argv) {
|
|
|
81
99
|
options.staleRunnerMaxAgeMs = Math.floor(parsed);
|
|
82
100
|
continue;
|
|
83
101
|
}
|
|
84
|
-
if (token === "
|
|
85
|
-
options.runId =
|
|
102
|
+
if (token.name === "run-id") {
|
|
103
|
+
options.runId = requireTokenValue(token, parseError).trim();
|
|
86
104
|
continue;
|
|
87
105
|
}
|
|
88
|
-
throw parseError(`Unknown argument: ${token}`);
|
|
106
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
89
107
|
}
|
|
90
108
|
if (options.repo === undefined || options.pr === undefined) {
|
|
91
109
|
throw parseError("detect-stale-runner requires both --repo <owner/name> and --pr <number>");
|