cleargate 0.14.0 → 0.15.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 (150) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/MANIFEST.json +72 -16
  3. package/dist/admin-api/index.cjs +0 -1
  4. package/dist/admin-api/index.js +1 -2
  5. package/dist/auth/factory.cjs +0 -1
  6. package/dist/auth/factory.js +2 -3
  7. package/dist/auth/require-token.cjs +0 -1
  8. package/dist/auth/require-token.js +1 -2
  9. package/dist/auth/token-store.cjs +0 -1
  10. package/dist/auth/token-store.js +1 -2
  11. package/dist/{bootstrap-root-QKSA5V75.js → bootstrap-root-2H5HVTCC.js} +1 -2
  12. package/dist/{chunk-PDE37WFQ.js → chunk-A7MSQUU7.js} +2 -3
  13. package/dist/{chunk-BTSZOEWC.js → chunk-P6KEDAK2.js} +0 -1
  14. package/dist/{chunk-E3X7IE5E.js → chunk-PY6FHGV5.js} +1 -2
  15. package/dist/{chunk-5DI2Z3C2.js → chunk-Y53ZZYYU.js} +1 -2
  16. package/dist/cli.cjs +1564 -1414
  17. package/dist/cli.js +1514 -1364
  18. package/dist/lib/ledger.cjs +0 -1
  19. package/dist/lib/ledger.js +1 -2
  20. package/dist/lib/lifecycle-reconcile.cjs +0 -1
  21. package/dist/lib/lifecycle-reconcile.js +2 -3
  22. package/dist/{whoami-EANGN46Z.js → whoami-JKQQPABQ.js} +3 -4
  23. package/package.json +4 -3
  24. package/templates/cleargate-planning/.claude/agents/architect-synth.md +2 -0
  25. package/templates/cleargate-planning/.claude/agents/architect.md +4 -2
  26. package/templates/cleargate-planning/.claude/agents/developer.md +4 -11
  27. package/templates/cleargate-planning/.claude/agents/qa.md +14 -6
  28. package/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +2 -2
  29. package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +19 -1
  30. package/templates/cleargate-planning/.cleargate/config.example.yml +16 -0
  31. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.deferred-verify.red.node.test.ts +245 -0
  32. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +227 -0
  33. package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +5 -4
  34. package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +75 -2
  35. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +48 -0
  36. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +57 -1
  37. package/templates/cleargate-planning/.cleargate/scripts/provision_worktree_config.sh +155 -0
  38. package/templates/cleargate-planning/.cleargate/scripts/qa_red_lint.mjs +380 -0
  39. package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +34 -1
  40. package/templates/cleargate-planning/.cleargate/scripts/test/cr077_eviction.red.sh +113 -0
  41. package/templates/cleargate-planning/.cleargate/scripts/test/cr078_init.test.sh +309 -0
  42. package/templates/cleargate-planning/.cleargate/scripts/test/cr079_provision.red.sh +262 -0
  43. package/templates/cleargate-planning/.cleargate/scripts/test/cr080_wrapper.test.sh +177 -0
  44. package/templates/cleargate-planning/.cleargate/scripts/test/cr081_qa_red_lint.red.sh +348 -0
  45. package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/.session-totals.json +1 -0
  46. package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/token-ledger.jsonl +222 -0
  47. package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +17 -0
  48. package/templates/cleargate-planning/.cleargate/templates/story.md +1 -0
  49. package/templates/cleargate-planning/MANIFEST.json +72 -16
  50. package/dist/admin-api/index.cjs.map +0 -1
  51. package/dist/admin-api/index.js.map +0 -1
  52. package/dist/auth/factory.cjs.map +0 -1
  53. package/dist/auth/factory.js.map +0 -1
  54. package/dist/auth/require-token.cjs.map +0 -1
  55. package/dist/auth/require-token.js.map +0 -1
  56. package/dist/auth/token-store.cjs.map +0 -1
  57. package/dist/auth/token-store.js.map +0 -1
  58. package/dist/bootstrap-root-QKSA5V75.js.map +0 -1
  59. package/dist/chunk-5DI2Z3C2.js.map +0 -1
  60. package/dist/chunk-BTSZOEWC.js.map +0 -1
  61. package/dist/chunk-E3X7IE5E.js.map +0 -1
  62. package/dist/chunk-PDE37WFQ.js.map +0 -1
  63. package/dist/cli.cjs.map +0 -1
  64. package/dist/cli.js.map +0 -1
  65. package/dist/lib/ledger.cjs.map +0 -1
  66. package/dist/lib/ledger.js.map +0 -1
  67. package/dist/lib/lifecycle-reconcile.cjs.map +0 -1
  68. package/dist/lib/lifecycle-reconcile.js.map +0 -1
  69. package/dist/templates/cleargate-planning/.claude/agents/architect-reader.md +0 -61
  70. package/dist/templates/cleargate-planning/.claude/agents/architect-synth.md +0 -124
  71. package/dist/templates/cleargate-planning/.claude/agents/architect.md +0 -230
  72. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +0 -108
  73. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +0 -194
  74. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +0 -261
  75. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-query.md +0 -143
  76. package/dist/templates/cleargate-planning/.claude/agents/developer.md +0 -185
  77. package/dist/templates/cleargate-planning/.claude/agents/devops.md +0 -257
  78. package/dist/templates/cleargate-planning/.claude/agents/qa.md +0 -171
  79. package/dist/templates/cleargate-planning/.claude/agents/reporter.md +0 -274
  80. package/dist/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +0 -209
  81. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +0 -33
  82. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +0 -58
  83. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit.sh +0 -19
  84. package/dist/templates/cleargate-planning/.claude/hooks/pre-edit-gate.sh +0 -162
  85. package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-autonomy.sh +0 -58
  86. package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +0 -148
  87. package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +0 -75
  88. package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +0 -43
  89. package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +0 -590
  90. package/dist/templates/cleargate-planning/.claude/settings.json +0 -68
  91. package/dist/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +0 -102
  92. package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +0 -742
  93. package/dist/templates/cleargate-planning/.cleargate/FLASHCARD.md +0 -7
  94. package/dist/templates/cleargate-planning/.cleargate/config.example.yml +0 -67
  95. package/dist/templates/cleargate-planning/.cleargate/config.yml +0 -18
  96. package/dist/templates/cleargate-planning/.cleargate/delivery/archive/.gitkeep +0 -0
  97. package/dist/templates/cleargate-planning/.cleargate/delivery/pending-sync/.gitkeep +0 -0
  98. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +0 -551
  99. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +0 -878
  100. package/dist/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +0 -160
  101. package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +0 -213
  102. package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +0 -71
  103. package/dist/templates/cleargate-planning/.cleargate/scripts/_migrate-schema-v3.mjs +0 -120
  104. package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +0 -265
  105. package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +0 -1012
  106. package/dist/templates/cleargate-planning/.cleargate/scripts/collision_surface.sh +0 -114
  107. package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +0 -62
  108. package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +0 -219
  109. package/dist/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +0 -320
  110. package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +0 -15
  111. package/dist/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +0 -38
  112. package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +0 -240
  113. package/dist/templates/cleargate-planning/.cleargate/scripts/launch_wave.mjs +0 -341
  114. package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +0 -54
  115. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +0 -206
  116. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +0 -371
  117. package/dist/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +0 -280
  118. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +0 -378
  119. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +0 -888
  120. package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +0 -209
  121. package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +0 -71
  122. package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +0 -127
  123. package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +0 -717
  124. package/dist/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +0 -27
  125. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +0 -261
  126. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +0 -210
  127. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +0 -190
  128. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +0 -482
  129. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +0 -327
  130. package/dist/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +0 -261
  131. package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +0 -246
  132. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +0 -111
  133. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +0 -184
  134. package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +0 -172
  135. package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +0 -126
  136. package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +0 -130
  137. package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +0 -137
  138. package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +0 -166
  139. package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +0 -111
  140. package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +0 -122
  141. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +0 -50
  142. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +0 -224
  143. package/dist/templates/cleargate-planning/.cleargate/templates/story.md +0 -213
  144. package/dist/templates/cleargate-planning/CLAUDE.md +0 -66
  145. package/dist/templates/cleargate-planning/MANIFEST.json +0 -503
  146. package/dist/templates/synthesis/active-sprint.md +0 -30
  147. package/dist/templates/synthesis/open-gates.md +0 -38
  148. package/dist/templates/synthesis/product-state.md +0 -31
  149. package/dist/templates/synthesis/roadmap.md +0 -63
  150. package/dist/whoami-EANGN46Z.js.map +0 -1
