cleargate 0.8.2 → 0.10.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +11 -0
  3. package/dist/MANIFEST.json +259 -28
  4. package/dist/{chunk-OM4FAEA7.js → chunk-Q3BTSXCK.js} +69 -3
  5. package/dist/chunk-Q3BTSXCK.js.map +1 -0
  6. package/dist/cli.cjs +2621 -548
  7. package/dist/cli.cjs.map +1 -1
  8. package/dist/cli.js +2548 -560
  9. package/dist/cli.js.map +1 -1
  10. package/dist/lib/ledger.cjs +120 -0
  11. package/dist/lib/ledger.cjs.map +1 -0
  12. package/dist/lib/ledger.d.cts +64 -0
  13. package/dist/lib/ledger.d.ts +64 -0
  14. package/dist/lib/ledger.js +96 -0
  15. package/dist/lib/ledger.js.map +1 -0
  16. package/dist/templates/cleargate-planning/.claude/agents/architect.md +10 -8
  17. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +108 -0
  18. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +49 -3
  19. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +6 -1
  20. package/dist/templates/cleargate-planning/.claude/agents/developer.md +29 -2
  21. package/dist/templates/cleargate-planning/.claude/agents/qa.md +50 -1
  22. package/dist/templates/cleargate-planning/.claude/agents/reporter.md +31 -9
  23. package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +148 -0
  24. package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +6 -0
  25. package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +314 -96
  26. package/dist/templates/cleargate-planning/.claude/settings.json +4 -0
  27. package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +473 -0
  28. package/dist/templates/cleargate-planning/.cleargate/config.example.yml +19 -0
  29. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +542 -0
  30. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +102 -428
  31. package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +31 -0
  32. package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +71 -0
  33. package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +24 -2
  34. package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +387 -27
  35. package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +219 -0
  36. package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +54 -0
  37. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +378 -0
  38. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +888 -0
  39. package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +71 -0
  40. package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +355 -13
  41. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
  42. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +482 -0
  43. package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +125 -0
  44. package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +24 -1
  45. package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +32 -1
  46. package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +48 -14
  47. package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +37 -3
  48. package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +50 -0
  49. package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +98 -29
  50. package/dist/templates/cleargate-planning/.cleargate/templates/proposal.md +17 -4
  51. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +23 -4
  52. package/dist/templates/cleargate-planning/.cleargate/templates/story.md +55 -3
  53. package/dist/templates/cleargate-planning/CLAUDE.md +28 -10
  54. package/dist/templates/cleargate-planning/MANIFEST.json +259 -28
  55. package/dist/{whoami-CX7CXJD5.js → whoami-W4U6DPVG.js} +17 -17
  56. package/dist/whoami-W4U6DPVG.js.map +1 -0
  57. package/package.json +13 -2
  58. package/templates/cleargate-planning/.claude/agents/architect.md +10 -8
  59. package/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +108 -0
  60. package/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +49 -3
  61. package/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +6 -1
  62. package/templates/cleargate-planning/.claude/agents/developer.md +29 -2
  63. package/templates/cleargate-planning/.claude/agents/qa.md +50 -1
  64. package/templates/cleargate-planning/.claude/agents/reporter.md +31 -9
  65. package/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +148 -0
  66. package/templates/cleargate-planning/.claude/hooks/session-start.sh +6 -0
  67. package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +314 -96
  68. package/templates/cleargate-planning/.claude/settings.json +4 -0
  69. package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +473 -0
  70. package/templates/cleargate-planning/.cleargate/config.example.yml +19 -0
  71. package/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +542 -0
  72. package/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +102 -428
  73. package/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +31 -0
  74. package/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +71 -0
  75. package/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +24 -2
  76. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +387 -27
  77. package/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +219 -0
  78. package/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +54 -0
  79. package/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +378 -0
  80. package/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +888 -0
  81. package/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +71 -0
  82. package/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +355 -13
  83. package/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
  84. package/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +482 -0
  85. package/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +125 -0
  86. package/templates/cleargate-planning/.cleargate/templates/Bug.md +24 -1
  87. package/templates/cleargate-planning/.cleargate/templates/CR.md +32 -1
  88. package/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +48 -14
  89. package/templates/cleargate-planning/.cleargate/templates/epic.md +37 -3
  90. package/templates/cleargate-planning/.cleargate/templates/hotfix.md +50 -0
  91. package/templates/cleargate-planning/.cleargate/templates/initiative.md +98 -29
  92. package/templates/cleargate-planning/.cleargate/templates/sprint_report.md +23 -4
  93. package/templates/cleargate-planning/.cleargate/templates/story.md +55 -3
  94. package/templates/cleargate-planning/CLAUDE.md +28 -10
  95. package/templates/cleargate-planning/MANIFEST.json +259 -28
  96. package/dist/chunk-OM4FAEA7.js.map +0 -1
  97. package/dist/whoami-CX7CXJD5.js.map +0 -1
  98. package/templates/cleargate-planning/.cleargate/templates/proposal.md +0 -61
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * close_sprint.mjs — Six-step sprint close pipeline
3
+ * close_sprint.mjs — Eight-step sprint close pipeline
4
4
  *
