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