cleargate 0.2.0 → 0.3.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/LICENSE +21 -0
- package/dist/MANIFEST.json +59 -17
- package/dist/admin-api/index.cjs +88 -1
- package/dist/admin-api/index.cjs.map +1 -1
- package/dist/admin-api/index.d.cts +105 -1
- package/dist/admin-api/index.d.ts +105 -1
- package/dist/admin-api/index.js +77 -1
- package/dist/admin-api/index.js.map +1 -1
- package/dist/bootstrap-root-FGWDICDT.js +130 -0
- package/dist/bootstrap-root-FGWDICDT.js.map +1 -0
- package/dist/chunk-OM4FAEA7.js +184 -0
- package/dist/chunk-OM4FAEA7.js.map +1 -0
- package/dist/cli.cjs +7995 -3984
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +4062 -561
- package/dist/cli.js.map +1 -1
- package/dist/templates/cleargate-planning/.claude/agents/architect.md +72 -0
- package/dist/templates/cleargate-planning/.claude/agents/developer.md +45 -3
- package/dist/templates/cleargate-planning/.claude/agents/qa.md +7 -3
- package/dist/templates/cleargate-planning/.claude/agents/reporter.md +72 -75
- package/dist/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +204 -0
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +10 -0
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +58 -0
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit.sh +19 -0
- package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +51 -0
- package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +1 -1
- package/dist/templates/cleargate-planning/.claude/settings.json +11 -0
- package/dist/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +31 -12
- package/dist/templates/cleargate-planning/.cleargate/FLASHCARD.md +2 -1
- package/dist/templates/cleargate-planning/.cleargate/config.example.yml +37 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +407 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +146 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +250 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +57 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +320 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +15 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +38 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +187 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +132 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +307 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +280 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +123 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +110 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +247 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +27 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +261 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +210 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +190 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +327 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +261 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +154 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +111 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +164 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +9 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +9 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +29 -3
- package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +9 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/proposal.md +9 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +42 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +175 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/story.md +29 -0
- package/dist/templates/cleargate-planning/CLAUDE.md +3 -0
- package/dist/templates/cleargate-planning/MANIFEST.json +59 -17
- package/dist/whoami-CX7CXJD5.js +76 -0
- package/dist/whoami-CX7CXJD5.js.map +1 -0
- package/package.json +6 -2
- package/templates/cleargate-planning/.claude/agents/architect.md +72 -0
- package/templates/cleargate-planning/.claude/agents/developer.md +45 -3
- package/templates/cleargate-planning/.claude/agents/qa.md +7 -3
- package/templates/cleargate-planning/.claude/agents/reporter.md +72 -75
- package/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +204 -0
- package/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +10 -0
- package/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +58 -0
- package/templates/cleargate-planning/.claude/hooks/pre-commit.sh +19 -0
- package/templates/cleargate-planning/.claude/hooks/session-start.sh +51 -0
- package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +1 -1
- package/templates/cleargate-planning/.claude/settings.json +11 -0
- package/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +31 -12
- package/templates/cleargate-planning/.cleargate/FLASHCARD.md +2 -1
- package/templates/cleargate-planning/.cleargate/config.example.yml +37 -0
- package/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +407 -0
- package/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +146 -0
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +250 -0
- package/templates/cleargate-planning/.cleargate/scripts/constants.mjs +57 -0
- package/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +320 -0
- package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +15 -0
- package/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +38 -0
- package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +187 -0
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +132 -0
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +307 -0
- package/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +280 -0
- package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +123 -0
- package/templates/cleargate-planning/.cleargate/scripts/state.schema.json +110 -0
- package/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +247 -0
- package/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +27 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +261 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +210 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +190 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +327 -0
- package/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +261 -0
- package/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +154 -0
- package/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +111 -0
- package/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +164 -0
- package/templates/cleargate-planning/.cleargate/templates/Bug.md +9 -0
- package/templates/cleargate-planning/.cleargate/templates/CR.md +9 -0
- package/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +29 -3
- package/templates/cleargate-planning/.cleargate/templates/epic.md +9 -0
- package/templates/cleargate-planning/.cleargate/templates/proposal.md +9 -0
- package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +42 -0
- package/templates/cleargate-planning/.cleargate/templates/sprint_report.md +175 -0
- package/templates/cleargate-planning/.cleargate/templates/story.md +29 -0
- package/templates/cleargate-planning/CLAUDE.md +3 -0
- package/templates/cleargate-planning/MANIFEST.json +59 -17
|
@@ -0,0 +1,247 @@
|
|
|
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
|
+
*
|
|
7
|
+
* Reads:
|
|
8
|
+
* - .cleargate/sprint-runs/<id>/REPORT.md §5 Framework Self-Assessment tables
|
|
9
|
+
* - .cleargate/sprint-runs/<prev-id>/improvement-suggestions.md (if present, for context)
|
|
10
|
+
*
|
|
11
|
+
* Emits:
|
|
12
|
+
* - .cleargate/sprint-runs/<id>/improvement-suggestions.md
|
|
13
|
+
* with stable SUG-<sprint>-<n> IDs
|
|
14
|
+
*
|
|
15
|
+
* Append-only idempotency (R5):
|
|
16
|
+
* - IDs are derived from a stable hash of (category, title) tuple
|
|
17
|
+
* - Re-running produces zero new entries if all suggestions already captured
|
|
18
|
+
* - Script exits 0 in both cases
|
|
19
|
+
*
|
|
20
|
+
* Note: "section" as used in §5 table extraction refers to the Framework Self-Assessment
|
|
21
|
+
* subsections: Templates, Handoffs, Skills, Process, Tooling.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { createHash } from 'node:crypto';
|
|
28
|
+
|
|
29
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
31
|
+
|
|
32
|
+
// §5 subsection names used in sprint_report.md template
|
|
33
|
+
const SELF_ASSESSMENT_SECTIONS = ['Templates', 'Handoffs', 'Skills', 'Process', 'Tooling'];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generate a stable short hash from a string.
|
|
37
|
+
* Used for SUG ID stability — same (category, title) always produces same ID.
|
|
38
|
+
* @param {string} input
|
|
39
|
+
* @returns {string} 6-char hex
|
|
40
|
+
*/
|
|
41
|
+
function stableHash(input) {
|
|
42
|
+
return createHash('sha256').update(input).digest('hex').slice(0, 6);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse §5 Framework Self-Assessment from REPORT.md content.
|
|
47
|
+
* Extracts Yellow/Red-rated rows as improvement candidates.
|
|
48
|
+
* @param {string} content
|
|
49
|
+
* @returns {{ category: string, item: string, rating: string, notes: string }[]}
|
|
50
|
+
*/
|
|
51
|
+
function parseSelfAssessment(content) {
|
|
52
|
+
const suggestions = [];
|
|
53
|
+
|
|
54
|
+
// Find the §5 Framework Self-Assessment section
|
|
55
|
+
const selfAssessmentMatch = content.match(/##\s*§5[^\n]*\n([\s\S]*?)(?=##\s*§6|$)/);
|
|
56
|
+
if (!selfAssessmentMatch) return suggestions;
|
|
57
|
+
|
|
58
|
+
const sectionContent = selfAssessmentMatch[1];
|
|
59
|
+
|
|
60
|
+
// Extract each subsection table
|
|
61
|
+
for (const category of SELF_ASSESSMENT_SECTIONS) {
|
|
62
|
+
// Find table rows under the category header
|
|
63
|
+
// Pattern: ### <category>\n | <item> | <rating> | <notes> |
|
|
64
|
+
const categoryMatch = sectionContent.match(
|
|
65
|
+
new RegExp(`###\\s+${category}\\s*\\n([\\s\\S]*?)(?=###|$)`, 'i')
|
|
66
|
+
);
|
|
67
|
+
if (!categoryMatch) continue;
|
|
68
|
+
|
|
69
|
+
const tableContent = categoryMatch[1];
|
|
70
|
+
// Parse table rows (skip header rows with ---)
|
|
71
|
+
const rows = tableContent.split('\n').filter(l => l.startsWith('|') && !l.includes('---'));
|
|
72
|
+
|
|
73
|
+
for (const row of rows) {
|
|
74
|
+
const cells = row.split('|').map(c => c.trim()).filter(Boolean);
|
|
75
|
+
if (cells.length < 2) continue;
|
|
76
|
+
|
|
77
|
+
const item = cells[0];
|
|
78
|
+
const rating = cells[1] || '';
|
|
79
|
+
const notes = cells[2] || '';
|
|
80
|
+
|
|
81
|
+
// Only flag Yellow or Red items as needing improvement
|
|
82
|
+
if (rating.toLowerCase().includes('yellow') || rating.toLowerCase().includes('red')) {
|
|
83
|
+
// Skip header rows
|
|
84
|
+
if (item === 'Item' || item === '---') continue;
|
|
85
|
+
suggestions.push({ category, item, rating, notes });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return suggestions;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parse existing improvement-suggestions.md to extract already-captured SUG IDs.
|
|
95
|
+
* @param {string} content
|
|
96
|
+
* @returns {Set<string>} Set of SUG IDs
|
|
97
|
+
*/
|
|
98
|
+
function parseExistingIds(content) {
|
|
99
|
+
const ids = new Set();
|
|
100
|
+
// Match SUG-<sprint>-<n> patterns
|
|
101
|
+
const matches = content.matchAll(/SUG-[A-Z0-9-]+-\d+/g);
|
|
102
|
+
for (const m of matches) {
|
|
103
|
+
ids.add(m[0]);
|
|
104
|
+
}
|
|
105
|
+
return ids;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Atomic write using tmp+rename pattern.
|
|
110
|
+
* @param {string} filePath
|
|
111
|
+
* @param {string} content
|
|
112
|
+
*/
|
|
113
|
+
function atomicWrite(filePath, content) {
|
|
114
|
+
const tmpFile = `${filePath}.tmp.${process.pid}`;
|
|
115
|
+
fs.writeFileSync(tmpFile, content, 'utf8');
|
|
116
|
+
fs.renameSync(tmpFile, filePath);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function main() {
|
|
120
|
+
const args = process.argv.slice(2);
|
|
121
|
+
if (args.length < 1) {
|
|
122
|
+
process.stderr.write('Usage: node suggest_improvements.mjs <sprint-id>\n');
|
|
123
|
+
process.exit(2);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const sprintId = args[0];
|
|
127
|
+
const sprintDir = process.env.CLEARGATE_SPRINT_DIR
|
|
128
|
+
? path.resolve(process.env.CLEARGATE_SPRINT_DIR)
|
|
129
|
+
: path.join(REPO_ROOT, '.cleargate', 'sprint-runs', sprintId);
|
|
130
|
+
|
|
131
|
+
if (!fs.existsSync(sprintDir)) {
|
|
132
|
+
process.stderr.write(`Error: sprint directory not found: ${sprintDir}\n`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const reportFile = path.join(sprintDir, 'REPORT.md');
|
|
137
|
+
const suggestionsFile = path.join(sprintDir, 'improvement-suggestions.md');
|
|
138
|
+
|
|
139
|
+
if (!fs.existsSync(reportFile)) {
|
|
140
|
+
process.stderr.write(`Error: REPORT.md not found at ${reportFile}\n`);
|
|
141
|
+
process.stderr.write('Run the Reporter agent first to generate the report.\n');
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const reportContent = fs.readFileSync(reportFile, 'utf8');
|
|
146
|
+
|
|
147
|
+
// Parse §5 self-assessment for improvement candidates
|
|
148
|
+
const candidates = parseSelfAssessment(reportContent);
|
|
149
|
+
|
|
150
|
+
// Read existing suggestions if present (idempotency)
|
|
151
|
+
let existingContent = '';
|
|
152
|
+
const existingIds = new Set();
|
|
153
|
+
let nextN = 1;
|
|
154
|
+
|
|
155
|
+
if (fs.existsSync(suggestionsFile)) {
|
|
156
|
+
existingContent = fs.readFileSync(suggestionsFile, 'utf8');
|
|
157
|
+
// Parse existing IDs
|
|
158
|
+
const parsed = parseExistingIds(existingContent);
|
|
159
|
+
for (const id of parsed) {
|
|
160
|
+
existingIds.add(id);
|
|
161
|
+
}
|
|
162
|
+
// Determine the highest existing N for this sprint to continue from
|
|
163
|
+
const sprintPrefix = `SUG-${sprintId}-`;
|
|
164
|
+
let maxN = 0;
|
|
165
|
+
for (const id of parsed) {
|
|
166
|
+
if (id.startsWith(sprintPrefix)) {
|
|
167
|
+
const n = parseInt(id.slice(sprintPrefix.length), 10);
|
|
168
|
+
if (!isNaN(n) && n > maxN) maxN = n;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
nextN = maxN + 1;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Determine which candidates are new (stable hash-based dedup)
|
|
175
|
+
const newEntries = [];
|
|
176
|
+
for (const candidate of candidates) {
|
|
177
|
+
const hashKey = `${candidate.category}|${candidate.item}`;
|
|
178
|
+
const hash = stableHash(hashKey);
|
|
179
|
+
// Check if we already have an entry with this hash in existing content
|
|
180
|
+
// We encode the hash in a comment to enable stable lookup
|
|
181
|
+
const hashMarker = `<!-- hash:${hash} -->`;
|
|
182
|
+
if (existingContent.includes(hashMarker)) {
|
|
183
|
+
continue; // Already captured
|
|
184
|
+
}
|
|
185
|
+
const sugId = `SUG-${sprintId}-${String(nextN).padStart(2, '0')}`;
|
|
186
|
+
nextN++;
|
|
187
|
+
newEntries.push({ sugId, hash, hashMarker, ...candidate });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (newEntries.length === 0) {
|
|
191
|
+
process.stdout.write(
|
|
192
|
+
`Idempotent: no new suggestions to add for sprint ${sprintId}.\n` +
|
|
193
|
+
`Existing file has ${existingIds.size} suggestion(s).\n`
|
|
194
|
+
);
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Build new entries markdown
|
|
199
|
+
const timestamp = new Date().toISOString();
|
|
200
|
+
const newEntriesLines = [];
|
|
201
|
+
|
|
202
|
+
// If file doesn't exist yet, write a header
|
|
203
|
+
if (!existingContent) {
|
|
204
|
+
newEntriesLines.push(`# Improvement Suggestions — ${sprintId}`);
|
|
205
|
+
newEntriesLines.push('');
|
|
206
|
+
newEntriesLines.push('Generated by `suggest_improvements.mjs`. Append-only; IDs are stable.');
|
|
207
|
+
newEntriesLines.push(`Vocabulary: Templates | Handoffs | Skills | Process | Tooling`);
|
|
208
|
+
newEntriesLines.push('');
|
|
209
|
+
newEntriesLines.push('---');
|
|
210
|
+
newEntriesLines.push('');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const entry of newEntries) {
|
|
214
|
+
newEntriesLines.push(`## ${entry.sugId} — ${entry.category}: ${entry.item}`);
|
|
215
|
+
newEntriesLines.push(`${entry.hashMarker}`);
|
|
216
|
+
newEntriesLines.push('');
|
|
217
|
+
newEntriesLines.push(`**Category:** ${entry.category}`);
|
|
218
|
+
newEntriesLines.push(`**Rating:** ${entry.rating}`);
|
|
219
|
+
newEntriesLines.push(`**Added:** ${timestamp}`);
|
|
220
|
+
newEntriesLines.push('');
|
|
221
|
+
if (entry.notes && entry.notes !== '' && entry.notes !== '<notes>') {
|
|
222
|
+
newEntriesLines.push(`**Context from report:** ${entry.notes}`);
|
|
223
|
+
newEntriesLines.push('');
|
|
224
|
+
}
|
|
225
|
+
newEntriesLines.push('**Suggested action:**');
|
|
226
|
+
newEntriesLines.push(`> _(to be filled by orchestrator or next sprint planning)_`);
|
|
227
|
+
newEntriesLines.push('');
|
|
228
|
+
newEntriesLines.push('---');
|
|
229
|
+
newEntriesLines.push('');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const appendContent = newEntriesLines.join('\n');
|
|
233
|
+
const finalContent = existingContent
|
|
234
|
+
? existingContent.trimEnd() + '\n\n' + appendContent
|
|
235
|
+
: appendContent;
|
|
236
|
+
|
|
237
|
+
atomicWrite(suggestionsFile, finalContent);
|
|
238
|
+
|
|
239
|
+
process.stdout.write(
|
|
240
|
+
`suggest_improvements: added ${newEntries.length} new suggestion(s) to ${suggestionsFile}\n`
|
|
241
|
+
);
|
|
242
|
+
for (const e of newEntries) {
|
|
243
|
+
process.stdout.write(` ${e.sugId}: [${e.category}] ${e.item} (${e.rating})\n`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
main();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# surface-whitelist.txt — Files always admitted by the pre-commit surface gate
|
|
2
|
+
# regardless of the active story's §3.1 declared surface.
|
|
3
|
+
#
|
|
4
|
+
# Format: one pattern per line. Supports bash glob patterns.
|
|
5
|
+
# * matches any non-path-separator sequence
|
|
6
|
+
# ** matches any sequence (including path separators)
|
|
7
|
+
# Lines starting with # are comments; blank lines are ignored.
|
|
8
|
+
#
|
|
9
|
+
# These patterns are matched against repo-root-relative staged file paths.
|
|
10
|
+
|
|
11
|
+
# Build manifest (regenerated by npm run build)
|
|
12
|
+
cleargate-planning/MANIFEST.json
|
|
13
|
+
|
|
14
|
+
# Hook execution logs
|
|
15
|
+
.cleargate/hook-log/*
|
|
16
|
+
|
|
17
|
+
# Token ledger files (auto-generated by SubagentStop hook)
|
|
18
|
+
.cleargate/sprint-runs/**/token-ledger.jsonl
|
|
19
|
+
|
|
20
|
+
# Pending-task sentinel files (auto-generated by PreToolUse hook)
|
|
21
|
+
.cleargate/sprint-runs/**/.pending-task-*.json
|
|
22
|
+
|
|
23
|
+
# Sprint state files (auto-managed)
|
|
24
|
+
.cleargate/sprint-runs/**/state.json
|
|
25
|
+
|
|
26
|
+
# Gate check log (auto-generated)
|
|
27
|
+
.cleargate/hook-log/gate-check.log
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_assert_story_files.sh — 4-scenario Gherkin test for STORY-014-02
|
|
3
|
+
#
|
|
4
|
+
# Tests:
|
|
5
|
+
# Scenario 1: v2 init refuses when stories are missing
|
|
6
|
+
# Scenario 2: v2 init succeeds when all stories exist
|
|
7
|
+
# Scenario 3: v1 init warns but does not block
|
|
8
|
+
# Scenario 4: assert_story_files standalone CLI
|
|
9
|
+
#
|
|
10
|
+
# Run: bash .cleargate/scripts/test/test_assert_story_files.sh
|
|
11
|
+
# Requires Node 24+.
|
|
12
|
+
#
|
|
13
|
+
# Story IDs must use digit-only parts (STORY-\d+-\d+), e.g. STORY-099-01.
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
18
|
+
REPO_ROOT="$(cd "${SCRIPTS_DIR}/../.." && pwd)"
|
|
19
|
+
|
|
20
|
+
PASS=0
|
|
21
|
+
FAIL=0
|
|
22
|
+
ERRORS=()
|
|
23
|
+
|
|
24
|
+
assert_eq() {
|
|
25
|
+
local label="$1" expected="$2" actual="$3"
|
|
26
|
+
if [ "$expected" = "$actual" ]; then
|
|
27
|
+
echo " PASS: $label"
|
|
28
|
+
PASS=$((PASS + 1))
|
|
29
|
+
else
|
|
30
|
+
echo " FAIL: $label"
|
|
31
|
+
echo " expected: $expected"
|
|
32
|
+
echo " actual: $actual"
|
|
33
|
+
FAIL=$((FAIL + 1))
|
|
34
|
+
ERRORS+=("$label")
|
|
35
|
+
fi
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
assert_contains() {
|
|
39
|
+
local label="$1" needle="$2" haystack="$3"
|
|
40
|
+
if echo "$haystack" | grep -qF "$needle"; then
|
|
41
|
+
echo " PASS: $label"
|
|
42
|
+
PASS=$((PASS + 1))
|
|
43
|
+
else
|
|
44
|
+
echo " FAIL: $label (needle not found)"
|
|
45
|
+
echo " needle: $needle"
|
|
46
|
+
echo " haystack: $haystack"
|
|
47
|
+
FAIL=$((FAIL + 1))
|
|
48
|
+
ERRORS+=("$label")
|
|
49
|
+
fi
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
assert_not_exists() {
|
|
53
|
+
local label="$1" filepath="$2"
|
|
54
|
+
if [ ! -e "$filepath" ]; then
|
|
55
|
+
echo " PASS: $label"
|
|
56
|
+
PASS=$((PASS + 1))
|
|
57
|
+
else
|
|
58
|
+
echo " FAIL: $label (file exists but should not)"
|
|
59
|
+
echo " path: $filepath"
|
|
60
|
+
FAIL=$((FAIL + 1))
|
|
61
|
+
ERRORS+=("$label")
|
|
62
|
+
fi
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
assert_exists() {
|
|
66
|
+
local label="$1" filepath="$2"
|
|
67
|
+
if [ -e "$filepath" ]; then
|
|
68
|
+
echo " PASS: $label"
|
|
69
|
+
PASS=$((PASS + 1))
|
|
70
|
+
else
|
|
71
|
+
echo " FAIL: $label (file does not exist)"
|
|
72
|
+
echo " path: $filepath"
|
|
73
|
+
FAIL=$((FAIL + 1))
|
|
74
|
+
ERRORS+=("$label")
|
|
75
|
+
fi
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Create a minimal sprint file with given execution_mode
|
|
79
|
+
# Story IDs must be STORY-\d+-\d+ (all numeric parts) for the regex to match.
|
|
80
|
+
# Usage: make_sprint_file <dir> <sprint-id> <execution_mode> <story-id-1> [<story-id-2> ...]
|
|
81
|
+
make_sprint_file() {
|
|
82
|
+
local dir="$1" sprint_id="$2" exec_mode="$3"
|
|
83
|
+
shift 3
|
|
84
|
+
local story_ids=("$@")
|
|
85
|
+
|
|
86
|
+
local sprint_file="${dir}/.cleargate/delivery/pending-sync/${sprint_id}_Test_Sprint.md"
|
|
87
|
+
mkdir -p "$(dirname "$sprint_file")"
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
echo "---"
|
|
91
|
+
echo "sprint_id: \"${sprint_id}\""
|
|
92
|
+
echo "execution_mode: \"${exec_mode}\""
|
|
93
|
+
echo "approved: true"
|
|
94
|
+
echo "---"
|
|
95
|
+
echo ""
|
|
96
|
+
echo "# ${sprint_id}: Test Sprint"
|
|
97
|
+
echo ""
|
|
98
|
+
echo "## 1. Consolidated Deliverables"
|
|
99
|
+
echo ""
|
|
100
|
+
echo "| Story | Complexity | Milestone |"
|
|
101
|
+
echo "|---|---|---|"
|
|
102
|
+
for sid in "${story_ids[@]}"; do
|
|
103
|
+
echo "| [\`${sid}\`](${sid}_Placeholder.md) Placeholder | L2 | M1 |"
|
|
104
|
+
done
|
|
105
|
+
echo ""
|
|
106
|
+
echo "## 2. Other Section"
|
|
107
|
+
echo ""
|
|
108
|
+
echo "Some content here."
|
|
109
|
+
} > "$sprint_file"
|
|
110
|
+
|
|
111
|
+
echo "$sprint_file"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Create a minimal story file in pending-sync/
|
|
115
|
+
make_story_file() {
|
|
116
|
+
local dir="$1" story_id="$2"
|
|
117
|
+
local pending_sync="${dir}/.cleargate/delivery/pending-sync"
|
|
118
|
+
mkdir -p "$pending_sync"
|
|
119
|
+
echo "# ${story_id}: Placeholder" > "${pending_sync}/${story_id}_Placeholder.md"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
echo ""
|
|
124
|
+
echo "=== Scenario 1: v2 init refuses when stories are missing ==="
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
TMP1="$(mktemp -d)"
|
|
127
|
+
trap 'rm -rf "$TMP1"' EXIT
|
|
128
|
+
|
|
129
|
+
SPRINT_ID="SPRINT-099"
|
|
130
|
+
make_sprint_file "$TMP1" "$SPRINT_ID" "v2" \
|
|
131
|
+
"STORY-099-01" "STORY-099-02" "STORY-099-03" > /dev/null
|
|
132
|
+
|
|
133
|
+
# Only create STORY-099-01, leave 02 and 03 missing
|
|
134
|
+
make_story_file "$TMP1" "STORY-099-01"
|
|
135
|
+
|
|
136
|
+
STATE_JSON="${TMP1}/.cleargate/sprint-runs/${SPRINT_ID}/state.json"
|
|
137
|
+
|
|
138
|
+
STDERR_OUT=""
|
|
139
|
+
EXIT_CODE=0
|
|
140
|
+
STDERR_OUT="$(CLEARGATE_REPO_ROOT="$TMP1" node "$SCRIPTS_DIR/init_sprint.mjs" \
|
|
141
|
+
"$SPRINT_ID" --stories "STORY-099-01,STORY-099-02,STORY-099-03" 2>&1 >/dev/null)" || EXIT_CODE=$?
|
|
142
|
+
|
|
143
|
+
assert_eq "Sc1: exit code non-zero" "1" "$EXIT_CODE"
|
|
144
|
+
assert_contains "Sc1: stderr mentions STORY-099-02" "STORY-099-02" "$STDERR_OUT"
|
|
145
|
+
assert_contains "Sc1: stderr mentions STORY-099-03" "STORY-099-03" "$STDERR_OUT"
|
|
146
|
+
assert_not_exists "Sc1: state.json NOT created" "$STATE_JSON"
|
|
147
|
+
|
|
148
|
+
trap - EXIT
|
|
149
|
+
rm -rf "$TMP1"
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
echo ""
|
|
153
|
+
echo "=== Scenario 2: v2 init succeeds when all stories exist ==="
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
TMP2="$(mktemp -d)"
|
|
156
|
+
trap 'rm -rf "$TMP2"' EXIT
|
|
157
|
+
|
|
158
|
+
SPRINT_ID="SPRINT-098"
|
|
159
|
+
make_sprint_file "$TMP2" "$SPRINT_ID" "v2" \
|
|
160
|
+
"STORY-098-01" "STORY-098-02" "STORY-098-03" > /dev/null
|
|
161
|
+
|
|
162
|
+
make_story_file "$TMP2" "STORY-098-01"
|
|
163
|
+
make_story_file "$TMP2" "STORY-098-02"
|
|
164
|
+
make_story_file "$TMP2" "STORY-098-03"
|
|
165
|
+
|
|
166
|
+
STATE_JSON="${TMP2}/.cleargate/sprint-runs/${SPRINT_ID}/state.json"
|
|
167
|
+
|
|
168
|
+
EXIT_CODE=0
|
|
169
|
+
CLEARGATE_REPO_ROOT="$TMP2" node "$SCRIPTS_DIR/init_sprint.mjs" \
|
|
170
|
+
"$SPRINT_ID" --stories "STORY-098-01,STORY-098-02,STORY-098-03" 2>/dev/null || EXIT_CODE=$?
|
|
171
|
+
|
|
172
|
+
assert_eq "Sc2: exit code 0" "0" "$EXIT_CODE"
|
|
173
|
+
assert_exists "Sc2: state.json created" "$STATE_JSON"
|
|
174
|
+
|
|
175
|
+
# Verify execution_mode in state.json is v2
|
|
176
|
+
if [ -f "$STATE_JSON" ]; then
|
|
177
|
+
EM="$(node -e "const s=JSON.parse(require('fs').readFileSync('$STATE_JSON','utf8')); console.log(s.execution_mode)")"
|
|
178
|
+
assert_eq "Sc2: state.json execution_mode=v2" "v2" "$EM"
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
trap - EXIT
|
|
182
|
+
rm -rf "$TMP2"
|
|
183
|
+
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
echo ""
|
|
186
|
+
echo "=== Scenario 3: v1 init warns but does not block ==="
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
TMP3="$(mktemp -d)"
|
|
189
|
+
trap 'rm -rf "$TMP3"' EXIT
|
|
190
|
+
|
|
191
|
+
SPRINT_ID="SPRINT-097"
|
|
192
|
+
make_sprint_file "$TMP3" "$SPRINT_ID" "v1" \
|
|
193
|
+
"STORY-097-01" "STORY-097-02" > /dev/null
|
|
194
|
+
|
|
195
|
+
# Only create STORY-097-01, leave 02 missing
|
|
196
|
+
make_story_file "$TMP3" "STORY-097-01"
|
|
197
|
+
|
|
198
|
+
STATE_JSON="${TMP3}/.cleargate/sprint-runs/${SPRINT_ID}/state.json"
|
|
199
|
+
|
|
200
|
+
EXIT_CODE=0
|
|
201
|
+
STDERR_OUT="$(CLEARGATE_REPO_ROOT="$TMP3" node "$SCRIPTS_DIR/init_sprint.mjs" \
|
|
202
|
+
"$SPRINT_ID" --stories "STORY-097-01,STORY-097-02" 2>&1 >/dev/null)" || EXIT_CODE=$?
|
|
203
|
+
|
|
204
|
+
assert_eq "Sc3: exit code 0 (v1 warns but continues)" "0" "$EXIT_CODE"
|
|
205
|
+
assert_exists "Sc3: state.json created despite missing file" "$STATE_JSON"
|
|
206
|
+
assert_contains "Sc3: stderr contains WARN" "WARN" "$STDERR_OUT"
|
|
207
|
+
assert_contains "Sc3: stderr mentions STORY-097-02" "STORY-097-02" "$STDERR_OUT"
|
|
208
|
+
|
|
209
|
+
trap - EXIT
|
|
210
|
+
rm -rf "$TMP3"
|
|
211
|
+
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
echo ""
|
|
214
|
+
echo "=== Scenario 4: assert_story_files standalone CLI ==="
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
TMP4="$(mktemp -d)"
|
|
217
|
+
trap 'rm -rf "$TMP4"' EXIT
|
|
218
|
+
|
|
219
|
+
SPRINT_ID="SPRINT-096"
|
|
220
|
+
SPRINT_FILE="$(make_sprint_file "$TMP4" "$SPRINT_ID" "v2" \
|
|
221
|
+
"STORY-096-01" "STORY-096-02")"
|
|
222
|
+
|
|
223
|
+
# Only create STORY-096-01, leave 02 missing
|
|
224
|
+
make_story_file "$TMP4" "STORY-096-01"
|
|
225
|
+
|
|
226
|
+
# Run standalone assert_story_files.mjs — should exit 1 with missing list
|
|
227
|
+
STDERR_OUT=""
|
|
228
|
+
EXIT_CODE=0
|
|
229
|
+
STDERR_OUT="$(CLEARGATE_REPO_ROOT="$TMP4" node "$SCRIPTS_DIR/assert_story_files.mjs" \
|
|
230
|
+
"$SPRINT_FILE" 2>&1 >/dev/null)" || EXIT_CODE=$?
|
|
231
|
+
|
|
232
|
+
assert_eq "Sc4: standalone exits non-zero when missing" "1" "$EXIT_CODE"
|
|
233
|
+
assert_contains "Sc4: stderr lists STORY-096-02 as missing" "STORY-096-02" "$STDERR_OUT"
|
|
234
|
+
|
|
235
|
+
# Now add the missing story file and re-run — should exit 0
|
|
236
|
+
make_story_file "$TMP4" "STORY-096-02"
|
|
237
|
+
|
|
238
|
+
EXIT_CODE=0
|
|
239
|
+
STDOUT_OUT="$(CLEARGATE_REPO_ROOT="$TMP4" node "$SCRIPTS_DIR/assert_story_files.mjs" \
|
|
240
|
+
"$SPRINT_FILE" 2>/dev/null)" || EXIT_CODE=$?
|
|
241
|
+
|
|
242
|
+
assert_eq "Sc4: standalone exits 0 when all present" "0" "$EXIT_CODE"
|
|
243
|
+
assert_contains "Sc4: stdout confirms all present" "OK:" "$STDOUT_OUT"
|
|
244
|
+
|
|
245
|
+
trap - EXIT
|
|
246
|
+
rm -rf "$TMP4"
|
|
247
|
+
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
echo ""
|
|
250
|
+
echo "=== Summary ==="
|
|
251
|
+
echo "Passed: $PASS"
|
|
252
|
+
echo "Failed: $FAIL"
|
|
253
|
+
if [ ${#ERRORS[@]} -gt 0 ]; then
|
|
254
|
+
echo "Failing scenarios:"
|
|
255
|
+
for e in "${ERRORS[@]}"; do
|
|
256
|
+
echo " - $e"
|
|
257
|
+
done
|
|
258
|
+
exit 1
|
|
259
|
+
fi
|
|
260
|
+
echo "All tests passed."
|
|
261
|
+
exit 0
|