@deftai/directive-core 0.67.0 → 0.68.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/check/index.d.ts +1 -1
  2. package/dist/check/index.js +1 -1
  3. package/dist/check/orchestrator.d.ts +14 -4
  4. package/dist/check/orchestrator.js +30 -6
  5. package/dist/doctor/checks.js +11 -11
  6. package/dist/doctor/constants.d.ts +2 -0
  7. package/dist/doctor/constants.js +2 -0
  8. package/dist/doctor/payload-staleness.js +15 -6
  9. package/dist/forward-coverage/evaluate.d.ts +55 -0
  10. package/dist/forward-coverage/evaluate.js +254 -0
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/init-deposit/constants.d.ts +14 -0
  14. package/dist/init-deposit/constants.js +10 -0
  15. package/dist/init-deposit/hygiene.d.ts +50 -0
  16. package/dist/init-deposit/hygiene.js +187 -0
  17. package/dist/init-deposit/init-deposit.d.ts +1 -0
  18. package/dist/init-deposit/init-deposit.js +7 -1
  19. package/dist/init-deposit/refresh.d.ts +2 -0
  20. package/dist/init-deposit/refresh.js +48 -30
  21. package/dist/init-deposit/scaffold.d.ts +3 -4
  22. package/dist/init-deposit/scaffold.js +3 -24
  23. package/dist/integration-e2e/helpers.d.ts +0 -1
  24. package/dist/integration-e2e/helpers.js +3 -7
  25. package/dist/layout/lifecycle-ref.d.ts +15 -0
  26. package/dist/layout/lifecycle-ref.js +55 -0
  27. package/dist/platform/agents-md.d.ts +28 -0
  28. package/dist/platform/agents-md.js +34 -0
  29. package/dist/policy/resolve.js +56 -46
  30. package/dist/pr-merge-readiness/compute.d.ts +10 -0
  31. package/dist/pr-merge-readiness/compute.js +53 -17
  32. package/dist/pr-merge-readiness/index.d.ts +2 -1
  33. package/dist/pr-merge-readiness/index.js +1 -0
  34. package/dist/pr-merge-readiness/mergeability.d.ts +43 -0
  35. package/dist/pr-merge-readiness/mergeability.js +105 -0
  36. package/dist/pr-monitor/index.d.ts +1 -1
  37. package/dist/pr-monitor/index.js +1 -1
  38. package/dist/pr-monitor/monitor.d.ts +8 -2
  39. package/dist/pr-monitor/monitor.js +35 -6
  40. package/dist/pr-wait-mergeable/wrappers.d.ts +12 -1
  41. package/dist/pr-wait-mergeable/wrappers.js +13 -5
  42. package/dist/pr-watch/constants.d.ts +33 -0
  43. package/dist/pr-watch/constants.js +33 -0
  44. package/dist/pr-watch/index.d.ts +6 -0
  45. package/dist/pr-watch/index.js +6 -0
  46. package/dist/pr-watch/main.d.ts +21 -0
  47. package/dist/pr-watch/main.js +206 -0
  48. package/dist/pr-watch/probe.d.ts +13 -0
  49. package/dist/pr-watch/probe.js +100 -0
  50. package/dist/pr-watch/types.d.ts +47 -0
  51. package/dist/pr-watch/types.js +2 -0
  52. package/dist/pr-watch/watch.d.ts +12 -0
  53. package/dist/pr-watch/watch.js +122 -0
  54. package/dist/render/project-render.js +114 -89
  55. package/dist/scope/capacity-stamp.d.ts +24 -2
  56. package/dist/scope/capacity-stamp.js +80 -28
  57. package/dist/scope/decomposed-refs.js +2 -2
  58. package/dist/scope/project-definition-sync.js +3 -3
  59. package/dist/scope/vbrief-ref.d.ts +2 -1
  60. package/dist/scope/vbrief-ref.js +3 -2
  61. package/dist/spec-authority/constants.d.ts +6 -0
  62. package/dist/spec-authority/constants.js +11 -0
  63. package/dist/spec-authority/resolver.js +3 -3
  64. package/dist/swarm/finalize-cohort-cli.d.ts +5 -0
  65. package/dist/swarm/finalize-cohort-cli.js +100 -0
  66. package/dist/swarm/finalize-cohort.d.ts +39 -0
  67. package/dist/swarm/finalize-cohort.js +589 -0
  68. package/dist/swarm/index.d.ts +2 -0
  69. package/dist/swarm/index.js +2 -0
  70. package/dist/swarm/launch.d.ts +7 -0
  71. package/dist/swarm/launch.js +17 -5
  72. package/dist/triage/queue/index.d.ts +1 -0
  73. package/dist/triage/queue/index.js +1 -0
  74. package/dist/triage/queue/project.d.ts +6 -1
  75. package/dist/triage/queue/project.js +9 -4
  76. package/dist/triage/queue/reconcile-live-state.d.ts +42 -0
  77. package/dist/triage/queue/reconcile-live-state.js +54 -0
  78. package/dist/triage/scope/mutations-core.js +91 -81
  79. package/dist/triage/scope/resolve.js +4 -3
  80. package/dist/triage/scope-drift/add-ignore.js +33 -32
  81. package/dist/triage/scope-drift/scope-rules.js +3 -3
  82. package/dist/triage/subscribe/index.js +51 -45
  83. package/dist/triage/welcome/writers.js +79 -64
  84. package/dist/vbrief-build/project-definition-io.js +8 -3
  85. package/dist/vbrief-validate/paths.js +8 -4
  86. package/dist/vbrief-validate/precutover.d.ts +1 -1
  87. package/dist/vbrief-validate/precutover.js +12 -10
  88. package/package.json +7 -7
  89. package/dist/install-upgrade/index.d.ts +0 -13
  90. package/dist/install-upgrade/index.js +0 -251
@@ -0,0 +1,39 @@
1
+ import type { RunGhFn } from "../pr-protected-issues/types.js";
2
+ import { type SweepResult } from "./complete-cohort.js";
3
+ import { runText } from "./subprocess.js";
4
+ export interface FinalizeCohortResult {
5
+ readonly project_root: string;
6
+ readonly dry_run: boolean;
7
+ readonly no_commit: boolean;
8
+ readonly pr_numbers: readonly number[];
9
+ readonly story_paths: readonly string[];
10
+ readonly closing_issues: readonly number[];
11
+ readonly sweep: SweepResult | null;
12
+ readonly commit_sha: string | null;
13
+ readonly branch: string | null;
14
+ readonly pr_url: string | null;
15
+ readonly errors: readonly string[];
16
+ readonly warnings: readonly string[];
17
+ readonly ok: boolean;
18
+ }
19
+ export interface FinalizeCohortArgs {
20
+ readonly prNumbers?: readonly number[];
21
+ readonly storyTokens?: readonly string[];
22
+ readonly repo?: string | null;
23
+ readonly projectRoot?: string;
24
+ readonly baseBranch?: string;
25
+ readonly label?: string | null;
26
+ readonly dryRun?: boolean;
27
+ readonly noCommit?: boolean;
28
+ readonly noOpenPr?: boolean;
29
+ readonly emitJson?: boolean;
30
+ readonly runGh?: RunGhFn;
31
+ readonly runGit?: typeof runText;
32
+ }
33
+ export declare function finalizeCohort(args: FinalizeCohortArgs): {
34
+ exitCode: number;
35
+ stdout: string;
36
+ stderr: string;
37
+ result: FinalizeCohortResult;
38
+ };
39
+ //# sourceMappingURL=finalize-cohort.d.ts.map
@@ -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
@@ -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";
@@ -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";
@@ -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[];