cleargate 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/MANIFEST.json +72 -16
  3. package/dist/admin-api/index.cjs +0 -1
  4. package/dist/admin-api/index.js +1 -2
  5. package/dist/auth/factory.cjs +0 -1
  6. package/dist/auth/factory.js +2 -3
  7. package/dist/auth/require-token.cjs +0 -1
  8. package/dist/auth/require-token.js +1 -2
  9. package/dist/auth/token-store.cjs +0 -1
  10. package/dist/auth/token-store.js +1 -2
  11. package/dist/{bootstrap-root-QKSA5V75.js → bootstrap-root-2H5HVTCC.js} +1 -2
  12. package/dist/{chunk-PDE37WFQ.js → chunk-A7MSQUU7.js} +2 -3
  13. package/dist/{chunk-BTSZOEWC.js → chunk-P6KEDAK2.js} +0 -1
  14. package/dist/{chunk-E3X7IE5E.js → chunk-PY6FHGV5.js} +1 -2
  15. package/dist/{chunk-5DI2Z3C2.js → chunk-Y53ZZYYU.js} +1 -2
  16. package/dist/cli.cjs +1564 -1414
  17. package/dist/cli.js +1514 -1364
  18. package/dist/lib/ledger.cjs +0 -1
  19. package/dist/lib/ledger.js +1 -2
  20. package/dist/lib/lifecycle-reconcile.cjs +0 -1
  21. package/dist/lib/lifecycle-reconcile.js +2 -3
  22. package/dist/{whoami-EANGN46Z.js → whoami-JKQQPABQ.js} +3 -4
  23. package/package.json +4 -3
  24. package/templates/cleargate-planning/.claude/agents/architect-synth.md +2 -0
  25. package/templates/cleargate-planning/.claude/agents/architect.md +4 -2
  26. package/templates/cleargate-planning/.claude/agents/developer.md +4 -11
  27. package/templates/cleargate-planning/.claude/agents/qa.md +14 -6
  28. package/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +2 -2
  29. package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +19 -1
  30. package/templates/cleargate-planning/.cleargate/config.example.yml +16 -0
  31. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.deferred-verify.red.node.test.ts +245 -0
  32. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +227 -0
  33. package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +5 -4
  34. package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +75 -2
  35. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +48 -0
  36. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +57 -1
  37. package/templates/cleargate-planning/.cleargate/scripts/provision_worktree_config.sh +155 -0
  38. package/templates/cleargate-planning/.cleargate/scripts/qa_red_lint.mjs +380 -0
  39. package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +34 -1
  40. package/templates/cleargate-planning/.cleargate/scripts/test/cr077_eviction.red.sh +113 -0
  41. package/templates/cleargate-planning/.cleargate/scripts/test/cr078_init.test.sh +309 -0
  42. package/templates/cleargate-planning/.cleargate/scripts/test/cr079_provision.red.sh +262 -0
  43. package/templates/cleargate-planning/.cleargate/scripts/test/cr080_wrapper.test.sh +177 -0
  44. package/templates/cleargate-planning/.cleargate/scripts/test/cr081_qa_red_lint.red.sh +348 -0
  45. package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/.session-totals.json +1 -0
  46. package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/token-ledger.jsonl +222 -0
  47. package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +17 -0
  48. package/templates/cleargate-planning/.cleargate/templates/story.md +1 -0
  49. package/templates/cleargate-planning/MANIFEST.json +72 -16
  50. package/dist/admin-api/index.cjs.map +0 -1
  51. package/dist/admin-api/index.js.map +0 -1
  52. package/dist/auth/factory.cjs.map +0 -1
  53. package/dist/auth/factory.js.map +0 -1
  54. package/dist/auth/require-token.cjs.map +0 -1
  55. package/dist/auth/require-token.js.map +0 -1
  56. package/dist/auth/token-store.cjs.map +0 -1
  57. package/dist/auth/token-store.js.map +0 -1
  58. package/dist/bootstrap-root-QKSA5V75.js.map +0 -1
  59. package/dist/chunk-5DI2Z3C2.js.map +0 -1
  60. package/dist/chunk-BTSZOEWC.js.map +0 -1
  61. package/dist/chunk-E3X7IE5E.js.map +0 -1
  62. package/dist/chunk-PDE37WFQ.js.map +0 -1
  63. package/dist/cli.cjs.map +0 -1
  64. package/dist/cli.js.map +0 -1
  65. package/dist/lib/ledger.cjs.map +0 -1
  66. package/dist/lib/ledger.js.map +0 -1
  67. package/dist/lib/lifecycle-reconcile.cjs.map +0 -1
  68. package/dist/lib/lifecycle-reconcile.js.map +0 -1
  69. package/dist/templates/cleargate-planning/.claude/agents/architect-reader.md +0 -61
  70. package/dist/templates/cleargate-planning/.claude/agents/architect-synth.md +0 -124
  71. package/dist/templates/cleargate-planning/.claude/agents/architect.md +0 -230
  72. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +0 -108
  73. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +0 -194
  74. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +0 -261
  75. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-query.md +0 -143
  76. package/dist/templates/cleargate-planning/.claude/agents/developer.md +0 -185
  77. package/dist/templates/cleargate-planning/.claude/agents/devops.md +0 -257
  78. package/dist/templates/cleargate-planning/.claude/agents/qa.md +0 -171
  79. package/dist/templates/cleargate-planning/.claude/agents/reporter.md +0 -274
  80. package/dist/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +0 -209
  81. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +0 -33
  82. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +0 -58
  83. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit.sh +0 -19
  84. package/dist/templates/cleargate-planning/.claude/hooks/pre-edit-gate.sh +0 -162
  85. package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-autonomy.sh +0 -58
  86. package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +0 -148
  87. package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +0 -75
  88. package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +0 -43
  89. package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +0 -590
  90. package/dist/templates/cleargate-planning/.claude/settings.json +0 -68
  91. package/dist/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +0 -102
  92. package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +0 -742
  93. package/dist/templates/cleargate-planning/.cleargate/FLASHCARD.md +0 -7
  94. package/dist/templates/cleargate-planning/.cleargate/config.example.yml +0 -67
  95. package/dist/templates/cleargate-planning/.cleargate/config.yml +0 -18
  96. package/dist/templates/cleargate-planning/.cleargate/delivery/archive/.gitkeep +0 -0
  97. package/dist/templates/cleargate-planning/.cleargate/delivery/pending-sync/.gitkeep +0 -0
  98. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +0 -551
  99. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +0 -878
  100. package/dist/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +0 -160
  101. package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +0 -213
  102. package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +0 -71
  103. package/dist/templates/cleargate-planning/.cleargate/scripts/_migrate-schema-v3.mjs +0 -120
  104. package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +0 -265
  105. package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +0 -1012
  106. package/dist/templates/cleargate-planning/.cleargate/scripts/collision_surface.sh +0 -114
  107. package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +0 -62
  108. package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +0 -219
  109. package/dist/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +0 -320
  110. package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +0 -15
  111. package/dist/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +0 -38
  112. package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +0 -240
  113. package/dist/templates/cleargate-planning/.cleargate/scripts/launch_wave.mjs +0 -341
  114. package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +0 -54
  115. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +0 -206
  116. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +0 -371
  117. package/dist/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +0 -280
  118. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +0 -378
  119. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +0 -888
  120. package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +0 -209
  121. package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +0 -71
  122. package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +0 -127
  123. package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +0 -717
  124. package/dist/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +0 -27
  125. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +0 -261
  126. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +0 -210
  127. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +0 -190
  128. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +0 -482
  129. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +0 -327
  130. package/dist/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +0 -261
  131. package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +0 -246
  132. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +0 -111
  133. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +0 -184
  134. package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +0 -172
  135. package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +0 -126
  136. package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +0 -130
  137. package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +0 -137
  138. package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +0 -166
  139. package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +0 -111
  140. package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +0 -122
  141. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +0 -50
  142. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +0 -224
  143. package/dist/templates/cleargate-planning/.cleargate/templates/story.md +0 -213
  144. package/dist/templates/cleargate-planning/CLAUDE.md +0 -66
  145. package/dist/templates/cleargate-planning/MANIFEST.json +0 -503
  146. package/dist/templates/synthesis/active-sprint.md +0 -30
  147. package/dist/templates/synthesis/open-gates.md +0 -38
  148. package/dist/templates/synthesis/product-state.md +0 -31
  149. package/dist/templates/synthesis/roadmap.md +0 -63
  150. package/dist/whoami-EANGN46Z.js.map +0 -1
