specsmd 0.0.0-dev.54 → 0.0.0-dev.55

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.
@@ -57,7 +57,7 @@ Before executing scripts, ensure required dependencies are installed:
57
57
  </mandate>
58
58
 
59
59
  <step n="1" title="Initialize Run">
60
- <action script="scripts/init-run.ts">Create run record</action>
60
+ <action script="scripts/init-run.js">Create run record</action>
61
61
  <action>Generate run ID: run-{NNN}</action>
62
62
  <action>Create folder: .specs-fire/runs/{run-id}/</action>
63
63
  <action>Set active_run in state.yaml</action>
@@ -189,7 +189,7 @@ Before executing scripts, ensure required dependencies are installed:
189
189
  </step>
190
190
 
191
191
  <step n="7" title="Complete Run">
192
- <action script="scripts/complete-run.ts">Finalize run record</action>
192
+ <action script="scripts/complete-run.js">Finalize run record</action>
193
193
  <action>Update state.yaml (clear active_run, mark work item complete)</action>
194
194
  <action>Generate run log: .specs-fire/runs/{run-id}/run.md</action>
195
195
  </step>
@@ -224,8 +224,8 @@ Before executing scripts, ensure required dependencies are installed:
224
224
 
225
225
  | Script | Purpose |
226
226
  |--------|---------|
227
- | `scripts/init-run.ts` | Initialize run record and folder |
228
- | `scripts/complete-run.ts` | Finalize run and update state |
227
+ | `scripts/init-run.js` | Initialize run record and folder |
228
+ | `scripts/complete-run.js` | Finalize run and update state |
229
229
 
230
230
  ---
231
231
 