5
5
  * Usage: node close_sprint.mjs <sprint-id> [--assume-ack]
6
6
  * node close_sprint.mjs <sprint-id> --report-body-stdin (STORY-014-10)
@@ -9,20 +9,56 @@
9
9
  * 1. Load and validate state.json via validateState
10
10
  * 2. Refuse if any story state is not in TERMINAL_STATES (exit non-zero, list offenders)
11
11
  * 3. Invoke prefill_report.mjs on all agent reports
12
+ * 3.5 Build curated Reporter context bundle via prep_reporter_context.mjs (non-fatal)
12
13
  * 4. Orchestrator spawns Reporter separately (script validates preconditions only)
13
14
  * 5. On Reporter success + user ack (or --assume-ack flag), flip sprint_status -> "Completed"
14
15
  * 6. Invoke suggest_improvements.mjs unconditionally
16
+ * 7. Auto-push per-artifact status updates to MCP via cleargate sync work-items (non-fatal)
17
+ * 8. Verbose post-close handoff list (6-item next-steps block to stdout)
18
+ *
19
+ * Report filename: SPRINT-<#>_REPORT.md for new sprints (SPRINT-18+).
20
+ * Backwards-compat: if SPRINT-<#>_REPORT.md is absent but REPORT.md exists (legacy
21
+ * SPRINT-01..17), fall back to REPORT.md for read operations. New writes always
22
+ * use SPRINT-<#>_REPORT.md when the sprint-id carries a numeric portion.
23
+ * If the sprint-id has no numeric portion (e.g. SPRINT-TEST), plain REPORT.md is used.
15
24
  *
16
25
  * Stdin fallback (STORY-014-10): when `--report-body-stdin` is passed, the script
17
- * reads the full REPORT.md body from stdin and writes it atomically in lieu of
26
+ * reads the full SPRINT-<#>_REPORT.md body from stdin and writes it atomically in lieu of
18
27
  * waiting for a Reporter-produced file. Replaces the Step-4 gate; implies ack.
19
- * Refuses empty stdin or pre-existing REPORT.md.
28
+ * Refuses empty stdin or pre-existing report file.
20
29
  *
21
30
  * Does NOT archive the sprint file (pending-sync -> archive stays human per EPIC-013 §4.5 step 7).
22
31
  *
23
32
  * Reuse: TERMINAL_STATES, VALID_STATES from constants.mjs
24
33
  * validateState from validate_state.mjs
25
34
  * atomicWrite pattern from update_state.mjs
35
+ *
36
+ * Test seams (CR-022 M1):
37
+ * CLEARGATE_SKIP_LIFECYCLE_CHECK=1 — skip Step 2.6 lifecycle reconciliation entirely
38
+ * (test environments where the CLI binary is present
39
+ * but real git history would produce drift false-positives).
40
+ * CLEARGATE_SKIP_WORKTREE_CHECK=1 — skip Step 2.7 entirely (test environments that cannot
41
+ * run git worktree list from a real git root).
42
+ * CLEARGATE_FORCE_WORKTREE_PATHS=p1,p2 — comma-separated fake worktree paths injected into
43
+ * Step 2.7 instead of running git worktree list.
44
+ * Used to exercise the v2 block / v1 advisory paths
45
+ * without a real .worktrees/STORY-* directory.
46
+ * CLEARGATE_SKIP_MERGE_CHECK=1 — skip Step 2.8 entirely (test environments where git
47
+ * refs are absent or merge state is irrelevant).
48
+ * CLEARGATE_FORCE_MERGE_STATUS=merged|unmerged — inject merge status for Step 2.8 without
49
+ * running git merge-base. Used to exercise
50
+ * the v2 block / v1 advisory paths.
51
+ * CLEARGATE_REPO_ROOT=<path> — override REPO_ROOT for Step 2.8 git commands
52
+ * (used in tests that need a controlled git repo).
53
+ * CLEARGATE_SKIP_SPRINT_TRENDS=1 — skip Step 6.5 entirely (test environments).
54
+ * CLEARGATE_SKIP_SKILL_CANDIDATES=1 — skip Step 6.6 entirely (test environments).
55
+ * CLEARGATE_SKIP_FLASHCARD_CLEANUP=1 — skip Step 6.7 entirely (test environments).
56
+ * CLEARGATE_SPRINT_RUNS_DIR=<path> — override .cleargate/sprint-runs/ root for
57
+ * sibling-sprint counting in sprint_trends.mjs.
58
+ * CLEARGATE_FLASHCARD_PATH=<path> — override .cleargate/FLASHCARD.md path for
59
+ * --flashcard-cleanup scan in suggest_improvements.mjs.
60
+ * CLEARGATE_FLASHCARD_LOOKBACK=<N> — override 3-sprint default lookback for
61
+ * --flashcard-cleanup scan.
26
62
  */