@@ -1,717 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * suggest_improvements.mjs — Generate stable improvement suggestions from REPORT.md
4
- *
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
8
- *
9
- * Reads:
10
- * - .cleargate/sprint-runs/<id>/REPORT.md §5 Framework Self-Assessment tables
11
- * - .cleargate/sprint-runs/<prev-id>/improvement-suggestions.md (if present, for context)
12
- *
13
- * Emits:
14
- * - .cleargate/sprint-runs/<id>/improvement-suggestions.md
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)
18
- *
19
- * Append-only idempotency (R5):
20
- * - IDs are derived from a stable hash of (category, title) tuple
21
- * - Re-running produces zero new entries if all suggestions already captured
22
- * - Script exits 0 in both cases
23
- *
24
- * Note: "section" as used in §5 table extraction refers to the Framework Self-Assessment
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
31
- */
32
-
33
- import fs from 'node:fs';
34
- import path from 'node:path';
35
- import { fileURLToPath } from 'node:url';
36
- import { createHash } from 'node:crypto';
37
- import { reportFilename } from './lib/report-filename.mjs';
38
-
39
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
40
- const REPO_ROOT = path.resolve(__dirname, '..', '..');
41
-
42
- // §5 subsection names used in sprint_report.md template
43
- const SELF_ASSESSMENT_SECTIONS = ['Templates', 'Handoffs', 'Skills', 'Process', 'Tooling'];
44
-
45
- /**
46
- * Generate a stable short hash from a string.
47
- * Used for SUG ID stability — same (category, title) always produces same ID.
48
- * @param {string} input
49
- * @returns {string} 6-char hex
50
- */
51
- function stableHash(input) {
52
- return createHash('sha256').update(input).digest('hex').slice(0, 6);
53
- }
54
-
55
- /**
56
- * Parse §5 Framework Self-Assessment from REPORT.md content.
57
- * Extracts Yellow/Red-rated rows as improvement candidates.
58
- * @param {string} content
59
- * @returns {{ category: string, item: string, rating: string, notes: string }[]}
60
- */
61
- function parseSelfAssessment(content) {
62
- const suggestions = [];
63
-
64
- // Find the §5 Framework Self-Assessment section
65
- const selfAssessmentMatch = content.match(/##\s*§5[^\n]*\n([\s\S]*?)(?=##\s*§6|$)/);
66
- if (!selfAssessmentMatch) return suggestions;
67
-
68
- const sectionContent = selfAssessmentMatch[1];
69
-
70
- // Extract each subsection table
71
- for (const category of SELF_ASSESSMENT_SECTIONS) {
72
- // Find table rows under the category header
73
- // Pattern: ### <category>\n | <item> | <rating> | <notes> |
74
- const categoryMatch = sectionContent.match(
75
- new RegExp(`###\\s+${category}\\s*\\n([\\s\\S]*?)(?=###|$)`, 'i')
76
- );
77
- if (!categoryMatch) continue;
78
-
79
- const tableContent = categoryMatch[1];
80
- // Parse table rows (skip header rows with ---)
81
- const rows = tableContent.split('\n').filter(l => l.startsWith('|') && !l.includes('---'));
82
-
83
- for (const row of rows) {
84
- const cells = row.split('|').map(c => c.trim()).filter(Boolean);
85
- if (cells.length < 2) continue;
86
-
87
- const item = cells[0];
88
- const rating = cells[1] || '';
89
- const notes = cells[2] || '';
90
-
91
- // Only flag Yellow or Red items as needing improvement
92
- if (rating.toLowerCase().includes('yellow') || rating.toLowerCase().includes('red')) {
93
- // Skip header rows
94
- if (item === 'Item' || item === '---') continue;
95
- suggestions.push({ category, item, rating, notes });
96
- }
97
- }
98
- }
99
-
100
- return suggestions;
101
- }
102
-
103
- /**
104
- * Parse existing improvement-suggestions.md to extract already-captured SUG and CAND IDs.
105
- * @param {string} content
106
- * @returns {Set<string>} Set of SUG/CAND IDs
107
- */
108
- function parseExistingIds(content) {
109
- const ids = new Set();
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) {
118
- ids.add(m[0]);
119
- }
120
- return ids;
121
- }
122
-
123
- /**
124
- * Atomic write using tmp+rename pattern.
125
- * @param {string} filePath
126
- * @param {string} content
127
- */
128
- function atomicWrite(filePath, content) {
129
- const tmpFile = `${filePath}.tmp.${process.pid}`;
130
- fs.writeFileSync(tmpFile, content, 'utf8');
131
- fs.renameSync(tmpFile, filePath);
132
- }
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
-
561
- function main() {
562
- const args = process.argv.slice(2);
563
- if (args.length < 1) {
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');
581
- process.exit(2);
582
- }
583
-
584
- const sprintDir = process.env.CLEARGATE_SPRINT_DIR
585
- ? path.resolve(process.env.CLEARGATE_SPRINT_DIR)
586
- : path.join(REPO_ROOT, '.cleargate', 'sprint-runs', sprintId);
587
-
588
- if (!fs.existsSync(sprintDir)) {
589
- process.stderr.write(`Error: sprint directory not found: ${sprintDir}\n`);
590
- process.exit(1);
591
- }
592
-
593
- const reportFile = reportFilename(sprintDir, sprintId, { forRead: true });
594
- const suggestionsFile = path.join(sprintDir, 'improvement-suggestions.md');
595
-
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;
613
- }
614
-
615
- const reportContent = fs.readFileSync(reportFile, 'utf8');
616
-
617
- // Parse §5 self-assessment for improvement candidates
618
- const candidates = parseSelfAssessment(reportContent);
619
-
620
- // Read existing suggestions if present (idempotency)
621
- let existingContent = '';
622
- const existingIds = new Set();
623
- let nextN = 1;
624
-
625
- if (fs.existsSync(suggestionsFile)) {
626
- existingContent = fs.readFileSync(suggestionsFile, 'utf8');
627
- // Parse existing IDs
628
- const parsed = parseExistingIds(existingContent);
629
- for (const id of parsed) {
630
- existingIds.add(id);
631
- }
632
- // Determine the highest existing N for this sprint to continue from
633
- const sprintPrefix = `SUG-${sprintId}-`;
634
- let maxN = 0;
635
- for (const id of parsed) {
636
- if (id.startsWith(sprintPrefix)) {
637
- const n = parseInt(id.slice(sprintPrefix.length), 10);
638
- if (!isNaN(n) && n > maxN) maxN = n;
639
- }
640
- }
641
- nextN = maxN + 1;
642
- }
643
-
644
- // Determine which candidates are new (stable hash-based dedup)
645
- const newEntries = [];
646
- for (const candidate of candidates) {
647
- const hashKey = `${candidate.category}|${candidate.item}`;
648
- const hash = stableHash(hashKey);
649
- // Check if we already have an entry with this hash in existing content
650
- // We encode the hash in a comment to enable stable lookup
651
- const hashMarker = `<!-- hash:${hash} -->`;
652
- if (existingContent.includes(hashMarker)) {
653
- continue; // Already captured
654
- }
655
- const sugId = `SUG-${sprintId}-${String(nextN).padStart(2, '0')}`;
656
- nextN++;
657
- newEntries.push({ sugId, hash, hashMarker, ...candidate });
658
- }
659
-
660
- if (newEntries.length === 0) {
661
- process.stdout.write(
662
- `Idempotent: no new suggestions to add for sprint ${sprintId}.\n` +
663
- `Existing file has ${existingIds.size} suggestion(s).\n`
664
- );
665
- process.exit(0);
666
- }
667
-
668
- // Build new entries markdown
669
- const timestamp = new Date().toISOString();
670
- const newEntriesLines = [];
671
-
672
- // If file doesn't exist yet, write a header
673
- if (!existingContent) {
674
- newEntriesLines.push(`# Improvement Suggestions — ${sprintId}`);
675
- newEntriesLines.push('');
676
- newEntriesLines.push('Generated by `suggest_improvements.mjs`. Append-only; IDs are stable.');
677
- newEntriesLines.push(`Vocabulary: Templates | Handoffs | Skills | Process | Tooling`);
678
- newEntriesLines.push('');
679
- newEntriesLines.push('---');
680
- newEntriesLines.push('');
681
- }
682
-
683
- for (const entry of newEntries) {
684
- newEntriesLines.push(`## ${entry.sugId} — ${entry.category}: ${entry.item}`);
685
- newEntriesLines.push(`${entry.hashMarker}`);
686
- newEntriesLines.push('');
687
- newEntriesLines.push(`**Category:** ${entry.category}`);
688
- newEntriesLines.push(`**Rating:** ${entry.rating}`);
689
- newEntriesLines.push(`**Added:** ${timestamp}`);
690
- newEntriesLines.push('');
691
- if (entry.notes && entry.notes !== '' && entry.notes !== '<notes>') {
692
- newEntriesLines.push(`**Context from report:** ${entry.notes}`);
693
- newEntriesLines.push('');
694
- }
695
- newEntriesLines.push('**Suggested action:**');
696
- newEntriesLines.push(`> _(to be filled by orchestrator or next sprint planning)_`);
697
- newEntriesLines.push('');
698
- newEntriesLines.push('---');
699
- newEntriesLines.push('');
700
- }
701
-
702
- const appendContent = newEntriesLines.join('\n');
703
- const finalContent = existingContent
704
- ? existingContent.trimEnd() + '\n\n' + appendContent
705
- : appendContent;
706
-
707
- atomicWrite(suggestionsFile, finalContent);
708
-
709
- process.stdout.write(
710
- `suggest_improvements: added ${newEntries.length} new suggestion(s) to ${suggestionsFile}\n`
711
- );
712
- for (const e of newEntries) {
713
- process.stdout.write(` ${e.sugId}: [${e.category}] ${e.item} (${e.rating})\n`);
714
- }
715
- }
716
-
717
- main();