specflow-cc 1.12.0 → 1.14.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.
@@ -0,0 +1,130 @@
1
+ /**
2
+ * bin/lib/spec.cjs — Spec operations
3
+ *
4
+ * Exports: cmdSpecLoad(), cmdSpecList(), cmdSpecNextId()
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { output, error, safeReadFile, parseFrontmatter } = require('./core.cjs');
12
+
13
+ /**
14
+ * Load and parse a spec file.
15
+ * @param {string} cwd - Working directory
16
+ * @param {string} id - Spec ID (e.g., "SPEC-007")
17
+ * @param {boolean} raw - Output raw string
18
+ */
19
+ function cmdSpecLoad(cwd, id, raw) {
20
+ if (!id) {
21
+ error('Missing spec ID. Usage: spec load <id>');
22
+ }
23
+
24
+ const specPath = path.join(cwd, '.specflow', 'specs', id + '.md');
25
+ const content = safeReadFile(specPath);
26
+
27
+ if (!content) {
28
+ error('Spec not found: ' + specPath);
29
+ }
30
+
31
+ const parsed = parseFrontmatter(content);
32
+
33
+ output({
34
+ frontmatter: parsed.frontmatter,
35
+ body: parsed.body,
36
+ }, raw, parsed.body);
37
+ }
38
+
39
+ /**
40
+ * List all specs in .specflow/specs/.
41
+ * Extracts id, title, status, complexity, priority from each.
42
+ * Title is parsed from the first # heading in the body.
43
+ * @param {string} cwd - Working directory
44
+ * @param {boolean} raw - Output raw string
45
+ */
46
+ function cmdSpecList(cwd, raw) {
47
+ const specsDir = path.join(cwd, '.specflow', 'specs');
48
+
49
+ let files;
50
+ try {
51
+ files = fs.readdirSync(specsDir).filter(f => f.endsWith('.md')).sort();
52
+ } catch (e) {
53
+ error('Cannot read specs directory: ' + specsDir);
54
+ }
55
+
56
+ const specs = [];
57
+
58
+ for (const file of files) {
59
+ const content = safeReadFile(path.join(specsDir, file));
60
+ if (!content) continue;
61
+
62
+ const parsed = parseFrontmatter(content);
63
+ const fm = parsed.frontmatter;
64
+
65
+ // Extract title from first # heading in body
66
+ let title = '';
67
+ const titleMatch = parsed.body.match(/^#\s+(.+)$/m);
68
+ if (titleMatch) {
69
+ title = titleMatch[1].trim();
70
+ }
71
+
72
+ specs.push({
73
+ id: fm.id || file.replace('.md', ''),
74
+ title: title,
75
+ status: fm.status || '',
76
+ complexity: fm.complexity || '',
77
+ priority: fm.priority || '',
78
+ });
79
+ }
80
+
81
+ output(specs, raw, specs.map(s => s.id).join('\n'));
82
+ }
83
+
84
+ /**
85
+ * Calculate the next available SPEC-XXX number.
86
+ * Scans both .specflow/specs/ and .specflow/archive/ directories.
87
+ * Ignores non-numeric spec IDs (e.g., SPEC-GSD-A).
88
+ * @param {string} cwd - Working directory
89
+ * @param {boolean} raw - Output raw string
90
+ */
91
+ function cmdSpecNextId(cwd, raw) {
92
+ const dirs = [
93
+ path.join(cwd, '.specflow', 'specs'),
94
+ path.join(cwd, '.specflow', 'archive'),
95
+ ];
96
+
97
+ let maxNum = 0;
98
+
99
+ for (const dir of dirs) {
100
+ let files;
101
+ try {
102
+ files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
103
+ } catch (e) {
104
+ continue; // directory may not exist
105
+ }
106
+
107
+ for (const file of files) {
108
+ // Match SPEC-NNN pattern (numeric only)
109
+ const match = file.match(/^SPEC-(\d+)\.md$/);
110
+ if (match) {
111
+ const num = parseInt(match[1], 10);
112
+ if (num > maxNum) maxNum = num;
113
+ }
114
+ }
115
+ }
116
+
117
+ const nextNumber = maxNum + 1;
118
+ const nextId = 'SPEC-' + String(nextNumber).padStart(3, '0');
119
+
120
+ output({
121
+ next_id: nextId,
122
+ next_number: nextNumber,
123
+ }, raw, nextId);
124
+ }
125
+
126
+ module.exports = {
127
+ cmdSpecLoad,
128
+ cmdSpecList,
129
+ cmdSpecNextId,
130
+ };
@@ -0,0 +1,241 @@
1
+ /**
2
+ * bin/lib/state.cjs — STATE.md CRUD operations
3
+ *
4
+ * Exports: cmdStateGet(), cmdStateSetActive(), cmdQueueNext()
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { output, error, safeReadFile } = require('./core.cjs');
12
+
13
+ /**
14
+ * Extract a bold-field value from STATE.md content.
15
+ * Matches patterns like: **Field:** value
16
+ * @param {string} content - STATE.md content
17
+ * @param {string} field - Field name (e.g., "Status")
18
+ * @returns {string|null}
19
+ */
20
+ function extractBoldField(content, field) {
21
+ const regex = new RegExp(`\\*\\*${field}:\\*\\*\\s*(.+)`, 'i');
22
+ const match = content.match(regex);
23
+ return match ? match[1].trim() : null;
24
+ }
25
+
26
+ /**
27
+ * Extract the active spec ID from STATE.md.
28
+ * The spec ID is on the line immediately after "## Active Specification".
29
+ * @param {string} content - STATE.md content
30
+ * @returns {string|null}
31
+ */
32
+ function extractActiveSpec(content) {
33
+ const lines = content.split('\n');
34
+ for (let i = 0; i < lines.length; i++) {
35
+ if (lines[i].trim() === '## Active Specification') {
36
+ // Next non-empty line has the spec ID
37
+ for (let j = i + 1; j < lines.length; j++) {
38
+ const line = lines[j].trim();
39
+ if (line && !line.startsWith('**') && !line.startsWith('#')) {
40
+ return line;
41
+ }
42
+ }
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+
48
+ /**
49
+ * Parse the queue table from STATE.md.
50
+ * Expects pipe-delimited markdown table with columns:
51
+ * Priority | ID | Title | Status | Complexity | Depends On
52
+ * @param {string} content - STATE.md content
53
+ * @returns {Array<Object>}
54
+ */
55
+ function parseQueueTable(content) {
56
+ const lines = content.split('\n');
57
+ const queue = [];
58
+ let inQueue = false;
59
+ let headerFound = false;
60
+ let separatorFound = false;
61
+
62
+ for (const line of lines) {
63
+ const trimmed = line.trim();
64
+
65
+ if (trimmed === '## Queue') {
66
+ inQueue = true;
67
+ continue;
68
+ }
69
+
70
+ if (inQueue && trimmed.startsWith('##') && trimmed !== '## Queue') {
71
+ break; // next section
72
+ }
73
+
74
+ if (!inQueue) continue;
75
+
76
+ if (trimmed.startsWith('|') && !headerFound) {
77
+ // Check if this is the header row
78
+ if (trimmed.toLowerCase().includes('priority') && trimmed.toLowerCase().includes('id')) {
79
+ headerFound = true;
80
+ continue;
81
+ }
82
+ }
83
+
84
+ if (headerFound && !separatorFound && trimmed.startsWith('|') && trimmed.includes('---')) {
85
+ separatorFound = true;
86
+ continue;
87
+ }
88
+
89
+ if (headerFound && separatorFound && trimmed.startsWith('|')) {
90
+ const cells = trimmed.split('|').map(c => c.trim()).filter(c => c !== '');
91
+ if (cells.length >= 4) {
92
+ queue.push({
93
+ priority: cells[0] || '',
94
+ id: cells[1] || '',
95
+ title: cells[2] || '',
96
+ status: cells[3] || '',
97
+ complexity: cells[4] || '',
98
+ depends_on: cells[5] || '',
99
+ });
100
+ }
101
+ }
102
+ }
103
+
104
+ return queue;
105
+ }
106
+
107
+ /**
108
+ * Get current active spec, status, and next step from STATE.md.
109
+ * @param {string} cwd - Working directory
110
+ * @param {boolean} raw - Output raw string
111
+ */
112
+ function cmdStateGet(cwd, raw) {
113
+ const statePath = path.join(cwd, '.specflow', 'STATE.md');
114
+ const content = safeReadFile(statePath);
115
+
116
+ if (!content) {
117
+ error('STATE.md not found at ' + statePath);
118
+ }
119
+
120
+ const activeSpec = extractActiveSpec(content);
121
+ const status = extractBoldField(content, 'Status');
122
+ const nextStep = extractBoldField(content, 'Next Step');
123
+
124
+ const result = {
125
+ active_spec: activeSpec || null,
126
+ status: status || null,
127
+ next_step: nextStep || null,
128
+ };
129
+
130
+ output(result, raw, result.active_spec);
131
+ }
132
+
133
+ /**
134
+ * Update active spec, status, and optionally next step in STATE.md.
135
+ * @param {string} cwd - Working directory
136
+ * @param {string} id - Spec ID (e.g., "SPEC-007")
137
+ * @param {string} status - New status
138
+ * @param {string} [nextStep] - Optional new next step
139
+ * @param {boolean} raw - Output raw string
140
+ */
141
+ function cmdStateSetActive(cwd, id, status, nextStep, raw) {
142
+ const statePath = path.join(cwd, '.specflow', 'STATE.md');
143
+ const content = safeReadFile(statePath);
144
+
145
+ if (!content) {
146
+ error('STATE.md not found at ' + statePath);
147
+ }
148
+
149
+ const lines = content.split('\n');
150
+ const result = [];
151
+ let inActiveSection = false;
152
+ let specIdReplaced = false;
153
+
154
+ for (let i = 0; i < lines.length; i++) {
155
+ const trimmed = lines[i].trim();
156
+
157
+ if (trimmed === '## Active Specification') {
158
+ inActiveSection = true;
159
+ result.push(lines[i]);
160
+ continue;
161
+ }
162
+
163
+ if (inActiveSection && trimmed.startsWith('## ') && trimmed !== '## Active Specification') {
164
+ inActiveSection = false;
165
+ }
166
+
167
+ if (inActiveSection && !specIdReplaced && trimmed && !trimmed.startsWith('**') && !trimmed.startsWith('#')) {
168
+ // This is the spec ID line — replace it
169
+ result.push(id);
170
+ specIdReplaced = true;
171
+ continue;
172
+ }
173
+
174
+ if (inActiveSection && trimmed.startsWith('**Status:**')) {
175
+ result.push('**Status:** ' + status);
176
+ continue;
177
+ }
178
+
179
+ if (inActiveSection && trimmed.startsWith('**Next Step:**')) {
180
+ if (nextStep !== undefined && nextStep !== null) {
181
+ result.push('**Next Step:** ' + nextStep);
182
+ } else {
183
+ result.push(lines[i]); // preserve existing
184
+ }
185
+ continue;
186
+ }
187
+
188
+ result.push(lines[i]);
189
+ }
190
+
191
+ fs.writeFileSync(statePath, result.join('\n'), 'utf8');
192
+
193
+ const resultObj = {
194
+ updated: true,
195
+ active_spec: id,
196
+ status: status,
197
+ next_step: nextStep || extractBoldField(result.join('\n'), 'Next Step'),
198
+ };
199
+
200
+ output(resultObj, raw, 'updated');
201
+ }
202
+
203
+ /**
204
+ * Get the first actionable spec from the queue.
205
+ * "Actionable" = status is not "done" or "complete".
206
+ * @param {string} cwd - Working directory
207
+ * @param {boolean} raw - Output raw string
208
+ */
209
+ function cmdQueueNext(cwd, raw) {
210
+ const statePath = path.join(cwd, '.specflow', 'STATE.md');
211
+ const content = safeReadFile(statePath);
212
+
213
+ if (!content) {
214
+ error('STATE.md not found at ' + statePath);
215
+ }
216
+
217
+ const queue = parseQueueTable(content);
218
+
219
+ const next = queue.find(entry => {
220
+ const s = entry.status.toLowerCase();
221
+ return s !== 'done' && s !== 'complete';
222
+ });
223
+
224
+ if (next) {
225
+ output({
226
+ id: next.id,
227
+ title: next.title,
228
+ status: next.status,
229
+ priority: next.priority,
230
+ }, raw, next.id);
231
+ } else {
232
+ output({ id: null }, raw, '');
233
+ }
234
+ }
235
+
236
+ module.exports = {
237
+ cmdStateGet,
238
+ cmdStateSetActive,
239
+ cmdQueueNext,
240
+ extractActiveSpec,
241
+ };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * bin/lib/verify.cjs — Structure verification checks
3
+ *
4
+ * Exports: cmdVerifyStructure()
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { output, safeReadFile, parseFrontmatter } = require('./core.cjs');
12
+ const { extractActiveSpec } = require('./state.cjs');
13
+
14
+ /**
15
+ * Verify .specflow/ directory structure and integrity.
16
+ * Performs 6 read-only checks. Does NOT repair issues.
17
+ *
18
+ * @param {string} cwd - Working directory
19
+ * @param {boolean} raw - Output raw string
20
+ */
21
+ function cmdVerifyStructure(cwd, raw) {
22
+ const checks = [];
23
+ const errors = [];
24
+
25
+ // Check 1: .specflow/ directory exists
26
+ const specflowDir = path.join(cwd, '.specflow');
27
+ const specflowExists = fs.existsSync(specflowDir) && fs.statSync(specflowDir).isDirectory();
28
+ checks.push({ name: '.specflow/ directory exists', passed: specflowExists });
29
+ if (!specflowExists) {
30
+ errors.push('.specflow/ directory not found');
31
+ }
32
+
33
+ // Check 2: STATE.md exists and has "Active Specification" section
34
+ const statePath = path.join(cwd, '.specflow', 'STATE.md');
35
+ const stateContent = safeReadFile(statePath);
36
+ const stateHasSection = stateContent ? stateContent.includes('## Active Specification') : false;
37
+ const stateValid = !!stateContent && stateHasSection;
38
+ checks.push({ name: 'STATE.md exists with Active Specification section', passed: stateValid });
39
+ if (!stateContent) {
40
+ errors.push('STATE.md not found');
41
+ } else if (!stateHasSection) {
42
+ errors.push('STATE.md missing "## Active Specification" section');
43
+ }
44
+
45
+ // Check 3: specs/ directory exists
46
+ const specsDir = path.join(cwd, '.specflow', 'specs');
47
+ const specsDirExists = fs.existsSync(specsDir) && fs.statSync(specsDir).isDirectory();
48
+ checks.push({ name: '.specflow/specs/ directory exists', passed: specsDirExists });
49
+ if (!specsDirExists) {
50
+ errors.push('.specflow/specs/ directory not found');
51
+ }
52
+
53
+ // Check 4: archive/ directory exists
54
+ const archiveDir = path.join(cwd, '.specflow', 'archive');
55
+ const archiveDirExists = fs.existsSync(archiveDir) && fs.statSync(archiveDir).isDirectory();
56
+ checks.push({ name: '.specflow/archive/ directory exists', passed: archiveDirExists });
57
+ if (!archiveDirExists) {
58
+ errors.push('.specflow/archive/ directory not found');
59
+ }
60
+
61
+ // Check 5: Active spec referenced in STATE.md exists in specs/ (if set)
62
+ let activeSpecCheck = true;
63
+ if (stateContent) {
64
+ const activeSpec = extractActiveSpec(stateContent);
65
+
66
+ if (activeSpec && activeSpec !== 'None' && activeSpec !== '(none)') {
67
+ const activeSpecPath = path.join(specsDir, activeSpec + '.md');
68
+ const activeSpecExists = fs.existsSync(activeSpecPath);
69
+ activeSpecCheck = activeSpecExists;
70
+ if (!activeSpecExists) {
71
+ errors.push('Active spec ' + activeSpec + ' not found in specs/');
72
+ }
73
+ }
74
+ }
75
+ checks.push({ name: 'Active spec exists in specs/ (if set)', passed: activeSpecCheck });
76
+
77
+ // Check 6: All specs in specs/ have valid YAML frontmatter with required fields
78
+ let allSpecsValid = true;
79
+ if (specsDirExists) {
80
+ let specFiles;
81
+ try {
82
+ specFiles = fs.readdirSync(specsDir).filter(f => f.endsWith('.md'));
83
+ } catch (e) {
84
+ specFiles = [];
85
+ }
86
+
87
+ for (const file of specFiles) {
88
+ const content = safeReadFile(path.join(specsDir, file));
89
+ if (!content) {
90
+ allSpecsValid = false;
91
+ errors.push('Cannot read spec file: ' + file);
92
+ continue;
93
+ }
94
+
95
+ const parsed = parseFrontmatter(content);
96
+ const fm = parsed.frontmatter;
97
+ const requiredFields = ['id', 'type', 'status'];
98
+ const missingFields = requiredFields.filter(f => !fm[f]);
99
+
100
+ if (missingFields.length > 0) {
101
+ allSpecsValid = false;
102
+ errors.push(file + ' missing required frontmatter fields: ' + missingFields.join(', '));
103
+ }
104
+ }
105
+ } else {
106
+ allSpecsValid = false;
107
+ }
108
+ checks.push({ name: 'All specs have valid frontmatter (id, type, status)', passed: allSpecsValid });
109
+
110
+ const valid = checks.every(c => c.passed);
111
+
112
+ output({ valid, checks, errors }, raw, valid ? 'valid' : 'invalid');
113
+ }
114
+
115
+ module.exports = {
116
+ cmdVerifyStructure,
117
+ };
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * bin/sf-tools.cjs — Centralized CLI for SpecFlow operations
5
+ *
6
+ * Usage: node bin/sf-tools.cjs <command> [args...]
7
+ *
8
+ * Commands:
9
+ * spec load <id> Parse spec file, return frontmatter + body
10
+ * spec list List all specs
11
+ * spec next-id Next available SPEC-XXX number
12
+ * queue next First actionable spec from queue
13
+ * state get Current active spec, status, next step
14
+ * state set-active <id> <status> [next] Update active spec in STATE.md
15
+ * resolve-model <agent-type> Model for agent by current profile
16
+ * verify-structure Check .specflow/ integrity
17
+ * generate-slug <text> Text to URL-safe slug
18
+ */
19
+
20
+ 'use strict';
21
+
22
+ const { output, error, generateSlug } = require('./lib/core.cjs');
23
+ const { cmdStateGet, cmdStateSetActive, cmdQueueNext } = require('./lib/state.cjs');
24
+ const { cmdSpecLoad, cmdSpecList, cmdSpecNextId } = require('./lib/spec.cjs');
25
+ const { cmdResolveModel } = require('./lib/config.cjs');
26
+ const { cmdVerifyStructure } = require('./lib/verify.cjs');
27
+
28
+ const cwd = process.cwd();
29
+ const args = process.argv.slice(2);
30
+ const raw = args.includes('--raw');
31
+ const filteredArgs = args.filter(a => a !== '--raw');
32
+
33
+ /**
34
+ * Command dispatch table.
35
+ * Keys are "command subcommand" or just "command".
36
+ */
37
+ const COMMANDS = {
38
+ 'spec load': () => {
39
+ if (!filteredArgs[2]) error('Missing spec ID. Usage: spec load <id>');
40
+ cmdSpecLoad(cwd, filteredArgs[2], raw);
41
+ },
42
+ 'spec list': () => cmdSpecList(cwd, raw),
43
+ 'spec next-id': () => cmdSpecNextId(cwd, raw),
44
+ 'queue next': () => cmdQueueNext(cwd, raw),
45
+ 'state get': () => cmdStateGet(cwd, raw),
46
+ 'state set-active': () => {
47
+ if (!filteredArgs[2] || !filteredArgs[3]) {
48
+ error('Missing arguments. Usage: state set-active <id> <status> [next_step]');
49
+ }
50
+ cmdStateSetActive(cwd, filteredArgs[2], filteredArgs[3], filteredArgs[4], raw);
51
+ },
52
+ 'resolve-model': () => {
53
+ if (!filteredArgs[1]) error('Missing agent type. Usage: resolve-model <agent-type>');
54
+ cmdResolveModel(cwd, filteredArgs[1], raw);
55
+ },
56
+ 'verify-structure': () => cmdVerifyStructure(cwd, raw),
57
+ 'generate-slug': () => {
58
+ if (!filteredArgs[1]) error('Missing text. Usage: generate-slug <text>');
59
+ output({ slug: generateSlug(filteredArgs[1]) }, raw, generateSlug(filteredArgs[1]));
60
+ },
61
+ };
62
+
63
+ function printUsage() {
64
+ const usage = `sf-tools — SpecFlow CLI
65
+
66
+ Usage: node bin/sf-tools.cjs <command> [args...] [--raw]
67
+
68
+ Commands:
69
+ spec load <id> Parse spec file, return frontmatter + body
70
+ spec list List all specs from .specflow/specs/
71
+ spec next-id Next available SPEC-XXX number
72
+ queue next First actionable spec from queue table
73
+ state get Current active spec, status, next step
74
+ state set-active <id> <status> [next] Update active spec, status, next step
75
+ resolve-model <agent-type> Resolve model for agent by current profile
76
+ verify-structure Check .specflow/ directory integrity
77
+ generate-slug <text> Convert text to URL-safe slug
78
+
79
+ Options:
80
+ --raw Output plain string instead of JSON
81
+
82
+ All commands output JSON to stdout. Errors go to stderr with exit code 1.
83
+ `;
84
+ process.stdout.write(usage);
85
+ }
86
+
87
+ // Main dispatch
88
+ if (filteredArgs.length === 0) {
89
+ printUsage();
90
+ process.exit(0);
91
+ }
92
+
93
+ // Try two-word command first, then single-word
94
+ const twoWord = filteredArgs[0] + ' ' + (filteredArgs[1] || '');
95
+ const oneWord = filteredArgs[0];
96
+
97
+ if (COMMANDS[twoWord]) {
98
+ COMMANDS[twoWord]();
99
+ } else if (COMMANDS[oneWord]) {
100
+ COMMANDS[oneWord]();
101
+ } else {
102
+ error('Unknown command: ' + filteredArgs.join(' ') + '. Run without arguments for usage help.');
103
+ }
@@ -242,75 +242,17 @@ The agent will:
242
242
 
243
243
  After the agent updates STATE.md, check if rotation is needed:
244
244
 
245
- ```bash
246
- LINE_COUNT=$(wc -l < .specflow/STATE.md)
247
- if [ $LINE_COUNT -gt 100 ]; then
248
- echo "STATE.md exceeds 100 lines ($LINE_COUNT), rotating old decisions..."
249
-
250
- # Parse decisions table and extract all decisions
251
- DECISIONS=$(awk '/^## Decisions$/ { found=1; next } /^## / && found { exit } found { print }' .specflow/STATE.md | grep -E '^\| [0-9]{4}-' || true)
252
- DECISION_COUNT=$(echo "$DECISIONS" | grep -c '^|' || echo 0)
253
-
254
- if [ "$DECISION_COUNT" -gt 7 ]; then
255
- # Keep only 5 most recent decisions
256
- RECENT_DECISIONS=$(echo "$DECISIONS" | tail -5)
257
- OLD_DECISION_COUNT=$((DECISION_COUNT - 5))
258
- OLD_DECISIONS=$(echo "$DECISIONS" | head -n $OLD_DECISION_COUNT)
259
-
260
- # Create or append to archive
261
- if [ ! -f .specflow/DECISIONS_ARCHIVE.md ]; then
262
- cat > .specflow/DECISIONS_ARCHIVE.md << 'EOF'
263
- # SpecFlow Decisions Archive
264
-
265
- Historical decisions rotated from STATE.md to maintain compactness.
266
-
267
- ## Archived Decisions
268
-
269
- | Date | Decision |
270
- |------|----------|
271
- EOF
272
- fi
273
-
274
- # Write old decisions to temp file for awk to read (awk -v cannot handle multiline strings)
275
- TEMP_OLD=$(mktemp)
276
- echo "$OLD_DECISIONS" > "$TEMP_OLD"
277
-
278
- # Append old decisions to archive (insert after table header)
279
- TEMP_ARCHIVE=$(mktemp)
280
- awk -v oldfile="$TEMP_OLD" '
281
- /^\| Date \| Decision \|$/ { print; getline; print; while ((getline line < oldfile) > 0) print line; close(oldfile); next }
282
- {print}
283
- ' .specflow/DECISIONS_ARCHIVE.md > "$TEMP_ARCHIVE"
284
- mv "$TEMP_ARCHIVE" .specflow/DECISIONS_ARCHIVE.md
285
- rm -f "$TEMP_OLD"
286
-
287
- # Write recent decisions to temp file for awk to read
288
- TEMP_RECENT=$(mktemp)
289
- echo "$RECENT_DECISIONS" > "$TEMP_RECENT"
290
-
291
- # Update STATE.md with only recent decisions
292
- TEMP_STATE=$(mktemp)
293
- awk -v recentfile="$TEMP_RECENT" '
294
- /^## Decisions$/ {
295
- print
296
- print ""
297
- print "| Date | Decision |"
298
- print "|------|----------|"
299
- while ((getline line < recentfile) > 0) print line
300
- close(recentfile)
301
- in_decisions=1
302
- next
303
- }
304
- /^## / && in_decisions { in_decisions=0 }
305
- !in_decisions || !/^\|/ { print }
306
- ' .specflow/STATE.md > "$TEMP_STATE"
307
- mv "$TEMP_STATE" .specflow/STATE.md
308
- rm -f "$TEMP_RECENT"
309
-
310
- echo "Rotated $(echo "$OLD_DECISIONS" | grep -c '^|') old decisions to DECISIONS_ARCHIVE.md"
311
- fi
312
- fi
313
- ```
245
+ 1. Use the Read tool to read `.specflow/STATE.md` and count total lines
246
+ 2. If total lines <= 100, no action needed
247
+ 3. If total lines > 100:
248
+ a. Read the `## Decisions` section and extract all decision rows (lines matching `| YYYY-`)
249
+ b. Count decision rows. If <= 7, no rotation needed
250
+ c. If > 7 decisions:
251
+ - Identify the 5 most recent decisions (last 5 rows) -- these STAY
252
+ - Identify older decisions (all rows except last 5) -- these MOVE to archive
253
+ - Read `.specflow/DECISIONS_ARCHIVE.md` (create with template if missing)
254
+ - Write updated DECISIONS_ARCHIVE.md: insert old decisions after the table header row
255
+ - Write updated STATE.md: replace Decisions section content with only the 5 most recent decisions
314
256
 
315
257
  ## Step 7: Display Result
316
258