cleargate 0.14.0 → 0.15.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 +16 -0
- package/dist/MANIFEST.json +71 -15
- package/dist/admin-api/index.cjs +0 -1
- package/dist/admin-api/index.js +1 -2
- package/dist/auth/factory.cjs +0 -1
- package/dist/auth/factory.js +2 -3
- package/dist/auth/require-token.cjs +0 -1
- package/dist/auth/require-token.js +1 -2
- package/dist/auth/token-store.cjs +0 -1
- package/dist/auth/token-store.js +1 -2
- package/dist/{bootstrap-root-QKSA5V75.js → bootstrap-root-2H5HVTCC.js} +1 -2
- package/dist/{chunk-PDE37WFQ.js → chunk-A7MSQUU7.js} +2 -3
- package/dist/{chunk-BTSZOEWC.js → chunk-P6KEDAK2.js} +0 -1
- package/dist/{chunk-E3X7IE5E.js → chunk-PY6FHGV5.js} +1 -2
- package/dist/{chunk-5DI2Z3C2.js → chunk-Y53ZZYYU.js} +1 -2
- package/dist/cli.cjs +1564 -1414
- package/dist/cli.js +1514 -1364
- package/dist/lib/ledger.cjs +0 -1
- package/dist/lib/ledger.js +1 -2
- package/dist/lib/lifecycle-reconcile.cjs +0 -1
- package/dist/lib/lifecycle-reconcile.js +2 -3
- package/dist/{whoami-EANGN46Z.js → whoami-JKQQPABQ.js} +3 -4
- package/package.json +4 -3
- package/templates/cleargate-planning/.claude/agents/architect.md +4 -2
- package/templates/cleargate-planning/.claude/agents/developer.md +4 -11
- package/templates/cleargate-planning/.claude/agents/qa.md +14 -6
- package/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +2 -2
- package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +19 -1
- package/templates/cleargate-planning/.cleargate/config.example.yml +16 -0
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.deferred-verify.red.node.test.ts +245 -0
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +227 -0
- package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +5 -4
- package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +75 -2
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +48 -0
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +57 -1
- package/templates/cleargate-planning/.cleargate/scripts/provision_worktree_config.sh +155 -0
- package/templates/cleargate-planning/.cleargate/scripts/qa_red_lint.mjs +380 -0
- package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +34 -1
- package/templates/cleargate-planning/.cleargate/scripts/test/cr077_eviction.red.sh +113 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr078_init.test.sh +309 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr079_provision.red.sh +262 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr080_wrapper.test.sh +177 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr081_qa_red_lint.red.sh +348 -0
- package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/.session-totals.json +1 -0
- package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/token-ledger.jsonl +27 -0
- package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +17 -0
- package/templates/cleargate-planning/.cleargate/templates/story.md +1 -0
- package/templates/cleargate-planning/MANIFEST.json +71 -15
- package/dist/admin-api/index.cjs.map +0 -1
- package/dist/admin-api/index.js.map +0 -1
- package/dist/auth/factory.cjs.map +0 -1
- package/dist/auth/factory.js.map +0 -1
- package/dist/auth/require-token.cjs.map +0 -1
- package/dist/auth/require-token.js.map +0 -1
- package/dist/auth/token-store.cjs.map +0 -1
- package/dist/auth/token-store.js.map +0 -1
- package/dist/bootstrap-root-QKSA5V75.js.map +0 -1
- package/dist/chunk-5DI2Z3C2.js.map +0 -1
- package/dist/chunk-BTSZOEWC.js.map +0 -1
- package/dist/chunk-E3X7IE5E.js.map +0 -1
- package/dist/chunk-PDE37WFQ.js.map +0 -1
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/lib/ledger.cjs.map +0 -1
- package/dist/lib/ledger.js.map +0 -1
- package/dist/lib/lifecycle-reconcile.cjs.map +0 -1
- package/dist/lib/lifecycle-reconcile.js.map +0 -1
- package/dist/templates/cleargate-planning/.claude/agents/architect-reader.md +0 -61
- package/dist/templates/cleargate-planning/.claude/agents/architect-synth.md +0 -124
- package/dist/templates/cleargate-planning/.claude/agents/architect.md +0 -230
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +0 -108
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +0 -194
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +0 -261
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-query.md +0 -143
- package/dist/templates/cleargate-planning/.claude/agents/developer.md +0 -185
- package/dist/templates/cleargate-planning/.claude/agents/devops.md +0 -257
- package/dist/templates/cleargate-planning/.claude/agents/qa.md +0 -171
- package/dist/templates/cleargate-planning/.claude/agents/reporter.md +0 -274
- package/dist/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +0 -209
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +0 -33
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +0 -58
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit.sh +0 -19
- package/dist/templates/cleargate-planning/.claude/hooks/pre-edit-gate.sh +0 -162
- package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-autonomy.sh +0 -58
- package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +0 -148
- package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +0 -75
- package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +0 -43
- package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +0 -590
- package/dist/templates/cleargate-planning/.claude/settings.json +0 -68
- package/dist/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +0 -102
- package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +0 -742
- package/dist/templates/cleargate-planning/.cleargate/FLASHCARD.md +0 -7
- package/dist/templates/cleargate-planning/.cleargate/config.example.yml +0 -67
- package/dist/templates/cleargate-planning/.cleargate/config.yml +0 -18
- package/dist/templates/cleargate-planning/.cleargate/delivery/archive/.gitkeep +0 -0
- package/dist/templates/cleargate-planning/.cleargate/delivery/pending-sync/.gitkeep +0 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +0 -551
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +0 -878
- package/dist/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +0 -160
- package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +0 -213
- package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +0 -71
- package/dist/templates/cleargate-planning/.cleargate/scripts/_migrate-schema-v3.mjs +0 -120
- package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +0 -265
- package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +0 -1012
- package/dist/templates/cleargate-planning/.cleargate/scripts/collision_surface.sh +0 -114
- package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +0 -62
- package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +0 -219
- package/dist/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +0 -320
- package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +0 -15
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +0 -38
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +0 -240
- package/dist/templates/cleargate-planning/.cleargate/scripts/launch_wave.mjs +0 -341
- package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +0 -54
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +0 -206
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +0 -371
- package/dist/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +0 -280
- package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +0 -378
- package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +0 -888
- package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +0 -209
- package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +0 -71
- package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +0 -127
- package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +0 -717
- package/dist/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +0 -27
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +0 -261
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +0 -210
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +0 -190
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +0 -482
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +0 -327
- package/dist/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +0 -261
- package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +0 -246
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +0 -111
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +0 -184
- package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +0 -172
- package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +0 -126
- package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +0 -130
- package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +0 -137
- package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +0 -166
- package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +0 -111
- package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +0 -122
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +0 -50
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +0 -224
- package/dist/templates/cleargate-planning/.cleargate/templates/story.md +0 -213
- package/dist/templates/cleargate-planning/CLAUDE.md +0 -66
- package/dist/templates/cleargate-planning/MANIFEST.json +0 -503
- package/dist/templates/synthesis/active-sprint.md +0 -30
- package/dist/templates/synthesis/open-gates.md +0 -38
- package/dist/templates/synthesis/product-state.md +0 -31
- package/dist/templates/synthesis/roadmap.md +0 -63
- 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();
|