cleargate 0.8.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +24 -1
- package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +32 -1
- package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +48 -14
- package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +37 -3
- package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +50 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +98 -29
- package/dist/templates/cleargate-planning/.cleargate/templates/proposal.md +17 -4
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +23 -4
- package/dist/templates/cleargate-planning/.cleargate/templates/story.md +55 -3
- 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 +24 -1
- package/templates/cleargate-planning/.cleargate/templates/CR.md +32 -1
- package/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +48 -14
- package/templates/cleargate-planning/.cleargate/templates/epic.md +37 -3
- package/templates/cleargate-planning/.cleargate/templates/hotfix.md +50 -0
- 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 +55 -3
- 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,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* sprint_trends.mjs — Sprint trends stub (full implementation deferred to CR-027)
|
|
4
|
+
*
|
|
5
|
+
* Usage: node sprint_trends.mjs <sprint-id>
|
|
6
|
+
*
|
|
7
|
+
* Counts sibling Completed sprints and appends a placeholder Trends section
|
|
8
|
+
* to the current sprint's improvement-suggestions.md.
|
|
9
|
+
*
|
|
10
|
+
* Test seams:
|
|
11
|
+
* CLEARGATE_SPRINT_DIR=<path> — override sprint dir resolution
|
|
12
|
+
* CLEARGATE_SPRINT_RUNS_DIR=<path> — override .cleargate/sprint-runs/ root for sibling counting
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
21
|
+
|
|
22
|
+
function main() {
|
|
23
|
+
const sprintId = process.argv[2];
|
|
24
|
+
if (!sprintId) {
|
|
25
|
+
process.stderr.write('Usage: node sprint_trends.mjs <sprint-id>\n');
|
|
26
|
+
process.exit(2);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const sprintDir = process.env.CLEARGATE_SPRINT_DIR
|
|
30
|
+
? path.resolve(process.env.CLEARGATE_SPRINT_DIR)
|
|
31
|
+
: path.join(REPO_ROOT, '.cleargate', 'sprint-runs', sprintId);
|
|
32
|
+
|
|
33
|
+
const sprintRunsDir = process.env.CLEARGATE_SPRINT_RUNS_DIR
|
|
34
|
+
? path.resolve(process.env.CLEARGATE_SPRINT_RUNS_DIR)
|
|
35
|
+
: path.dirname(sprintDir);
|
|
36
|
+
|
|
37
|
+
// Count sibling sprint dirs whose state.json has sprint_status === 'Completed'
|
|
38
|
+
let completedCount = 0;
|
|
39
|
+
if (fs.existsSync(sprintRunsDir)) {
|
|
40
|
+
for (const entry of fs.readdirSync(sprintRunsDir)) {
|
|
41
|
+
if (entry === path.basename(sprintDir)) continue;
|
|
42
|
+
const siblingState = path.join(sprintRunsDir, entry, 'state.json');
|
|
43
|
+
if (!fs.existsSync(siblingState)) continue;
|
|
44
|
+
try {
|
|
45
|
+
const state = JSON.parse(fs.readFileSync(siblingState, 'utf8'));
|
|
46
|
+
if (state.sprint_status === 'Completed') completedCount++;
|
|
47
|
+
} catch { /* skip malformed state.json */ }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const suggestionsFile = path.join(sprintDir, 'improvement-suggestions.md');
|
|
52
|
+
const trendsSection = `\n## Trends\n\nTrends: ${completedCount} closed sprints visible — full analysis deferred to CR-027.\n`;
|
|
53
|
+
|
|
54
|
+
if (!fs.existsSync(suggestionsFile)) {
|
|
55
|
+
const header = `# Improvement Suggestions — ${sprintId}\n\n`;
|
|
56
|
+
const tmpFile = `${suggestionsFile}.tmp.${process.pid}`;
|
|
57
|
+
fs.writeFileSync(tmpFile, header + trendsSection, 'utf8');
|
|
58
|
+
fs.renameSync(tmpFile, suggestionsFile);
|
|
59
|
+
} else {
|
|
60
|
+
const existing = fs.readFileSync(suggestionsFile, 'utf8');
|
|
61
|
+
const tmpFile = `${suggestionsFile}.tmp.${process.pid}`;
|
|
62
|
+
fs.writeFileSync(tmpFile, existing.trimEnd() + trendsSection, 'utf8');
|
|
63
|
+
fs.renameSync(tmpFile, suggestionsFile);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
process.stdout.write(
|
|
67
|
+
`sprint_trends: stub — full implementation deferred to CR-027 (counted ${completedCount} closed sprints).\n`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
main();
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* suggest_improvements.mjs — Generate stable improvement suggestions from REPORT.md
|
|
4
4
|
*
|
|
5
5
|
* Usage: node suggest_improvements.mjs <sprint-id>
|
|
6
|
+
* node suggest_improvements.mjs <sprint-id> --skill-candidates
|
|
7
|
+
* node suggest_improvements.mjs <sprint-id> --flashcard-cleanup
|
|
6
8
|
*
|
|
7
9
|
* Reads:
|
|
8
10
|
* - .cleargate/sprint-runs/<id>/REPORT.md §5 Framework Self-Assessment tables
|
|
@@ -10,7 +12,9 @@
|
|
|
10
12
|
*
|
|
11
13
|
* Emits:
|
|
12
14
|
* - .cleargate/sprint-runs/<id>/improvement-suggestions.md
|
|
13
|
-
* with stable SUG-<sprint>-<n> IDs
|
|
15
|
+
* with stable SUG-<sprint>-<n> IDs (default mode)
|
|
16
|
+
* or appends ## Skill Creation Candidates (--skill-candidates mode)
|
|
17
|
+
* or appends ## FLASHCARD Cleanup Candidates (--flashcard-cleanup mode)
|
|
14
18
|
*
|
|
15
19
|
* Append-only idempotency (R5):
|
|
16
20
|
* - IDs are derived from a stable hash of (category, title) tuple
|
|
@@ -19,12 +23,18 @@
|
|
|
19
23
|
*
|
|
20
24
|
* Note: "section" as used in §5 table extraction refers to the Framework Self-Assessment
|
|
21
25
|
* subsections: Templates, Handoffs, Skills, Process, Tooling.
|
|
26
|
+
*
|
|
27
|
+
* Test seams (CR-022 M6):
|
|
28
|
+
* CLEARGATE_FLASHCARD_PATH=<path> — override .cleargate/FLASHCARD.md path
|
|
29
|
+
* CLEARGATE_FLASHCARD_LOOKBACK=<N> — override 3-sprint lookback window (default 3)
|
|
30
|
+
* CLEARGATE_SPRINT_RUNS_DIR=<path> — override .cleargate/sprint-runs/ root for sibling lookups
|
|
22
31
|
*/
|
|
23
32
|
|
|
24
33
|
import fs from 'node:fs';
|
|
25
34
|
import path from 'node:path';
|
|
26
35
|
import { fileURLToPath } from 'node:url';
|
|
27
36
|
import { createHash } from 'node:crypto';
|
|
37
|
+
import { reportFilename } from './lib/report-filename.mjs';
|
|
28
38
|
|
|
29
39
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
30
40
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
@@ -91,15 +101,20 @@ function parseSelfAssessment(content) {
|
|
|
91
101
|
}
|
|
92
102
|
|
|
93
103
|
/**
|
|
94
|
-
* Parse existing improvement-suggestions.md to extract already-captured SUG IDs.
|
|
104
|
+
* Parse existing improvement-suggestions.md to extract already-captured SUG and CAND IDs.
|
|
95
105
|
* @param {string} content
|
|
96
|
-
* @returns {Set<string>} Set of SUG IDs
|
|
106
|
+
* @returns {Set<string>} Set of SUG/CAND IDs
|
|
97
107
|
*/
|
|
98
108
|
function parseExistingIds(content) {
|
|
99
109
|
const ids = new Set();
|
|
100
|
-
// Match SUG-<sprint>-<n> patterns
|
|
101
|
-
const
|
|
102
|
-
for (const m of
|
|
110
|
+
// Match SUG-<sprint>-<n> patterns (default mode)
|
|
111
|
+
const sugMatches = content.matchAll(/SUG-[A-Z0-9-]+-\d+/g);
|
|
112
|
+
for (const m of sugMatches) {
|
|
113
|
+
ids.add(m[0]);
|
|
114
|
+
}
|
|
115
|
+
// Match CAND-<sprint>-[SF]<n> patterns (--skill-candidates / --flashcard-cleanup modes)
|
|
116
|
+
const candMatches = content.matchAll(/CAND-[A-Z0-9-]+-[SF]\d+/g);
|
|
117
|
+
for (const m of candMatches) {
|
|
103
118
|
ids.add(m[0]);
|
|
104
119
|
}
|
|
105
120
|
return ids;
|
|
@@ -116,14 +131,328 @@ function atomicWrite(filePath, content) {
|
|
|
116
131
|
fs.renameSync(tmpFile, filePath);
|
|
117
132
|
}
|
|
118
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Scan token-ledger.jsonl and FLASHCARD.md for skill creation candidates.
|
|
136
|
+
* Appends or replaces the "## Skill Creation Candidates" section in improvement-suggestions.md.
|
|
137
|
+
* @param {string} sprintId
|
|
138
|
+
* @param {string} sprintDir
|
|
139
|
+
* @param {string} suggestionsFile
|
|
140
|
+
*/
|
|
141
|
+
function scanSkillCandidates(sprintId, sprintDir, suggestionsFile) {
|
|
142
|
+
const flashcardPath = process.env.CLEARGATE_FLASHCARD_PATH
|
|
143
|
+
? path.resolve(process.env.CLEARGATE_FLASHCARD_PATH)
|
|
144
|
+
: path.join(REPO_ROOT, '.cleargate', 'FLASHCARD.md');
|
|
145
|
+
const ledgerPath = path.join(sprintDir, 'token-ledger.jsonl');
|
|
146
|
+
|
|
147
|
+
// Count repeated (work_item_id, agent_type) tuples from token-ledger.jsonl
|
|
148
|
+
/** @type {Map<string, number>} */
|
|
149
|
+
const tupleCounts = new Map();
|
|
150
|
+
if (fs.existsSync(ledgerPath)) {
|
|
151
|
+
const lines = fs.readFileSync(ledgerPath, 'utf8').split('\n').filter(l => l.trim());
|
|
152
|
+
for (const line of lines) {
|
|
153
|
+
try {
|
|
154
|
+
const entry = JSON.parse(line);
|
|
155
|
+
if (entry.work_item_id && entry.agent_type) {
|
|
156
|
+
const key = `${entry.work_item_id}|${entry.agent_type}`;
|
|
157
|
+
tupleCounts.set(key, (tupleCounts.get(key) ?? 0) + 1);
|
|
158
|
+
}
|
|
159
|
+
} catch { /* skip malformed lines */ }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Find tuples repeated ≥3×
|
|
164
|
+
const repeatedTuples = [...tupleCounts.entries()].filter(([, count]) => count >= 3);
|
|
165
|
+
|
|
166
|
+
// Grep FLASHCARD.md for "also do" patterns
|
|
167
|
+
const alsoDoMatches = [];
|
|
168
|
+
if (fs.existsSync(flashcardPath)) {
|
|
169
|
+
const fcContent = fs.readFileSync(flashcardPath, 'utf8');
|
|
170
|
+
const lines = fcContent.split('\n');
|
|
171
|
+
for (const line of lines) {
|
|
172
|
+
if (/remember to also|also do X|also need to/i.test(line)) {
|
|
173
|
+
alsoDoMatches.push(line.trim());
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Read existing suggestions file content
|
|
179
|
+
let existingContent = fs.existsSync(suggestionsFile)
|
|
180
|
+
? fs.readFileSync(suggestionsFile, 'utf8')
|
|
181
|
+
: `# Improvement Suggestions — ${sprintId}\n\nGenerated by \`suggest_improvements.mjs\`. Append-only; IDs are stable.\nVocabulary: Templates | Handoffs | Skills | Process | Tooling\n\n---\n\n`;
|
|
182
|
+
|
|
183
|
+
// Build the candidates
|
|
184
|
+
const candidates = [];
|
|
185
|
+
let candN = 1;
|
|
186
|
+
for (const [key] of repeatedTuples) {
|
|
187
|
+
const [workItemId, agentType] = key.split('|');
|
|
188
|
+
const candId = `CAND-${sprintId}-S${String(candN).padStart(2, '0')}`;
|
|
189
|
+
const hashKey = `skill|${key}`;
|
|
190
|
+
const hash = stableHash(hashKey);
|
|
191
|
+
if (!existingContent.includes(`<!-- hash:${hash} -->`)) {
|
|
192
|
+
candidates.push({ candId, hash, workItemId, agentType, source: 'ledger' });
|
|
193
|
+
candN++;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const line of alsoDoMatches) {
|
|
197
|
+
const candId = `CAND-${sprintId}-S${String(candN).padStart(2, '0')}`;
|
|
198
|
+
const hashKey = `skill|flashcard|${line.slice(0, 60)}`;
|
|
199
|
+
const hash = stableHash(hashKey);
|
|
200
|
+
if (!existingContent.includes(`<!-- hash:${hash} -->`)) {
|
|
201
|
+
candidates.push({ candId, hash, workItemId: null, agentType: null, source: 'flashcard', line });
|
|
202
|
+
candN++;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Build the section content
|
|
207
|
+
const sectionLines = [
|
|
208
|
+
'## Skill Creation Candidates',
|
|
209
|
+
'',
|
|
210
|
+
'<!-- generated-by: suggest_improvements.mjs --skill-candidates -->',
|
|
211
|
+
'',
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
if (candidates.length === 0) {
|
|
215
|
+
sectionLines.push('_No candidates detected this sprint._');
|
|
216
|
+
sectionLines.push('');
|
|
217
|
+
} else {
|
|
218
|
+
for (const c of candidates) {
|
|
219
|
+
sectionLines.push(`### ${c.candId}: ${c.source === 'ledger' ? `${c.workItemId} × ${c.agentType}` : 'flashcard pattern'}`);
|
|
220
|
+
sectionLines.push(`<!-- hash:${c.hash} -->`);
|
|
221
|
+
sectionLines.push('');
|
|
222
|
+
if (c.source === 'ledger') {
|
|
223
|
+
sectionLines.push(`**Pattern detected:** ${c.workItemId} × ${c.agentType} repeated ≥3× in token-ledger`);
|
|
224
|
+
} else {
|
|
225
|
+
sectionLines.push(`**Pattern detected:** "also do" pattern in FLASHCARD.md`);
|
|
226
|
+
sectionLines.push(`**Source line:** \`${c.line}\``);
|
|
227
|
+
}
|
|
228
|
+
sectionLines.push(`**Proposed skill:** \`.claude/skills/<slug>/SKILL.md\``);
|
|
229
|
+
sectionLines.push('');
|
|
230
|
+
sectionLines.push('---');
|
|
231
|
+
sectionLines.push('');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const sectionContent = sectionLines.join('\n');
|
|
236
|
+
// Anchored heading regex — strict match, no trailing text
|
|
237
|
+
const headingRe = /^## Skill Creation Candidates$/m;
|
|
238
|
+
|
|
239
|
+
let finalContent;
|
|
240
|
+
if (headingRe.test(existingContent)) {
|
|
241
|
+
// Replace existing section (splice out old, append new)
|
|
242
|
+
const nextHeadingRe = /^## /m;
|
|
243
|
+
const headingIdx = existingContent.search(headingRe);
|
|
244
|
+
const afterHeading = existingContent.slice(headingIdx + existingContent.match(headingRe)[0].length);
|
|
245
|
+
const nextMatch = afterHeading.search(/^## /m);
|
|
246
|
+
if (nextMatch === -1) {
|
|
247
|
+
finalContent = existingContent.slice(0, headingIdx).trimEnd() + '\n\n' + sectionContent;
|
|
248
|
+
} else {
|
|
249
|
+
finalContent = existingContent.slice(0, headingIdx).trimEnd() + '\n\n' + sectionContent + '\n' + afterHeading.slice(nextMatch);
|
|
250
|
+
}
|
|
251
|
+
void nextHeadingRe;
|
|
252
|
+
} else {
|
|
253
|
+
finalContent = existingContent.trimEnd() + '\n\n' + sectionContent;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
atomicWrite(suggestionsFile, finalContent);
|
|
257
|
+
process.stdout.write(`suggest_improvements: skill-candidates section written to ${suggestionsFile}\n`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Scan FLASHCARD.md for cleanup candidates (stale / superseded / resolved).
|
|
262
|
+
* Appends or replaces the "## FLASHCARD Cleanup Candidates" section in improvement-suggestions.md.
|
|
263
|
+
* @param {string} sprintId
|
|
264
|
+
* @param {string} sprintDir
|
|
265
|
+
* @param {string} suggestionsFile
|
|
266
|
+
*/
|
|
267
|
+
function scanFlashcardCleanup(sprintId, sprintDir, suggestionsFile) {
|
|
268
|
+
const flashcardPath = process.env.CLEARGATE_FLASHCARD_PATH
|
|
269
|
+
? path.resolve(process.env.CLEARGATE_FLASHCARD_PATH)
|
|
270
|
+
: path.join(REPO_ROOT, '.cleargate', 'FLASHCARD.md');
|
|
271
|
+
|
|
272
|
+
const lookback = parseInt(process.env.CLEARGATE_FLASHCARD_LOOKBACK ?? '3', 10);
|
|
273
|
+
const sprintRunsDir = process.env.CLEARGATE_SPRINT_RUNS_DIR
|
|
274
|
+
? path.resolve(process.env.CLEARGATE_SPRINT_RUNS_DIR)
|
|
275
|
+
: path.dirname(sprintDir);
|
|
276
|
+
|
|
277
|
+
if (!fs.existsSync(flashcardPath)) {
|
|
278
|
+
process.stderr.write(`suggest_improvements --flashcard-cleanup: FLASHCARD.md not found at ${flashcardPath}, skipping\n`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const fcContent = fs.readFileSync(flashcardPath, 'utf8');
|
|
283
|
+
// Parse entries: YYYY-MM-DD · #tag1 #tag2 · lesson
|
|
284
|
+
const entryRe = /^(\d{4}-\d{2}-\d{2})\s+·\s+(.*?)\s+·\s+(.+)$/;
|
|
285
|
+
const entries = fcContent.split('\n').filter(l => entryRe.test(l.trim()));
|
|
286
|
+
|
|
287
|
+
// Determine lookback sprint dirs (numerical extraction from sprintId)
|
|
288
|
+
const numMatch = sprintId.match(/(\d+)$/);
|
|
289
|
+
const currentNum = numMatch ? parseInt(numMatch[1], 10) : null;
|
|
290
|
+
const sprintPrefix = numMatch ? sprintId.replace(/\d+$/, '') : null;
|
|
291
|
+
|
|
292
|
+
// Collect prior sprint REPORT.md content for "resolved" detection
|
|
293
|
+
const priorReportContent = [];
|
|
294
|
+
if (currentNum !== null && sprintPrefix !== null) {
|
|
295
|
+
for (let i = 1; i <= lookback; i++) {
|
|
296
|
+
const priorId = `${sprintPrefix}${currentNum - i}`;
|
|
297
|
+
const priorDir = path.join(sprintRunsDir, priorId);
|
|
298
|
+
if (!fs.existsSync(priorDir)) continue;
|
|
299
|
+
try {
|
|
300
|
+
const rFile = reportFilename(priorDir, priorId, { forRead: true });
|
|
301
|
+
if (fs.existsSync(rFile)) {
|
|
302
|
+
priorReportContent.push(fs.readFileSync(rFile, 'utf8'));
|
|
303
|
+
}
|
|
304
|
+
} catch { /* skip missing */ }
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Collect prior sprint dirs for stale detection
|
|
309
|
+
const priorSprintDirs = [];
|
|
310
|
+
if (currentNum !== null && sprintPrefix !== null) {
|
|
311
|
+
for (let i = 1; i <= lookback; i++) {
|
|
312
|
+
const priorId = `${sprintPrefix}${currentNum - i}`;
|
|
313
|
+
const priorDir = path.join(sprintRunsDir, priorId);
|
|
314
|
+
if (fs.existsSync(priorDir)) priorSprintDirs.push(priorDir);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** @type {{ entry: string, date: string, tags: string, lesson: string, category: 'stale'|'superseded'|'resolved', reason: string }[]} */
|
|
319
|
+
const candidates = [];
|
|
320
|
+
const processedHashes = new Set();
|
|
321
|
+
|
|
322
|
+
for (const rawEntry of entries) {
|
|
323
|
+
const m = rawEntry.trim().match(entryRe);
|
|
324
|
+
if (!m) continue;
|
|
325
|
+
const [, date, tags, lesson] = m;
|
|
326
|
+
const entry = rawEntry.trim();
|
|
327
|
+
const hashKey = `flashcard|${entry.slice(0, 80)}`;
|
|
328
|
+
const hash = stableHash(hashKey);
|
|
329
|
+
if (processedHashes.has(hash)) continue;
|
|
330
|
+
processedHashes.add(hash);
|
|
331
|
+
|
|
332
|
+
// Extract first keyword from lesson (first word-run, stripping punctuation)
|
|
333
|
+
const keyword = lesson.split(/\s+/)[0].replace(/[^a-zA-Z0-9_-]/g, '').toLowerCase();
|
|
334
|
+
if (!keyword) continue;
|
|
335
|
+
|
|
336
|
+
// Check "resolved": keyword appears in §6 Tooling "Resolved" of a prior REPORT.md
|
|
337
|
+
let resolved = false;
|
|
338
|
+
for (const rc of priorReportContent) {
|
|
339
|
+
const resolvedSection = rc.match(/##\s*§6[^\n]*\n([\s\S]*?)(?=##\s*§|$)/);
|
|
340
|
+
if (resolvedSection && resolvedSection[1].toLowerCase().includes(keyword)) {
|
|
341
|
+
resolved = true;
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (resolved) {
|
|
346
|
+
candidates.push({ entry, date, tags, lesson, category: 'resolved', reason: 'keyword found in a prior §6 Tooling section' });
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Check "stale": zero grep hits for keyword across prior sprint dirs
|
|
351
|
+
if (priorSprintDirs.length > 0) {
|
|
352
|
+
let hitCount = 0;
|
|
353
|
+
for (const priorDir of priorSprintDirs) {
|
|
354
|
+
try {
|
|
355
|
+
const files = fs.readdirSync(priorDir);
|
|
356
|
+
for (const f of files) {
|
|
357
|
+
const fPath = path.join(priorDir, f);
|
|
358
|
+
if (fs.statSync(fPath).isFile()) {
|
|
359
|
+
const content = fs.readFileSync(fPath, 'utf8');
|
|
360
|
+
if (content.toLowerCase().includes(keyword)) { hitCount++; break; }
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
} catch { /* skip unreadable dirs */ }
|
|
364
|
+
}
|
|
365
|
+
if (hitCount === 0) {
|
|
366
|
+
candidates.push({ entry, date, tags, lesson, category: 'stale', reason: `stale: zero grep hits across last ${priorSprintDirs.length} sprint dir(s)` });
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Read existing suggestions file content
|
|
373
|
+
let existingContent = fs.existsSync(suggestionsFile)
|
|
374
|
+
? fs.readFileSync(suggestionsFile, 'utf8')
|
|
375
|
+
: `# Improvement Suggestions — ${sprintId}\n\nGenerated by \`suggest_improvements.mjs\`. Append-only; IDs are stable.\nVocabulary: Templates | Handoffs | Skills | Process | Tooling\n\n---\n\n`;
|
|
376
|
+
|
|
377
|
+
// Build section
|
|
378
|
+
const sectionLines = [
|
|
379
|
+
'## FLASHCARD Cleanup Candidates',
|
|
380
|
+
'',
|
|
381
|
+
'<!-- generated-by: suggest_improvements.mjs --flashcard-cleanup -->',
|
|
382
|
+
'',
|
|
383
|
+
];
|
|
384
|
+
|
|
385
|
+
// Filter out already-captured candidates
|
|
386
|
+
const newCandidates = candidates.filter(c => {
|
|
387
|
+
const hash = stableHash(`flashcard|${c.entry.slice(0, 80)}`);
|
|
388
|
+
return !existingContent.includes(`<!-- hash:${hash} -->`);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
if (newCandidates.length === 0) {
|
|
392
|
+
sectionLines.push('_No candidates detected this sprint._');
|
|
393
|
+
sectionLines.push('');
|
|
394
|
+
} else {
|
|
395
|
+
newCandidates.forEach((c, i) => {
|
|
396
|
+
const candId = `CAND-${sprintId}-F${String(i + 1).padStart(2, '0')}`;
|
|
397
|
+
const hash = stableHash(`flashcard|${c.entry.slice(0, 80)}`);
|
|
398
|
+
sectionLines.push(`### ${candId}: ${c.lesson.slice(0, 60)}`);
|
|
399
|
+
sectionLines.push(`<!-- hash:${hash} -->`);
|
|
400
|
+
sectionLines.push('');
|
|
401
|
+
sectionLines.push(`**Category:** ${c.category}`);
|
|
402
|
+
sectionLines.push(`**Reason:** ${c.reason}`);
|
|
403
|
+
sectionLines.push(`**Original entry:** \`${c.entry}\``);
|
|
404
|
+
sectionLines.push('**Suggested action:** approve to remove via `cleargate flashcard prune` (run /improve)');
|
|
405
|
+
sectionLines.push('');
|
|
406
|
+
sectionLines.push('---');
|
|
407
|
+
sectionLines.push('');
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const sectionContent = sectionLines.join('\n');
|
|
412
|
+
// Anchored heading regex — strict match, no trailing text
|
|
413
|
+
const headingRe = /^## FLASHCARD Cleanup Candidates$/m;
|
|
414
|
+
|
|
415
|
+
let finalContent;
|
|
416
|
+
if (headingRe.test(existingContent)) {
|
|
417
|
+
const headingIdx = existingContent.search(headingRe);
|
|
418
|
+
const afterHeading = existingContent.slice(headingIdx + existingContent.match(headingRe)[0].length);
|
|
419
|
+
const nextMatch = afterHeading.search(/^## /m);
|
|
420
|
+
if (nextMatch === -1) {
|
|
421
|
+
finalContent = existingContent.slice(0, headingIdx).trimEnd() + '\n\n' + sectionContent;
|
|
422
|
+
} else {
|
|
423
|
+
finalContent = existingContent.slice(0, headingIdx).trimEnd() + '\n\n' + sectionContent + '\n' + afterHeading.slice(nextMatch);
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
finalContent = existingContent.trimEnd() + '\n\n' + sectionContent;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
atomicWrite(suggestionsFile, finalContent);
|
|
430
|
+
process.stdout.write(`suggest_improvements: flashcard-cleanup section written to ${suggestionsFile}\n`);
|
|
431
|
+
}
|
|
432
|
+
|
|
119
433
|
function main() {
|
|
120
434
|
const args = process.argv.slice(2);
|
|
121
435
|
if (args.length < 1) {
|
|
122
|
-
process.stderr.write('Usage: node suggest_improvements.mjs <sprint-id
|
|
436
|
+
process.stderr.write('Usage: node suggest_improvements.mjs <sprint-id> [--skill-candidates | --flashcard-cleanup]\n');
|
|
437
|
+
process.exit(2);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Flag-aware parse: detect flags first, then extract sprintId from positional args.
|
|
441
|
+
// Sprint IDs match ^SPRINT-\d+$ so they never start with '--'; no collision possible.
|
|
442
|
+
const skillCandidatesMode = args.includes('--skill-candidates');
|
|
443
|
+
const flashcardCleanupMode = args.includes('--flashcard-cleanup');
|
|
444
|
+
|
|
445
|
+
if (skillCandidatesMode && flashcardCleanupMode) {
|
|
446
|
+
process.stderr.write('Error: --skill-candidates and --flashcard-cleanup are mutually exclusive\n');
|
|
447
|
+
process.exit(2);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const sprintId = args.find(a => !a.startsWith('--'));
|
|
451
|
+
if (!sprintId) {
|
|
452
|
+
process.stderr.write('Usage: node suggest_improvements.mjs <sprint-id> [--skill-candidates | --flashcard-cleanup]\n');
|
|
123
453
|
process.exit(2);
|
|
124
454
|
}
|
|
125
455
|
|
|
126
|
-
const sprintId = args[0];
|
|
127
456
|
const sprintDir = process.env.CLEARGATE_SPRINT_DIR
|
|
128
457
|
? path.resolve(process.env.CLEARGATE_SPRINT_DIR)
|
|
129
458
|
: path.join(REPO_ROOT, '.cleargate', 'sprint-runs', sprintId);
|
|
@@ -133,13 +462,26 @@ function main() {
|
|
|
133
462
|
process.exit(1);
|
|
134
463
|
}
|
|
135
464
|
|
|
136
|
-
const reportFile =
|
|
465
|
+
const reportFile = reportFilename(sprintDir, sprintId, { forRead: true });
|
|
137
466
|
const suggestionsFile = path.join(sprintDir, 'improvement-suggestions.md');
|
|
138
467
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
468
|
+
// Default mode requires REPORT.md; flag-driven modes do not.
|
|
469
|
+
if (!skillCandidatesMode && !flashcardCleanupMode) {
|
|
470
|
+
if (!fs.existsSync(reportFile)) {
|
|
471
|
+
process.stderr.write(`Error: report file not found at ${reportFile}\n`);
|
|
472
|
+
process.stderr.write('Run the Reporter agent first to generate the report.\n');
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Route flag-driven modes before default processing
|
|
478
|
+
if (skillCandidatesMode) {
|
|
479
|
+
scanSkillCandidates(sprintId, sprintDir, suggestionsFile);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (flashcardCleanupMode) {
|
|
483
|
+
scanFlashcardCleanup(sprintId, sprintDir, suggestionsFile);
|
|
484
|
+
return;
|
|
143
485
|
}
|
|
144
486
|
|
|
145
487
|
const reportContent = fs.readFileSync(reportFile, 'utf8');
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Strategy: grep-based. Creates a synthetic dev-report fixture with
|
|
6
6
|
# flashcards_flagged, simulates a mock worktree-creation step, and asserts
|
|
7
|
-
# the gate contract documented in protocol §
|
|
7
|
+
# the gate contract documented in protocol §4 and the agent output-shape
|
|
8
8
|
# blocks in developer.md + qa.md.
|
|
9
9
|
#
|
|
10
10
|
# Usage: bash .cleargate/scripts/test/test_flashcard_gate.sh
|
|
@@ -30,14 +30,14 @@ fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); }
|
|
|
30
30
|
# And upon approval each card is appended to .cleargate/FLASHCARD.md
|
|
31
31
|
# And only then does worktree creation proceed
|
|
32
32
|
#
|
|
33
|
-
# This script validates the contract (protocol §
|
|
33
|
+
# This script validates the contract (protocol §4 + output-shape fields)
|
|
34
34
|
# that makes the scenario enforceable. The gate logic itself is orchestrator-
|
|
35
35
|
# out-of-band (v1: informational; v2: mandatory). The test therefore checks:
|
|
36
36
|
# (a) developer.md output-shape contains flashcards_flagged field
|
|
37
37
|
# (b) qa.md output-shape contains flashcards_flagged field
|
|
38
|
-
# (c) protocol.md §
|
|
39
|
-
# (d) §
|
|
40
|
-
# (e) §
|
|
38
|
+
# (c) protocol.md §4 exists with the correct heading
|
|
39
|
+
# (d) §4 specifies the approve/reject processing rule
|
|
40
|
+
# (e) §4 specifies the worktree creation gate
|
|
41
41
|
# (f) live vs mirror diff is empty for all three files
|
|
42
42
|
|
|
43
43
|
DEV_MD="$REPO_ROOT/.claude/agents/developer.md"
|
|
@@ -62,25 +62,25 @@ else
|
|
|
62
62
|
fail "qa.md missing flashcards_flagged field in output-shape"
|
|
63
63
|
fi
|
|
64
64
|
|
|
65
|
-
# (c) protocol.md §
|
|
65
|
+
# (c) protocol.md §4 heading exists
|
|
66
66
|
if grep -q "^## 18. Immediate Flashcard Gate (v2)" "$PROTOCOL_MD"; then
|
|
67
67
|
pass "protocol.md contains ## 18. Immediate Flashcard Gate (v2)"
|
|
68
68
|
else
|
|
69
69
|
fail "protocol.md missing ## 18. Immediate Flashcard Gate (v2)"
|
|
70
70
|
fi
|
|
71
71
|
|
|
72
|
-
# (d) §
|
|
72
|
+
# (d) §4 specifies approve + reject processing
|
|
73
73
|
if grep -q "Approve" "$PROTOCOL_MD" && grep -q "Reject" "$PROTOCOL_MD"; then
|
|
74
|
-
pass "protocol §
|
|
74
|
+
pass "protocol §4 documents Approve/Reject processing rule"
|
|
75
75
|
else
|
|
76
|
-
fail "protocol §
|
|
76
|
+
fail "protocol §4 missing Approve/Reject processing rule"
|
|
77
77
|
fi
|
|
78
78
|
|
|
79
|
-
# (e) §
|
|
79
|
+
# (e) §4 specifies the worktree creation gate
|
|
80
80
|
if grep -q "Worktree creation gate\|worktree creation gate\|MUST NOT.*worktree\|worktree.*MUST NOT" "$PROTOCOL_MD"; then
|
|
81
|
-
pass "protocol §
|
|
81
|
+
pass "protocol §4 documents worktree creation gate"
|
|
82
82
|
else
|
|
83
|
-
fail "protocol §
|
|
83
|
+
fail "protocol §4 missing worktree creation gate rule"
|
|
84
84
|
fi
|
|
85
85
|
|
|
86
86
|
# (f) qa.md says QA list is additive to Developer's
|
|
@@ -90,18 +90,18 @@ else
|
|
|
90
90
|
fail "qa.md missing note that flashcards_flagged is additive"
|
|
91
91
|
fi
|
|
92
92
|
|
|
93
|
-
# (g) developer.md references protocol §
|
|
94
|
-
if grep -q "protocol §
|
|
95
|
-
pass "developer.md references protocol §
|
|
93
|
+
# (g) developer.md references protocol §4
|
|
94
|
+
if grep -q "protocol §4\|§4" "$DEV_MD"; then
|
|
95
|
+
pass "developer.md references protocol §4"
|
|
96
96
|
else
|
|
97
|
-
fail "developer.md does not reference protocol §
|
|
97
|
+
fail "developer.md does not reference protocol §4"
|
|
98
98
|
fi
|
|
99
99
|
|
|
100
|
-
# (h) qa.md references protocol §
|
|
101
|
-
if grep -q "protocol §
|
|
102
|
-
pass "qa.md references protocol §
|
|
100
|
+
# (h) qa.md references protocol §4
|
|
101
|
+
if grep -q "protocol §4\|§4" "$QA_MD"; then
|
|
102
|
+
pass "qa.md references protocol §4"
|
|
103
103
|
else
|
|
104
|
-
fail "qa.md does not reference protocol §
|
|
104
|
+
fail "qa.md does not reference protocol §4"
|
|
105
105
|
fi
|
|
106
106
|
|
|
107
107
|
# (i) three-surface diff: live developer.md vs mirror
|