@@ -1,1012 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * close_sprint.mjs — Eight-step sprint close pipeline
4
- *
5
- * Usage: node close_sprint.mjs <sprint-id> [--assume-ack]
6
- * node close_sprint.mjs <sprint-id> --report-body-stdin (STORY-014-10)
7
- *
8
- * Steps:
9
- * 1. Load and validate state.json via validateState
10
- * 2. Refuse if any story state is not in TERMINAL_STATES (exit non-zero, list offenders)
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)
13
- * 4. Orchestrator spawns Reporter separately (script validates preconditions only)
14
- * 5. On Reporter success + user ack (or --assume-ack flag), flip sprint_status -> "Completed"
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.
24
- *
25
- * Stdin fallback (STORY-014-10): when `--report-body-stdin` is passed, the script
26
- * reads the full SPRINT-<#>_REPORT.md body from stdin and writes it atomically in lieu of
27
- * waiting for a Reporter-produced file. Replaces the Step-4 gate; implies ack.
28
- * Refuses empty stdin or pre-existing report file.
29
- *
30
- * Does NOT archive the sprint file (pending-sync -> archive stays human per EPIC-013 §4.5 step 7).
31
- *
32
- * Reuse: TERMINAL_STATES, VALID_STATES from constants.mjs
33
- * validateState from validate_state.mjs
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.
66
- */
67
-
68
- import fs from 'node:fs';
69
- import path from 'node:path';
70
- import { fileURLToPath } from 'node:url';
71
- import { execSync } from 'node:child_process';
72
- import { TERMINAL_STATES } from './constants.mjs';
73
- import { validateState } from './validate_state.mjs';
74
- import { migrateStateToV3 } from './_migrate-schema-v3.mjs';
75
- import { reportFilename } from './lib/report-filename.mjs';
76
-
77
- /**
78
- * Migrate a v1 state.json to v2 by injecting lane fields with defaults.
79
- * Inlined from update_state.mjs:migrateV1ToV2 to avoid triggering that
80
- * script's CLI main() on import (update_state.mjs has no module guard).
81
- * @param {object} state - Parsed v1 state object
82
- * @returns {object} - The mutated (now v2) state object
83
- */
84
- function migrateV1ToV2(state) {
85
- state.schema_version = 2;
86
- const storyIds = Object.keys(state.stories || {});
87
- for (const id of storyIds) {
88
- const story = state.stories[id];
89
- if (story.lane == null) story.lane = 'standard';
90
- if (story.lane_assigned_by == null) story.lane_assigned_by = 'migration-default';
91
- if (story.lane_demoted_at === undefined) story.lane_demoted_at = null;
92
- if (story.lane_demotion_reason === undefined) story.lane_demotion_reason = null;
93
- }
94
- process.stderr.write(
95
- `migration: schema_version 1 → 2 for sprint ${state.sprint_id} (${storyIds.length} stories defaulted to lane: standard)\n`
96
- );
97
- return state;
98
- }
99
-
100
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
101
- const REPO_ROOT = process.env.CLEARGATE_REPO_ROOT
102
- ? path.resolve(process.env.CLEARGATE_REPO_ROOT)
103
- : path.resolve(__dirname, '..', '..');
104
- const SCRIPTS_DIR = __dirname;
105
-
106
- function usage() {
107
- process.stderr.write(
108
- 'Usage: node close_sprint.mjs <sprint-id> [--assume-ack | --report-body-stdin]\n' +
109
- '\n' +
110
- 'Options:\n' +
111
- ' --assume-ack Skip user acknowledgement prompt (automated tests ONLY — conversational orchestrators MUST NOT pass this)\n' +
112
- ' --report-body-stdin Read SPRINT-<#>_REPORT.md body from stdin; implies ack (STORY-014-10)\n'
113
- );
114
- process.exit(2);
115
- }
116
-
117
-
118
- /**
119
- * Atomic write using tmp+rename pattern (per M1 update_state.mjs convention).
120
- * @param {string} filePath
121
- * @param {object} data
122
- */
123
- function atomicWrite(filePath, data) {
124
- const tmpFile = `${filePath}.tmp.${process.pid}`;
125
- fs.writeFileSync(tmpFile, JSON.stringify(data, null, 2) + '\n', 'utf8');
126
- fs.renameSync(tmpFile, filePath);
127
- }
128
-
129
- /**
130
- * Atomic write for a string body. Separate from atomicWrite() so we don't
131
- * accidentally JSON.stringify a markdown body.
132
- * @param {string} filePath
133
- * @param {string} body
134
- */
135
- function atomicWriteString(filePath, body) {
136
- const tmpFile = `${filePath}.tmp.${process.pid}`;
137
- fs.writeFileSync(tmpFile, body, 'utf8');
138
- fs.renameSync(tmpFile, filePath);
139
- }
140
-
141
- /**
142
- * Invoke a script via node (for .mjs scripts in the same directory).
143
- * Throws on non-zero exit.
144
- * @param {string} scriptName
145
- * @param {string[]} scriptArgs
146
- * @param {object} env
147
- */
148
- function invokeScript(scriptName, scriptArgs, env) {
149
- const scriptPath = path.join(SCRIPTS_DIR, scriptName);
150
- if (!fs.existsSync(scriptPath)) {
151
- throw new Error(`Script not found: ${scriptPath}`);
152
- }
153
- const argStr = scriptArgs.map(a => JSON.stringify(a)).join(' ');
154
- const cmd = `node ${JSON.stringify(scriptPath)} ${argStr}`;
155
- execSync(cmd, {
156
- stdio: 'inherit',
157
- env: Object.assign({}, process.env, env || {}),
158
- });
159
- }
160
-
161
- async function main() {
162
- const args = process.argv.slice(2);
163
-
164
- if (args.length < 1) usage();
165
-
166
- const sprintId = args[0];
167
- const reportBodyStdin = args.includes('--report-body-stdin');
168
- const assumeAck = args.includes('--assume-ack') || reportBodyStdin;
169
-
170
- const sprintDir = process.env.CLEARGATE_SPRINT_DIR
171
- ? path.resolve(process.env.CLEARGATE_SPRINT_DIR)
172
- : path.join(REPO_ROOT, '.cleargate', 'sprint-runs', sprintId);
173
-
174
- if (!fs.existsSync(sprintDir)) {
175
- process.stderr.write(`Error: sprint directory not found: ${sprintDir}\n`);
176
- process.exit(1);
177
- }
178
-
179
- const stateFile = process.env.CLEARGATE_STATE_FILE
180
- ? path.resolve(process.env.CLEARGATE_STATE_FILE)
181
- : path.join(sprintDir, 'state.json');
182
-
183
- // ── Step 1: Load and validate state.json ──────────────────────────────────
184
- if (!fs.existsSync(stateFile)) {
185
- process.stderr.write(
186
- `Error: state.json not found at ${stateFile}\n` +
187
- `Hint: run init_sprint.mjs ${sprintId} --stories <ids> first\n`
188
- );
189
- process.exit(1);
190
- }
191
-
192
- let state;
193
- try {
194
- state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
195
- } catch (err) {
196
- process.stderr.write(`Error: failed to parse state.json: ${err.message}\n`);
197
- process.exit(1);
198
- }
199
-
200
- // Migrate v1 → v2 if needed before strict validation
201
- if (state.schema_version === 1) {
202
- state = migrateV1ToV2(state);
203
- atomicWrite(stateFile, state);
204
- }
205
-
206
- // Migrate v2 → v3: strip execution_mode (STORY-070-01)
207
- const { changed: v3Changed } = migrateStateToV3(state, stateFile);
208
- if (v3Changed) {
209
- atomicWrite(stateFile, state);
210
- }
211
-
212
- const { valid, errors } = validateState(state);
213
- if (!valid) {
214
- process.stderr.write('Error: state.json validation failed:\n');
215
- for (const e of errors) process.stderr.write(` - ${e}\n`);
216
- process.exit(1);
217
- }
218
-
219
- // ── Step 2: Refuse if any story not in TERMINAL_STATES ────────────────────
220
- const nonTerminal = [];
221
- for (const [storyId, story] of Object.entries(state.stories || {})) {
222
- if (!TERMINAL_STATES.includes(story.state)) {
223
- nonTerminal.push(`${storyId}: ${story.state} — not terminal`);
224
- }
225
- }
226
-
227
- if (nonTerminal.length > 0) {
228
- process.stderr.write('Error: sprint cannot close — non-terminal stories:\n');
229
- for (const msg of nonTerminal) {
230
- process.stderr.write(` ${msg}\n`);
231
- }
232
- process.exit(1);
233
- }
234
-
235
- process.stdout.write(`Step 1-2 passed: all ${Object.keys(state.stories || {}).length} stories are terminal.\n`);
236
-
237
- // ── Step 2.5: v2.1 validation — activation-gated ──────────────────────────
238
- // Activation gate: at least one story has lane: 'fast'
239
- // (schema_version is always >= 3 post-STORY-070-01 migrator)
240
- const hasFastLane = Object.values(state.stories || {}).some(
241
- (s) => /** @type {any} */ (s).lane === 'fast'
242
- );
243
-
244
- if (hasFastLane) {
245
- // Naming convention: sprint dir must match ^SPRINT-\d{2,3}$
246
- const sprintDirName = path.basename(sprintDir);
247
- if (!/^SPRINT-\d{2,3}$/.test(sprintDirName)) {
248
- process.stderr.write(
249
- `close_sprint: sprint dir "${sprintDirName}" does not match ^SPRINT-\\d{2,3}$\n` +
250
- ` Expected format: SPRINT-NN or SPRINT-NNN (e.g. SPRINT-14)\n` +
251
- ` Got: "${sprintDirName}" at path: ${sprintDir}\n`
252
- );
253
- process.exit(1);
254
- }
255
-
256
- // Read SPRINT-<#>_REPORT.md (with legacy REPORT.md fallback for pre-CR-021 sprints)
257
- const reportFile2 = reportFilename(sprintDir, sprintId, { forRead: true });
258
- if (!fs.existsSync(reportFile2)) {
259
- process.stderr.write(
260
- `close_sprint: v2.1 validation requires ${path.basename(reportFile2)} at ${reportFile2}\n` +
261
- ' Run the Reporter agent first, then re-run close_sprint.mjs.\n'
262
- );
263
- process.exit(1);
264
- }
265
- const report = fs.readFileSync(reportFile2, 'utf8');
266
-
267
- // Check required §3 metric rows
268
- const requiredMetricRows = [
269
- /Fast-Track Ratio/,
270
- /Fast-Track Demotion Rate/,
271
- /Hotfix Count/,
272
- /Hotfix-to-Story Ratio/,
273
- /Hotfix Cap Breaches/,
274
- /LD events/,
275
- ];
276
- const missingMetrics = requiredMetricRows.filter((rx) => !rx.test(report));
277
- if (missingMetrics.length > 0) {
278
- process.stderr.write(
279
- `close_sprint: §3 missing rows: ${missingMetrics.map((rx) => rx.source).join(', ')}\n`
280
- );
281
- process.exit(1);
282
- }
283
-
284
- // Check required §5 sections
285
- const requiredSections = [
286
- /Lane Audit/,
287
- /Hotfix Audit/,
288
- /Hotfix Trend/,
289
- ];
290
- const missingSections = requiredSections.filter((rx) => !rx.test(report));
291
- if (missingSections.length > 0) {
292
- process.stderr.write(
293
- `close_sprint: §5 missing: ${missingSections.map((rx) => rx.source).join(', ')}\n`
294
- );
295
- process.exit(1);
296
- }
297
-
298
- process.stdout.write('Step 2.5 passed: v2.1 validation — all required §3 metrics and §5 sections present.\n');
299
- }
300
-
301
- // ── WS8(e) Dist Fail-Closed Assertion ────────────────────────────────────
302
- // Verify that cleargate-cli/dist/cli.js is present BEFORE attempting the
303
- // Step 2.6 cascade. Absent dist means the build is stale and the lifecycle/
304
- // orphan/parent-rollup/backsync/merge gates will silently no-op — that is
305
- // worse than failing loudly. Exit 1 early so the operator fixes the build.
306
- //
307
- // Test seam bypass: CLEARGATE_SKIP_LIFECYCLE_CHECK=1 skips this assertion
308
- // (the deliberate skip-env path for test environments where the CLI binary
309
- // is intentionally absent — e.g. sandbox-only sprint dirs).
310
- if (process.env.CLEARGATE_SKIP_LIFECYCLE_CHECK !== '1') {
311
- const cliBinEarly = path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'cli.js');
312
- if (!fs.existsSync(cliBinEarly)) {
313
- process.stderr.write(
314
- `dist not built — run \`npm run build\` in cleargate-cli/\n` +
315
- ` Expected: ${cliBinEarly}\n` +
316
- ` The lifecycle/orphan/parent-rollup/backsync/merge gates require a built CLI dist.\n`
317
- );
318
- process.exit(1);
319
- }
320
- }
321
-
322
- // ── Step 2.6: Lifecycle Reconciliation (CR-017) ──────────────────────────
323
- // Block close if any artifact referenced in this sprint's commits is still
324
- // non-terminal in pending-sync (excluding carry_over: true).
325
- // Invokes `cleargate sprint reconcile-lifecycle <sprint-id>` CLI wrapper.
326
- // Fail-open if CLI binary is unavailable (non-blocking for test environments).
327
- // Test seam: CLEARGATE_SKIP_LIFECYCLE_CHECK=1 skips this step entirely (non-fatal).
328
- process.stdout.write('Step 2.6: running lifecycle reconciliation...\n');
329
- if (process.env.CLEARGATE_SKIP_LIFECYCLE_CHECK === '1') {
330
- process.stdout.write('Step 2.6 skipped: CLEARGATE_SKIP_LIFECYCLE_CHECK=1 set (test seam).\n');
331
- } else {
332
- try {
333
- // Resolve CLI binary: prefer local dist/
334
- const cliBin = path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'cli.js');
335
-
336
- if (fs.existsSync(cliBin)) {
337
- // Read sprint start_date from frontmatter for the --since arg
338
- let sinceArg = '';
339
- try {
340
- const pendingDir = path.join(REPO_ROOT, '.cleargate', 'delivery', 'pending-sync');
341
- if (fs.existsSync(pendingDir)) {
342
- const entries = fs.readdirSync(pendingDir);
343
- const sprintFile = entries.find(
344
- (e) => (e.startsWith(`${sprintId}_`) || e === `${sprintId}.md`) && e.endsWith('.md')
345
- );
346
- if (sprintFile) {
347
- const raw = fs.readFileSync(path.join(pendingDir, sprintFile), 'utf8');
348
- const startDateMatch = /^start_date:\s*(.+)$/m.exec(raw);
349
- if (startDateMatch && startDateMatch[1]) {
350
- sinceArg = `--since ${startDateMatch[1].trim()}`;
351
- }
352
- }
353
- }
354
- } catch { /* ignore */ }
355
-
356
- const reconcileArgs = [
357
- 'node', JSON.stringify(cliBin), 'sprint', 'reconcile-lifecycle', JSON.stringify(sprintId),
358
- ];
359
- if (sinceArg) reconcileArgs.push(sinceArg);
360
- const reconcileCmd = reconcileArgs.join(' ');
361
-
362
- try {
363
- execSync(reconcileCmd, { stdio: 'inherit', env: process.env });
364
- process.stdout.write('Step 2.6 passed: lifecycle reconciliation clean.\n');
365
- } catch (_reconcileErr) {
366
- // Exit code 1 from reconcile-lifecycle means drift found
367
- process.stderr.write(
368
- 'close_sprint: Step 2.6 FAILED — lifecycle drift blocks sprint close.\n' +
369
- ' Remediate the listed artifacts and re-run close_sprint.mjs.\n' +
370
- ' To carry over an artifact: set carry_over: true in its frontmatter.\n'
371
- );
372
- process.exit(1);
373
- }
374
- } else {
375
- process.stdout.write('Step 2.6 skipped: CLI binary not found at cleargate-cli/dist/cli.js (non-fatal).\n');
376
- }
377
- } catch (step26Err) {
378
- // Unexpected error — fail-open (log but do not block)
379
- process.stderr.write(`Step 2.6 warning: lifecycle reconciliation unavailable: ${step26Err.message}\n`);
380
- }
381
- }
382
-
383
- // ── Step 2.6b: Cross-Sprint Orphan Drift Check (CR-048) ─────────────────────
384
- // Detect items in pending-sync/ with non-terminal status whose state.json entry
385
- // in any closed sprint shows Done — i.e., completed but never archived.
386
- // Always enforced (STORY-070-01: execution_mode retired).
387
- // Test seam: CLEARGATE_SKIP_LIFECYCLE_CHECK=1 also skips this step.
388
- process.stdout.write('Step 2.6b: checking for cross-sprint orphan drift...\n');
389
- if (process.env.CLEARGATE_SKIP_LIFECYCLE_CHECK !== '1') {
390
- try {
391
- const cliBin26b = path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'cli.js');
392
- if (fs.existsSync(cliBin26b)) {
393
- // Dynamic import the compiled reconciler function
394
- const reconcilerMod = await import(
395
- path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'lib', 'lifecycle-reconcile.js')
396
- ).catch(() => null);
397
-
398
- if (reconcilerMod && typeof reconcilerMod.reconcileCrossSprintOrphans === 'function') {
399
- const deliveryRoot = path.join(REPO_ROOT, '.cleargate', 'delivery');
400
- const sprintRunsRoot = path.join(REPO_ROOT, '.cleargate', 'sprint-runs');
401
- const orphanResult = reconcilerMod.reconcileCrossSprintOrphans({ deliveryRoot, sprintRunsRoot });
402
-
403
- if (orphanResult.drift.length > 0) {
404
- process.stderr.write(
405
- `Step 2.6b: ${orphanResult.drift.length} cross-sprint orphan(s) detected:\n`
406
- );
407
- for (const item of orphanResult.drift) {
408
- process.stderr.write(
409
- ` ${item.id} — status: ${item.pending_sync_status} in pending-sync, ` +
410
- `state: ${item.state_json_state} in ${item.state_json_sprint}\n`
411
- );
412
- }
413
- // Always enforced (STORY-070-01: execution_mode retired)
414
- process.stderr.write(
415
- 'close_sprint: Step 2.6b FAILED — orphan drift blocks sprint close.\n' +
416
- ' Archive the listed items and re-run close_sprint.mjs.\n'
417
- );
418
- process.exit(1);
419
- } else {
420
- process.stdout.write('Step 2.6b passed: no cross-sprint orphan drift.\n');
421
- }
422
- } else {
423
- process.stdout.write('Step 2.6b skipped: reconcileCrossSprintOrphans not available in built CLI.\n');
424
- }
425
- } else {
426
- process.stdout.write('Step 2.6b skipped: CLI binary not found (non-fatal).\n');
427
- }
428
- } catch (step26bErr) {
429
- process.stderr.write(`Step 2.6b warning: orphan check unavailable: ${step26bErr.message}\n`);
430
- }
431
- } else {
432
- process.stdout.write('Step 2.6b skipped: CLEARGATE_SKIP_LIFECYCLE_CHECK=1 set (test seam).\n');
433
- }
434
-
435
- // ── Step 2.6c: Parent (Epic/Sprint) Rollup (CR-066) ──────────────────────
436
- // Runs unconditionally (not gated by CLEARGATE_SKIP_LIFECYCLE_CHECK).
437
- // For each active parent in delivery/pending-sync/, checks children coverage:
438
- // auto-flip → rewrite status: Completed atomically, log one line.
439
- // halt-partial → collect into haltList; exit 1 after processing all.
440
- // halt-zero-children → collect into haltList; exit 1 after processing all.
441
- // no-op / skip-deferred → silent.
442
- // Defensive guard: if walkActiveParents is not a function (stale dist/), skip with warning.
443
-
444
- /**
445
- * Atomic in-place status rewrite (raw-bytes regex-replace).
446
- * Follows FLASHCARD 2026-04-24 #frontmatter #write-back pattern.
447
- * @param {string} filePath
448
- * @param {string} newStatus
449
- */
450
- function setFrontmatterStatusAtomic(filePath, newStatus) {
451
- const raw = fs.readFileSync(filePath, 'utf8');
452
- const fm = raw.match(/^---\n([\s\S]*?)\n---/);
453
- if (!fm) throw new Error(`No frontmatter in ${filePath}`);
454
- const newFm = fm[1].replace(/^status:.*$/m, `status: ${newStatus}`);
455
- const newRaw = raw.replace(fm[1], newFm);
456
- const tmp = filePath + '.tmp.' + process.pid;
457
- fs.writeFileSync(tmp, newRaw, 'utf8');
458
- fs.renameSync(tmp, filePath);
459
- }
460
-
461
- // ── Step 2.6c test seam ──────────────────────────────────────────────────────
462
- if (process.env.CLEARGATE_SKIP_PARENT_ROLLUP === '1') {
463
- process.stdout.write('Step 2.6c skipped: CLEARGATE_SKIP_PARENT_ROLLUP=1 set (test seam).\n');
464
- } else {
465
- process.stdout.write('Step 2.6c: rolling up parent statuses...\n');
466
- try {
467
- // Use __dirname-relative path so the import finds the ACTUAL built dist,
468
- // not a fixture tmpdir override (CLEARGATE_REPO_ROOT may point elsewhere in tests).
469
- const scriptRepoRoot26c = path.resolve(SCRIPTS_DIR, '..', '..');
470
- const reconcilerMod26c = await import(
471
- path.join(scriptRepoRoot26c, 'cleargate-cli', 'dist', 'lib', 'lifecycle-reconcile.js')
472
- ).catch(() => null);
473
-
474
- if (!reconcilerMod26c || typeof reconcilerMod26c.walkActiveParents !== 'function') {
475
- process.stdout.write('Step 2.6c skipped: walkActiveParents not in built CLI — rebuild cleargate-cli/.\n');
476
- } else {
477
- // Delivery paths come from REPO_ROOT (may be fixture tmpdir in tests)
478
- const deliveryRoot26c = path.join(REPO_ROOT, '.cleargate', 'delivery');
479
- const archiveRoot26c = path.join(deliveryRoot26c, 'archive');
480
- const results26c = await reconcilerMod26c.walkActiveParents({
481
- deliveryRoot: deliveryRoot26c,
482
- archiveRoot: archiveRoot26c,
483
- });
484
- const flips26c = results26c.filter((r) => r.verdict === 'auto-flip');
485
- const halts26c = results26c.filter(
486
- (r) => r.verdict === 'halt-partial' || r.verdict === 'halt-zero-children',
487
- );
488
-
489
- for (const f of flips26c) {
490
- setFrontmatterStatusAtomic(f.parent_path, 'Completed');
491
- process.stdout.write(
492
- `Step 2.6c: ${f.parent_id} status ${f.current_status} → Completed` +
493
- ` (${f.terminal_children.length}/${f.terminal_children.length} children Completed:` +
494
- ` ${f.terminal_children.join(', ')})\n`,
495
- );
496
- }
497
-
498
- if (halts26c.length > 0) {
499
- process.stderr.write(`Step 2.6c HALT: ${halts26c.length} parent(s) require manual ack:\n`);
500
- for (const h of halts26c) {
501
- process.stderr.write(` - [${h.verdict}] ${h.halt_reason}\n`);
502
- }
503
- process.exit(1);
504
- }
505
-
506
- process.stdout.write(`Step 2.6c passed: ${flips26c.length} parent(s) auto-flipped; no halts.\n`);
507
- }
508
- } catch (e26c) {
509
- process.stderr.write(`Step 2.6c warning: parent rollup unavailable: ${e26c.message}\n`);
510
- }
511
- } // end CLEARGATE_SKIP_PARENT_ROLLUP else block
512
-
513
- // ── Step 2.6d: Same-Sprint Story Backsync (BUG-032) ─────────────────────────
514
- // Flip all Done-state stories in the closing sprint from their current
515
- // frontmatter status to `status: Completed, approved: true`, then move
516
- // the file from pending-sync/ to archive/.
517
- //
518
- // Root cause: reconcileCrossSprintOrphans explicitly SKIPS the active sprint
519
- // (reads .active sentinel). No prior step flipped same-sprint story frontmatter.
520
- // This step fills that gap.
521
- //
522
- // Idempotence: stories already at a terminal status are silently skipped.
523
- // Runs unconditionally (no CLEARGATE_SKIP_* seam) — the function is pure FS,
524
- // no git calls, no CLI binary required. CLEARGATE_REPO_ROOT overrides delivery paths.
525
- process.stdout.write('Step 2.6d: back-syncing same-sprint story frontmatter...\n');
526
- try {
527
- const reconcilerMod26d = await import(
528
- path.join(SCRIPTS_DIR, '..', '..', 'cleargate-cli', 'dist', 'lib', 'lifecycle-reconcile.js')
529
- ).catch(() => null);
530
-
531
- if (!reconcilerMod26d || typeof reconcilerMod26d.reconcileCurrentSprintStories !== 'function') {
532
- process.stdout.write(
533
- 'Step 2.6d skipped: reconcileCurrentSprintStories not in built CLI — rebuild cleargate-cli/.\n',
534
- );
535
- } else {
536
- const deliveryRoot26d = path.join(REPO_ROOT, '.cleargate', 'delivery');
537
- const sprintRunsRoot26d = path.join(REPO_ROOT, '.cleargate', 'sprint-runs');
538
-
539
- const backsyncResult = reconcilerMod26d.reconcileCurrentSprintStories({
540
- deliveryRoot: deliveryRoot26d,
541
- sprintRunsRoot: sprintRunsRoot26d,
542
- sprintId,
543
- retroactive: false,
544
- });
545
-
546
- if (backsyncResult.flipped.length > 0) {
547
- for (const item of backsyncResult.flipped) {
548
- process.stdout.write(
549
- `Step 2.6d: ${item.id} status ${item.old_status} → Completed` +
550
- ` (state.json: Done) → archived at ${item.file_path}\n`,
551
- );
552
- }
553
- process.stdout.write(
554
- `Step 2.6d passed: ${backsyncResult.flipped.length} story/stories back-synced and archived.\n`,
555
- );
556
- } else {
557
- process.stdout.write(
558
- `Step 2.6d passed: no same-sprint story backsync needed` +
559
- ` (${backsyncResult.skipped_already_terminal} already terminal, ` +
560
- `${backsyncResult.skipped_not_done} pending non-Done).\n`,
561
- );
562
- }
563
- }
564
- } catch (e26d) {
565
- process.stderr.write(`Step 2.6d warning: same-sprint backsync unavailable: ${e26d.message}\n`);
566
- }
567
-
568
- // ── Step 2.7: Worktree-Closed Check (CR-022 M1) ──────────────────────────
569
- // Block close if any .worktrees/STORY-* path is present.
570
- // Always enforced (STORY-070-01: execution_mode retired).
571
- // Skip if git worktree list is unavailable (non-fatal — tests run against tmpdirs).
572
- // Test seams: CLEARGATE_SKIP_WORKTREE_CHECK=1 bypasses entirely;
573
- // CLEARGATE_FORCE_WORKTREE_PATHS=p1,p2 injects fake paths (no git call).
574
- process.stdout.write('Step 2.7: checking for leftover worktrees...\n');
575
- {
576
- if (process.env.CLEARGATE_SKIP_WORKTREE_CHECK === '1') {
577
- process.stdout.write('Step 2.7 skipped: CLEARGATE_SKIP_WORKTREE_CHECK=1 set (test seam).\n');
578
- } else {
579
- let leftoverWorktrees = [];
580
- let worktreeListAvailable = true;
581
-
582
- if (process.env.CLEARGATE_FORCE_WORKTREE_PATHS) {
583
- // Test seam: inject fake worktree paths without running git
584
- leftoverWorktrees = process.env.CLEARGATE_FORCE_WORKTREE_PATHS
585
- .split(',')
586
- .map((p) => p.trim())
587
- .filter(Boolean);
588
- } else {
589
- try {
590
- const output = execSync('git worktree list --porcelain', {
591
- cwd: REPO_ROOT,
592
- encoding: 'utf8',
593
- stdio: ['ignore', 'pipe', 'ignore'],
594
- });
595
- for (const line of output.split('\n')) {
596
- const trimmed = line.trim();
597
- if (!trimmed.startsWith('worktree ')) continue;
598
- const wtPath = trimmed.slice('worktree '.length);
599
- if (/[/\\]\.worktrees[/\\]STORY-/.test(wtPath)) {
600
- const m = /(\.(worktrees)[/\\]STORY-.+)$/.exec(wtPath);
601
- leftoverWorktrees.push(m ? m[1] : wtPath);
602
- }
603
- }
604
- } catch {
605
- worktreeListAvailable = false;
606
- }
607
- }
608
-
609
- // Step 2.7 always enforced (STORY-070-01: execution_mode retired).
610
-
611
- if (!worktreeListAvailable) {
612
- process.stdout.write('Step 2.7 skipped: git worktree list unavailable (non-fatal).\n');
613
- } else if (leftoverWorktrees.length === 0) {
614
- process.stdout.write('Step 2.7 passed: no leftover worktrees.\n');
615
- } else {
616
- // Always block close on leftover worktrees
617
- process.stderr.write(
618
- `close_sprint: Step 2.7 failed: leftover worktree at ${leftoverWorktrees[0]}\n` +
619
- ` ${leftoverWorktrees.length === 1 ? '' : `(plus ${leftoverWorktrees.length - 1} more)\n `}` +
620
- `Run \`git worktree remove ${leftoverWorktrees[0]}\` if abandoned, or merge the work in progress.\n` +
621
- ` All worktrees must be closed before sprint close.\n`
622
- );
623
- process.exit(1);
624
- }
625
- }
626
- }
627
-
628
- // ── Step 2.8: Sprint branch merged to main (verify-only, NO auto-merge) ──────
629
- // CR-022 §1: verify-only — script asserts merge ancestry, does NOT run the merge.
630
- // On miss: list unmerged commits + exit 1 (v2 enforcing); warn + continue (v1 advisory).
631
- // Skip when sprintId has no numeric portion (e.g. SPRINT-TEST fixture).
632
- // Test seams: CLEARGATE_SKIP_MERGE_CHECK=1 bypasses entirely;
633
- // CLEARGATE_FORCE_MERGE_STATUS=merged|unmerged injects status without git call.
634
- {
635
- if (process.env.CLEARGATE_SKIP_MERGE_CHECK === '1') {
636
- process.stdout.write('Step 2.8 skipped: CLEARGATE_SKIP_MERGE_CHECK=1 set (test seam).\n');
637
- } else {
638
- const sprintNumMatch = /^SPRINT-(\d{2,3})$/.exec(sprintId);
639
- if (!sprintNumMatch) {
640
- process.stdout.write(`Step 2.8 skipped: sprint-id "${sprintId}" has no numeric portion.\n`);
641
- } else {
642
- const sprintBranch = `refs/heads/sprint/S-${sprintNumMatch[1]}`;
643
- const mainBranch = 'refs/heads/main';
644
- process.stdout.write(`Step 2.8: verifying ${sprintBranch} merged to ${mainBranch}...\n`);
645
-
646
- // Step 2.8 always enforced (STORY-070-01: execution_mode retired).
647
- const forcedStatus = process.env.CLEARGATE_FORCE_MERGE_STATUS;
648
- let isMerged = false;
649
- let mergeCheckAvailable = true;
650
-
651
- if (forcedStatus === 'merged') {
652
- isMerged = true;
653
- } else if (forcedStatus === 'unmerged') {
654
- isMerged = false;
655
- } else {
656
- try {
657
- execSync(
658
- `git merge-base --is-ancestor ${sprintBranch} ${mainBranch}`,
659
- { stdio: 'pipe', cwd: REPO_ROOT, env: process.env }
660
- );
661
- isMerged = true;
662
- } catch (mergeErr) {
663
- const exitStatus = /** @type {any} */ (mergeErr).status;
664
- if (exitStatus === 1) {
665
- isMerged = false;
666
- } else {
667
- // exit 128: refs missing or other git failure — fail-open with warning
668
- mergeCheckAvailable = false;
669
- process.stderr.write(
670
- `Step 2.8 warning: git merge-base check unavailable (${/** @type {Error} */ (mergeErr).message}). ` +
671
- `Skipping merge verification.\n`
672
- );
673
- }
674
- }
675
- }
676
-
677
- if (!mergeCheckAvailable) {
678
- // fail-open: refs missing or git unavailable — continue to Step 3
679
- } else if (isMerged) {
680
- process.stdout.write(`Step 2.8 passed: ${sprintBranch} is merged to ${mainBranch}.\n`);
681
- } else {
682
- // Always enforced (STORY-070-01: execution_mode retired)
683
- let unmergedLog = '';
684
- if (!forcedStatus) {
685
- try {
686
- unmergedLog = execSync(
687
- `git log ${mainBranch}..${sprintBranch} --oneline`,
688
- { encoding: 'utf8', cwd: REPO_ROOT, env: process.env }
689
- );
690
- } catch { /* unmerged-log fetch failed — proceed without */ }
691
- }
692
- process.stderr.write(
693
- `Step 2.8 failed: sprint/S-${sprintNumMatch[1]} not merged to main.\n` +
694
- (unmergedLog ? ` Unmerged commits:\n${unmergedLog}` : '') +
695
- ` Resolve: merge sprint/S-${sprintNumMatch[1]} → main, then re-run close_sprint.mjs.\n`
696
- );
697
- process.exit(1);
698
- }
699
- }
700
- }
701
- }
702
-
703
- // ── Step 3: Invoke prefill_report.mjs ─────────────────────────────────────
704
- process.stdout.write('Step 3: running prefill_report.mjs...\n');
705
- try {
706
- invokeScript('prefill_report.mjs', [sprintId], {
707
- CLEARGATE_STATE_FILE: stateFile,
708
- CLEARGATE_SPRINT_DIR: sprintDir,
709
- });
710
- } catch (err) {
711
- process.stderr.write(`Error: prefill_report.mjs failed: ${err.message}\n`);
712
- process.exit(1);
713
- }
714
-
715
- // ── Step 3.5: Build curated Reporter context bundle ───────────────────────
716
- const bundlePath = path.join(sprintDir, '.reporter-context.md');
717
- const MIN_BUNDLE_BYTES = 2048;
718
- if (process.env.CLEARGATE_SKIP_BUNDLE_CHECK === '1') {
719
- process.stdout.write('Step 3.5 skipped: CLEARGATE_SKIP_BUNDLE_CHECK=1 set (test seam).\n');
720
- } else {
721
- process.stdout.write('Step 3.5: building Reporter context bundle...\n');
722
- try {
723
- invokeScript('prep_reporter_context.mjs', [sprintId], {
724
- CLEARGATE_STATE_FILE: stateFile,
725
- CLEARGATE_SPRINT_DIR: sprintDir,
726
- });
727
- if (!fs.existsSync(bundlePath)) {
728
- throw new Error(`bundle not written at ${bundlePath}`);
729
- }
730
- const bundleSize = fs.statSync(bundlePath).size;
731
- if (bundleSize < MIN_BUNDLE_BYTES) {
732
- throw new Error(`bundle too small (${bundleSize}B < ${MIN_BUNDLE_BYTES}B): ${bundlePath}`);
733
- }
734
- process.stdout.write(`Step 3.5 passed: ${bundlePath} ready (${Math.round(bundleSize / 1024)}KB).\n`);
735
- } catch (err) {
736
- // Always enforced (STORY-070-01: execution_mode retired)
737
- const msg = /** @type {Error} */ (err).message;
738
- process.stderr.write(
739
- `close_sprint: Step 3.5 FAILED: ${msg}\n` +
740
- ` Cannot dispatch Reporter without bundle. Fix prep_reporter_context.mjs.\n` +
741
- ` Diagnostic: node .cleargate/scripts/prep_reporter_context.mjs ${sprintId}\n`
742
- );
743
- process.exit(1);
744
- }
745
- }
746
-
747
- // ── Step 4: Orchestrator spawns Reporter separately ───────────────────────
748
- // This script only validates preconditions; it does NOT fork the Reporter agent.
749
- const reportFile = reportFilename(sprintDir, sprintId);
750
- const reportBasename = path.basename(reportFile);
751
- process.stdout.write(
752
- 'Step 4: preconditions satisfied — orchestrator should now spawn the Reporter agent.\n' +
753
- ` The Reporter writes ${reportBasename} using the sprint_report.md template.\n` +
754
- ` Expected output: ${reportFile}\n`
755
- );
756
-
757
- // ── Step 4.5 (STORY-014-10): --report-body-stdin fallback ────────────────
758
- // Orchestrator pipes the Reporter's markdown body here when the Reporter's
759
- // Write tool is blocked. Refuses empty stdin + pre-existing report file.
760
- if (reportBodyStdin) {
761
- if (fs.existsSync(reportFile)) {
762
- process.stderr.write(
763
- `Error: ${reportBasename} already exists at ${reportFile}\n` +
764
- 'Delete it or skip --report-body-stdin mode to use the primary Reporter-write path.\n'
765
- );
766
- process.exit(1);
767
- }
768
- let body;
769
- try {
770
- body = fs.readFileSync(0, 'utf8');
771
- } catch (err) {
772
- process.stderr.write(`Error: failed to read stdin: ${/** @type {Error} */ (err).message}\n`);
773
- process.exit(1);
774
- }
775
- if (!body || body.trim().length === 0) {
776
- process.stderr.write('Error: empty report body — refusing to write.\n');
777
- process.exit(1);
778
- }
779
- atomicWriteString(reportFile, body);
780
- process.stdout.write(
781
- `Step 4.5 (stdin mode): ${reportBasename} written (${body.length} bytes) at ${reportFile}\n`
782
- );
783
- // Fall through to Step 5 + 6 + 7 unconditionally — stdin mode implies ack.
784
- } else if (!assumeAck) {
785
- // Apply read-fallback for legacy sprints (e.g. SPRINT-15 with plain REPORT.md)
786
- const reportFileForCheck = reportFilename(sprintDir, sprintId, { forRead: true });
787
- if (!fs.existsSync(reportFileForCheck)) {
788
- process.stdout.write(
789
- `\nWaiting for Reporter to produce ${reportBasename}...\n` +
790
- 'After Reporter succeeds, re-run with --assume-ack to complete the close.\n'
791
- );
792
- process.exit(0);
793
- }
794
- // In non-assume-ack mode with existing report, prompt user
795
- process.stdout.write(
796
- `\n${reportBasename} found at ${reportFileForCheck}\n` +
797
- 'Review the report, then confirm close by re-running with --assume-ack\n'
798
- );
799
- process.exit(0);
800
- }
801
-
802
- // ── Step 5: Flip sprint_status to "Completed" ────────────────────────────
803
- process.stdout.write('Step 5: flipping sprint_status to "Completed"...\n');
804
- const now = new Date().toISOString();
805
- state.sprint_status = 'Completed';
806
- state.last_action = `close_sprint: sprint ${sprintId} completed`;
807
- state.updated_at = now;
808
- atomicWrite(stateFile, state);
809
- process.stdout.write(`sprint_status flipped to "Completed" at ${now}\n`);
810
-
811
- // ── Step 6: Invoke suggest_improvements.mjs unconditionally ───────────────
812
- process.stdout.write('Step 6: running suggest_improvements.mjs...\n');
813
- try {
814
- invokeScript('suggest_improvements.mjs', [sprintId], {
815
- CLEARGATE_STATE_FILE: stateFile,
816
- CLEARGATE_SPRINT_DIR: sprintDir,
817
- });
818
- } catch (err) {
819
- // suggest_improvements failure is non-fatal — log but do not abort
820
- process.stderr.write(`Warning: suggest_improvements.mjs failed: ${/** @type {Error} */ (err).message}\n`);
821
- process.stderr.write('Sprint is still marked Completed; improvement suggestions may be incomplete.\n');
822
- }
823
-
824
- // ── Step 6.5: Run sprint_trends.mjs (stub — full impl deferred to CR-027) ──
825
- if (process.env.CLEARGATE_SKIP_SPRINT_TRENDS !== '1') {
826
- process.stdout.write('Step 6.5: running sprint_trends.mjs (stub)...\n');
827
- try {
828
- invokeScript('sprint_trends.mjs', [sprintId], {
829
- CLEARGATE_STATE_FILE: stateFile,
830
- CLEARGATE_SPRINT_DIR: sprintDir,
831
- });
832
- } catch (err) {
833
- // Non-fatal — sprint stays Completed; trends are advisory only.
834
- process.stderr.write(`Step 6.5 warning: sprint_trends.mjs failed: ${/** @type {Error} */ (err).message}\n`);
835
- }
836
- }
837
-
838
- // ── Step 6.6: Skill-candidate detection (folds into suggest_improvements.mjs) ──
839
- if (process.env.CLEARGATE_SKIP_SKILL_CANDIDATES !== '1') {
840
- process.stdout.write('Step 6.6: scanning for skill candidates...\n');
841
- try {
842
- invokeScript('suggest_improvements.mjs', [sprintId, '--skill-candidates'], {
843
- CLEARGATE_STATE_FILE: stateFile,
844
- CLEARGATE_SPRINT_DIR: sprintDir,
845
- });
846
- } catch (err) {
847
- process.stderr.write(`Step 6.6 warning: skill-candidate scan failed: ${/** @type {Error} */ (err).message}\n`);
848
- }
849
- }
850
-
851
- // ── Step 6.7: FLASHCARD cleanup pass (folds into suggest_improvements.mjs) ──
852
- if (process.env.CLEARGATE_SKIP_FLASHCARD_CLEANUP !== '1') {
853
- process.stdout.write('Step 6.7: scanning FLASHCARD.md for cleanup candidates...\n');
854
- try {
855
- invokeScript('suggest_improvements.mjs', [sprintId, '--flashcard-cleanup'], {
856
- CLEARGATE_STATE_FILE: stateFile,
857
- CLEARGATE_SPRINT_DIR: sprintDir,
858
- });
859
- } catch (err) {
860
- process.stderr.write(`Step 6.7 warning: FLASHCARD cleanup scan failed: ${/** @type {Error} */ (err).message}\n`);
861
- }
862
- }
863
-
864
- // ── Step 7: Auto-push per-artifact status updates to MCP ─────────────────
865
- // Runs after Gate 4 ack succeeds. Non-fatal: sprint stays Completed on failure.
866
- process.stdout.write('Step 7: pushing per-artifact status updates to MCP...\n');
867
- try {
868
- const cliBin = path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'cli.js');
869
- if (fs.existsSync(cliBin)) {
870
- // cleargate sync work-items takes ZERO positional args (verified cli.ts:592-598).
871
- // CR-021 §3.2.3 spec shows a sprint-id arg — that is spec drift; drop it.
872
- execSync(`node ${JSON.stringify(cliBin)} sync work-items`, {
873
- stdio: 'inherit',
874
- env: process.env,
875
- timeout: 30000,
876
- });
877
- process.stdout.write('Step 7 passed: work-item statuses synced.\n');
878
- } else {
879
- process.stdout.write('Step 7 skipped: CLI binary not found (non-fatal).\n');
880
- }
881
- } catch (err) {
882
- // Non-fatal — sprint stays Completed; sync can be retried manually
883
- process.stderr.write(`Step 7 warning: sync work-items failed: ${/** @type {Error} */ (err).message}\n`);
884
- process.stderr.write('Run `cleargate sync work-items` manually to retry.\n');
885
- }
886
-
887
- // ── Step 7.4: MCP push of sprint plan + report ───────────────────────────────
888
- // CR-064: mcp push sprint plan + report
889
- // Runs after Step 7 (work-item sync) and BEFORE Step 7.5 (CR-063 wiki ingest).
890
- // Per SDR-locked ordering: MCP push first, wiki ingest second (CR-064 §0.5 Q4 accepted).
891
- // Non-fatal on failure: sprint stays Completed. MCP push can be retried manually.
892
- process.stdout.write('Step 7.4: pushing sprint plan + report to MCP...\n');
893
- try {
894
- const cliBin74 = path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'cli.js');
895
- if (fs.existsSync(cliBin74)) {
896
- // Resolve sprint plan path: prefer archive/, fall back to pending-sync/
897
- const deliveryBase = path.join(REPO_ROOT, '.cleargate', 'delivery');
898
- const archiveDir = path.join(deliveryBase, 'archive');
899
- const pendingDir = path.join(deliveryBase, 'pending-sync');
900
- let planPath = null;
901
- for (const dir of [archiveDir, pendingDir]) {
902
- if (!fs.existsSync(dir)) continue;
903
- const match = fs.readdirSync(dir).find(
904
- (f) => f.startsWith(sprintId + '_') && f.endsWith('.md'),
905
- );
906
- if (match) { planPath = path.join(dir, match); break; }
907
- }
908
- if (planPath && fs.existsSync(planPath)) {
909
- try {
910
- execSync(`node ${JSON.stringify(cliBin74)} push ${JSON.stringify(planPath)}`, {
911
- stdio: 'inherit',
912
- env: process.env,
913
- timeout: 60000,
914
- });
915
- process.stdout.write('Step 7.4a passed: sprint plan pushed to MCP.\n');
916
- } catch (err74a) {
917
- process.stderr.write(`Step 7.4a warning: sprint plan push failed: ${/** @type {Error} */ (err74a).message}\n`);
918
- }
919
- } else {
920
- process.stdout.write('Step 7.4a skipped: sprint plan file not found.\n');
921
- }
922
- // Resolve sprint report path: reuses reportFile resolved via legacy-fallback in Step 4
923
- if (reportFile && fs.existsSync(reportFile)) {
924
- try {
925
- execSync(`node ${JSON.stringify(cliBin74)} push ${JSON.stringify(reportFile)}`, {
926
- stdio: 'inherit',
927
- env: process.env,
928
- timeout: 60000,
929
- });
930
- process.stdout.write('Step 7.4b passed: sprint report pushed to MCP.\n');
931
- } catch (err74b) {
932
- process.stderr.write(`Step 7.4b warning: sprint report push failed: ${/** @type {Error} */ (err74b).message}\n`);
933
- }
934
- } else {
935
- process.stdout.write('Step 7.4b skipped: sprint report file not found (legacy sprint or report not yet written).\n');
936
- }
937
- } else {
938
- process.stdout.write('Step 7.4 skipped: CLI binary not found (non-fatal).\n');
939
- }
940
- } catch (err) {
941
- // Non-fatal — sprint stays Completed; MCP push can be retried manually
942
- process.stderr.write(`Step 7.4 warning: MCP push failed: ${/** @type {Error} */ (err).message}\n`);
943
- process.stderr.write('Run `cleargate push <plan-or-report-path>` manually to retry.\n');
944
- }
945
-
946
- // ── Step 7.5: wiki ingest sprint report ──────────────────────────────────
947
- // CR-063: wiki ingest sprint report
948
- // Runs after Step 7 (MCP sync). Non-fatal: sprint stays Completed on failure.
949
- // CR-064 (Wave 3) inserts its MCP-push step at Step 7.4, immediately before this anchor.
950
- process.stdout.write('Step 7.5: ingesting sprint report into wiki...\n');
951
- try {
952
- const cliBin = path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'cli.js');
953
- if (fs.existsSync(cliBin)) {
954
- // Resolve report path using the same legacy-fallback rule as Step 4
955
- // (SPRINT-NN_REPORT.md preferred; fall back to REPORT.md for legacy sprints)
956
- const reportPath = reportFile; // reportFile is already resolved via legacy-fallback in Step 4
957
- if (fs.existsSync(reportPath)) {
958
- execSync(`node ${JSON.stringify(cliBin)} wiki ingest ${JSON.stringify(reportPath)}`, {
959
- stdio: 'inherit',
960
- env: process.env,
961
- timeout: 60000,
962
- });
963
- process.stdout.write('Step 7.5 passed: sprint report ingested into wiki.\n');
964
- } else {
965
- process.stdout.write('Step 7.5 skipped: report file not found (legacy sprint or report not yet written).\n');
966
- }
967
- } else {
968
- process.stdout.write('Step 7.5 skipped: CLI binary not found (non-fatal).\n');
969
- }
970
- } catch (err) {
971
- // Non-fatal — sprint stays Completed; ingest can be retried manually
972
- process.stderr.write(`Step 7.5 warning: wiki ingest sprint report failed: ${/** @type {Error} */ (err).message}\n`);
973
- process.stderr.write('Run `cleargate wiki ingest <report-path>` manually to retry.\n');
974
- }
975
-
976
- // ── Step 8: Verbose post-close handoff list ───────────────────────────────
977
- // Prints 6 explicit next-step items to stdout (CR-022 §3 M4).
978
- {
979
- const sprintNumMatch = /^SPRINT-(\d{2,3})$/.exec(sprintId);
980
- const nextSprintNum = sprintNumMatch
981
- ? String(parseInt(sprintNumMatch[1], 10) + 1).padStart(sprintNumMatch[1].length, '0')
982
- : null;
983
- const nextSprintId = nextSprintNum ? `SPRINT-${nextSprintNum}` : '<next-sprint-id>';
984
- const reportBasename = path.basename(reportFile);
985
- const suggestionsPath = path.join(sprintDir, 'improvement-suggestions.md');
986
-
987
- process.stdout.write(`\n${sprintId} closed. Next steps:\n`);
988
- process.stdout.write(` 1. Review ${reportBasename}\n`);
989
- process.stdout.write(
990
- ` 2. Review improvement-suggestions.md (sections: Suggestions / Skill Candidates / FLASHCARD Cleanup)\n`,
991
- );
992
- process.stdout.write(
993
- ` 3. Approve or reject Skill Candidates → run /improve or cleargate skill create <name>\n`,
994
- );
995
- process.stdout.write(
996
- ` 4. Approve or reject FLASHCARD cleanup entries → run /improve or cleargate flashcard prune\n`,
997
- );
998
- process.stdout.write(
999
- ` 5. Push approved status changes to MCP if Step 7 warned (\`cleargate sync work-items\`)\n`,
1000
- );
1001
- process.stdout.write(
1002
- ` 6. Initialize next sprint: \`cleargate sprint init ${nextSprintId} --stories <ids>\`\n`,
1003
- );
1004
-
1005
- // Surface artifact paths for convenience
1006
- process.stdout.write(`\nArtifacts:\n`);
1007
- process.stdout.write(` report: ${reportFile}\n`);
1008
- process.stdout.write(` improvement-suggestions: ${suggestionsPath}\n`);
1009
- }
1010
- }
1011
-
1012
- await main();