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.
- package/CHANGELOG.md +210 -0
- package/README.md +22 -1
- package/dist/MANIFEST.json +276 -31
- package/dist/chunk-HZPJ5QX4.js +459 -0
- package/dist/chunk-HZPJ5QX4.js.map +1 -0
- package/dist/{chunk-OM4FAEA7.js → chunk-Q3BTSXCK.js} +69 -3
- package/dist/chunk-Q3BTSXCK.js.map +1 -0
- package/dist/cli.cjs +2888 -598
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2481 -619
- 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/lib/lifecycle-reconcile.cjs +497 -0
- package/dist/lib/lifecycle-reconcile.cjs.map +1 -0
- package/dist/lib/lifecycle-reconcile.d.cts +136 -0
- package/dist/lib/lifecycle-reconcile.d.ts +136 -0
- package/dist/lib/lifecycle-reconcile.js +20 -0
- package/dist/lib/lifecycle-reconcile.js.map +1 -0
- package/dist/templates/cleargate-planning/.claude/agents/architect.md +65 -10
- 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 +51 -2
- package/dist/templates/cleargate-planning/.claude/agents/devops.md +249 -0
- package/dist/templates/cleargate-planning/.claude/agents/qa.md +91 -1
- package/dist/templates/cleargate-planning/.claude/agents/reporter.md +72 -14
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +21 -0
- 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/stamp-and-gate.sh +12 -1
- package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +334 -96
- package/dist/templates/cleargate-planning/.claude/settings.json +4 -0
- package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +644 -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/mid-sprint-triage-rubric.md +160 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +72 -9
- 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 +471 -29
- package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +219 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +3 -3
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +86 -10
- 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/run_script.sh +173 -87
- package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +71 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +483 -13
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +482 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +32 -8
- package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +136 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +27 -1
- package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +35 -1
- package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +48 -14
- package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +40 -3
- package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +53 -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_context.md +8 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +23 -4
- package/dist/templates/cleargate-planning/.cleargate/templates/story.md +58 -3
- package/dist/templates/cleargate-planning/CLAUDE.md +30 -10
- package/dist/templates/cleargate-planning/MANIFEST.json +276 -31
- package/dist/{whoami-CX7CXJD5.js → whoami-W4U6DPVG.js} +17 -17
- package/dist/whoami-W4U6DPVG.js.map +1 -0
- package/package.json +20 -6
- package/templates/cleargate-planning/.claude/agents/architect.md +65 -10
- 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 +51 -2
- package/templates/cleargate-planning/.claude/agents/devops.md +249 -0
- package/templates/cleargate-planning/.claude/agents/qa.md +91 -1
- package/templates/cleargate-planning/.claude/agents/reporter.md +72 -14
- package/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +21 -0
- 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/stamp-and-gate.sh +12 -1
- package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +334 -96
- package/templates/cleargate-planning/.claude/settings.json +4 -0
- package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +644 -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/mid-sprint-triage-rubric.md +160 -0
- package/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +72 -9
- 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 +471 -29
- package/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +219 -0
- package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +3 -3
- package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +86 -10
- 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/run_script.sh +173 -87
- package/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +71 -0
- package/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +483 -13
- package/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +482 -0
- package/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +32 -8
- package/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +136 -0
- package/templates/cleargate-planning/.cleargate/templates/Bug.md +27 -1
- package/templates/cleargate-planning/.cleargate/templates/CR.md +35 -1
- package/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +48 -14
- package/templates/cleargate-planning/.cleargate/templates/epic.md +40 -3
- package/templates/cleargate-planning/.cleargate/templates/hotfix.md +53 -0
- package/templates/cleargate-planning/.cleargate/templates/initiative.md +98 -29
- package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +8 -0
- package/templates/cleargate-planning/.cleargate/templates/sprint_report.md +23 -4
- package/templates/cleargate-planning/.cleargate/templates/story.md +58 -3
- package/templates/cleargate-planning/CLAUDE.md +30 -10
- package/templates/cleargate-planning/MANIFEST.json +276 -31
- 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
|
@@ -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,456 @@ function atomicWrite(filePath, content) {
|
|
|
116
131
|
fs.renameSync(tmpFile, filePath);
|
|
117
132
|
}
|
|
118
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Read all ledger entries from a token-ledger.jsonl file.
|
|
136
|
+
* @param {string} ledgerPath
|
|
137
|
+
* @returns {{ work_item_id: string, agent_type: string, session_id?: string, sprint_id?: string }[]}
|
|
138
|
+
*/
|
|
139
|
+
function readLedgerEntries(ledgerPath) {
|
|
140
|
+
if (!fs.existsSync(ledgerPath)) return [];
|
|
141
|
+
const lines = fs.readFileSync(ledgerPath, 'utf8').split('\n').filter(l => l.trim());
|
|
142
|
+
const entries = [];
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
try {
|
|
145
|
+
const entry = JSON.parse(line);
|
|
146
|
+
if (entry.work_item_id && entry.agent_type) {
|
|
147
|
+
entries.push({
|
|
148
|
+
work_item_id: entry.work_item_id,
|
|
149
|
+
agent_type: entry.agent_type,
|
|
150
|
+
session_id: entry.session_id ?? '',
|
|
151
|
+
sprint_id: entry.sprint_id ?? '',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
} catch { /* skip malformed lines */ }
|
|
155
|
+
}
|
|
156
|
+
return entries;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if a (work_item_id|agent_type) bucket is a session-attribution artifact.
|
|
161
|
+
*
|
|
162
|
+
* Session-shared filter (CR-056): When multiple Architect (or other agent) dispatches run
|
|
163
|
+
* within the same session, the token ledger attributes them all to the same bucket keyed
|
|
164
|
+
* by the first-merged work_item_id. The canonical example is "CR-045 × architect" in
|
|
165
|
+
* SPRINT-23/SPRINT-24 — all 17 entries share the SAME session_id 48aa90c9-..., making
|
|
166
|
+
* them one session mis-attributed as 17 distinct repeats.
|
|
167
|
+
*
|
|
168
|
+
* Rule: session-attribution artifact if ALL entries with a known session_id share the
|
|
169
|
+
* SAME session_id (i.e., exactly 1 distinct session across all entries). When multiple
|
|
170
|
+
* distinct sessions are present, there is genuine independent repetition.
|
|
171
|
+
*
|
|
172
|
+
* Known false-positive class: "CR-045 × architect" — 17 entries, 1 session UUID.
|
|
173
|
+
*
|
|
174
|
+
* @param {{ session_id?: string }[]} entries all entries for this bucket (across sprints)
|
|
175
|
+
* @returns {boolean} true if bucket should be filtered as session-shared artifact
|
|
176
|
+
*/
|
|
177
|
+
function isSessionShared(entries) {
|
|
178
|
+
if (entries.length < 3) return false;
|
|
179
|
+
const distinctSessions = new Set(
|
|
180
|
+
entries.map(e => e.session_id ?? '').filter(s => s !== '')
|
|
181
|
+
);
|
|
182
|
+
// If exactly 1 distinct session accounts for all entries, this is a session-attribution artifact
|
|
183
|
+
return distinctSessions.size === 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Scan token-ledger.jsonl and FLASHCARD.md for skill creation candidates.
|
|
188
|
+
*
|
|
189
|
+
* Heuristic (CR-056 tightened):
|
|
190
|
+
* 1. Session-shared filter: if ≥2 of ≥3 entries for a bucket share the same
|
|
191
|
+
* session_id → skip (token-attribution artifact, not a real recurring pattern).
|
|
192
|
+
* Known false-positive class: "CR-045 × architect" from SPRINT-23/SPRINT-24 —
|
|
193
|
+
* all 17 entries share session_id 48aa90c9-... (session-shared).
|
|
194
|
+
* 2. Cross-sprint aggregation: collect entries from the current sprint's ledger PLUS
|
|
195
|
+
* prior sprints' ledgers (via CLEARGATE_SPRINT_RUNS_DIR). Count ≥3× total across
|
|
196
|
+
* ≥2 distinct sprints.
|
|
197
|
+
* 3. Cross-sprint dedup: if the candidate's hash already appears in any prior sprint's
|
|
198
|
+
* improvement-suggestions.md → surface as seen_in: [...] instead of re-flagging.
|
|
199
|
+
* 4. Threshold raised to "≥3× across ≥2 distinct sprints AND not session-shared".
|
|
200
|
+
*
|
|
201
|
+
* Appends or replaces the "## Skill Creation Candidates" section in improvement-suggestions.md.
|
|
202
|
+
* @param {string} sprintId
|
|
203
|
+
* @param {string} sprintDir
|
|
204
|
+
* @param {string} suggestionsFile
|
|
205
|
+
*/
|
|
206
|
+
function scanSkillCandidates(sprintId, sprintDir, suggestionsFile) {
|
|
207
|
+
const flashcardPath = process.env.CLEARGATE_FLASHCARD_PATH
|
|
208
|
+
? path.resolve(process.env.CLEARGATE_FLASHCARD_PATH)
|
|
209
|
+
: path.join(REPO_ROOT, '.cleargate', 'FLASHCARD.md');
|
|
210
|
+
const ledgerPath = path.join(sprintDir, 'token-ledger.jsonl');
|
|
211
|
+
|
|
212
|
+
// Resolve sprint-runs root (used for cross-sprint lookback)
|
|
213
|
+
const sprintRunsDir = process.env.CLEARGATE_SPRINT_RUNS_DIR
|
|
214
|
+
? path.resolve(process.env.CLEARGATE_SPRINT_RUNS_DIR)
|
|
215
|
+
: path.dirname(sprintDir);
|
|
216
|
+
|
|
217
|
+
// ── Step 1: collect all ledger entries across current + prior sprints ─────────
|
|
218
|
+
// Collect from current sprint
|
|
219
|
+
const currentEntries = readLedgerEntries(ledgerPath);
|
|
220
|
+
|
|
221
|
+
// Collect from prior sprints (all sprint dirs except current)
|
|
222
|
+
const allPriorEntries = [];
|
|
223
|
+
const priorSuggestionsContents = [];
|
|
224
|
+
if (fs.existsSync(sprintRunsDir)) {
|
|
225
|
+
let priorDirs;
|
|
226
|
+
try {
|
|
227
|
+
priorDirs = fs.readdirSync(sprintRunsDir)
|
|
228
|
+
.filter(name => name !== sprintId && !name.startsWith('.'))
|
|
229
|
+
.map(name => path.join(sprintRunsDir, name))
|
|
230
|
+
.filter(p => {
|
|
231
|
+
try { return fs.statSync(p).isDirectory(); } catch { return false; }
|
|
232
|
+
});
|
|
233
|
+
} catch { priorDirs = []; }
|
|
234
|
+
|
|
235
|
+
for (const priorDir of priorDirs) {
|
|
236
|
+
const priorLedger = path.join(priorDir, 'token-ledger.jsonl');
|
|
237
|
+
const entries = readLedgerEntries(priorLedger);
|
|
238
|
+
allPriorEntries.push(...entries);
|
|
239
|
+
|
|
240
|
+
// Collect prior improvement-suggestions.md for cross-sprint dedup
|
|
241
|
+
const priorSuggFile = path.join(priorDir, 'improvement-suggestions.md');
|
|
242
|
+
if (fs.existsSync(priorSuggFile)) {
|
|
243
|
+
try {
|
|
244
|
+
priorSuggestionsContents.push(fs.readFileSync(priorSuggFile, 'utf8'));
|
|
245
|
+
} catch { /* skip unreadable */ }
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Step 2: build cross-sprint bucket map ────────────────────────────────────
|
|
251
|
+
// Map: key (work_item_id|agent_type) → { entries: [...], sprintIds: Set<string> }
|
|
252
|
+
/** @type {Map<string, { entries: { session_id?: string, sprint_id?: string }[], sprintIds: Set<string> }>} */
|
|
253
|
+
const buckets = new Map();
|
|
254
|
+
|
|
255
|
+
for (const e of currentEntries) {
|
|
256
|
+
const key = `${e.work_item_id}|${e.agent_type}`;
|
|
257
|
+
if (!buckets.has(key)) buckets.set(key, { entries: [], sprintIds: new Set() });
|
|
258
|
+
const b = buckets.get(key);
|
|
259
|
+
b.entries.push(e);
|
|
260
|
+
b.sprintIds.add(sprintId);
|
|
261
|
+
}
|
|
262
|
+
for (const e of allPriorEntries) {
|
|
263
|
+
const key = `${e.work_item_id}|${e.agent_type}`;
|
|
264
|
+
if (!buckets.has(key)) buckets.set(key, { entries: [], sprintIds: new Set() });
|
|
265
|
+
const b = buckets.get(key);
|
|
266
|
+
b.entries.push(e);
|
|
267
|
+
if (e.sprint_id) b.sprintIds.add(e.sprint_id);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Step 3: apply heuristic filters ──────────────────────────────────────────
|
|
271
|
+
// Threshold: ≥3× total AND ≥2 distinct sprints AND NOT session-shared
|
|
272
|
+
const repeatedTuples = [...buckets.entries()].filter(([, b]) => {
|
|
273
|
+
if (b.entries.length < 3) return false;
|
|
274
|
+
if (b.sprintIds.size < 2) return false;
|
|
275
|
+
if (isSessionShared(b.entries)) return false;
|
|
276
|
+
return true;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Grep FLASHCARD.md for "also do" patterns
|
|
280
|
+
const alsoDoMatches = [];
|
|
281
|
+
if (fs.existsSync(flashcardPath)) {
|
|
282
|
+
const fcContent = fs.readFileSync(flashcardPath, 'utf8');
|
|
283
|
+
const lines = fcContent.split('\n');
|
|
284
|
+
for (const line of lines) {
|
|
285
|
+
if (/remember to also|also do X|also need to/i.test(line)) {
|
|
286
|
+
alsoDoMatches.push(line.trim());
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Read existing suggestions file content (current sprint)
|
|
292
|
+
let existingContent = fs.existsSync(suggestionsFile)
|
|
293
|
+
? fs.readFileSync(suggestionsFile, 'utf8')
|
|
294
|
+
: `# Improvement Suggestions — ${sprintId}\n\nGenerated by \`suggest_improvements.mjs\`. Append-only; IDs are stable.\nVocabulary: Templates | Handoffs | Skills | Process | Tooling\n\n---\n\n`;
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Check if a hash already appears in current sprint's suggestions OR any prior sprint's.
|
|
298
|
+
* @param {string} hash
|
|
299
|
+
* @returns {boolean}
|
|
300
|
+
*/
|
|
301
|
+
function hashAlreadySeen(hash) {
|
|
302
|
+
const marker = `<!-- hash:${hash} -->`;
|
|
303
|
+
if (existingContent.includes(marker)) return true;
|
|
304
|
+
for (const priorContent of priorSuggestionsContents) {
|
|
305
|
+
if (priorContent.includes(marker)) return true;
|
|
306
|
+
}
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Build the candidates
|
|
311
|
+
const candidates = [];
|
|
312
|
+
let candN = 1;
|
|
313
|
+
for (const [key, bucket] of repeatedTuples) {
|
|
314
|
+
const [workItemId, agentType] = key.split('|');
|
|
315
|
+
const candId = `CAND-${sprintId}-S${String(candN).padStart(2, '0')}`;
|
|
316
|
+
const hashKey = `skill|${key}`;
|
|
317
|
+
const hash = stableHash(hashKey);
|
|
318
|
+
if (!hashAlreadySeen(hash)) {
|
|
319
|
+
candidates.push({ candId, hash, workItemId, agentType, source: 'ledger', sprintIds: bucket.sprintIds });
|
|
320
|
+
candN++;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
for (const line of alsoDoMatches) {
|
|
324
|
+
const candId = `CAND-${sprintId}-S${String(candN).padStart(2, '0')}`;
|
|
325
|
+
const hashKey = `skill|flashcard|${line.slice(0, 60)}`;
|
|
326
|
+
const hash = stableHash(hashKey);
|
|
327
|
+
if (!hashAlreadySeen(hash)) {
|
|
328
|
+
candidates.push({ candId, hash, workItemId: null, agentType: null, source: 'flashcard', line, sprintIds: new Set() });
|
|
329
|
+
candN++;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Build the section content
|
|
334
|
+
const sectionLines = [
|
|
335
|
+
'## Skill Creation Candidates',
|
|
336
|
+
'',
|
|
337
|
+
'<!-- generated-by: suggest_improvements.mjs --skill-candidates -->',
|
|
338
|
+
'',
|
|
339
|
+
];
|
|
340
|
+
|
|
341
|
+
if (candidates.length === 0) {
|
|
342
|
+
sectionLines.push('_No candidates detected this sprint._');
|
|
343
|
+
sectionLines.push('');
|
|
344
|
+
} else {
|
|
345
|
+
for (const c of candidates) {
|
|
346
|
+
sectionLines.push(`### ${c.candId}: ${c.source === 'ledger' ? `${c.workItemId} × ${c.agentType}` : 'flashcard pattern'}`);
|
|
347
|
+
sectionLines.push(`<!-- hash:${c.hash} -->`);
|
|
348
|
+
sectionLines.push('');
|
|
349
|
+
if (c.source === 'ledger') {
|
|
350
|
+
const sprintList = c.sprintIds ? [...c.sprintIds].sort().join(', ') : sprintId;
|
|
351
|
+
sectionLines.push(`**Pattern detected:** ${c.workItemId} × ${c.agentType} repeated ≥3× across ≥2 distinct sprints (${sprintList})`);
|
|
352
|
+
} else {
|
|
353
|
+
sectionLines.push(`**Pattern detected:** "also do" pattern in FLASHCARD.md`);
|
|
354
|
+
sectionLines.push(`**Source line:** \`${c.line}\``);
|
|
355
|
+
}
|
|
356
|
+
sectionLines.push(`**Proposed skill:** \`.claude/skills/<slug>/SKILL.md\``);
|
|
357
|
+
sectionLines.push('');
|
|
358
|
+
sectionLines.push('---');
|
|
359
|
+
sectionLines.push('');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const sectionContent = sectionLines.join('\n');
|
|
364
|
+
// Anchored heading regex — strict match, no trailing text
|
|
365
|
+
const headingRe = /^## Skill Creation Candidates$/m;
|
|
366
|
+
|
|
367
|
+
let finalContent;
|
|
368
|
+
if (headingRe.test(existingContent)) {
|
|
369
|
+
// Replace existing section (splice out old, append new)
|
|
370
|
+
const nextHeadingRe = /^## /m;
|
|
371
|
+
const headingIdx = existingContent.search(headingRe);
|
|
372
|
+
const afterHeading = existingContent.slice(headingIdx + existingContent.match(headingRe)[0].length);
|
|
373
|
+
const nextMatch = afterHeading.search(/^## /m);
|
|
374
|
+
if (nextMatch === -1) {
|
|
375
|
+
finalContent = existingContent.slice(0, headingIdx).trimEnd() + '\n\n' + sectionContent;
|
|
376
|
+
} else {
|
|
377
|
+
finalContent = existingContent.slice(0, headingIdx).trimEnd() + '\n\n' + sectionContent + '\n' + afterHeading.slice(nextMatch);
|
|
378
|
+
}
|
|
379
|
+
void nextHeadingRe;
|
|
380
|
+
} else {
|
|
381
|
+
finalContent = existingContent.trimEnd() + '\n\n' + sectionContent;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
atomicWrite(suggestionsFile, finalContent);
|
|
385
|
+
process.stdout.write(`suggest_improvements: skill-candidates section written to ${suggestionsFile}\n`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Scan FLASHCARD.md for cleanup candidates (stale / superseded / resolved).
|
|
390
|
+
* Appends or replaces the "## FLASHCARD Cleanup Candidates" section in improvement-suggestions.md.
|
|
391
|
+
* @param {string} sprintId
|
|
392
|
+
* @param {string} sprintDir
|
|
393
|
+
* @param {string} suggestionsFile
|
|
394
|
+
*/
|
|
395
|
+
function scanFlashcardCleanup(sprintId, sprintDir, suggestionsFile) {
|
|
396
|
+
const flashcardPath = process.env.CLEARGATE_FLASHCARD_PATH
|
|
397
|
+
? path.resolve(process.env.CLEARGATE_FLASHCARD_PATH)
|
|
398
|
+
: path.join(REPO_ROOT, '.cleargate', 'FLASHCARD.md');
|
|
399
|
+
|
|
400
|
+
const lookback = parseInt(process.env.CLEARGATE_FLASHCARD_LOOKBACK ?? '3', 10);
|
|
401
|
+
const sprintRunsDir = process.env.CLEARGATE_SPRINT_RUNS_DIR
|
|
402
|
+
? path.resolve(process.env.CLEARGATE_SPRINT_RUNS_DIR)
|
|
403
|
+
: path.dirname(sprintDir);
|
|
404
|
+
|
|
405
|
+
if (!fs.existsSync(flashcardPath)) {
|
|
406
|
+
process.stderr.write(`suggest_improvements --flashcard-cleanup: FLASHCARD.md not found at ${flashcardPath}, skipping\n`);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const fcContent = fs.readFileSync(flashcardPath, 'utf8');
|
|
411
|
+
// Parse entries: YYYY-MM-DD · #tag1 #tag2 · lesson
|
|
412
|
+
const entryRe = /^(\d{4}-\d{2}-\d{2})\s+·\s+(.*?)\s+·\s+(.+)$/;
|
|
413
|
+
const entries = fcContent.split('\n').filter(l => entryRe.test(l.trim()));
|
|
414
|
+
|
|
415
|
+
// Determine lookback sprint dirs (numerical extraction from sprintId)
|
|
416
|
+
const numMatch = sprintId.match(/(\d+)$/);
|
|
417
|
+
const currentNum = numMatch ? parseInt(numMatch[1], 10) : null;
|
|
418
|
+
const sprintPrefix = numMatch ? sprintId.replace(/\d+$/, '') : null;
|
|
419
|
+
|
|
420
|
+
// Collect prior sprint REPORT.md content for "resolved" detection
|
|
421
|
+
const priorReportContent = [];
|
|
422
|
+
if (currentNum !== null && sprintPrefix !== null) {
|
|
423
|
+
for (let i = 1; i <= lookback; i++) {
|
|
424
|
+
const priorId = `${sprintPrefix}${currentNum - i}`;
|
|
425
|
+
const priorDir = path.join(sprintRunsDir, priorId);
|
|
426
|
+
if (!fs.existsSync(priorDir)) continue;
|
|
427
|
+
try {
|
|
428
|
+
const rFile = reportFilename(priorDir, priorId, { forRead: true });
|
|
429
|
+
if (fs.existsSync(rFile)) {
|
|
430
|
+
priorReportContent.push(fs.readFileSync(rFile, 'utf8'));
|
|
431
|
+
}
|
|
432
|
+
} catch { /* skip missing */ }
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Collect prior sprint dirs for stale detection
|
|
437
|
+
const priorSprintDirs = [];
|
|
438
|
+
if (currentNum !== null && sprintPrefix !== null) {
|
|
439
|
+
for (let i = 1; i <= lookback; i++) {
|
|
440
|
+
const priorId = `${sprintPrefix}${currentNum - i}`;
|
|
441
|
+
const priorDir = path.join(sprintRunsDir, priorId);
|
|
442
|
+
if (fs.existsSync(priorDir)) priorSprintDirs.push(priorDir);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** @type {{ entry: string, date: string, tags: string, lesson: string, category: 'stale'|'superseded'|'resolved', reason: string }[]} */
|
|
447
|
+
const candidates = [];
|
|
448
|
+
const processedHashes = new Set();
|
|
449
|
+
|
|
450
|
+
for (const rawEntry of entries) {
|
|
451
|
+
const m = rawEntry.trim().match(entryRe);
|
|
452
|
+
if (!m) continue;
|
|
453
|
+
const [, date, tags, lesson] = m;
|
|
454
|
+
const entry = rawEntry.trim();
|
|
455
|
+
const hashKey = `flashcard|${entry.slice(0, 80)}`;
|
|
456
|
+
const hash = stableHash(hashKey);
|
|
457
|
+
if (processedHashes.has(hash)) continue;
|
|
458
|
+
processedHashes.add(hash);
|
|
459
|
+
|
|
460
|
+
// Extract first keyword from lesson (first word-run, stripping punctuation)
|
|
461
|
+
const keyword = lesson.split(/\s+/)[0].replace(/[^a-zA-Z0-9_-]/g, '').toLowerCase();
|
|
462
|
+
if (!keyword) continue;
|
|
463
|
+
|
|
464
|
+
// Check "resolved": keyword appears in §6 Tooling "Resolved" of a prior REPORT.md
|
|
465
|
+
let resolved = false;
|
|
466
|
+
for (const rc of priorReportContent) {
|
|
467
|
+
const resolvedSection = rc.match(/##\s*§6[^\n]*\n([\s\S]*?)(?=##\s*§|$)/);
|
|
468
|
+
if (resolvedSection && resolvedSection[1].toLowerCase().includes(keyword)) {
|
|
469
|
+
resolved = true;
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (resolved) {
|
|
474
|
+
candidates.push({ entry, date, tags, lesson, category: 'resolved', reason: 'keyword found in a prior §6 Tooling section' });
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Check "stale": zero grep hits for keyword across prior sprint dirs
|
|
479
|
+
if (priorSprintDirs.length > 0) {
|
|
480
|
+
let hitCount = 0;
|
|
481
|
+
for (const priorDir of priorSprintDirs) {
|
|
482
|
+
try {
|
|
483
|
+
const files = fs.readdirSync(priorDir);
|
|
484
|
+
for (const f of files) {
|
|
485
|
+
const fPath = path.join(priorDir, f);
|
|
486
|
+
if (fs.statSync(fPath).isFile()) {
|
|
487
|
+
const content = fs.readFileSync(fPath, 'utf8');
|
|
488
|
+
if (content.toLowerCase().includes(keyword)) { hitCount++; break; }
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
} catch { /* skip unreadable dirs */ }
|
|
492
|
+
}
|
|
493
|
+
if (hitCount === 0) {
|
|
494
|
+
candidates.push({ entry, date, tags, lesson, category: 'stale', reason: `stale: zero grep hits across last ${priorSprintDirs.length} sprint dir(s)` });
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Read existing suggestions file content
|
|
501
|
+
let existingContent = fs.existsSync(suggestionsFile)
|
|
502
|
+
? fs.readFileSync(suggestionsFile, 'utf8')
|
|
503
|
+
: `# Improvement Suggestions — ${sprintId}\n\nGenerated by \`suggest_improvements.mjs\`. Append-only; IDs are stable.\nVocabulary: Templates | Handoffs | Skills | Process | Tooling\n\n---\n\n`;
|
|
504
|
+
|
|
505
|
+
// Build section
|
|
506
|
+
const sectionLines = [
|
|
507
|
+
'## FLASHCARD Cleanup Candidates',
|
|
508
|
+
'',
|
|
509
|
+
'<!-- generated-by: suggest_improvements.mjs --flashcard-cleanup -->',
|
|
510
|
+
'',
|
|
511
|
+
];
|
|
512
|
+
|
|
513
|
+
// Filter out already-captured candidates
|
|
514
|
+
const newCandidates = candidates.filter(c => {
|
|
515
|
+
const hash = stableHash(`flashcard|${c.entry.slice(0, 80)}`);
|
|
516
|
+
return !existingContent.includes(`<!-- hash:${hash} -->`);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
if (newCandidates.length === 0) {
|
|
520
|
+
sectionLines.push('_No candidates detected this sprint._');
|
|
521
|
+
sectionLines.push('');
|
|
522
|
+
} else {
|
|
523
|
+
newCandidates.forEach((c, i) => {
|
|
524
|
+
const candId = `CAND-${sprintId}-F${String(i + 1).padStart(2, '0')}`;
|
|
525
|
+
const hash = stableHash(`flashcard|${c.entry.slice(0, 80)}`);
|
|
526
|
+
sectionLines.push(`### ${candId}: ${c.lesson.slice(0, 60)}`);
|
|
527
|
+
sectionLines.push(`<!-- hash:${hash} -->`);
|
|
528
|
+
sectionLines.push('');
|
|
529
|
+
sectionLines.push(`**Category:** ${c.category}`);
|
|
530
|
+
sectionLines.push(`**Reason:** ${c.reason}`);
|
|
531
|
+
sectionLines.push(`**Original entry:** \`${c.entry}\``);
|
|
532
|
+
sectionLines.push('**Suggested action:** approve to remove via `cleargate flashcard prune` (run /improve)');
|
|
533
|
+
sectionLines.push('');
|
|
534
|
+
sectionLines.push('---');
|
|
535
|
+
sectionLines.push('');
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const sectionContent = sectionLines.join('\n');
|
|
540
|
+
// Anchored heading regex — strict match, no trailing text
|
|
541
|
+
const headingRe = /^## FLASHCARD Cleanup Candidates$/m;
|
|
542
|
+
|
|
543
|
+
let finalContent;
|
|
544
|
+
if (headingRe.test(existingContent)) {
|
|
545
|
+
const headingIdx = existingContent.search(headingRe);
|
|
546
|
+
const afterHeading = existingContent.slice(headingIdx + existingContent.match(headingRe)[0].length);
|
|
547
|
+
const nextMatch = afterHeading.search(/^## /m);
|
|
548
|
+
if (nextMatch === -1) {
|
|
549
|
+
finalContent = existingContent.slice(0, headingIdx).trimEnd() + '\n\n' + sectionContent;
|
|
550
|
+
} else {
|
|
551
|
+
finalContent = existingContent.slice(0, headingIdx).trimEnd() + '\n\n' + sectionContent + '\n' + afterHeading.slice(nextMatch);
|
|
552
|
+
}
|
|
553
|
+
} else {
|
|
554
|
+
finalContent = existingContent.trimEnd() + '\n\n' + sectionContent;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
atomicWrite(suggestionsFile, finalContent);
|
|
558
|
+
process.stdout.write(`suggest_improvements: flashcard-cleanup section written to ${suggestionsFile}\n`);
|
|
559
|
+
}
|
|
560
|
+
|
|
119
561
|
function main() {
|
|
120
562
|
const args = process.argv.slice(2);
|
|
121
563
|
if (args.length < 1) {
|
|
122
|
-
process.stderr.write('Usage: node suggest_improvements.mjs <sprint-id
|
|
564
|
+
process.stderr.write('Usage: node suggest_improvements.mjs <sprint-id> [--skill-candidates | --flashcard-cleanup]\n');
|
|
565
|
+
process.exit(2);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Flag-aware parse: detect flags first, then extract sprintId from positional args.
|
|
569
|
+
// Sprint IDs match ^SPRINT-\d+$ so they never start with '--'; no collision possible.
|
|
570
|
+
const skillCandidatesMode = args.includes('--skill-candidates');
|
|
571
|
+
const flashcardCleanupMode = args.includes('--flashcard-cleanup');
|
|
572
|
+
|
|
573
|
+
if (skillCandidatesMode && flashcardCleanupMode) {
|
|
574
|
+
process.stderr.write('Error: --skill-candidates and --flashcard-cleanup are mutually exclusive\n');
|
|
575
|
+
process.exit(2);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const sprintId = args.find(a => !a.startsWith('--'));
|
|
579
|
+
if (!sprintId) {
|
|
580
|
+
process.stderr.write('Usage: node suggest_improvements.mjs <sprint-id> [--skill-candidates | --flashcard-cleanup]\n');
|
|
123
581
|
process.exit(2);
|
|
124
582
|
}
|
|
125
583
|
|
|
126
|
-
const sprintId = args[0];
|
|
127
584
|
const sprintDir = process.env.CLEARGATE_SPRINT_DIR
|
|
128
585
|
? path.resolve(process.env.CLEARGATE_SPRINT_DIR)
|
|
129
586
|
: path.join(REPO_ROOT, '.cleargate', 'sprint-runs', sprintId);
|
|
@@ -133,13 +590,26 @@ function main() {
|
|
|
133
590
|
process.exit(1);
|
|
134
591
|
}
|
|
135
592
|
|
|
136
|
-
const reportFile =
|
|
593
|
+
const reportFile = reportFilename(sprintDir, sprintId, { forRead: true });
|
|
137
594
|
const suggestionsFile = path.join(sprintDir, 'improvement-suggestions.md');
|
|
138
595
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
596
|
+
// Default mode requires REPORT.md; flag-driven modes do not.
|
|
597
|
+
if (!skillCandidatesMode && !flashcardCleanupMode) {
|
|
598
|
+
if (!fs.existsSync(reportFile)) {
|
|
599
|
+
process.stderr.write(`Error: report file not found at ${reportFile}\n`);
|
|
600
|
+
process.stderr.write('Run the Reporter agent first to generate the report.\n');
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Route flag-driven modes before default processing
|
|
606
|
+
if (skillCandidatesMode) {
|
|
607
|
+
scanSkillCandidates(sprintId, sprintDir, suggestionsFile);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (flashcardCleanupMode) {
|
|
611
|
+
scanFlashcardCleanup(sprintId, sprintDir, suggestionsFile);
|
|
612
|
+
return;
|
|
143
613
|
}
|
|
144
614
|
|
|
145
615
|
const reportContent = fs.readFileSync(reportFile, 'utf8');
|