@deftai/directive-core 0.66.2 → 0.68.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/dist/agents-md-advisory/evaluate.d.ts +44 -0
- package/dist/agents-md-advisory/evaluate.js +115 -0
- package/dist/agents-md-advisory/index.d.ts +2 -0
- package/dist/agents-md-advisory/index.js +2 -0
- package/dist/agents-md-budget/evaluate.d.ts +38 -0
- package/dist/agents-md-budget/evaluate.js +152 -0
- package/dist/agents-md-budget/index.d.ts +2 -0
- package/dist/agents-md-budget/index.js +2 -0
- package/dist/check/index.d.ts +1 -1
- package/dist/check/index.js +1 -1
- package/dist/check/orchestrator.d.ts +14 -4
- package/dist/check/orchestrator.js +30 -6
- package/dist/codebase/map.d.ts +1 -0
- package/dist/codebase/map.js +5 -2
- package/dist/doctor/checks.js +11 -11
- package/dist/doctor/constants.d.ts +7 -2
- package/dist/doctor/constants.js +17 -1
- package/dist/doctor/flags.js +5 -1
- package/dist/doctor/main.js +99 -8
- package/dist/doctor/payload-staleness.js +15 -6
- package/dist/doctor/types.d.ts +7 -0
- package/dist/forward-coverage/evaluate.d.ts +55 -0
- package/dist/forward-coverage/evaluate.js +254 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init-deposit/constants.d.ts +14 -0
- package/dist/init-deposit/constants.js +10 -0
- package/dist/init-deposit/hygiene.d.ts +50 -0
- package/dist/init-deposit/hygiene.js +176 -0
- package/dist/init-deposit/init-deposit.d.ts +1 -0
- package/dist/init-deposit/init-deposit.js +7 -1
- package/dist/init-deposit/refresh.d.ts +2 -0
- package/dist/init-deposit/refresh.js +48 -30
- package/dist/init-deposit/scaffold.d.ts +3 -4
- package/dist/init-deposit/scaffold.js +3 -24
- package/dist/intake/github-auth-modes.d.ts +1 -0
- package/dist/intake/github-auth-modes.js +1 -0
- package/dist/intake/issue-ingest.d.ts +6 -2
- package/dist/intake/issue-ingest.js +65 -9
- package/dist/integration-e2e/helpers.d.ts +0 -1
- package/dist/integration-e2e/helpers.js +3 -7
- package/dist/layout/lifecycle-ref.d.ts +15 -0
- package/dist/layout/lifecycle-ref.js +55 -0
- package/dist/platform/agents-consumer-header.d.ts +13 -0
- package/dist/platform/agents-consumer-header.js +57 -0
- package/dist/platform/agents-md.d.ts +28 -0
- package/dist/platform/agents-md.js +38 -1
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/policy/agents-md-advisory.d.ts +52 -0
- package/dist/policy/agents-md-advisory.js +63 -0
- package/dist/policy/agents-md-budget.d.ts +24 -0
- package/dist/policy/agents-md-budget.js +89 -0
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +1 -0
- package/dist/policy/resolve.js +56 -46
- package/dist/pr-merge-readiness/ci-gate.d.ts +28 -0
- package/dist/pr-merge-readiness/ci-gate.js +79 -0
- package/dist/pr-merge-readiness/compute.d.ts +15 -1
- package/dist/pr-merge-readiness/compute.js +129 -9
- package/dist/pr-merge-readiness/gh.d.ts +8 -0
- package/dist/pr-merge-readiness/gh.js +30 -5
- package/dist/pr-merge-readiness/index.d.ts +5 -1
- package/dist/pr-merge-readiness/index.js +3 -0
- package/dist/pr-merge-readiness/main.d.ts +3 -0
- package/dist/pr-merge-readiness/main.js +73 -6
- package/dist/pr-merge-readiness/mergeability.d.ts +43 -0
- package/dist/pr-merge-readiness/mergeability.js +105 -0
- package/dist/pr-merge-readiness/output.js +20 -0
- package/dist/pr-merge-readiness/slizard-gate.d.ts +47 -0
- package/dist/pr-merge-readiness/slizard-gate.js +119 -0
- package/dist/pr-monitor/index.d.ts +1 -1
- package/dist/pr-monitor/index.js +1 -1
- package/dist/pr-monitor/monitor.d.ts +8 -2
- package/dist/pr-monitor/monitor.js +35 -6
- package/dist/pr-wait-mergeable/wrappers.d.ts +12 -1
- package/dist/pr-wait-mergeable/wrappers.js +13 -5
- package/dist/pr-watch/constants.d.ts +33 -0
- package/dist/pr-watch/constants.js +33 -0
- package/dist/pr-watch/index.d.ts +6 -0
- package/dist/pr-watch/index.js +6 -0
- package/dist/pr-watch/main.d.ts +21 -0
- package/dist/pr-watch/main.js +206 -0
- package/dist/pr-watch/probe.d.ts +13 -0
- package/dist/pr-watch/probe.js +100 -0
- package/dist/pr-watch/types.d.ts +47 -0
- package/dist/pr-watch/types.js +2 -0
- package/dist/pr-watch/watch.d.ts +12 -0
- package/dist/pr-watch/watch.js +122 -0
- package/dist/preflight-cache/evaluate.d.ts +1 -0
- package/dist/preflight-cache/evaluate.js +17 -10
- package/dist/render/project-render.d.ts +16 -2
- package/dist/render/project-render.js +155 -97
- package/dist/scope/capacity-stamp.d.ts +24 -2
- package/dist/scope/capacity-stamp.js +80 -28
- package/dist/scope/decomposed-refs.js +2 -2
- package/dist/scope/project-definition-sync.js +3 -3
- package/dist/scope/vbrief-ref.d.ts +2 -1
- package/dist/scope/vbrief-ref.js +3 -2
- package/dist/spec-authority/constants.d.ts +6 -0
- package/dist/spec-authority/constants.js +11 -0
- package/dist/spec-authority/resolver.js +3 -3
- package/dist/swarm/finalize-cohort-cli.d.ts +5 -0
- package/dist/swarm/finalize-cohort-cli.js +100 -0
- package/dist/swarm/finalize-cohort.d.ts +39 -0
- package/dist/swarm/finalize-cohort.js +589 -0
- package/dist/swarm/index.d.ts +2 -0
- package/dist/swarm/index.js +2 -0
- package/dist/swarm/launch.d.ts +13 -0
- package/dist/swarm/launch.js +19 -7
- package/dist/swarm/routing-verify.d.ts +1 -1
- package/dist/swarm/routing-verify.js +11 -10
- package/dist/swarm/routing.d.ts +9 -0
- package/dist/swarm/routing.js +46 -0
- package/dist/swarm/verify-review-clean-cli.js +25 -1
- package/dist/swarm/verify-review-clean.d.ts +9 -1
- package/dist/swarm/verify-review-clean.js +76 -3
- package/dist/triage/queue/index.d.ts +1 -0
- package/dist/triage/queue/index.js +1 -0
- package/dist/triage/queue/project.d.ts +6 -1
- package/dist/triage/queue/project.js +9 -4
- package/dist/triage/queue/reconcile-live-state.d.ts +42 -0
- package/dist/triage/queue/reconcile-live-state.js +54 -0
- package/dist/triage/scope/mutations-core.js +91 -81
- package/dist/triage/scope/resolve.js +4 -3
- package/dist/triage/scope-drift/add-ignore.js +33 -32
- package/dist/triage/scope-drift/scope-rules.js +3 -3
- package/dist/triage/subscribe/index.js +51 -45
- package/dist/triage/welcome/writers.js +79 -64
- package/dist/vbrief-build/project-definition-io.js +8 -3
- package/dist/vbrief-validate/paths.js +8 -4
- package/dist/vbrief-validate/precutover.d.ts +1 -1
- package/dist/vbrief-validate/precutover.js +12 -10
- package/dist/verify-source/biome-config.d.ts +41 -0
- package/dist/verify-source/biome-config.js +97 -0
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/xbrief-migrate/agents-header.d.ts +69 -0
- package/dist/xbrief-migrate/agents-header.js +179 -0
- package/dist/xbrief-migrate/fs-helpers.d.ts +3 -0
- package/dist/xbrief-migrate/fs-helpers.js +11 -0
- package/dist/xbrief-migrate/index.d.ts +1 -0
- package/dist/xbrief-migrate/index.js +1 -0
- package/dist/xbrief-migrate/migrate-project.js +29 -10
- package/package.json +15 -7
- package/dist/install-upgrade/index.d.ts +0 -13
- package/dist/install-upgrade/index.js +0 -251
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { evaluate as evaluateBranchPolicy } from "../branch/evaluate.js";
|
|
4
|
+
import { resolveLifecycleRoot } from "../layout/resolve.js";
|
|
5
|
+
import { defaultRunGh, fetchClosingIssuesReferences } from "../pr-protected-issues/gh.js";
|
|
6
|
+
import { completeCohort } from "./complete-cohort.js";
|
|
7
|
+
import { EXIT_CONFIG_ERROR, EXIT_GATE_FAILED, EXIT_OK } from "./constants.js";
|
|
8
|
+
import { completedBriefReferencesIssue, resolveStories } from "./launch.js";
|
|
9
|
+
import { runText } from "./subprocess.js";
|
|
10
|
+
function splitCsv(values) {
|
|
11
|
+
const out = [];
|
|
12
|
+
for (const value of values) {
|
|
13
|
+
for (const piece of value.split(",")) {
|
|
14
|
+
const trimmed = piece.trim();
|
|
15
|
+
if (trimmed.length > 0) {
|
|
16
|
+
out.push(trimmed);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
function safeSegment(text) {
|
|
23
|
+
let cleaned = "";
|
|
24
|
+
for (const ch of text.trim()) {
|
|
25
|
+
if ((ch >= "A" && ch <= "Z") ||
|
|
26
|
+
(ch >= "a" && ch <= "z") ||
|
|
27
|
+
(ch >= "0" && ch <= "9") ||
|
|
28
|
+
ch === "." ||
|
|
29
|
+
ch === "_" ||
|
|
30
|
+
ch === "-") {
|
|
31
|
+
cleaned += ch;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
cleaned += "-";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
let start = 0;
|
|
38
|
+
let end = cleaned.length;
|
|
39
|
+
while (start < end && (cleaned[start] === "-" || cleaned[start] === ".")) {
|
|
40
|
+
start += 1;
|
|
41
|
+
}
|
|
42
|
+
while (end > start && (cleaned[end - 1] === "-" || cleaned[end - 1] === ".")) {
|
|
43
|
+
end -= 1;
|
|
44
|
+
}
|
|
45
|
+
cleaned = cleaned.slice(start, end);
|
|
46
|
+
return cleaned.length > 0 ? cleaned : "cohort";
|
|
47
|
+
}
|
|
48
|
+
function oneLine(text) {
|
|
49
|
+
return text.replace(/\r?\n/g, " ");
|
|
50
|
+
}
|
|
51
|
+
function parseRepo(repo) {
|
|
52
|
+
const slash = repo.indexOf("/");
|
|
53
|
+
if (slash <= 0 || slash >= repo.length - 1) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return { owner: repo.slice(0, slash), name: repo.slice(slash + 1) };
|
|
57
|
+
}
|
|
58
|
+
function fetchPrMergedAt(prNumber, repo, runGh) {
|
|
59
|
+
const parsed = parseRepo(repo);
|
|
60
|
+
if (parsed === null) {
|
|
61
|
+
return { mergedAt: null, error: `invalid --repo value: ${JSON.stringify(repo)}` };
|
|
62
|
+
}
|
|
63
|
+
const path = `repos/${parsed.owner}/${parsed.name}/pulls/${prNumber}`;
|
|
64
|
+
const result = runGh(["gh", "api", path]);
|
|
65
|
+
if (result.returncode !== 0) {
|
|
66
|
+
return {
|
|
67
|
+
mergedAt: null,
|
|
68
|
+
error: `gh api ${path} failed: ${result.stderr.trim() || result.stdout.trim()}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(result.stdout);
|
|
73
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
74
|
+
return {
|
|
75
|
+
mergedAt: null,
|
|
76
|
+
error: `unexpected gh api response for PR #${prNumber}: not a JSON object`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const payload = parsed;
|
|
80
|
+
const mergedAt = payload.merged_at;
|
|
81
|
+
if (mergedAt === null) {
|
|
82
|
+
return { mergedAt: null, error: `PR #${prNumber} is not merged yet.` };
|
|
83
|
+
}
|
|
84
|
+
if (typeof mergedAt !== "string" || mergedAt.length === 0) {
|
|
85
|
+
return { mergedAt: null, error: `PR #${prNumber} is not merged yet.` };
|
|
86
|
+
}
|
|
87
|
+
return { mergedAt, error: null };
|
|
88
|
+
}
|
|
89
|
+
catch (exc) {
|
|
90
|
+
const message = exc instanceof Error ? exc.message : String(exc);
|
|
91
|
+
return {
|
|
92
|
+
mergedAt: null,
|
|
93
|
+
error: `failed to parse gh api response for PR #${prNumber}: ${message}`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function fetchClosingIssues(prNumber, repo, runGh) {
|
|
98
|
+
const parsed = parseRepo(repo);
|
|
99
|
+
if (parsed === null) {
|
|
100
|
+
return { issues: [], error: `invalid --repo value: ${JSON.stringify(repo)}` };
|
|
101
|
+
}
|
|
102
|
+
const linked = fetchClosingIssuesReferences(prNumber, repo, runGh);
|
|
103
|
+
if (linked === null) {
|
|
104
|
+
return {
|
|
105
|
+
issues: [],
|
|
106
|
+
error: `failed to fetch closing issue references for PR #${prNumber}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return { issues: [...new Set(linked)].sort((a, b) => a - b), error: null };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Read an issue's open/closed state via the scm shim (`gh api .../issues/<N>`).
|
|
113
|
+
* Returns true only when the live state is definitively "closed". Any lookup
|
|
114
|
+
* failure (no repo, gh error, unparseable payload) returns false so a genuine
|
|
115
|
+
* misconfig is surfaced rather than silently swallowed as a benign skip (#2247).
|
|
116
|
+
*/
|
|
117
|
+
function fetchIssueClosed(issue, repo, runGh) {
|
|
118
|
+
if (repo === null || repo.length === 0) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
const parsed = parseRepo(repo);
|
|
122
|
+
if (parsed === null) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
const path = `repos/${parsed.owner}/${parsed.name}/issues/${issue}`;
|
|
126
|
+
const result = runGh(["gh", "api", path]);
|
|
127
|
+
if (result.returncode !== 0) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const payload = JSON.parse(result.stdout);
|
|
132
|
+
if (payload === null || typeof payload !== "object") {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
return payload.state === "closed";
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function deriveLabel(label, prNumbers, storyTokens) {
|
|
142
|
+
if (label !== null && label !== undefined && label.trim().length > 0) {
|
|
143
|
+
return safeSegment(label);
|
|
144
|
+
}
|
|
145
|
+
if (prNumbers.length > 0) {
|
|
146
|
+
return safeSegment(`pr-${prNumbers.join("-")}`);
|
|
147
|
+
}
|
|
148
|
+
if (storyTokens.length > 0) {
|
|
149
|
+
return safeSegment(storyTokens.slice(0, 3).join("-"));
|
|
150
|
+
}
|
|
151
|
+
return "cohort";
|
|
152
|
+
}
|
|
153
|
+
function storySlugs(storyPaths) {
|
|
154
|
+
return storyPaths
|
|
155
|
+
.map((p) => {
|
|
156
|
+
const name = p.split(/[/\\]/).pop() ?? p;
|
|
157
|
+
return name.replace(/\.(xbrief|vbrief)\.json$/i, "");
|
|
158
|
+
})
|
|
159
|
+
.join(", ");
|
|
160
|
+
}
|
|
161
|
+
function syncBaseBranch(projectRoot, baseBranch, runGit) {
|
|
162
|
+
const fetch = runGit(["git", "fetch", "origin", baseBranch], { cwd: projectRoot });
|
|
163
|
+
if (fetch.returncode !== 0) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
error: `git fetch origin ${baseBranch} failed: ${fetch.stderr.trim() || fetch.stdout.trim()}`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const merge = runGit(["git", "merge", "--ff-only", `origin/${baseBranch}`], { cwd: projectRoot });
|
|
170
|
+
if (merge.returncode !== 0) {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
error: `git merge --ff-only origin/${baseBranch} failed: ` +
|
|
174
|
+
`${merge.stderr.trim() || merge.stdout.trim()}`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return { ok: true, error: null };
|
|
178
|
+
}
|
|
179
|
+
function ensureFeatureBranch(projectRoot, branchName, runGit) {
|
|
180
|
+
const branchCheck = evaluateBranchPolicy(projectRoot);
|
|
181
|
+
if (branchCheck.exitCode === 1) {
|
|
182
|
+
const create = runGit(["git", "switch", "-c", branchName], { cwd: projectRoot });
|
|
183
|
+
if (create.returncode !== 0) {
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
error: `git switch -c ${branchName} failed: ${create.stderr.trim()}`,
|
|
187
|
+
branch: null,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return { ok: true, error: null, branch: branchName };
|
|
191
|
+
}
|
|
192
|
+
if (branchCheck.exitCode === 2) {
|
|
193
|
+
return { ok: false, error: branchCheck.message, branch: null };
|
|
194
|
+
}
|
|
195
|
+
const current = runGit(["git", "symbolic-ref", "--quiet", "--short", "HEAD"], {
|
|
196
|
+
cwd: projectRoot,
|
|
197
|
+
});
|
|
198
|
+
const currentBranch = current.stdout.trim();
|
|
199
|
+
if (current.returncode !== 0 || currentBranch.length === 0) {
|
|
200
|
+
const create = runGit(["git", "switch", "-c", branchName], { cwd: projectRoot });
|
|
201
|
+
if (create.returncode !== 0) {
|
|
202
|
+
return {
|
|
203
|
+
ok: false,
|
|
204
|
+
error: `git switch -c ${branchName} failed: ${create.stderr.trim()}`,
|
|
205
|
+
branch: null,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return { ok: true, error: null, branch: branchName };
|
|
209
|
+
}
|
|
210
|
+
return { ok: true, error: null, branch: currentBranch };
|
|
211
|
+
}
|
|
212
|
+
function commitLifecycleMoves(projectRoot, storyPaths, runGit) {
|
|
213
|
+
const branchCheck = evaluateBranchPolicy(projectRoot);
|
|
214
|
+
if (branchCheck.exitCode !== 0) {
|
|
215
|
+
return { ok: false, error: branchCheck.message, sha: null };
|
|
216
|
+
}
|
|
217
|
+
const add = runGit(["git", "add", "-A", "xbrief/"], { cwd: projectRoot });
|
|
218
|
+
if (add.returncode !== 0) {
|
|
219
|
+
return { ok: false, error: `git add failed: ${add.stderr.trim()}`, sha: null };
|
|
220
|
+
}
|
|
221
|
+
const slugs = storySlugs(storyPaths);
|
|
222
|
+
const subject = `chore(xbrief): complete ${slugs} post-merge`;
|
|
223
|
+
const commit = runGit(["git", "commit", "-m", subject], { cwd: projectRoot });
|
|
224
|
+
if (commit.returncode !== 0) {
|
|
225
|
+
const status = runGit(["git", "status", "--short"], { cwd: projectRoot });
|
|
226
|
+
if (status.stdout.trim().length === 0) {
|
|
227
|
+
return { ok: true, error: null, sha: null };
|
|
228
|
+
}
|
|
229
|
+
return { ok: false, error: `git commit failed: ${commit.stderr.trim()}`, sha: null };
|
|
230
|
+
}
|
|
231
|
+
const rev = runGit(["git", "rev-parse", "HEAD"], { cwd: projectRoot });
|
|
232
|
+
return {
|
|
233
|
+
ok: true,
|
|
234
|
+
error: null,
|
|
235
|
+
sha: rev.returncode === 0 ? rev.stdout.trim() : null,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function pushAndOpenPr(projectRoot, branch, repo, baseBranch, storyPaths, prNumbers, runGit, runGh) {
|
|
239
|
+
const push = runGit(["git", "push", "-u", "origin", branch], { cwd: projectRoot });
|
|
240
|
+
if (push.returncode !== 0) {
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
error: `git push failed: ${push.stderr.trim() || push.stdout.trim()}`,
|
|
244
|
+
prUrl: null,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const slugs = storySlugs(storyPaths);
|
|
248
|
+
const prList = prNumbers.length > 0 ? prNumbers.map((n) => `#${n}`).join(", ") : "cohort";
|
|
249
|
+
const title = `chore(xbrief): complete ${slugs} post-merge`;
|
|
250
|
+
const body = `## Summary\n` +
|
|
251
|
+
`Automated cohort lifecycle sweep after merge cascade (${prList}).\n\n` +
|
|
252
|
+
`## Test plan\n` +
|
|
253
|
+
`- [x] \`task xbrief:validate\` green\n` +
|
|
254
|
+
`- [x] WIP reset via active/ -> completed/ moves\n`;
|
|
255
|
+
const create = runGh([
|
|
256
|
+
"gh",
|
|
257
|
+
"pr",
|
|
258
|
+
"create",
|
|
259
|
+
"--repo",
|
|
260
|
+
repo,
|
|
261
|
+
"--base",
|
|
262
|
+
baseBranch,
|
|
263
|
+
"--head",
|
|
264
|
+
branch,
|
|
265
|
+
"--title",
|
|
266
|
+
title,
|
|
267
|
+
"--body",
|
|
268
|
+
body,
|
|
269
|
+
]);
|
|
270
|
+
if (create.returncode !== 0) {
|
|
271
|
+
return {
|
|
272
|
+
ok: false,
|
|
273
|
+
error: `gh pr create failed: ${create.stderr.trim()}`,
|
|
274
|
+
prUrl: null,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return { ok: true, error: null, prUrl: create.stdout.trim() };
|
|
278
|
+
}
|
|
279
|
+
export function finalizeCohort(args) {
|
|
280
|
+
const projectRoot = resolve(args.projectRoot ?? process.cwd());
|
|
281
|
+
const runGh = args.runGh ?? defaultRunGh;
|
|
282
|
+
const runGit = args.runGit ?? runText;
|
|
283
|
+
const prNumbers = [...(args.prNumbers ?? [])].sort((a, b) => a - b);
|
|
284
|
+
const storyTokens = splitCsv(args.storyTokens ?? []);
|
|
285
|
+
const repo = args.repo ?? process.env.GH_REPO ?? null;
|
|
286
|
+
const baseBranch = args.baseBranch ?? "master";
|
|
287
|
+
const dryRun = args.dryRun ?? false;
|
|
288
|
+
const noCommit = args.noCommit ?? false;
|
|
289
|
+
const noOpenPr = args.noOpenPr ?? false;
|
|
290
|
+
const errors = [];
|
|
291
|
+
if (!existsSync(projectRoot)) {
|
|
292
|
+
return buildResponse({
|
|
293
|
+
projectRoot,
|
|
294
|
+
dryRun,
|
|
295
|
+
noCommit,
|
|
296
|
+
prNumbers,
|
|
297
|
+
storyPaths: [],
|
|
298
|
+
closingIssues: [],
|
|
299
|
+
sweep: null,
|
|
300
|
+
commitSha: null,
|
|
301
|
+
branch: null,
|
|
302
|
+
prUrl: null,
|
|
303
|
+
errors: [`project root does not exist: ${projectRoot}`],
|
|
304
|
+
warnings: [],
|
|
305
|
+
ok: false,
|
|
306
|
+
emitJson: args.emitJson ?? false,
|
|
307
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
if (!existsSync(resolveLifecycleRoot(projectRoot))) {
|
|
311
|
+
return buildResponse({
|
|
312
|
+
projectRoot,
|
|
313
|
+
dryRun,
|
|
314
|
+
noCommit,
|
|
315
|
+
prNumbers,
|
|
316
|
+
storyPaths: [],
|
|
317
|
+
closingIssues: [],
|
|
318
|
+
sweep: null,
|
|
319
|
+
commitSha: null,
|
|
320
|
+
branch: null,
|
|
321
|
+
prUrl: null,
|
|
322
|
+
errors: [`no xbrief/ directory under project root: ${projectRoot}`],
|
|
323
|
+
warnings: [],
|
|
324
|
+
ok: false,
|
|
325
|
+
emitJson: args.emitJson ?? false,
|
|
326
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
const closingIssues = new Set();
|
|
330
|
+
if (prNumbers.length > 0) {
|
|
331
|
+
if (repo === null || repo.length === 0) {
|
|
332
|
+
errors.push("--repo OWNER/REPO is required when --pr is supplied (or set $GH_REPO).");
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
for (const prNumber of prNumbers) {
|
|
336
|
+
const merged = fetchPrMergedAt(prNumber, repo, runGh);
|
|
337
|
+
if (merged.error !== null) {
|
|
338
|
+
errors.push(merged.error);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const closing = fetchClosingIssues(prNumber, repo, runGh);
|
|
342
|
+
if (closing.error !== null) {
|
|
343
|
+
errors.push(closing.error);
|
|
344
|
+
}
|
|
345
|
+
for (const issue of closing.issues) {
|
|
346
|
+
closingIssues.add(issue);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (storyTokens.length === 0 && closingIssues.size === 0) {
|
|
352
|
+
errors.push("empty cohort: pass --pr <numbers> and/or --stories <ids|paths>.");
|
|
353
|
+
}
|
|
354
|
+
const warnings = [];
|
|
355
|
+
const storyPaths = [];
|
|
356
|
+
const seenPaths = new Set();
|
|
357
|
+
const addStory = (path) => {
|
|
358
|
+
const resolvedPath = resolve(path);
|
|
359
|
+
if (!seenPaths.has(resolvedPath)) {
|
|
360
|
+
seenPaths.add(resolvedPath);
|
|
361
|
+
storyPaths.push(path);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
// Operator-supplied story tokens keep the hard-error contract: an explicit
|
|
365
|
+
// --stories token that does not resolve to an active brief is a real error.
|
|
366
|
+
if (storyTokens.length > 0) {
|
|
367
|
+
const resolved = resolveStories(projectRoot, storyTokens);
|
|
368
|
+
for (const story of resolved.resolved) {
|
|
369
|
+
addStory(story.path);
|
|
370
|
+
}
|
|
371
|
+
errors.push(...resolved.errors);
|
|
372
|
+
}
|
|
373
|
+
// Closing-issue tokens are incidental (they come from a merged PR's structured
|
|
374
|
+
// closing refs, not the operator). A benign ref -- one whose issue is already
|
|
375
|
+
// closed OR already has a brief in completed/ -- is SKIPPED WITH A WARNING
|
|
376
|
+
// rather than aborting the whole sweep. A genuine misconfig (open issue, no
|
|
377
|
+
// active and no completed brief) is still surfaced as a hard error (#2247).
|
|
378
|
+
for (const issue of [...closingIssues].sort((a, b) => a - b)) {
|
|
379
|
+
const resolved = resolveStories(projectRoot, [String(issue)]);
|
|
380
|
+
if (resolved.resolved.length > 0) {
|
|
381
|
+
for (const story of resolved.resolved) {
|
|
382
|
+
addStory(story.path);
|
|
383
|
+
}
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const noActiveBrief = resolved.errors.some((e) => e.includes("no active story references"));
|
|
387
|
+
if (!noActiveBrief) {
|
|
388
|
+
// A different resolution problem (e.g. ambiguous match) is not a benign
|
|
389
|
+
// incidental ref -- surface it verbatim.
|
|
390
|
+
errors.push(...resolved.errors);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const completedBrief = completedBriefReferencesIssue(projectRoot, issue);
|
|
394
|
+
const issueClosed = completedBrief || fetchIssueClosed(issue, repo, runGh);
|
|
395
|
+
if (completedBrief || issueClosed) {
|
|
396
|
+
const reason = completedBrief
|
|
397
|
+
? "a completed brief already exists"
|
|
398
|
+
: "the issue is already closed";
|
|
399
|
+
warnings.push(`#${issue}: no active story references this closing issue; skipped (${reason}).`);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
errors.push(`#${issue}: no active story references this closing issue.`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (storyPaths.length === 0) {
|
|
406
|
+
// When every closing ref was a benign skip and no real stories remain, the
|
|
407
|
+
// run is clean-with-warnings (nothing to sweep), not a config error.
|
|
408
|
+
const cleanNoop = errors.length === 0 && warnings.length > 0;
|
|
409
|
+
return buildResponse({
|
|
410
|
+
projectRoot,
|
|
411
|
+
dryRun,
|
|
412
|
+
noCommit,
|
|
413
|
+
prNumbers,
|
|
414
|
+
storyPaths,
|
|
415
|
+
closingIssues: [...closingIssues],
|
|
416
|
+
sweep: null,
|
|
417
|
+
commitSha: null,
|
|
418
|
+
branch: null,
|
|
419
|
+
prUrl: null,
|
|
420
|
+
errors,
|
|
421
|
+
warnings,
|
|
422
|
+
ok: cleanNoop,
|
|
423
|
+
emitJson: args.emitJson ?? false,
|
|
424
|
+
exitCode: cleanNoop
|
|
425
|
+
? EXIT_OK
|
|
426
|
+
: errors.some((e) => e.includes("not merged"))
|
|
427
|
+
? EXIT_GATE_FAILED
|
|
428
|
+
: EXIT_CONFIG_ERROR,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
let commitSha = null;
|
|
432
|
+
let branch = null;
|
|
433
|
+
let prUrl = null;
|
|
434
|
+
if (!dryRun && !noCommit) {
|
|
435
|
+
const sync = syncBaseBranch(projectRoot, baseBranch, runGit);
|
|
436
|
+
if (!sync.ok) {
|
|
437
|
+
errors.push(sync.error ?? "base branch sync failed");
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
const branchName = `swarm/finalize/${deriveLabel(args.label, prNumbers, storyTokens)}`;
|
|
441
|
+
const branchResult = ensureFeatureBranch(projectRoot, branchName, runGit);
|
|
442
|
+
if (!branchResult.ok) {
|
|
443
|
+
errors.push(branchResult.error ?? "branch setup failed");
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
branch = branchResult.branch;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const sweepResult = completeCohort({
|
|
451
|
+
stories: storyPaths,
|
|
452
|
+
projectRoot,
|
|
453
|
+
dryRun,
|
|
454
|
+
emitJson: false,
|
|
455
|
+
});
|
|
456
|
+
if (sweepResult.exitCode !== 0) {
|
|
457
|
+
errors.push("cohort completion sweep failed.");
|
|
458
|
+
return buildResponse({
|
|
459
|
+
projectRoot,
|
|
460
|
+
dryRun,
|
|
461
|
+
noCommit,
|
|
462
|
+
prNumbers,
|
|
463
|
+
storyPaths,
|
|
464
|
+
closingIssues: [...closingIssues],
|
|
465
|
+
sweep: null,
|
|
466
|
+
commitSha: null,
|
|
467
|
+
branch: null,
|
|
468
|
+
prUrl: null,
|
|
469
|
+
errors,
|
|
470
|
+
warnings,
|
|
471
|
+
ok: false,
|
|
472
|
+
emitJson: args.emitJson ?? false,
|
|
473
|
+
exitCode: EXIT_GATE_FAILED,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
if (!dryRun && !noCommit && errors.length === 0) {
|
|
477
|
+
const commitResult = commitLifecycleMoves(projectRoot, storyPaths, runGit);
|
|
478
|
+
if (!commitResult.ok) {
|
|
479
|
+
errors.push(commitResult.error ?? "commit failed");
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
commitSha = commitResult.sha;
|
|
483
|
+
if (!noOpenPr && repo !== null && branch !== null && branch.length > 0) {
|
|
484
|
+
const prResult = pushAndOpenPr(projectRoot, branch, repo, baseBranch, storyPaths, prNumbers, runGit, runGh);
|
|
485
|
+
if (!prResult.ok) {
|
|
486
|
+
errors.push(prResult.error ?? "PR open failed");
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
prUrl = prResult.prUrl;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const ok = errors.length === 0;
|
|
495
|
+
return buildResponse({
|
|
496
|
+
projectRoot,
|
|
497
|
+
dryRun,
|
|
498
|
+
noCommit,
|
|
499
|
+
prNumbers,
|
|
500
|
+
storyPaths,
|
|
501
|
+
closingIssues: [...closingIssues],
|
|
502
|
+
sweep: null,
|
|
503
|
+
commitSha,
|
|
504
|
+
branch,
|
|
505
|
+
prUrl,
|
|
506
|
+
errors,
|
|
507
|
+
warnings,
|
|
508
|
+
ok,
|
|
509
|
+
emitJson: args.emitJson ?? false,
|
|
510
|
+
exitCode: ok ? EXIT_OK : EXIT_GATE_FAILED,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
function buildResponse(input) {
|
|
514
|
+
const result = {
|
|
515
|
+
project_root: input.projectRoot,
|
|
516
|
+
dry_run: input.dryRun,
|
|
517
|
+
no_commit: input.noCommit,
|
|
518
|
+
pr_numbers: input.prNumbers,
|
|
519
|
+
story_paths: input.storyPaths,
|
|
520
|
+
closing_issues: input.closingIssues,
|
|
521
|
+
sweep: input.sweep,
|
|
522
|
+
commit_sha: input.commitSha,
|
|
523
|
+
branch: input.branch,
|
|
524
|
+
pr_url: input.prUrl,
|
|
525
|
+
errors: input.errors,
|
|
526
|
+
warnings: input.warnings,
|
|
527
|
+
ok: input.ok,
|
|
528
|
+
};
|
|
529
|
+
if (input.emitJson) {
|
|
530
|
+
return {
|
|
531
|
+
exitCode: input.exitCode,
|
|
532
|
+
stdout: `${JSON.stringify(result, null, 2)}\n`,
|
|
533
|
+
stderr: "",
|
|
534
|
+
result,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
const lines = [
|
|
538
|
+
`Swarm cohort finalize ${input.dryRun ? "DRY-RUN" : "live"} ` +
|
|
539
|
+
`(${input.storyPaths.length} stor${input.storyPaths.length === 1 ? "y" : "ies"})`,
|
|
540
|
+
` Project root: ${input.projectRoot}`,
|
|
541
|
+
];
|
|
542
|
+
if (input.prNumbers.length > 0) {
|
|
543
|
+
lines.push(` PRs: ${input.prNumbers.map((n) => `#${n}`).join(", ")}`);
|
|
544
|
+
}
|
|
545
|
+
if (input.closingIssues.length > 0) {
|
|
546
|
+
lines.push(` Closing issues: ${input.closingIssues.map((n) => `#${n}`).join(", ")}`);
|
|
547
|
+
}
|
|
548
|
+
if (input.storyPaths.length > 0) {
|
|
549
|
+
lines.push(" Stories:");
|
|
550
|
+
for (const path of input.storyPaths) {
|
|
551
|
+
lines.push(` - ${oneLine(path)}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (input.branch !== null) {
|
|
555
|
+
lines.push(` Branch: ${input.branch}`);
|
|
556
|
+
}
|
|
557
|
+
if (input.commitSha !== null) {
|
|
558
|
+
lines.push(` Commit: ${input.commitSha}`);
|
|
559
|
+
}
|
|
560
|
+
if (input.prUrl !== null) {
|
|
561
|
+
lines.push(` PR: ${input.prUrl}`);
|
|
562
|
+
}
|
|
563
|
+
if (input.warnings.length > 0) {
|
|
564
|
+
lines.push(" Warnings:");
|
|
565
|
+
for (const warning of input.warnings) {
|
|
566
|
+
lines.push(` - ${oneLine(warning)}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (input.errors.length > 0) {
|
|
570
|
+
lines.push(" Errors:");
|
|
571
|
+
for (const err of input.errors) {
|
|
572
|
+
lines.push(` - ${oneLine(err)}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
lines.push("");
|
|
576
|
+
const skipNote = input.warnings.length > 0
|
|
577
|
+
? ` (${input.warnings.length} incidental closing ref${input.warnings.length === 1 ? "" : "s"} skipped)`
|
|
578
|
+
: "";
|
|
579
|
+
lines.push(input.ok
|
|
580
|
+
? `Result: FINALIZE CLEAN -- cohort briefs swept to completed/.${skipNote}`
|
|
581
|
+
: "Result: FINALIZE INCOMPLETE -- see errors above.");
|
|
582
|
+
return {
|
|
583
|
+
exitCode: input.exitCode,
|
|
584
|
+
stdout: `${lines.join("\n")}\n`,
|
|
585
|
+
stderr: "",
|
|
586
|
+
result,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
//# sourceMappingURL=finalize-cohort.js.map
|
package/dist/swarm/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from "./complete-cohort.js";
|
|
2
2
|
export { completeCohortMain } from "./complete-cohort-cli.js";
|
|
3
3
|
export * from "./constants.js";
|
|
4
|
+
export * from "./finalize-cohort.js";
|
|
5
|
+
export { finalizeCohortMain, parseFinalizeCohortArgv } from "./finalize-cohort-cli.js";
|
|
4
6
|
export * from "./launch.js";
|
|
5
7
|
export { launchMain, parseLaunchArgv } from "./launch-cli.js";
|
|
6
8
|
export * from "./readiness.js";
|
package/dist/swarm/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from "./complete-cohort.js";
|
|
2
2
|
export { completeCohortMain } from "./complete-cohort-cli.js";
|
|
3
3
|
export * from "./constants.js";
|
|
4
|
+
export * from "./finalize-cohort.js";
|
|
5
|
+
export { finalizeCohortMain, parseFinalizeCohortArgv } from "./finalize-cohort-cli.js";
|
|
4
6
|
export * from "./launch.js";
|
|
5
7
|
export { launchMain, parseLaunchArgv } from "./launch-cli.js";
|
|
6
8
|
export * from "./readiness.js";
|
package/dist/swarm/launch.d.ts
CHANGED
|
@@ -21,6 +21,13 @@ export type RuntimeAuthProbeFn = () => [string, string];
|
|
|
21
21
|
export declare const defaultPreflightGate: PreflightGateFn;
|
|
22
22
|
export declare const defaultReadinessGate: ReadinessGateFn;
|
|
23
23
|
export declare const defaultRuntimeAuthProbe: RuntimeAuthProbeFn;
|
|
24
|
+
/**
|
|
25
|
+
* True when a brief in `xbrief/completed/` (or the legacy `vbrief/completed/`)
|
|
26
|
+
* references the given issue number in its `references[]` / `Traces`. Used by
|
|
27
|
+
* finalize-cohort (#2247) to classify an incidental closing ref to an
|
|
28
|
+
* already-completed issue as a benign skip rather than a hard error.
|
|
29
|
+
*/
|
|
30
|
+
export declare function completedBriefReferencesIssue(projectRoot: string, issue: number): boolean;
|
|
24
31
|
export declare function looksLikePath(token: string): boolean;
|
|
25
32
|
export declare function resolveStories(projectRoot: string, tokens: readonly string[]): {
|
|
26
33
|
resolved: ResolvedStory[];
|
|
@@ -68,6 +75,12 @@ export interface LaunchArgs {
|
|
|
68
75
|
readinessGate?: ReadinessGateFn;
|
|
69
76
|
worktreeResolver?: WorktreeResolverFn;
|
|
70
77
|
runtimeAuthProbe?: RuntimeAuthProbeFn;
|
|
78
|
+
/**
|
|
79
|
+
* Injection seam for the routing-provider environment lookup, mirroring
|
|
80
|
+
* `resolveRoutingPath`'s `environ` parameter (#1877 Greptile follow-up).
|
|
81
|
+
* Defaults to `process.env` when unset.
|
|
82
|
+
*/
|
|
83
|
+
environ?: NodeJS.ProcessEnv;
|
|
71
84
|
}
|
|
72
85
|
export declare function swarmLaunch(args: LaunchArgs): {
|
|
73
86
|
exitCode: number;
|
package/dist/swarm/launch.js
CHANGED
|
@@ -8,7 +8,7 @@ import { issueNumbersFromPlan, scopeMetadataRank } from "../triage/queue/scope-w
|
|
|
8
8
|
import { selectionOrderingKey } from "../triage/queue/selection.js";
|
|
9
9
|
import { DEFAULT_BASE_BRANCH, EXIT_CONFIG_ERROR, EXIT_GATE_FAILED, EXIT_OK, GATE_ADVISE, GATE_ENFORCE, LEAF_CODING_WORKER_ROLE, } from "./constants.js";
|
|
10
10
|
import { readinessReport } from "./readiness.js";
|
|
11
|
-
import {
|
|
11
|
+
import { loadRoutingFile, resolveDispatchProvider, resolveModelRoute, resolveRoutingPath, } from "./routing.js";
|
|
12
12
|
import { dispatchProviderFor, enforceSubagentBackendPolicy } from "./subagent-backend.js";
|
|
13
13
|
import { resolveWorktreeMap } from "./worktrees.js";
|
|
14
14
|
export const defaultPreflightGate = (vbriefPath) => {
|
|
@@ -126,17 +126,17 @@ function projectRel(projectRoot, path) {
|
|
|
126
126
|
return path.replace(/\\/g, "/");
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
-
function
|
|
130
|
-
const
|
|
129
|
+
function indexStoriesInFolder(projectRoot, folder) {
|
|
130
|
+
const dir = resolveLifecycleFolder(projectRoot, folder);
|
|
131
131
|
const index = [];
|
|
132
|
-
if (!existsSync(
|
|
132
|
+
if (!existsSync(dir)) {
|
|
133
133
|
return index;
|
|
134
134
|
}
|
|
135
|
-
for (const name of readdirSync(
|
|
135
|
+
for (const name of readdirSync(dir).sort()) {
|
|
136
136
|
if (!hasArtifactSuffix(name)) {
|
|
137
137
|
continue;
|
|
138
138
|
}
|
|
139
|
-
const path = join(
|
|
139
|
+
const path = join(dir, name);
|
|
140
140
|
const data = loadJson(path);
|
|
141
141
|
if (data === null) {
|
|
142
142
|
continue;
|
|
@@ -146,6 +146,18 @@ function indexActiveStories(projectRoot) {
|
|
|
146
146
|
}
|
|
147
147
|
return index;
|
|
148
148
|
}
|
|
149
|
+
function indexActiveStories(projectRoot) {
|
|
150
|
+
return indexStoriesInFolder(projectRoot, "active");
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* True when a brief in `xbrief/completed/` (or the legacy `vbrief/completed/`)
|
|
154
|
+
* references the given issue number in its `references[]` / `Traces`. Used by
|
|
155
|
+
* finalize-cohort (#2247) to classify an incidental closing ref to an
|
|
156
|
+
* already-completed issue as a benign skip rather than a hard error.
|
|
157
|
+
*/
|
|
158
|
+
export function completedBriefReferencesIssue(projectRoot, issue) {
|
|
159
|
+
return indexStoriesInFolder(projectRoot, "completed").some((s) => s.issues.has(issue));
|
|
160
|
+
}
|
|
149
161
|
export function looksLikePath(token) {
|
|
150
162
|
return (token.endsWith(".json") ||
|
|
151
163
|
token.includes("/") ||
|
|
@@ -525,7 +537,7 @@ export function swarmLaunch(args) {
|
|
|
525
537
|
let modelSource = null;
|
|
526
538
|
let routingProvider = null;
|
|
527
539
|
if (routingFile !== null) {
|
|
528
|
-
routingProvider =
|
|
540
|
+
routingProvider = resolveDispatchProvider(args.environ ?? process.env);
|
|
529
541
|
const route = resolveModelRoute(routingFile, routingProvider, LEAF_CODING_WORKER_ROLE);
|
|
530
542
|
// A malformed decision object must fail loud here: the legacy backend gate
|
|
531
543
|
// was already bypassed above (routingFile !== null), so silently continuing
|