@@ -0,0 +1,384 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * FIRE Run Completion Script
5
+ *
6
+ * Finalizes a run by:
7
+ * 1. Recording the completed run in state.yaml runs.completed history
8
+ * 2. Updating work item status to completed
9
+ * 3. Clearing active_run
10
+ * 4. Updating run.md with completion data
11
+ *
12
+ * Usage: node complete-run.js <rootPath> <runId> [--files-created=JSON] [--files-modified=JSON] [--decisions=JSON] [--tests=N] [--coverage=N]
13
+ *
14
+ * Example: node complete-run.js /project run-003 --tests=5 --coverage=85
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const yaml = require('yaml');
20
+
21
+ // =============================================================================
22
+ // Error Helper
23
+ // =============================================================================
24
+
25
+ function fireError(message, code, suggestion) {
26
+ const err = new Error(`FIRE Error [${code}]: ${message} ${suggestion}`);
27
+ err.code = code;
28
+ err.suggestion = suggestion;
29
+ return err;
30
+ }
31
+
32
+ // =============================================================================
33
+ // Validation
34
+ // =============================================================================
35
+
36
+ function validateInputs(rootPath, runId) {
37
+ if (!rootPath || typeof rootPath !== 'string' || rootPath.trim() === '') {
38
+ throw fireError('rootPath is required.', 'COMPLETE_001', 'Provide a valid project root path.');
39
+ }
40
+
41
+ if (!runId || typeof runId !== 'string' || runId.trim() === '') {
42
+ throw fireError('runId is required.', 'COMPLETE_002', 'Provide the run ID to complete.');
43
+ }
44
+
45
+ if (!fs.existsSync(rootPath)) {
46
+ throw fireError(
47
+ `Project root not found: "${rootPath}".`,
48
+ 'COMPLETE_003',
49
+ 'Ensure the path exists and is accessible.'
50
+ );
51
+ }
52
+ }
53
+
54
+ function validateFireProject(rootPath, runId) {
55
+ const fireDir = path.join(rootPath, '.specs-fire');
56
+ const statePath = path.join(fireDir, 'state.yaml');
57
+ const runsPath = path.join(fireDir, 'runs');
58
+ const runPath = path.join(runsPath, runId);
59
+ const runLogPath = path.join(runPath, 'run.md');
60
+
61
+ if (!fs.existsSync(fireDir)) {
62
+ throw fireError(
63
+ `FIRE project not initialized at: "${rootPath}".`,
64
+ 'COMPLETE_010',
65
+ 'Run fire-init first to initialize the project.'
66
+ );
67
+ }
68
+
69
+ if (!fs.existsSync(statePath)) {
70
+ throw fireError(
71
+ `State file not found at: "${statePath}".`,
72
+ 'COMPLETE_011',
73
+ 'The project may be corrupted. Try re-initializing.'
74
+ );
75
+ }
76
+
77
+ if (!fs.existsSync(runPath)) {
78
+ throw fireError(
79
+ `Run folder not found: "${runPath}".`,
80
+ 'COMPLETE_012',
81
+ `Ensure run "${runId}" was properly initialized.`
82
+ );
83
+ }
84
+
85
+ if (!fs.existsSync(runLogPath)) {
86
+ throw fireError(
87
+ `Run log not found: "${runLogPath}".`,
88
+ 'COMPLETE_013',
89
+ `The run may have been partially initialized.`
90
+ );
91
+ }
92
+
93
+ return { statePath, runPath, runLogPath };
94
+ }
95
+
96
+ // =============================================================================
97
+ // State Operations
98
+ // =============================================================================
99
+
100
+ function readState(statePath) {
101
+ try {
102
+ const content = fs.readFileSync(statePath, 'utf8');
103
+ const state = yaml.parse(content);
104
+ if (!state || typeof state !== 'object') {
105
+ throw fireError('State file is empty or invalid.', 'COMPLETE_020', 'Check state.yaml format.');
106
+ }
107
+ return state;
108
+ } catch (err) {
109
+ if (err.code && err.code.startsWith('COMPLETE_')) throw err;
110
+ throw fireError(
111
+ `Failed to read state file: ${err.message}`,
112
+ 'COMPLETE_021',
113
+ 'Check file permissions and YAML syntax.'
114
+ );
115
+ }
116
+ }
117
+
118
+ function writeState(statePath, state) {
119
+ try {
120
+ fs.writeFileSync(statePath, yaml.stringify(state));
121
+ } catch (err) {
122
+ throw fireError(
123
+ `Failed to write state file: ${err.message}`,
124
+ 'COMPLETE_022',
125
+ 'Check file permissions and disk space.'
126
+ );
127
+ }
128
+ }
129
+
130
+ // =============================================================================
131
+ // Run Log Operations
132
+ // =============================================================================
133
+
134
+ function updateRunLog(runLogPath, params, completedTime) {
135
+ let content;
136
+ try {
137
+ content = fs.readFileSync(runLogPath, 'utf8');
138
+ } catch (err) {
139
+ throw fireError(
140
+ `Failed to read run log: ${err.message}`,
141
+ 'COMPLETE_030',
142
+ 'Check file permissions.'
143
+ );
144
+ }
145
+
146
+ // Update status
147
+ content = content.replace(/status: in_progress/, 'status: completed');
148
+ content = content.replace(/completed: null/, `completed: ${completedTime}`);
149
+
150
+ // Format file lists
151
+ const filesCreatedText = params.filesCreated.length > 0
152
+ ? params.filesCreated.map(f => `- \`${f.path}\`: ${f.purpose || '(no purpose)'}`).join('\n')
153
+ : '(none)';
154
+
155
+ const filesModifiedText = params.filesModified.length > 0
156
+ ? params.filesModified.map(f => `- \`${f.path}\`: ${f.changes || '(no changes)'}`).join('\n')
157
+ : '(none)';
158
+
159
+ const decisionsText = params.decisions.length > 0
160
+ ? params.decisions.map(d => `- **${d.decision}**: ${d.choice} (${d.rationale || 'no rationale'})`).join('\n')
161
+ : '(none)';
162
+
163
+ // Replace placeholder sections
164
+ content = content.replace('## Files Created\n(none yet)', `## Files Created\n${filesCreatedText}`);
165
+ content = content.replace('## Files Modified\n(none yet)', `## Files Modified\n${filesModifiedText}`);
166
+ content = content.replace('## Decisions\n(none yet)', `## Decisions\n${decisionsText}`);
167
+
168
+ // Add summary if not present
169
+ if (!content.includes('## Summary')) {
170
+ content += `
171
+
172
+ ## Summary
173
+
174
+ - Files created: ${params.filesCreated.length}
175
+ - Files modified: ${params.filesModified.length}
176
+ - Tests added: ${params.testsAdded}
177
+ - Coverage: ${params.coverage}%
178
+ - Completed: ${completedTime}
179
+ `;
180
+ }
181
+
182
+ try {
183
+ fs.writeFileSync(runLogPath, content);
184
+ } catch (err) {
185
+ throw fireError(
186
+ `Failed to write run log: ${err.message}`,
187
+ 'COMPLETE_031',
188
+ 'Check file permissions.'
189
+ );
190
+ }
191
+ }
192
+
193
+ // =============================================================================
194
+ // Main Function
195
+ // =============================================================================
196
+
197
+ function completeRun(rootPath, runId, params = {}) {
198
+ // Defaults for optional params
199
+ const completionParams = {
200
+ filesCreated: params.filesCreated || [],
201
+ filesModified: params.filesModified || [],
202
+ decisions: params.decisions || [],
203
+ testsAdded: params.testsAdded || 0,
204
+ coverage: params.coverage || 0,
205
+ };
206
+
207
+ // Validate inputs
208
+ validateInputs(rootPath, runId);
209
+
210
+ // Validate FIRE project structure
211
+ const { statePath, runLogPath } = validateFireProject(rootPath, runId);
212
+
213
+ // Read state
214
+ const state = readState(statePath);
215
+
216
+ // Validate active run matches
217
+ if (!state.active_run) {
218
+ throw fireError(
219
+ 'No active run found in state.yaml.',
220
+ 'COMPLETE_040',
221
+ 'The run may have already been completed or was never started.'
222
+ );
223
+ }
224
+
225
+ if (state.active_run.id !== runId) {
226
+ throw fireError(
227
+ `Run ID mismatch. Active run is "${state.active_run.id}" but trying to complete "${runId}".`,
228
+ 'COMPLETE_041',
229
+ `Complete the active run "${state.active_run.id}" first.`
230
+ );
231
+ }
232
+
233
+ // Extract work item and intent from active run
234
+ const workItemId = state.active_run.work_item;
235
+ const intentId = state.active_run.intent;
236
+ const completedTime = new Date().toISOString();
237
+
238
+ // Update run log
239
+ updateRunLog(runLogPath, completionParams, completedTime);
240
+
241
+ // Build completed run record
242
+ const completedRun = {
243
+ id: runId,
244
+ work_item: workItemId,
245
+ intent: intentId,
246
+ completed: completedTime,
247
+ };
248
+
249
+ // Get existing runs history or initialize
250
+ const existingRuns = state.runs || { completed: [] };
251
+ const existingCompleted = Array.isArray(existingRuns.completed) ? existingRuns.completed : [];
252
+
253
+ // Check for duplicate (idempotency)
254
+ const alreadyRecorded = existingCompleted.some(r => r.id === runId);
255
+
256
+ // Update work item status in intents
257
+ if (Array.isArray(state.intents)) {
258
+ for (const intent of state.intents) {
259
+ if (intent.id === intentId && Array.isArray(intent.work_items)) {
260
+ for (const workItem of intent.work_items) {
261
+ if (workItem.id === workItemId) {
262
+ workItem.status = 'completed';
263
+ workItem.run_id = runId;
264
+ break;
265
+ }
266
+ }
267
+ break;
268
+ }
269
+ }
270
+ }
271
+
272
+ // Update state
273
+ state.active_run = null;
274
+ state.runs = {
275
+ completed: alreadyRecorded ? existingCompleted : [...existingCompleted, completedRun],
276
+ };
277
+
278
+ // Save state
279
+ writeState(statePath, state);
280
+
281
+ // Return result
282
+ return {
283
+ success: true,
284
+ runId: runId,
285
+ workItemId: workItemId,
286
+ intentId: intentId,
287
+ completedAt: completedTime,
288
+ filesCreated: completionParams.filesCreated.length,
289
+ filesModified: completionParams.filesModified.length,
290
+ testsAdded: completionParams.testsAdded,
291
+ coverage: completionParams.coverage,
292
+ };
293
+ }
294
+
295
+ // =============================================================================
296
+ // CLI Argument Parsing
297
+ // =============================================================================
298
+
299
+ function parseArgs(args) {
300
+ const result = {
301
+ rootPath: args[0],
302
+ runId: args[1],
303
+ filesCreated: [],
304
+ filesModified: [],
305
+ decisions: [],
306
+ testsAdded: 0,
307
+ coverage: 0,
308
+ };
309
+
310
+ for (let i = 2; i < args.length; i++) {
311
+ const arg = args[i];
312
+ if (arg.startsWith('--files-created=')) {
313
+ try {
314
+ result.filesCreated = JSON.parse(arg.substring('--files-created='.length));
315
+ } catch (e) {
316
+ console.error('Warning: Could not parse --files-created JSON');
317
+ }
318
+ } else if (arg.startsWith('--files-modified=')) {
319
+ try {
320
+ result.filesModified = JSON.parse(arg.substring('--files-modified='.length));
321
+ } catch (e) {
322
+ console.error('Warning: Could not parse --files-modified JSON');
323
+ }
324
+ } else if (arg.startsWith('--decisions=')) {
325
+ try {
326
+ result.decisions = JSON.parse(arg.substring('--decisions='.length));
327
+ } catch (e) {
328
+ console.error('Warning: Could not parse --decisions JSON');
329
+ }
330
+ } else if (arg.startsWith('--tests=')) {
331
+ result.testsAdded = parseInt(arg.substring('--tests='.length), 10) || 0;
332
+ } else if (arg.startsWith('--coverage=')) {
333
+ result.coverage = parseFloat(arg.substring('--coverage='.length)) || 0;
334
+ }
335
+ }
336
+
337
+ return result;
338
+ }
339
+
340
+ // =============================================================================
341
+ // CLI Interface
342
+ // =============================================================================
343
+
344
+ if (require.main === module) {
345
+ const args = process.argv.slice(2);
346
+
347
+ if (args.length < 2) {
348
+ console.error('Usage: node complete-run.js <rootPath> <runId> [options]');
349
+ console.error('');
350
+ console.error('Arguments:');
351
+ console.error(' rootPath - Project root directory');
352
+ console.error(' runId - Run ID to complete (e.g., run-003)');
353
+ console.error('');
354
+ console.error('Options:');
355
+ console.error(' --files-created=JSON - JSON array of {path, purpose}');
356
+ console.error(' --files-modified=JSON - JSON array of {path, changes}');
357
+ console.error(' --decisions=JSON - JSON array of {decision, choice, rationale}');
358
+ console.error(' --tests=N - Number of tests added');
359
+ console.error(' --coverage=N - Coverage percentage');
360
+ console.error('');
361
+ console.error('Example:');
362
+ console.error(' node complete-run.js /my/project run-003 --tests=5 --coverage=85');
363
+ process.exit(1);
364
+ }
365
+
366
+ const params = parseArgs(args);
367
+
368
+ try {
369
+ const result = completeRun(params.rootPath, params.runId, {
370
+ filesCreated: params.filesCreated,
371
+ filesModified: params.filesModified,
372
+ decisions: params.decisions,
373
+ testsAdded: params.testsAdded,
374
+ coverage: params.coverage,
375
+ });
376
+ console.log(JSON.stringify(result, null, 2));
377
+ process.exit(0);
378
+ } catch (err) {
379
+ console.error(err.message);
380
+ process.exit(1);
381
+ }
382
+ }
383
+
384
+ module.exports = { completeRun };