27
63
 
28
64
  import fs from 'node:fs';
@@ -31,6 +67,7 @@ import { fileURLToPath } from 'node:url';
31
67
  import { execSync } from 'node:child_process';
32
68
  import { TERMINAL_STATES } from './constants.mjs';
33
69
  import { validateState } from './validate_state.mjs';
70
+ import { reportFilename } from './lib/report-filename.mjs';
34
71
 
35
72
  /**
36
73
  * Migrate a v1 state.json to v2 by injecting lane fields with defaults.
@@ -56,7 +93,9 @@ function migrateV1ToV2(state) {
56
93
  }
57
94
 
58
95
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
59
- const REPO_ROOT = path.resolve(__dirname, '..', '..');
96
+ const REPO_ROOT = process.env.CLEARGATE_REPO_ROOT
97
+ ? path.resolve(process.env.CLEARGATE_REPO_ROOT)
98
+ : path.resolve(__dirname, '..', '..');
60
99
  const SCRIPTS_DIR = __dirname;
61
100
 
62
101
  function usage() {
@@ -64,12 +103,13 @@ function usage() {
64
103
  'Usage: node close_sprint.mjs <sprint-id> [--assume-ack | --report-body-stdin]\n' +
65
104
  '\n' +
66
105
  'Options:\n' +
67
- ' --assume-ack Skip user acknowledgement prompt (for automated tests)\n' +
68
- ' --report-body-stdin Read REPORT.md body from stdin; implies ack (STORY-014-10)\n'
106
+ ' --assume-ack Skip user acknowledgement prompt (automated tests ONLY — conversational orchestrators MUST NOT pass this)\n' +
107
+ ' --report-body-stdin Read SPRINT-<#>_REPORT.md body from stdin; implies ack (STORY-014-10)\n'
69
108
  );
70
109
  process.exit(2);
71
110
  }
72
111
 
112
+
73
113
  /**
74
114
  * Atomic write using tmp+rename pattern (per M1 update_state.mjs convention).
75
115
  * @param {string} filePath
@@ -202,11 +242,11 @@ function main() {
202
242
  process.exit(1);
203
243
  }
204
244
 
205
- // Read REPORT.md
206
- const reportFile2 = path.join(sprintDir, 'REPORT.md');
245
+ // Read SPRINT-<#>_REPORT.md (with legacy REPORT.md fallback for pre-CR-021 sprints)
246
+ const reportFile2 = reportFilename(sprintDir, sprintId, { forRead: true });
207
247
  if (!fs.existsSync(reportFile2)) {
208
248
  process.stderr.write(
209
- `close_sprint: v2.1 validation requires REPORT.md at ${reportFile2}\n` +
249
+ `close_sprint: v2.1 validation requires ${path.basename(reportFile2)} at ${reportFile2}\n` +
210
250
  ' Run the Reporter agent first, then re-run close_sprint.mjs.\n'
211
251
  );
212
252
  process.exit(1);
@@ -247,6 +287,217 @@ function main() {
247
287
  process.stdout.write('Step 2.5 passed: v2.1 validation — all required §3 metrics and §5 sections present.\n');
248
288
  }
249
289
 
290
+ // ── Step 2.6: Lifecycle Reconciliation (CR-017) ──────────────────────────
291
+ // Block close if any artifact referenced in this sprint's commits is still
292
+ // non-terminal in pending-sync (excluding carry_over: true).
293
+ // Invokes `cleargate sprint reconcile-lifecycle <sprint-id>` CLI wrapper.
294
+ // Fail-open if CLI binary is unavailable (non-blocking for test environments).
295
+ // Test seam: CLEARGATE_SKIP_LIFECYCLE_CHECK=1 skips this step entirely (non-fatal).
296
+ process.stdout.write('Step 2.6: running lifecycle reconciliation...\n');
297
+ if (process.env.CLEARGATE_SKIP_LIFECYCLE_CHECK === '1') {
298
+ process.stdout.write('Step 2.6 skipped: CLEARGATE_SKIP_LIFECYCLE_CHECK=1 set (test seam).\n');
299
+ } else {
300
+ try {
301
+ // Resolve CLI binary: prefer local dist/
302
+ const cliBin = path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'cli.js');
303
+
304
+ if (fs.existsSync(cliBin)) {
305
+ // Read sprint start_date from frontmatter for the --since arg
306
+ let sinceArg = '';
307
+ try {
308
+ const pendingDir = path.join(REPO_ROOT, '.cleargate', 'delivery', 'pending-sync');
309
+ if (fs.existsSync(pendingDir)) {
310
+ const entries = fs.readdirSync(pendingDir);
311
+ const sprintFile = entries.find(
312
+ (e) => (e.startsWith(`${sprintId}_`) || e === `${sprintId}.md`) && e.endsWith('.md')
313
+ );
314
+ if (sprintFile) {
315
+ const raw = fs.readFileSync(path.join(pendingDir, sprintFile), 'utf8');
316
+ const startDateMatch = /^start_date:\s*(.+)$/m.exec(raw);
317
+ if (startDateMatch && startDateMatch[1]) {
318
+ sinceArg = `--since ${startDateMatch[1].trim()}`;
319
+ }
320
+ }
321
+ }
322
+ } catch { /* ignore */ }
323
+
324
+ const reconcileArgs = [
325
+ 'node', JSON.stringify(cliBin), 'sprint', 'reconcile-lifecycle', JSON.stringify(sprintId),
326
+ ];
327
+ if (sinceArg) reconcileArgs.push(sinceArg);
328
+ const reconcileCmd = reconcileArgs.join(' ');
329
+
330
+ try {
331
+ execSync(reconcileCmd, { stdio: 'inherit', env: process.env });
332
+ process.stdout.write('Step 2.6 passed: lifecycle reconciliation clean.\n');
333
+ } catch (_reconcileErr) {
334
+ // Exit code 1 from reconcile-lifecycle means drift found
335
+ process.stderr.write(
336
+ 'close_sprint: Step 2.6 FAILED — lifecycle drift blocks sprint close.\n' +
337
+ ' Remediate the listed artifacts and re-run close_sprint.mjs.\n' +
338
+ ' To carry over an artifact: set carry_over: true in its frontmatter.\n'
339
+ );
340
+ process.exit(1);
341
+ }
342
+ } else {
343
+ process.stdout.write('Step 2.6 skipped: CLI binary not found at cleargate-cli/dist/cli.js (non-fatal).\n');
344
+ }
345
+ } catch (step26Err) {
346
+ // Unexpected error — fail-open (log but do not block)
347
+ process.stderr.write(`Step 2.6 warning: lifecycle reconciliation unavailable: ${step26Err.message}\n`);
348
+ }
349
+ }
350
+
351
+ // ── Step 2.7: Worktree-Closed Check (CR-022 M1) ──────────────────────────
352
+ // Block close if any .worktrees/STORY-* path is present.
353
+ // v2 enforcing (exit 1); v1 advisory (warn + continue).
354
+ // Skip if git worktree list is unavailable (non-fatal — tests run against tmpdirs).
355
+ // Test seams: CLEARGATE_SKIP_WORKTREE_CHECK=1 bypasses entirely;
356
+ // CLEARGATE_FORCE_WORKTREE_PATHS=p1,p2 injects fake paths (no git call).
357
+ process.stdout.write('Step 2.7: checking for leftover worktrees...\n');
358
+ {
359
+ if (process.env.CLEARGATE_SKIP_WORKTREE_CHECK === '1') {
360
+ process.stdout.write('Step 2.7 skipped: CLEARGATE_SKIP_WORKTREE_CHECK=1 set (test seam).\n');
361
+ } else {
362
+ let leftoverWorktrees = [];
363
+ let worktreeListAvailable = true;
364
+
365
+ if (process.env.CLEARGATE_FORCE_WORKTREE_PATHS) {
366
+ // Test seam: inject fake worktree paths without running git
367
+ leftoverWorktrees = process.env.CLEARGATE_FORCE_WORKTREE_PATHS
368
+ .split(',')
369
+ .map((p) => p.trim())
370
+ .filter(Boolean);
371
+ } else {
372
+ try {
373
+ const output = execSync('git worktree list --porcelain', {
374
+ cwd: REPO_ROOT,
375
+ encoding: 'utf8',
376
+ stdio: ['ignore', 'pipe', 'ignore'],
377
+ });
378
+ for (const line of output.split('\n')) {
379
+ const trimmed = line.trim();
380
+ if (!trimmed.startsWith('worktree ')) continue;
381
+ const wtPath = trimmed.slice('worktree '.length);
382
+ if (/[/\\]\.worktrees[/\\]STORY-/.test(wtPath)) {
383
+ const m = /(\.(worktrees)[/\\]STORY-.+)$/.exec(wtPath);
384
+ leftoverWorktrees.push(m ? m[1] : wtPath);
385
+ }
386
+ }
387
+ } catch {
388
+ worktreeListAvailable = false;
389
+ }
390
+ }
391
+
392
+ // Step 2.7 enforcing mode: v2 execution_mode (not just schema_version, since
393
+ // migration bumps schema_version to 2 for all sprints before isV2 is evaluated).
394
+ // Using execution_mode preserves v1 advisory behaviour for sprints initialised
395
+ // with execution_mode: "v1" even after their schema is migrated.
396
+ const isEnforcingV2 = isV2 && state.execution_mode === 'v2';
397
+
398
+ if (!worktreeListAvailable) {
399
+ process.stdout.write('Step 2.7 skipped: git worktree list unavailable (non-fatal).\n');
400
+ } else if (leftoverWorktrees.length === 0) {
401
+ process.stdout.write('Step 2.7 passed: no leftover worktrees.\n');
402
+ } else if (isEnforcingV2) {
403
+ // v2 enforcing — block close
404
+ process.stderr.write(
405
+ `close_sprint: Step 2.7 failed: leftover worktree at ${leftoverWorktrees[0]}\n` +
406
+ ` ${leftoverWorktrees.length === 1 ? '' : `(plus ${leftoverWorktrees.length - 1} more)\n `}` +
407
+ `Run \`git worktree remove ${leftoverWorktrees[0]}\` if abandoned, or merge the work in progress.\n` +
408
+ ` All worktrees must be closed before sprint close.\n`
409
+ );
410
+ process.exit(1);
411
+ } else {
412
+ // v1 advisory — warn + continue
413
+ process.stderr.write(
414
+ `Step 2.7 warning: leftover worktree at ${leftoverWorktrees[0]} (advisory in v1).\n`
415
+ );
416
+ }
417
+ }
418
+ }
419
+
420
+ // ── Step 2.8: Sprint branch merged to main (verify-only, NO auto-merge) ──────
421
+ // CR-022 §1: verify-only — script asserts merge ancestry, does NOT run the merge.
422
+ // On miss: list unmerged commits + exit 1 (v2 enforcing); warn + continue (v1 advisory).
423
+ // Skip when sprintId has no numeric portion (e.g. SPRINT-TEST fixture).
424
+ // Test seams: CLEARGATE_SKIP_MERGE_CHECK=1 bypasses entirely;
425
+ // CLEARGATE_FORCE_MERGE_STATUS=merged|unmerged injects status without git call.
426
+ {
427
+ if (process.env.CLEARGATE_SKIP_MERGE_CHECK === '1') {
428
+ process.stdout.write('Step 2.8 skipped: CLEARGATE_SKIP_MERGE_CHECK=1 set (test seam).\n');
429
+ } else {
430
+ const sprintNumMatch = /^SPRINT-(\d{2,3})$/.exec(sprintId);
431
+ if (!sprintNumMatch) {
432
+ process.stdout.write(`Step 2.8 skipped: sprint-id "${sprintId}" has no numeric portion.\n`);
433
+ } else {
434
+ const sprintBranch = `refs/heads/sprint/S-${sprintNumMatch[1]}`;
435
+ const mainBranch = 'refs/heads/main';
436
+ process.stdout.write(`Step 2.8: verifying ${sprintBranch} merged to ${mainBranch}...\n`);
437
+
438
+ const isEnforcingV2 = isV2 && state.execution_mode === 'v2';
439
+
440
+ const forcedStatus = process.env.CLEARGATE_FORCE_MERGE_STATUS;
441
+ let isMerged = false;
442
+ let mergeCheckAvailable = true;
443
+
444
+ if (forcedStatus === 'merged') {
445
+ isMerged = true;
446
+ } else if (forcedStatus === 'unmerged') {
447
+ isMerged = false;
448
+ } else {
449
+ try {
450
+ execSync(
451
+ `git merge-base --is-ancestor ${sprintBranch} ${mainBranch}`,
452
+ { stdio: 'pipe', cwd: REPO_ROOT, env: process.env }
453
+ );
454
+ isMerged = true;
455
+ } catch (mergeErr) {
456
+ const exitStatus = /** @type {any} */ (mergeErr).status;
457
+ if (exitStatus === 1) {
458
+ isMerged = false;
459
+ } else {
460
+ // exit 128: refs missing or other git failure — fail-open with warning
461
+ mergeCheckAvailable = false;
462
+ process.stderr.write(
463
+ `Step 2.8 warning: git merge-base check unavailable (${/** @type {Error} */ (mergeErr).message}). ` +
464
+ `Skipping merge verification.\n`
465
+ );
466
+ }
467
+ }
468
+ }
469
+
470
+ if (!mergeCheckAvailable) {
471
+ // fail-open: refs missing or git unavailable — continue to Step 3
472
+ } else if (isMerged) {
473
+ process.stdout.write(`Step 2.8 passed: ${sprintBranch} is merged to ${mainBranch}.\n`);
474
+ } else if (isEnforcingV2) {
475
+ // v2 enforcing — block close
476
+ let unmergedLog = '';
477
+ if (!forcedStatus) {
478
+ try {
479
+ unmergedLog = execSync(
480
+ `git log ${mainBranch}..${sprintBranch} --oneline`,
481
+ { encoding: 'utf8', cwd: REPO_ROOT, env: process.env }
482
+ );
483
+ } catch { /* unmerged-log fetch failed — proceed without */ }
484
+ }
485
+ process.stderr.write(
486
+ `Step 2.8 failed: sprint/S-${sprintNumMatch[1]} not merged to main.\n` +
487
+ (unmergedLog ? ` Unmerged commits:\n${unmergedLog}` : '') +
488
+ ` Resolve: merge sprint/S-${sprintNumMatch[1]} → main, then re-run close_sprint.mjs.\n`
489
+ );
490
+ process.exit(1);
491
+ } else {
492
+ // v1 advisory — warn + continue
493
+ process.stderr.write(
494
+ `Step 2.8 warning: sprint/S-${sprintNumMatch[1]} not merged to main (advisory in v1).\n`
495
+ );
496
+ }
497
+ }
498
+ }
499
+ }
500
+
250
501
  // ── Step 3: Invoke prefill_report.mjs ─────────────────────────────────────
