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
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dedupe_frontmatter.mjs — BUG-025
4
+ *
5
+ * One-shot corpus dedupe pass: scan every .md file under
6
+ * .cleargate/delivery/pending-sync/ and .cleargate/delivery/archive/.
7
+ * For any file whose YAML frontmatter contains duplicate top-level keys,
8
+ * keep the LAST occurrence of each key (closest to the body — this is what
9
+ * the stamp hook writes most recently) and rewrite the file.
10
+ *
11
+ * Idempotent: re-running produces zero diff when no duplicates remain.
12
+ *
13
+ * Usage:
14
+ * node .cleargate/scripts/dedupe_frontmatter.mjs [--dry-run] [<dir>]
15
+ *
16
+ * --dry-run Print which files would be rewritten without writing.
17
+ * <dir> Walk only this directory instead of the canonical corpus dirs.
18
+ * Used by integration tests targeting a tmpdir.
19
+ */
20
+
21
+ import * as fs from 'node:fs';
22
+ import * as path from 'node:path';
23
+ import * as url from 'node:url';
24
+
25
+ const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
26
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
27
+
28
+ const args = process.argv.slice(2);
29
+ const DRY_RUN = args.includes('--dry-run');
30
+ const DIR_ARG = args.find((a) => !a.startsWith('--'));
31
+
32
+ const PENDING_SYNC = path.join(REPO_ROOT, '.cleargate', 'delivery', 'pending-sync');
33
+ const ARCHIVE = path.join(REPO_ROOT, '.cleargate', 'delivery', 'archive');
34
+
35
+ /**
36
+ * Collect all .md files in a flat directory (non-recursive — delivery dirs are flat).
37
+ */
38
+ function collectMd(dir) {
39
+ let entries;
40
+ try {
41
+ entries = fs.readdirSync(dir, { withFileTypes: true });
42
+ } catch {
43
+ return [];
44
+ }
45
+ return entries
46
+ .filter((e) => e.isFile() && e.name.endsWith('.md'))
47
+ .map((e) => path.join(dir, e.name));
48
+ }
49
+
50
+ /**
51
+ * Parse a raw markdown string into:
52
+ * fmLines — the lines between the opening and closing `---` delimiters
53
+ * closeIdx — index of the closing `---` line (in the full `lines` array)
54
+ * lines — all lines of the file
55
+ *
56
+ * Returns null if the file has no valid frontmatter.
57
+ */
58
+ function parseFmLines(raw) {
59
+ const lines = raw.split('\n');
60
+ if (lines[0] !== '---') return null;
61
+ let closeIdx = -1;
62
+ for (let i = 1; i < lines.length; i++) {
63
+ if (lines[i] === '---') {
64
+ closeIdx = i;
65
+ break;
66
+ }
67
+ }
68
+ if (closeIdx === -1) return null;
69
+ return { lines, closeIdx, fmLines: lines.slice(1, closeIdx) };
70
+ }
71
+
72
+ /**
73
+ * Given the frontmatter lines, detect duplicate top-level keys.
74
+ * Returns a Map from key → array of line-indices (within fmLines) where it appears.
75
+ * Only entries with ≥2 occurrences indicate duplicates.
76
+ *
77
+ * A "top-level key" is a line that starts with a non-space character followed by
78
+ * `:<space>` or `:<end-of-line>`. Multi-line values (YAML scalars, blocks) that
79
+ * contain `:` on continuation lines are NOT top-level keys (they start with space/tab).
80
+ */
81
+ function findDuplicateKeys(fmLines) {
82
+ /** @type {Map<string, number[]>} */
83
+ const keyMap = new Map();
84
+ for (let i = 0; i < fmLines.length; i++) {
85
+ const line = fmLines[i];
86
+ // Top-level key: starts at column 0, has `:` after a word character sequence
87
+ const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*):/);
88
+ if (m) {
89
+ const key = m[1];
90
+ if (!keyMap.has(key)) {
91
+ keyMap.set(key, []);
92
+ }
93
+ keyMap.get(key).push(i);
94
+ }
95
+ }
96
+ // Return only keys with duplicates
97
+ /** @type {Map<string, number[]>} */
98
+ const dupes = new Map();
99
+ for (const [k, indices] of keyMap) {
100
+ if (indices.length > 1) {
101
+ dupes.set(k, indices);
102
+ }
103
+ }
104
+ return dupes;
105
+ }
106
+
107
+ /**
108
+ * Deduplicate frontmatter lines: for each duplicate key, keep the LAST occurrence
109
+ * and discard all earlier ones (including their potential multi-line values).
110
+ *
111
+ * Multi-line value detection: lines that follow a key line and start with
112
+ * whitespace (` ` or `\t`) belong to the preceding key's value.
113
+ *
114
+ * Returns the deduplicated fmLines array (may be the same reference if no changes).
115
+ */
116
+ function dedupeLines(fmLines, dupes) {
117
+ if (dupes.size === 0) return fmLines;
118
+
119
+ // Build a set of fmLine indices to DROP (all but the last occurrence of each dup key,
120
+ // including their continuation lines).
121
+ /** @type {Set<number>} */
122
+ const dropSet = new Set();
123
+
124
+ for (const [, indices] of dupes) {
125
+ // Keep last occurrence; drop all earlier ones (+ their continuations)
126
+ const toRemove = indices.slice(0, -1); // all but last
127
+ for (const startIdx of toRemove) {
128
+ dropSet.add(startIdx);
129
+ // Mark continuation lines (indent-starting lines following a key line)
130
+ let j = startIdx + 1;
131
+ while (j < fmLines.length && /^[ \t]/.test(fmLines[j])) {
132
+ dropSet.add(j);
133
+ j++;
134
+ }
135
+ }
136
+ }
137
+
138
+ return fmLines.filter((_, i) => !dropSet.has(i));
139
+ }
140
+
141
+ /**
142
+ * Atomic write: write to a .tmp file then rename over the target.
143
+ */
144
+ function writeAtomic(filePath, content) {
145
+ const tmpPath = `${filePath}.tmp.${Date.now()}`;
146
+ fs.writeFileSync(tmpPath, content, 'utf8');
147
+ fs.renameSync(tmpPath, filePath);
148
+ }
149
+
150
+ // ── Main ──────────────────────────────────────────────────────────────────────
151
+
152
+ const files = DIR_ARG
153
+ ? collectMd(path.resolve(DIR_ARG))
154
+ : [...collectMd(PENDING_SYNC), ...collectMd(ARCHIVE)];
155
+
156
+ let rewritten = 0;
157
+ let skipped = 0;
158
+ let errors = 0;
159
+
160
+ for (const filePath of files) {
161
+ let raw;
162
+ try {
163
+ raw = fs.readFileSync(filePath, 'utf8');
164
+ } catch (e) {
165
+ console.error(`error reading ${filePath}: ${e.message}`);
166
+ errors++;
167
+ continue;
168
+ }
169
+
170
+ const parsed = parseFmLines(raw);
171
+ if (!parsed) {
172
+ skipped++;
173
+ continue;
174
+ }
175
+
176
+ const { lines, closeIdx, fmLines } = parsed;
177
+ const dupes = findDuplicateKeys(fmLines);
178
+
179
+ if (dupes.size === 0) {
180
+ skipped++;
181
+ continue;
182
+ }
183
+
184
+ const relPath = path.relative(REPO_ROOT, filePath);
185
+ const dupeSummary = Array.from(dupes.keys()).join(', ');
186
+
187
+ if (DRY_RUN) {
188
+ console.log(`would-rewrite: ${relPath} (duplicate keys: ${dupeSummary})`);
189
+ rewritten++;
190
+ continue;
191
+ }
192
+
193
+ // Build the new file content: deduplicated frontmatter + rest unchanged
194
+ const cleanedFmLines = dedupeLines(fmLines, dupes);
195
+ const newLines = [
196
+ lines[0], // opening ---
197
+ ...cleanedFmLines,
198
+ lines[closeIdx], // closing ---
199
+ ...lines.slice(closeIdx + 1),
200
+ ];
201
+ const newContent = newLines.join('\n');
202
+
203
+ // Verify the result is actually different (guard against no-op edge cases)
204
+ if (newContent === raw) {
205
+ skipped++;
206
+ continue;
207
+ }
208
+
209
+ writeAtomic(filePath, newContent);
210
+ console.log(`rewritten: ${relPath} (removed duplicate keys: ${dupeSummary})`);
211
+ rewritten++;
212
+ }
213
+
214
+ console.log(
215
+ `\nDedupe complete: ${rewritten} ${DRY_RUN ? 'would-rewrite' : 'rewritten'}, ${skipped} skipped, ${errors} errors.`,
216
+ );
217
+ if (errors > 0) {
218
+ process.exit(1);
219
+ }
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "schema_version": 1,
3
3
  "qa": {
4
- "typecheck": "npm run typecheck",
4
+ "typecheck": "cd cleargate-cli && npm run typecheck",
5
5
  "debug_patterns": ["console.log", "console.debug", "debugger"],
6
6
  "todo_patterns": ["TODO", "FIXME", "XXX"],
7
- "test": "npm test"
7
+ "test": "cd cleargate-cli && npm test"
8
8
  },
