cleargate 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +190 -0
- package/README.md +11 -0
- package/dist/MANIFEST.json +259 -28
- package/dist/{chunk-OM4FAEA7.js → chunk-Q3BTSXCK.js} +69 -3
- package/dist/chunk-Q3BTSXCK.js.map +1 -0
- package/dist/cli.cjs +2621 -548
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2548 -560
- package/dist/cli.js.map +1 -1
- package/dist/lib/ledger.cjs +120 -0
- package/dist/lib/ledger.cjs.map +1 -0
- package/dist/lib/ledger.d.cts +64 -0
- package/dist/lib/ledger.d.ts +64 -0
- package/dist/lib/ledger.js +96 -0
- package/dist/lib/ledger.js.map +1 -0
- package/dist/templates/cleargate-planning/.claude/agents/architect.md +10 -8
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +108 -0
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +49 -3
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +6 -1
- package/dist/templates/cleargate-planning/.claude/agents/developer.md +29 -2
- package/dist/templates/cleargate-planning/.claude/agents/qa.md +50 -1
- package/dist/templates/cleargate-planning/.claude/agents/reporter.md +31 -9
- package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +148 -0
- package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +6 -0
- package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +314 -96
- package/dist/templates/cleargate-planning/.claude/settings.json +4 -0
- package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +473 -0
- package/dist/templates/cleargate-planning/.cleargate/config.example.yml +19 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +542 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +102 -428
- package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +31 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +71 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +24 -2
- package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +387 -27
- package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +219 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +54 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +378 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +888 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +71 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +355 -13
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +482 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +125 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +33 -10
- package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +41 -10
- package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +48 -14
- package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +46 -12
- package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +51 -1
- package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +98 -29
- package/dist/templates/cleargate-planning/.cleargate/templates/proposal.md +26 -13
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +23 -4
- package/dist/templates/cleargate-planning/.cleargate/templates/story.md +64 -12
- package/dist/templates/cleargate-planning/CLAUDE.md +28 -10
- package/dist/templates/cleargate-planning/MANIFEST.json +259 -28
- package/dist/{whoami-CX7CXJD5.js → whoami-W4U6DPVG.js} +17 -17
- package/dist/whoami-W4U6DPVG.js.map +1 -0
- package/package.json +13 -2
- package/templates/cleargate-planning/.claude/agents/architect.md +10 -8
- package/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +108 -0
- package/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +49 -3
- package/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +6 -1
- package/templates/cleargate-planning/.claude/agents/developer.md +29 -2
- package/templates/cleargate-planning/.claude/agents/qa.md +50 -1
- package/templates/cleargate-planning/.claude/agents/reporter.md +31 -9
- package/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +148 -0
- package/templates/cleargate-planning/.claude/hooks/session-start.sh +6 -0
- package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +314 -96
- package/templates/cleargate-planning/.claude/settings.json +4 -0
- package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +473 -0
- package/templates/cleargate-planning/.cleargate/config.example.yml +19 -0
- package/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +542 -0
- package/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +102 -428
- package/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +31 -0
- package/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +71 -0
- package/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +24 -2
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +387 -27
- package/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +219 -0
- package/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +54 -0
- package/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +378 -0
- package/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +888 -0
- package/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +71 -0
- package/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +355 -13
- package/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
- package/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +482 -0
- package/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +125 -0
- package/templates/cleargate-planning/.cleargate/templates/Bug.md +33 -10
- package/templates/cleargate-planning/.cleargate/templates/CR.md +41 -10
- package/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +48 -14
- package/templates/cleargate-planning/.cleargate/templates/epic.md +46 -12
- package/templates/cleargate-planning/.cleargate/templates/hotfix.md +51 -1
- package/templates/cleargate-planning/.cleargate/templates/initiative.md +98 -29
- package/templates/cleargate-planning/.cleargate/templates/sprint_report.md +23 -4
- package/templates/cleargate-planning/.cleargate/templates/story.md +64 -12
- package/templates/cleargate-planning/CLAUDE.md +28 -10
- package/templates/cleargate-planning/MANIFEST.json +259 -28
- package/dist/chunk-OM4FAEA7.js.map +0 -1
- package/dist/whoami-CX7CXJD5.js.map +0 -1
- 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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* prep_doc_refresh.mjs <sprint-id>
|
|
4
|
+
*
|
|
5
|
+
* Generates a per-sprint tailored doc-refresh checklist by scanning what
|
|
6
|
+
* changed in the sprint window and matching trigger patterns from the
|
|
7
|
+
* canonical checklist at .cleargate/knowledge/sprint-closeout-checklist.md.
|
|
8
|
+
*
|
|
9
|
+
* Output: .cleargate/sprint-runs/<sprint-id>/.doc-refresh-checklist.md
|
|
10
|
+
*
|
|
11
|
+
* Each canonical-checklist category is evaluated; items where the trigger
|
|
12
|
+
* pattern matches at least one changed file get `- [ ]`; items where it
|
|
13
|
+
* does not get `- [x] — no changes detected, skip`.
|
|
14
|
+
*
|
|
15
|
+
* Does NOT modify any documentation. Application is human-driven.
|
|
16
|
+
*
|
|
17
|
+
* Exit codes:
|
|
18
|
+
* 0 — success
|
|
19
|
+
* 1 — sprint state.json not found (sprint dir missing)
|
|
20
|
+
* 2 — usage error (missing sprint-id argument)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { execSync } from 'node:child_process';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
30
|
+
|
|
31
|
+
/** @typedef {{ surface: string, trigger: RegExp | null, triggerDesc: string }} Item */
|
|
32
|
+
/** @typedef {{ name: string, items: Item[] }} Category */
|
|
33
|
+
|
|
34
|
+
/** @type {Category[]} */
|
|
35
|
+
const CATEGORIES = [
|
|
36
|
+
{
|
|
37
|
+
name: '1. Project READMEs',
|
|
38
|
+
items: [
|
|
39
|
+
{
|
|
40
|
+
surface: '`README.md`',
|
|
41
|
+
trigger: /^README\.md$/,
|
|
42
|
+
triggerDesc: 'any feature that changes user-visible product behavior',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
surface: '`cleargate-cli/README.md`',
|
|
46
|
+
trigger: /^cleargate-cli\/src\/commands\//,
|
|
47
|
+
triggerDesc: 'any change to cleargate-cli/src/commands/*.ts',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
surface: '`cleargate-planning/README.md`',
|
|
51
|
+
trigger: /^cleargate-planning\//,
|
|
52
|
+
triggerDesc: 'any change under cleargate-planning/',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
surface: '`mcp/README.md`',
|
|
56
|
+
trigger: /^mcp\/src\//,
|
|
57
|
+
triggerDesc: 'any change under mcp/src/ (nested repo — check separately)',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
surface: '`admin/README.md`',
|
|
61
|
+
trigger: /^admin\//,
|
|
62
|
+
triggerDesc: 'any change under admin/ (currently stub)',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: '2. CHANGELOG files (Common-Changelog format per STORY-016-03)',
|
|
68
|
+
items: [
|
|
69
|
+
{
|
|
70
|
+
surface: '`cleargate-cli/CHANGELOG.md`',
|
|
71
|
+
trigger: /^cleargate-cli\//,
|
|
72
|
+
triggerDesc: 'any user-visible change in cleargate-cli/ (CLI surface, error messages, package contents)',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
surface: '`mcp/CHANGELOG.md`',
|
|
76
|
+
trigger: /^mcp\//,
|
|
77
|
+
triggerDesc: 'any user-visible change in mcp/ (if file exists)',
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: '3. Manifest / package metadata',
|
|
83
|
+
items: [
|
|
84
|
+
{
|
|
85
|
+
surface: '`cleargate-planning/MANIFEST.json`',
|
|
86
|
+
trigger: /^(\.claude\/agents\/|\.cleargate\/templates\/|\.cleargate\/knowledge\/|\.cleargate\/scripts\/)/,
|
|
87
|
+
triggerDesc: 'any change to .claude/agents/*.md, .cleargate/templates/*, .cleargate/knowledge/*, or .cleargate/scripts/*',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
surface: '`cleargate-cli/package.json` (version bump)',
|
|
91
|
+
trigger: /^cleargate-cli\//,
|
|
92
|
+
triggerDesc: 'only if releasing this sprint (release lane is separate from sprint close)',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
surface: '`mcp/package.json` (version bump)',
|
|
96
|
+
trigger: /^mcp\//,
|
|
97
|
+
triggerDesc: 'only if releasing this sprint',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: '4. CLAUDE.md "Active state" subsection',
|
|
103
|
+
items: [
|
|
104
|
+
{
|
|
105
|
+
surface: '`CLAUDE.md` "Active state" section',
|
|
106
|
+
trigger: /\.(cleargate\/delivery\/archive\/|cleargate\/delivery\/pending-sync\/)/,
|
|
107
|
+
triggerDesc: 'any EPIC / CR / Bug / Hotfix archived this sprint, OR any stack version bumped',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
surface: '`cleargate-planning/CLAUDE.md` mirror',
|
|
111
|
+
trigger: /\.(cleargate\/delivery\/archive\/|cleargate\/delivery\/pending-sync\/)/,
|
|
112
|
+
triggerDesc: 'same edit as live (CLEARGATE-tag-block region only — outside-block diverges intentionally)',
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: '5. Wiki surfaces (auto-rebuilt by PostToolUse hooks; verify after close)',
|
|
118
|
+
items: [
|
|
119
|
+
{
|
|
120
|
+
surface: '`.cleargate/wiki/active-sprint.md`',
|
|
121
|
+
trigger: null,
|
|
122
|
+
triggerDesc: 'always verify at sprint close — confirm sprint ID, status, and date are current',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
surface: '`.cleargate/wiki/index.md`',
|
|
126
|
+
trigger: null,
|
|
127
|
+
triggerDesc: 'always verify at sprint close — confirm new artifacts appear in relevant sections',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
surface: '`.cleargate/wiki/product-state.md`',
|
|
131
|
+
trigger: null,
|
|
132
|
+
triggerDesc: 'always verify at sprint close — confirm shipped capabilities are listed',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
surface: '`.cleargate/wiki/roadmap.md`',
|
|
136
|
+
trigger: null,
|
|
137
|
+
triggerDesc: 'always verify at sprint close — confirm closed sprint moved from Active to Completed',
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: '6. INDEX surfaces (manual updates)',
|
|
143
|
+
items: [
|
|
144
|
+
{
|
|
145
|
+
surface: '`.cleargate/INDEX.md`',
|
|
146
|
+
trigger: null,
|
|
147
|
+
triggerDesc: 'if maintained as a curated roadmap; update when sprint closes',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
surface: '`.cleargate/delivery/INDEX.md`',
|
|
151
|
+
trigger: null,
|
|
152
|
+
triggerDesc: 'update epic/sprint map when new artifacts archived',
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: '7. Frontmatter version stamps',
|
|
158
|
+
items: [
|
|
159
|
+
{
|
|
160
|
+
surface: 'Modified `.cleargate/templates/*.md` (run `cleargate stamp <path>`)',
|
|
161
|
+
trigger: /^\.cleargate\/templates\//,
|
|
162
|
+
triggerDesc: 'any .cleargate/templates/*.md modified this sprint',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
surface: '`.cleargate/knowledge/cleargate-protocol.md` (run `cleargate stamp`)',
|
|
166
|
+
trigger: /^\.cleargate\/knowledge\/cleargate-protocol\.md$/,
|
|
167
|
+
triggerDesc: 'if cleargate-protocol.md was modified this sprint (post-EPIC-024 slim)',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
surface: '`.cleargate/knowledge/cleargate-enforcement.md` (run `cleargate stamp`)',
|
|
171
|
+
trigger: /^\.cleargate\/knowledge\/cleargate-enforcement\.md$/,
|
|
172
|
+
triggerDesc: 'if cleargate-enforcement.md was modified this sprint (post-EPIC-024 split)',
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
surface: 'Other modified `.cleargate/knowledge/*.md` (run `cleargate stamp <path>`)',
|
|
176
|
+
trigger: /^\.cleargate\/knowledge\//,
|
|
177
|
+
triggerDesc: 'any other .cleargate/knowledge/*.md touched this sprint',
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: '8. Knowledge doc cross-references',
|
|
183
|
+
items: [
|
|
184
|
+
{
|
|
185
|
+
surface: 'Knowledge docs citing `§N` of protocol or enforcement.md',
|
|
186
|
+
trigger: /^\.cleargate\/knowledge\//,
|
|
187
|
+
triggerDesc: 'any knowledge doc modified — verify §N citations still resolve post-rewrite',
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: '9. Mirror parity audit',
|
|
193
|
+
items: [
|
|
194
|
+
{
|
|
195
|
+
surface: '`cleargate-planning/.claude/agents/` diff',
|
|
196
|
+
trigger: /^\.claude\/agents\//,
|
|
197
|
+
triggerDesc: 'any change to .claude/agents/*.md — run diff -r to verify parity',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
surface: '`cleargate-planning/.cleargate/templates/` diff',
|
|
201
|
+
trigger: /^\.cleargate\/templates\//,
|
|
202
|
+
triggerDesc: 'any change to .cleargate/templates/ — run diff -r to verify parity',
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
surface: '`cleargate-planning/.cleargate/knowledge/` diff',
|
|
206
|
+
trigger: /^\.cleargate\/knowledge\//,
|
|
207
|
+
triggerDesc: 'any change to .cleargate/knowledge/ — run diff -r to verify parity',
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get changed files for a sprint using available strategies.
|
|
215
|
+
* Strategy 1: git log sprint/<sprint-id> ^main --name-only (if branch exists)
|
|
216
|
+
* Strategy 2: git log --since <start_date> --name-only (from sprint frontmatter)
|
|
217
|
+
* Strategy 3: git log --grep <sprint-id> --name-only (commit-message grep fallback)
|
|
218
|
+
*
|
|
219
|
+
* @param {string} sprintId
|
|
220
|
+
* @param {string} sprintDir
|
|
221
|
+
* @returns {string[]} deduped array of changed file paths
|
|
222
|
+
*/
|
|
223
|
+
function getChangedFiles(sprintId, sprintDir) {
|
|
224
|
+
// Strategy 1: sprint branch
|
|
225
|
+
try {
|
|
226
|
+
const branchCheck = execSync(`git rev-parse --verify "refs/heads/sprint/${sprintId}"`, {
|
|
227
|
+
cwd: REPO_ROOT,
|
|
228
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
229
|
+
}).toString().trim();
|
|
230
|
+
if (branchCheck) {
|
|
231
|
+
const out = execSync(
|
|
232
|
+
`git log "sprint/${sprintId}" ^main --name-only --format=""`,
|
|
233
|
+
{ cwd: REPO_ROOT, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
234
|
+
).toString();
|
|
235
|
+
const files = out.split('\n').map(f => f.trim()).filter(Boolean);
|
|
236
|
+
if (files.length > 0) {
|
|
237
|
+
return [...new Set(files)];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} catch {
|
|
241
|
+
// branch does not exist — fall through to next strategy
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Strategy 2: --since <start_date> from sprint frontmatter
|
|
245
|
+
const startDate = readStartDateFromFrontmatter(sprintId);
|
|
246
|
+
if (startDate) {
|
|
247
|
+
try {
|
|
248
|
+
const out = execSync(
|
|
249
|
+
`git log --since="${startDate}" --name-only --format=""`,
|
|
250
|
+
{ cwd: REPO_ROOT, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
251
|
+
).toString();
|
|
252
|
+
const files = out.split('\n').map(f => f.trim()).filter(Boolean);
|
|
253
|
+
if (files.length > 0) {
|
|
254
|
+
return [...new Set(files)];
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
// fall through to strategy 3
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Strategy 3: commit-message grep for sprint ID
|
|
262
|
+
try {
|
|
263
|
+
const out = execSync(
|
|
264
|
+
`git log --grep="${sprintId}" --name-only --format=""`,
|
|
265
|
+
{ cwd: REPO_ROOT, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
266
|
+
).toString();
|
|
267
|
+
const files = out.split('\n').map(f => f.trim()).filter(Boolean);
|
|
268
|
+
return [...new Set(files)];
|
|
269
|
+
} catch {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Read start_date from sprint frontmatter file.
|
|
276
|
+
* Looks in pending-sync first, then archive.
|
|
277
|
+
*
|
|
278
|
+
* @param {string} sprintId
|
|
279
|
+
* @returns {string | null}
|
|
280
|
+
*/
|
|
281
|
+
function readStartDateFromFrontmatter(sprintId) {
|
|
282
|
+
const dirs = [
|
|
283
|
+
path.join(REPO_ROOT, '.cleargate', 'delivery', 'pending-sync'),
|
|
284
|
+
path.join(REPO_ROOT, '.cleargate', 'delivery', 'archive'),
|
|
285
|
+
];
|
|
286
|
+
for (const dir of dirs) {
|
|
287
|
+
if (!fs.existsSync(dir)) continue;
|
|
288
|
+
const entries = fs.readdirSync(dir);
|
|
289
|
+
const match = entries.find(e => e.startsWith(`${sprintId}_`) && e.endsWith('.md'));
|
|
290
|
+
if (match) {
|
|
291
|
+
const content = fs.readFileSync(path.join(dir, match), 'utf8');
|
|
292
|
+
const m = content.match(/^start_date:\s*["']?(.+?)["']?\s*$/m);
|
|
293
|
+
if (m) return m[1].trim();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Evaluate each checklist category against the changed file set.
|
|
301
|
+
*
|
|
302
|
+
* @param {string[]} changedFiles
|
|
303
|
+
* @returns {{ category: Category, results: Array<{ item: Item, triggered: boolean }> }[]}
|
|
304
|
+
*/
|
|
305
|
+
function evaluateChecklist(changedFiles) {
|
|
306
|
+
return CATEGORIES.map(category => ({
|
|
307
|
+
category,
|
|
308
|
+
results: category.items.map(item => ({
|
|
309
|
+
item,
|
|
310
|
+
triggered: item.trigger === null
|
|
311
|
+
? true // null trigger = always review
|
|
312
|
+
: changedFiles.some(f => item.trigger.test(f)),
|
|
313
|
+
})),
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Render the checklist markdown.
|
|
319
|
+
*
|
|
320
|
+
* @param {ReturnType<typeof evaluateChecklist>} evaluation
|
|
321
|
+
* @param {string} sprintId
|
|
322
|
+
* @returns {string}
|
|
323
|
+
*/
|
|
324
|
+
function renderChecklist(evaluation, sprintId) {
|
|
325
|
+
const lines = [
|
|
326
|
+
`# Doc & Metadata Refresh Checklist — ${sprintId}`,
|
|
327
|
+
'',
|
|
328
|
+
`> Generated by \`prep_doc_refresh.mjs\` at ${new Date().toISOString()}.`,
|
|
329
|
+
'> Each item is pre-evaluated against the sprint\'s changed file set.',
|
|
330
|
+
'> `- [ ]` = action required; `- [x]` = no changes detected, skip.',
|
|
331
|
+
'> Apply or defer each `- [ ]` item during Gate 4 ack.',
|
|
332
|
+
'> Canonical checklist: `.cleargate/knowledge/sprint-closeout-checklist.md`',
|
|
333
|
+
'',
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
for (const { category, results } of evaluation) {
|
|
337
|
+
lines.push(`### ${category.name}`);
|
|
338
|
+
lines.push('');
|
|
339
|
+
for (const { item, triggered } of results) {
|
|
340
|
+
if (triggered) {
|
|
341
|
+
lines.push(`- [ ] ${item.surface} — **trigger:** ${item.triggerDesc}`);
|
|
342
|
+
} else {
|
|
343
|
+
lines.push(`- [x] ${item.surface} — no changes detected, skip`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
lines.push('');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return lines.join('\n');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function main() {
|
|
353
|
+
const sprintId = process.argv[2];
|
|
354
|
+
if (!sprintId) {
|
|
355
|
+
console.error('Usage: prep_doc_refresh.mjs <sprint-id>');
|
|
356
|
+
console.error('Example: prep_doc_refresh.mjs SPRINT-16');
|
|
357
|
+
process.exit(2);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const sprintDir = path.join(REPO_ROOT, '.cleargate', 'sprint-runs', sprintId);
|
|
361
|
+
if (!fs.existsSync(sprintDir)) {
|
|
362
|
+
console.error(`Error: sprint state.json not found at ${path.join(sprintDir, 'state.json')}`);
|
|
363
|
+
console.error(`Sprint directory does not exist: ${sprintDir}`);
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const changedFiles = getChangedFiles(sprintId, sprintDir);
|
|
368
|
+
console.log(`Found ${changedFiles.length} changed files in sprint window.`);
|
|
369
|
+
|
|
370
|
+
const evaluation = evaluateChecklist(changedFiles);
|
|
371
|
+
const markdown = renderChecklist(evaluation, sprintId);
|
|
372
|
+
|
|
373
|
+
const outFile = path.join(sprintDir, '.doc-refresh-checklist.md');
|
|
374
|
+
fs.writeFileSync(outFile, markdown, 'utf8');
|
|
375
|
+
console.log(`Wrote ${outFile}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
main();
|