251
502
  process.stdout.write('Step 3: running prefill_report.mjs...\n');
252
503
  try {
@@ -259,24 +510,37 @@ function main() {
259
510
  process.exit(1);
260
511
  }
261
512
 
513
+ // ── Step 3.5: Build curated Reporter context bundle ───────────────────────
514
+ process.stdout.write('Step 3.5: building Reporter context bundle...\n');
515
+ try {
516
+ invokeScript('prep_reporter_context.mjs', [sprintId], {
517
+ CLEARGATE_STATE_FILE: stateFile,
518
+ CLEARGATE_SPRINT_DIR: sprintDir,
519
+ });
520
+ process.stdout.write(`Step 3.5 passed: ${sprintDir}/.reporter-context.md ready.\n`);
521
+ } catch (err) {
522
+ // Non-fatal — Reporter falls back to source files
523
+ process.stderr.write(`Step 3.5 warning: prep_reporter_context.mjs failed: ${/** @type {Error} */ (err).message}\n`);
524
+ process.stderr.write('Reporter will fall back to broad-fetch context loading.\n');
525
+ }
526
+
262
527
  // ── Step 4: Orchestrator spawns Reporter separately ───────────────────────
263
528
  // This script only validates preconditions; it does NOT fork the Reporter agent.
529
+ const reportFile = reportFilename(sprintDir, sprintId);
530
+ const reportBasename = path.basename(reportFile);
264
531
  process.stdout.write(
265
532
  'Step 4: preconditions satisfied — orchestrator should now spawn the Reporter agent.\n' +
266
- ' The Reporter writes REPORT.md using the sprint_report.md template.\n' +
267
- ` Expected output: ${path.join(sprintDir, 'REPORT.md')}\n`
533
+ ` The Reporter writes ${reportBasename} using the sprint_report.md template.\n` +
534
+ ` Expected output: ${reportFile}\n`
268
535
  );
269
536
 
270
- // Check if REPORT.md already exists (e.g., --assume-ack path in tests)
271
- const reportFile = path.join(sprintDir, 'REPORT.md');
272
-
273
537
  // ── Step 4.5 (STORY-014-10): --report-body-stdin fallback ────────────────
274
538
  // Orchestrator pipes the Reporter's markdown body here when the Reporter's
275
- // Write tool is blocked. Refuses empty stdin + pre-existing REPORT.md.
539
+ // Write tool is blocked. Refuses empty stdin + pre-existing report file.
276
540
  if (reportBodyStdin) {
277
541
  if (fs.existsSync(reportFile)) {
278
542
  process.stderr.write(
279
- `Error: REPORT.md already exists at ${reportFile}\n` +
543
+ `Error: ${reportBasename} already exists at ${reportFile}\n` +
280
544
  'Delete it or skip --report-body-stdin mode to use the primary Reporter-write path.\n'
281
545
  );
282
546
  process.exit(1);
@@ -285,7 +549,7 @@ function main() {
285
549
  try {
286
550
  body = fs.readFileSync(0, 'utf8');
287
551
  } catch (err) {
288
- process.stderr.write(`Error: failed to read stdin: ${err.message}\n`);
552
+ process.stderr.write(`Error: failed to read stdin: ${/** @type {Error} */ (err).message}\n`);
289
553
  process.exit(1);
290
554
  }
291
555
  if (!body || body.trim().length === 0) {
@@ -294,20 +558,22 @@ function main() {
294
558
  }
295
559
  atomicWriteString(reportFile, body);
296
560
  process.stdout.write(
297
- `Step 4.5 (stdin mode): REPORT.md written (${body.length} bytes) at ${reportFile}\n`
561
+ `Step 4.5 (stdin mode): ${reportBasename} written (${body.length} bytes) at ${reportFile}\n`
298
562
  );
299
- // Fall through to Step 5 + 6 unconditionally — stdin mode implies ack.
563
+ // Fall through to Step 5 + 6 + 7 unconditionally — stdin mode implies ack.
300
564
  } else if (!assumeAck) {
301
- if (!fs.existsSync(reportFile)) {
565
+ // Apply read-fallback for legacy sprints (e.g. SPRINT-15 with plain REPORT.md)
566
+ const reportFileForCheck = reportFilename(sprintDir, sprintId, { forRead: true });
567
+ if (!fs.existsSync(reportFileForCheck)) {
302
568
  process.stdout.write(
303
- '\nWaiting for Reporter to produce REPORT.md...\n' +
569
+ `\nWaiting for Reporter to produce ${reportBasename}...\n` +
304
570
  'After Reporter succeeds, re-run with --assume-ack to complete the close.\n'
305
571
  );
306
572
  process.exit(0);
307
573
  }
308
- // In non-assume-ack mode with existing REPORT.md, prompt user
574
+ // In non-assume-ack mode with existing report, prompt user
309
575
  process.stdout.write(
310
- `\nREPORT.md found at ${reportFile}\n` +
576
+ `\n${reportBasename} found at ${reportFileForCheck}\n` +
311
577
  'Review the report, then confirm close by re-running with --assume-ack\n'
312
578
  );
313
579
  process.exit(0);
@@ -331,13 +597,107 @@ function main() {
331
597
  });
332
598
  } catch (err) {
333
599
  // suggest_improvements failure is non-fatal — log but do not abort
334
- process.stderr.write(`Warning: suggest_improvements.mjs failed: ${err.message}\n`);
600
+ process.stderr.write(`Warning: suggest_improvements.mjs failed: ${/** @type {Error} */ (err).message}\n`);
335
601
  process.stderr.write('Sprint is still marked Completed; improvement suggestions may be incomplete.\n');
336
602
  }
337
603
 
338
- process.stdout.write(`\nSprint ${sprintId} close pipeline complete.\n`);
339
- process.stdout.write(` state.json: sprint_status = Completed\n`);
340
- process.stdout.write(` improvement-suggestions.md: ${path.join(sprintDir, 'improvement-suggestions.md')}\n`);
604
+ // ── Step 6.5: Run sprint_trends.mjs (stub full impl deferred to CR-027) ──
605
+ if (process.env.CLEARGATE_SKIP_SPRINT_TRENDS !== '1') {
606
+ process.stdout.write('Step 6.5: running sprint_trends.mjs (stub)...\n');
607
+ try {
608
+ invokeScript('sprint_trends.mjs', [sprintId], {
609
+ CLEARGATE_STATE_FILE: stateFile,
610
+ CLEARGATE_SPRINT_DIR: sprintDir,
611
+ });
612
+ } catch (err) {
613
+ // Non-fatal — sprint stays Completed; trends are advisory only.
614
+ process.stderr.write(`Step 6.5 warning: sprint_trends.mjs failed: ${/** @type {Error} */ (err).message}\n`);
615
+ }
616
+ }
617
+
618
+ // ── Step 6.6: Skill-candidate detection (folds into suggest_improvements.mjs) ──
619
+ if (process.env.CLEARGATE_SKIP_SKILL_CANDIDATES !== '1') {
620
+ process.stdout.write('Step 6.6: scanning for skill candidates...\n');
621
+ try {
622
+ invokeScript('suggest_improvements.mjs', [sprintId, '--skill-candidates'], {
623
+ CLEARGATE_STATE_FILE: stateFile,
624
+ CLEARGATE_SPRINT_DIR: sprintDir,
625
+ });
626
+ } catch (err) {
627
+ process.stderr.write(`Step 6.6 warning: skill-candidate scan failed: ${/** @type {Error} */ (err).message}\n`);
628
+ }
629
+ }
630
+
631
+ // ── Step 6.7: FLASHCARD cleanup pass (folds into suggest_improvements.mjs) ──
632
+ if (process.env.CLEARGATE_SKIP_FLASHCARD_CLEANUP !== '1') {
633
+ process.stdout.write('Step 6.7: scanning FLASHCARD.md for cleanup candidates...\n');
634
+ try {
635
+ invokeScript('suggest_improvements.mjs', [sprintId, '--flashcard-cleanup'], {
636
+ CLEARGATE_STATE_FILE: stateFile,
637
+ CLEARGATE_SPRINT_DIR: sprintDir,
638
+ });
639
+ } catch (err) {
640
+ process.stderr.write(`Step 6.7 warning: FLASHCARD cleanup scan failed: ${/** @type {Error} */ (err).message}\n`);
641
+ }
642
+ }
643
+
644
+ // ── Step 7: Auto-push per-artifact status updates to MCP ─────────────────
645
+ // Runs after Gate 4 ack succeeds. Non-fatal: sprint stays Completed on failure.
646
+ process.stdout.write('Step 7: pushing per-artifact status updates to MCP...\n');
647
+ try {
648
+ const cliBin = path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'cli.js');
649
+ if (fs.existsSync(cliBin)) {
650
+ // cleargate sync work-items takes ZERO positional args (verified cli.ts:592-598).
651
+ // CR-021 §3.2.3 spec shows a sprint-id arg — that is spec drift; drop it.
652
+ execSync(`node ${JSON.stringify(cliBin)} sync work-items`, {
653
+ stdio: 'inherit',
654
+ env: process.env,
655
+ timeout: 30000,
656
+ });
657
+ process.stdout.write('Step 7 passed: work-item statuses synced.\n');
658
+ } else {
659
+ process.stdout.write('Step 7 skipped: CLI binary not found (non-fatal).\n');
660
+ }
661
+ } catch (err) {
662
+ // Non-fatal — sprint stays Completed; sync can be retried manually
663
+ process.stderr.write(`Step 7 warning: sync work-items failed: ${/** @type {Error} */ (err).message}\n`);
664
+ process.stderr.write('Run `cleargate sync work-items` manually to retry.\n');
665
+ }
666
+
667
+ // ── Step 8: Verbose post-close handoff list ───────────────────────────────
668
+ // Prints 6 explicit next-step items to stdout (CR-022 §3 M4).
669
+ {
670
+ const sprintNumMatch = /^SPRINT-(\d{2,3})$/.exec(sprintId);
671
+ const nextSprintNum = sprintNumMatch
672
+ ? String(parseInt(sprintNumMatch[1], 10) + 1).padStart(sprintNumMatch[1].length, '0')
673
+ : null;
674
+ const nextSprintId = nextSprintNum ? `SPRINT-${nextSprintNum}` : '<next-sprint-id>';
675
+ const reportBasename = path.basename(reportFile);
676
+ const suggestionsPath = path.join(sprintDir, 'improvement-suggestions.md');
677
+
678
+ process.stdout.write(`\n${sprintId} closed. Next steps:\n`);
679
+ process.stdout.write(` 1. Review ${reportBasename}\n`);
680
+ process.stdout.write(
681
+ ` 2. Review improvement-suggestions.md (sections: Suggestions / Skill Candidates / FLASHCARD Cleanup)\n`,
682
+ );
683
+ process.stdout.write(
684
+ ` 3. Approve or reject Skill Candidates → run /improve or cleargate skill create <name>\n`,
685
+ );
686
+ process.stdout.write(
687
+ ` 4. Approve or reject FLASHCARD cleanup entries → run /improve or cleargate flashcard prune\n`,
688
+ );
689
+ process.stdout.write(
690
+ ` 5. Push approved status changes to MCP if Step 7 warned (\`cleargate sync work-items\`)\n`,
691
+ );
692
+ process.stdout.write(
693
+ ` 6. Initialize next sprint: \`cleargate sprint init ${nextSprintId} --stories <ids>\`\n`,
694
+ );
695
+
696
+ // Surface artifact paths for convenience
697
+ process.stdout.write(`\nArtifacts:\n`);
698
+ process.stdout.write(` report: ${reportFile}\n`);
699
+ process.stdout.write(` improvement-suggestions: ${suggestionsPath}\n`);
700
+ }
341
701
  }
342
702
 
343
703
  main();