9
9
  "arch": {
10
- "typecheck": "npm run typecheck",
10
+ "typecheck": "cd cleargate-cli && npm run typecheck",
11
11
  "new_deps_check": true,
12
12
  "stray_env_files": [".env", ".env.local", ".env.production"],
13
13
  "file_count_report": true
@@ -2,7 +2,12 @@
2
2
  /**
3
3
  * init_sprint.mjs — Initialize a sprint state.json
4
4
  *
5
- * Usage: node init_sprint.mjs <sprint-id> --stories ID1,ID2,... [--force]
5
+ * Usage: node init_sprint.mjs <sprint-id> --stories ID1,ID2,... [--force] [--preserve-bounces]
6
+ *
7
+ * --preserve-bounces requires --force; reads existing state.json and carries
8
+ * forward qa_bounces / arch_bounces / state / lane / worktree per matching
9
+ * story-id. Items not in the new --stories list are dropped. Useful when
10
+ * mid-sprint dogfood / rerun must not lose kickback history.
6
11
  *
7
12
  * Creates .cleargate/sprint-runs/<sprint-id>/state.json with initial state
8
13
  * "Ready to Bounce" for each story. Refuses if state.json already exists
@@ -117,6 +122,7 @@ function main() {
117
122
  }
118
123
 
119
124
  const force = args.includes('--force');
125
+ const preserveBounces = args.includes('--preserve-bounces');
120
126
 
121
127
  const sprintDir = path.join(REPO_ROOT, '.cleargate', 'sprint-runs', sprintId);
122
128
  const stateFile = path.join(sprintDir, 'state.json');
@@ -128,6 +134,20 @@ function main() {
128
134
  process.exit(1);
129
135
  }
130
136
 
137
+ // --- CR-049-followup: Preserve bounce counters when --force re-inits an in-flight sprint ---
138
+ // Default --force overwrites everything (initial design). With --preserve-bounces,
139
+ // qa_bounces / arch_bounces / state are read from the existing state.json and merged
140
+ // back per-story. Only Ready-to-Bounce default fields get reset.
141
+ let preserved = {};
142
+ if (force && preserveBounces && fs.existsSync(stateFile)) {
143
+ try {
144
+ const existing = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
145
+ preserved = existing.stories || {};
146
+ } catch (err) {
147
+ process.stderr.write(`WARN: --preserve-bounces could not read existing state.json: ${err.message}\n`);
148
+ }
149
+ }
150
+
131
151
  // --- Read execution_mode from sprint frontmatter ---
132
152
  const sprintFilePath = findSprintFile(REPO_ROOT, sprintId);
133
153
  const executionMode = sprintFilePath ? readExecutionMode(sprintFilePath) : 'v1';
@@ -159,17 +179,18 @@ function main() {
159
179
  const now = new Date().toISOString();
160
180
  const stories = {};
161
181
  for (const id of storyIds) {
182
+ const carry = preserved[id] || {};
162
183
  stories[id] = {
163
- state: 'Ready to Bounce',
164
- qa_bounces: 0,
165
- arch_bounces: 0,
166
- worktree: null,
184
+ state: carry.state ?? 'Ready to Bounce',
185
+ qa_bounces: carry.qa_bounces ?? 0,
186
+ arch_bounces: carry.arch_bounces ?? 0,
187
+ worktree: carry.worktree ?? null,
167
188
  updated_at: now,
168
- notes: '',
169
- lane: 'standard',
170
- lane_assigned_by: 'migration-default',
171
- lane_demoted_at: null,
172
- lane_demotion_reason: null,
189
+ notes: carry.notes ?? '',
190
+ lane: carry.lane ?? 'standard',
191
+ lane_assigned_by: carry.lane_assigned_by ?? 'migration-default',
192
+ lane_demoted_at: carry.lane_demoted_at ?? null,
193
+ lane_demotion_reason: carry.lane_demotion_reason ?? null,
173
194
  };
174
195
  }
175
196
 
@@ -189,6 +210,61 @@ function main() {
189
210
  fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2) + '\n', 'utf8');
190
211
  fs.renameSync(tmpFile, stateFile);
191
212
 
213
+ // --- CR-045: Write sprint-context.md from template ---
214
+ // Reads .cleargate/templates/sprint_context.md, substitutes frontmatter fields,
215
+ // optionally splices the sprint goal from the sprint plan §0, and writes atomically.
216
+ // Skip if file already exists and --force was not passed (idempotency-safe re-init).
217
+ const ctxTemplate = path.join(REPO_ROOT, '.cleargate', 'templates', 'sprint_context.md');
218
+ const ctxOut = path.join(sprintDir, 'sprint-context.md');
219
+
220
+ if (!fs.existsSync(ctxOut) || force) {
221
+ let ctxContent;
222
+ try {
223
+ ctxContent = fs.readFileSync(ctxTemplate, 'utf8');
224
+ } catch {
225
+ // Template absent — log warning and continue (non-fatal)
226
+ process.stderr.write(
227
+ `WARN: sprint-context.md template not found at ${ctxTemplate}; skipping sprint-context.md write.\n`
228
+ );
229
+ ctxContent = null;
230
+ }
231
+
232
+ if (ctxContent !== null) {
233
+ // Substitute frontmatter placeholders
234
+ ctxContent = ctxContent.replace(/sprint_id:\s*["']?S-NN["']?/, `sprint_id: "${sprintId}"`);
235
+ ctxContent = ctxContent.replace(/created_at:\s*["']?YYYY-MM-DDTHH:MM:SSZ["']?/, `created_at: "${now}"`);
236
+ ctxContent = ctxContent.replace(/last_updated:\s*["']?YYYY-MM-DDTHH:MM:SSZ["']?/, `last_updated: "${now}"`);
237
+
238
+ // Optionally extract sprint goal from sprint plan §0.
239
+ // Regex matches `- **Sprint Goal:** <text>` within first 200 lines (after H1).
240
+ // Falls back to placeholder if absent — non-fatal.
241
+ if (sprintFilePath) {
242
+ try {
243
+ const planContent = fs.readFileSync(sprintFilePath, 'utf8');
244
+ const planLines = planContent.split('\n').slice(0, 200);
245
+ const goalLine = planLines.find((l) => /^- \*\*Sprint Goal:\*\* (.+)$/.test(l.trim()));
246
+ if (goalLine) {
247
+ const goalMatch = goalLine.trim().match(/^- \*\*Sprint Goal:\*\* (.+)$/);
248
+ if (goalMatch) {
249
+ const goalText = goalMatch[1].trim();
250
+ ctxContent = ctxContent.replace(
251
+ '_(populated by orchestrator from sprint plan §0 at kickoff)_',
252
+ goalText
253
+ );
254
+ }
255
+ }
256
+ } catch {
257
+ // Goal extraction failed — leave placeholder; non-fatal
258
+ }
259
+ }
260
+
261
+ // Write atomically via tmpFile + renameSync (mirrors state.json pattern)
262
+ const ctxTmp = `${ctxOut}.tmp.${process.pid}`;
263
+ fs.writeFileSync(ctxTmp, ctxContent, 'utf8');
264
+ fs.renameSync(ctxTmp, ctxOut);
265
+ }
266
+ }
267
+
192
268
  process.stdout.write(`Initialized state.json for sprint ${sprintId} with ${storyIds.length} stories\n`);
193
269
  }
194
270
 
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * report-filename.mjs — Shared helper for computing the sprint report filename.
4
+ *
5
+ * Named export only — no default.
6
+ *
7
+ * Dependencies: node:path, node:fs only. No third-party deps.
8
+ *
9
+ * Design notes:
10
+ * - Helper is pure given its arguments + a filesystem read (when opts.forRead=true).
11
+ * - Never throws — callers decide whether to fs.readFileSync and handle ENOENT.
12
+ * - Never reads env vars. Sprint dir resolution stays in callers.
13
+ * (Each consumer owns CLEARGATE_SPRINT_DIR resolution.)
14
+ */
15
+
16
+ import path from 'node:path';
17
+ import fs from 'node:fs';
18
+
19
+ /**
20
+ * Compute the report filename for a given sprint directory + sprint ID.
21
+ *
22
+ * New naming (SPRINT-18+): SPRINT-<#>_REPORT.md where <#> is the numeric
23
+ * portion of the sprint-id (e.g. "18" for "SPRINT-18").
24
+ * No-numeric-portion ids (e.g. "SPRINT-TEST") → plain REPORT.md.
25
+ *
26
+ * Backwards-compat read-fallback: when opts.forRead === true AND the new-name
27
+ * file is absent BUT legacy REPORT.md exists, return the legacy path.
28
+ * Covers SPRINT-01..17 archives written before STORY-025-03's naming change.
29
+ * MUST NOT rename or rewrite those pre-existing files.
30
+ *
31
+ * @param {string} sprintDirPath absolute path to the sprint directory
32
+ * @param {string} sprintId e.g. "SPRINT-18" or "SPRINT-TEST"
33
+ * @param {{ forRead?: boolean }} [opts]
34
+ * @returns {string} absolute path to the report file
35
+ */
36
+ export function reportFilename(sprintDirPath, sprintId, opts) {
37
+ const numMatch = sprintId.match(/^SPRINT-(\d+)$/);
38
+ if (!numMatch) {
39
+ // No numeric portion — use plain REPORT.md (e.g. SPRINT-TEST)
40
+ return path.join(sprintDirPath, 'REPORT.md');
41
+ }
42
+ const sprintNumber = numMatch[1];
43
+ const newName = path.join(sprintDirPath, `SPRINT-${sprintNumber}_REPORT.md`);
44
+
45
+ // Read-fallback: if the new-name file doesn't exist but legacy REPORT.md does, use legacy.
46
+ if (opts?.forRead) {
47
+ const legacyName = path.join(sprintDirPath, 'REPORT.md');
48
+ if (!fs.existsSync(newName) && fs.existsSync(legacyName)) {
49
+ return legacyName;
50
+ }
51
+ }
52
+
53
+ return newName;
54
+ }