specsmd 0.0.0-dev.41 → 0.0.0-dev.42
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/flows/fire/README.md +130 -0
- package/flows/fire/agents/builder/agent.md +192 -0
- package/flows/fire/agents/builder/skills/run-execute/SKILL.md +196 -0
- package/flows/fire/agents/builder/skills/run-execute/scripts/complete-run.ts +806 -0
- package/flows/fire/agents/builder/skills/run-execute/scripts/init-run.ts +575 -0
- package/flows/fire/agents/builder/skills/run-status/SKILL.md +94 -0
- package/flows/fire/agents/builder/skills/walkthrough-generate/SKILL.md +140 -0
- package/flows/fire/agents/builder/skills/walkthrough-generate/scripts/render-walkthrough.ts +755 -0
- package/flows/fire/agents/orchestrator/agent.md +113 -0
- package/flows/fire/agents/orchestrator/skills/project-init/SKILL.md +124 -0
- package/flows/fire/agents/orchestrator/skills/route/SKILL.md +126 -0
- package/flows/fire/agents/orchestrator/skills/status/SKILL.md +99 -0
- package/flows/fire/agents/planner/agent.md +122 -0
- package/flows/fire/agents/planner/skills/design-doc-generate/SKILL.md +211 -0
- package/flows/fire/agents/planner/skills/intent-capture/SKILL.md +142 -0
- package/flows/fire/agents/planner/skills/work-item-decompose/SKILL.md +174 -0
- package/flows/fire/commands/fire-builder.md +56 -0
- package/flows/fire/commands/fire-planner.md +48 -0
- package/flows/fire/commands/fire.md +46 -0
- package/flows/fire/fire-config.yaml +109 -0
- package/package.json +1 -1
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initialize a new run
|
|
3
|
+
*
|
|
4
|
+
* Creates run record in state.yaml and run folder structure
|
|
5
|
+
*
|
|
6
|
+
* This script is defensive and provides clear error messages for both
|
|
7
|
+
* humans and AI agents to understand failures and how to fix them.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
readFileSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
mkdirSync,
|
|
14
|
+
existsSync,
|
|
15
|
+
readdirSync,
|
|
16
|
+
rmSync,
|
|
17
|
+
} from 'fs';
|
|
18
|
+
import { join, isAbsolute } from 'path';
|
|
19
|
+
import * as yaml from 'yaml';
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Types
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
interface RunInit {
|
|
26
|
+
workItemId: string;
|
|
27
|
+
intentId: string;
|
|
28
|
+
mode: 'autopilot' | 'confirm' | 'validate';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface WorkItem {
|
|
32
|
+
id: string;
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface Intent {
|
|
37
|
+
id: string;
|
|
38
|
+
work_items?: WorkItem[];
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface ActiveRun {
|
|
43
|
+
id: string;
|
|
44
|
+
work_item: string;
|
|
45
|
+
intent: string;
|
|
46
|
+
mode: string;
|
|
47
|
+
started: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface State {
|
|
51
|
+
project?: { name?: string };
|
|
52
|
+
intents?: Intent[];
|
|
53
|
+
active_run?: ActiveRun | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// Custom Error Class
|
|
58
|
+
// =============================================================================
|
|
59
|
+
|
|
60
|
+
export class FireError extends Error {
|
|
61
|
+
constructor(
|
|
62
|
+
message: string,
|
|
63
|
+
public readonly code: string,
|
|
64
|
+
public readonly suggestion: string
|
|
65
|
+
) {
|
|
66
|
+
super(`FIRE Error [${code}]: ${message} ${suggestion}`);
|
|
67
|
+
this.name = 'FireError';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// =============================================================================
|
|
72
|
+
// Validation Helpers
|
|
73
|
+
// =============================================================================
|
|
74
|
+
|
|
75
|
+
const VALID_MODES = ['autopilot', 'confirm', 'validate'] as const;
|
|
76
|
+
|
|
77
|
+
function validateRootPath(rootPath: unknown): asserts rootPath is string {
|
|
78
|
+
if (rootPath === undefined || rootPath === null) {
|
|
79
|
+
throw new FireError(
|
|
80
|
+
'rootPath is required but was not provided.',
|
|
81
|
+
'INIT_001',
|
|
82
|
+
'Ensure the calling code passes a valid project root path.'
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (typeof rootPath !== 'string') {
|
|
87
|
+
throw new FireError(
|
|
88
|
+
`rootPath must be a string, but received ${typeof rootPath}.`,
|
|
89
|
+
'INIT_002',
|
|
90
|
+
'Ensure the calling code passes a string path.'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (rootPath.trim() === '') {
|
|
95
|
+
throw new FireError(
|
|
96
|
+
'rootPath cannot be empty.',
|
|
97
|
+
'INIT_003',
|
|
98
|
+
'Provide a valid project root path.'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!isAbsolute(rootPath)) {
|
|
103
|
+
throw new FireError(
|
|
104
|
+
`rootPath must be an absolute path, but received relative path: "${rootPath}".`,
|
|
105
|
+
'INIT_004',
|
|
106
|
+
'Convert the path to absolute using path.resolve() before calling initRun().'
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!existsSync(rootPath)) {
|
|
111
|
+
throw new FireError(
|
|
112
|
+
`Project root directory does not exist: "${rootPath}".`,
|
|
113
|
+
'INIT_005',
|
|
114
|
+
'Verify the path is correct and the directory exists.'
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function validateParams(params: unknown): asserts params is RunInit {
|
|
120
|
+
if (params === undefined || params === null) {
|
|
121
|
+
throw new FireError(
|
|
122
|
+
'params object is required but was not provided.',
|
|
123
|
+
'INIT_010',
|
|
124
|
+
'Pass a params object with workItemId, intentId, and mode.'
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (typeof params !== 'object') {
|
|
129
|
+
throw new FireError(
|
|
130
|
+
`params must be an object, but received ${typeof params}.`,
|
|
131
|
+
'INIT_011',
|
|
132
|
+
'Pass a params object with workItemId, intentId, and mode.'
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const p = params as Record<string, unknown>;
|
|
137
|
+
|
|
138
|
+
// Validate workItemId
|
|
139
|
+
if (p.workItemId === undefined || p.workItemId === null) {
|
|
140
|
+
throw new FireError(
|
|
141
|
+
'workItemId is required but was not provided.',
|
|
142
|
+
'INIT_012',
|
|
143
|
+
'Include workItemId in the params object (e.g., "WI-001").'
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (typeof p.workItemId !== 'string' || p.workItemId.trim() === '') {
|
|
148
|
+
throw new FireError(
|
|
149
|
+
'workItemId must be a non-empty string.',
|
|
150
|
+
'INIT_013',
|
|
151
|
+
'Provide a valid work item ID (e.g., "WI-001").'
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Validate intentId
|
|
156
|
+
if (p.intentId === undefined || p.intentId === null) {
|
|
157
|
+
throw new FireError(
|
|
158
|
+
'intentId is required but was not provided.',
|
|
159
|
+
'INIT_014',
|
|
160
|
+
'Include intentId in the params object (e.g., "INT-001").'
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (typeof p.intentId !== 'string' || p.intentId.trim() === '') {
|
|
165
|
+
throw new FireError(
|
|
166
|
+
'intentId must be a non-empty string.',
|
|
167
|
+
'INIT_015',
|
|
168
|
+
'Provide a valid intent ID (e.g., "INT-001").'
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Validate mode
|
|
173
|
+
if (p.mode === undefined || p.mode === null) {
|
|
174
|
+
throw new FireError(
|
|
175
|
+
'mode is required but was not provided.',
|
|
176
|
+
'INIT_016',
|
|
177
|
+
`Include mode in the params object. Valid values: ${VALID_MODES.join(', ')}.`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!VALID_MODES.includes(p.mode as (typeof VALID_MODES)[number])) {
|
|
182
|
+
throw new FireError(
|
|
183
|
+
`Invalid mode: "${p.mode}".`,
|
|
184
|
+
'INIT_017',
|
|
185
|
+
`Use one of the valid modes: ${VALID_MODES.join(', ')}.`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function validateStateStructure(state: unknown, statePath: string): asserts state is State {
|
|
191
|
+
if (state === undefined || state === null) {
|
|
192
|
+
throw new FireError(
|
|
193
|
+
`state.yaml is empty or invalid at: "${statePath}".`,
|
|
194
|
+
'INIT_020',
|
|
195
|
+
'The state.yaml file must contain valid YAML content. Run /specsmd-fire to reinitialize if needed.'
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (typeof state !== 'object') {
|
|
200
|
+
throw new FireError(
|
|
201
|
+
`state.yaml must contain a YAML object, but found ${typeof state}.`,
|
|
202
|
+
'INIT_021',
|
|
203
|
+
'Check state.yaml structure. It should be a YAML object with project, intents, and active_run fields.'
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const s = state as Record<string, unknown>;
|
|
208
|
+
|
|
209
|
+
// Validate intents array exists (optional but needed for validation)
|
|
210
|
+
if (s.intents !== undefined && !Array.isArray(s.intents)) {
|
|
211
|
+
throw new FireError(
|
|
212
|
+
'state.yaml "intents" field must be an array.',
|
|
213
|
+
'INIT_022',
|
|
214
|
+
'Check state.yaml structure. The "intents" field should be an array of intent objects.'
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// =============================================================================
|
|
220
|
+
// State File Operations
|
|
221
|
+
// =============================================================================
|
|
222
|
+
|
|
223
|
+
function readStateFile(statePath: string): State {
|
|
224
|
+
// Check if .specs-fire directory exists
|
|
225
|
+
const specsFireDir = join(statePath, '..');
|
|
226
|
+
if (!existsSync(specsFireDir)) {
|
|
227
|
+
throw new FireError(
|
|
228
|
+
`.specs-fire directory not found at: "${specsFireDir}".`,
|
|
229
|
+
'INIT_030',
|
|
230
|
+
'Run /specsmd-fire to initialize the FIRE project first.'
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Check if state.yaml exists
|
|
235
|
+
if (!existsSync(statePath)) {
|
|
236
|
+
throw new FireError(
|
|
237
|
+
`state.yaml not found at: "${statePath}".`,
|
|
238
|
+
'INIT_031',
|
|
239
|
+
'Run /specsmd-fire to initialize the FIRE project first.'
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Read state file
|
|
244
|
+
let stateContent: string;
|
|
245
|
+
try {
|
|
246
|
+
stateContent = readFileSync(statePath, 'utf8');
|
|
247
|
+
} catch (error) {
|
|
248
|
+
const err = error as NodeJS.ErrnoException;
|
|
249
|
+
if (err.code === 'EACCES') {
|
|
250
|
+
throw new FireError(
|
|
251
|
+
`Permission denied reading state.yaml at: "${statePath}".`,
|
|
252
|
+
'INIT_032',
|
|
253
|
+
'Check file permissions. Ensure the current user has read access.'
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
throw new FireError(
|
|
257
|
+
`Failed to read state.yaml at: "${statePath}". System error: ${err.message}`,
|
|
258
|
+
'INIT_033',
|
|
259
|
+
'Check if the file is locked by another process or if there are disk issues.'
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Parse YAML
|
|
264
|
+
let state: unknown;
|
|
265
|
+
try {
|
|
266
|
+
state = yaml.parse(stateContent);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
const err = error as Error;
|
|
269
|
+
throw new FireError(
|
|
270
|
+
`state.yaml contains invalid YAML at: "${statePath}". Parse error: ${err.message}`,
|
|
271
|
+
'INIT_034',
|
|
272
|
+
'Fix the YAML syntax in state.yaml or run /specsmd-fire to reinitialize.'
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
validateStateStructure(state, statePath);
|
|
277
|
+
return state;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function writeStateFile(statePath: string, state: State): void {
|
|
281
|
+
try {
|
|
282
|
+
const yamlContent = yaml.stringify(state);
|
|
283
|
+
writeFileSync(statePath, yamlContent, 'utf8');
|
|
284
|
+
} catch (error) {
|
|
285
|
+
const err = error as NodeJS.ErrnoException;
|
|
286
|
+
if (err.code === 'EACCES') {
|
|
287
|
+
throw new FireError(
|
|
288
|
+
`Permission denied writing to state.yaml at: "${statePath}".`,
|
|
289
|
+
'INIT_040',
|
|
290
|
+
'Check file permissions. Ensure the current user has write access.'
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
if (err.code === 'ENOSPC') {
|
|
294
|
+
throw new FireError(
|
|
295
|
+
`Disk full - cannot write to state.yaml at: "${statePath}".`,
|
|
296
|
+
'INIT_041',
|
|
297
|
+
'Free up disk space and try again.'
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
throw new FireError(
|
|
301
|
+
`Failed to write state.yaml at: "${statePath}". System error: ${err.message}`,
|
|
302
|
+
'INIT_042',
|
|
303
|
+
'Check if the file is locked by another process or if there are disk issues.'
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// =============================================================================
|
|
309
|
+
// Run Operations
|
|
310
|
+
// =============================================================================
|
|
311
|
+
|
|
312
|
+
function checkForActiveRun(state: State): void {
|
|
313
|
+
if (state.active_run && state.active_run.id) {
|
|
314
|
+
throw new FireError(
|
|
315
|
+
`An active run already exists: "${state.active_run.id}" for work item "${state.active_run.work_item}".`,
|
|
316
|
+
'INIT_050',
|
|
317
|
+
'Complete or cancel the existing run first using /run-complete or /run-cancel, then start a new run.'
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function validateIntentAndWorkItem(state: State, intentId: string, workItemId: string): void {
|
|
323
|
+
// If no intents defined, we can't validate but we'll allow it (might be added later)
|
|
324
|
+
if (!state.intents || state.intents.length === 0) {
|
|
325
|
+
// Warning: proceeding without validation
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Find the intent
|
|
330
|
+
const intent = state.intents.find((i) => i.id === intentId);
|
|
331
|
+
if (!intent) {
|
|
332
|
+
const availableIntents = state.intents.map((i) => i.id).join(', ');
|
|
333
|
+
throw new FireError(
|
|
334
|
+
`Intent "${intentId}" not found in state.yaml.`,
|
|
335
|
+
'INIT_051',
|
|
336
|
+
`Available intents: ${availableIntents || '(none)'}. Create the intent first or use an existing one.`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Find the work item within the intent
|
|
341
|
+
if (intent.work_items && intent.work_items.length > 0) {
|
|
342
|
+
const workItem = intent.work_items.find((w) => w.id === workItemId);
|
|
343
|
+
if (!workItem) {
|
|
344
|
+
const availableWorkItems = intent.work_items.map((w) => w.id).join(', ');
|
|
345
|
+
throw new FireError(
|
|
346
|
+
`Work item "${workItemId}" not found in intent "${intentId}".`,
|
|
347
|
+
'INIT_052',
|
|
348
|
+
`Available work items in this intent: ${availableWorkItems || '(none)'}. Create the work item first or use an existing one.`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function generateRunId(runsPath: string): string {
|
|
355
|
+
// Create runs directory if it doesn't exist
|
|
356
|
+
if (!existsSync(runsPath)) {
|
|
357
|
+
try {
|
|
358
|
+
mkdirSync(runsPath, { recursive: true });
|
|
359
|
+
} catch (error) {
|
|
360
|
+
const err = error as NodeJS.ErrnoException;
|
|
361
|
+
throw new FireError(
|
|
362
|
+
`Failed to create runs directory at: "${runsPath}". System error: ${err.message}`,
|
|
363
|
+
'INIT_060',
|
|
364
|
+
'Check directory permissions and disk space.'
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
return 'run-001'; // First run
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Read existing runs
|
|
371
|
+
let entries: string[];
|
|
372
|
+
try {
|
|
373
|
+
entries = readdirSync(runsPath);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
const err = error as NodeJS.ErrnoException;
|
|
376
|
+
throw new FireError(
|
|
377
|
+
`Failed to read runs directory at: "${runsPath}". System error: ${err.message}`,
|
|
378
|
+
'INIT_061',
|
|
379
|
+
'Check directory permissions.'
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Find the highest run number
|
|
384
|
+
const runNumbers = entries
|
|
385
|
+
.filter((f) => /^run-\d{3,}$/.test(f))
|
|
386
|
+
.map((f) => parseInt(f.replace('run-', ''), 10))
|
|
387
|
+
.filter((n) => !isNaN(n));
|
|
388
|
+
|
|
389
|
+
const maxNum = runNumbers.length > 0 ? Math.max(...runNumbers) : 0;
|
|
390
|
+
const nextNum = maxNum + 1;
|
|
391
|
+
|
|
392
|
+
return `run-${String(nextNum).padStart(3, '0')}`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function createRunFolder(runPath: string): void {
|
|
396
|
+
try {
|
|
397
|
+
mkdirSync(runPath, { recursive: true });
|
|
398
|
+
} catch (error) {
|
|
399
|
+
const err = error as NodeJS.ErrnoException;
|
|
400
|
+
if (err.code === 'EACCES') {
|
|
401
|
+
throw new FireError(
|
|
402
|
+
`Permission denied creating run folder at: "${runPath}".`,
|
|
403
|
+
'INIT_070',
|
|
404
|
+
'Check directory permissions. Ensure the current user has write access.'
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
if (err.code === 'ENOSPC') {
|
|
408
|
+
throw new FireError(
|
|
409
|
+
`Disk full - cannot create run folder at: "${runPath}".`,
|
|
410
|
+
'INIT_071',
|
|
411
|
+
'Free up disk space and try again.'
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
throw new FireError(
|
|
415
|
+
`Failed to create run folder at: "${runPath}". System error: ${err.message}`,
|
|
416
|
+
'INIT_072',
|
|
417
|
+
'Check if the path is valid and there are no disk issues.'
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function createRunLog(runPath: string, runId: string, params: RunInit, startTime: string): void {
|
|
423
|
+
const runLog = `---
|
|
424
|
+
id: ${runId}
|
|
425
|
+
work_item: ${params.workItemId}
|
|
426
|
+
intent: ${params.intentId}
|
|
427
|
+
mode: ${params.mode}
|
|
428
|
+
status: in_progress
|
|
429
|
+
started: ${startTime}
|
|
430
|
+
completed: null
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
# Run: ${runId}
|
|
434
|
+
|
|
435
|
+
## Work Item
|
|
436
|
+
${params.workItemId}
|
|
437
|
+
|
|
438
|
+
## Files Created
|
|
439
|
+
(none yet)
|
|
440
|
+
|
|
441
|
+
## Files Modified
|
|
442
|
+
(none yet)
|
|
443
|
+
|
|
444
|
+
## Decisions
|
|
445
|
+
(none yet)
|
|
446
|
+
`;
|
|
447
|
+
|
|
448
|
+
const runLogPath = join(runPath, 'run.md');
|
|
449
|
+
try {
|
|
450
|
+
writeFileSync(runLogPath, runLog, 'utf8');
|
|
451
|
+
} catch (error) {
|
|
452
|
+
const err = error as NodeJS.ErrnoException;
|
|
453
|
+
throw new FireError(
|
|
454
|
+
`Failed to create run.md at: "${runLogPath}". System error: ${err.message}`,
|
|
455
|
+
'INIT_080',
|
|
456
|
+
'Check file permissions and disk space.'
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function rollbackRun(runPath: string): void {
|
|
462
|
+
try {
|
|
463
|
+
if (existsSync(runPath)) {
|
|
464
|
+
rmSync(runPath, { recursive: true, force: true });
|
|
465
|
+
}
|
|
466
|
+
} catch {
|
|
467
|
+
// Best effort rollback - ignore errors
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// =============================================================================
|
|
472
|
+
// Main Export
|
|
473
|
+
// =============================================================================
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Initialize a new FIRE run
|
|
477
|
+
*
|
|
478
|
+
* @param rootPath - Absolute path to the project root
|
|
479
|
+
* @param params - Run initialization parameters
|
|
480
|
+
* @returns The generated run ID (e.g., "run-001")
|
|
481
|
+
* @throws {FireError} If validation fails or file operations fail
|
|
482
|
+
*
|
|
483
|
+
* @example
|
|
484
|
+
* ```typescript
|
|
485
|
+
* const runId = initRun('/path/to/project', {
|
|
486
|
+
* workItemId: 'WI-001',
|
|
487
|
+
* intentId: 'INT-001',
|
|
488
|
+
* mode: 'autopilot'
|
|
489
|
+
* });
|
|
490
|
+
* console.log(`Started run: ${runId}`);
|
|
491
|
+
* ```
|
|
492
|
+
*/
|
|
493
|
+
export function initRun(rootPath: string, params: RunInit): string {
|
|
494
|
+
// ==========================================================================
|
|
495
|
+
// Step 1: Validate inputs
|
|
496
|
+
// ==========================================================================
|
|
497
|
+
validateRootPath(rootPath);
|
|
498
|
+
validateParams(params);
|
|
499
|
+
|
|
500
|
+
// ==========================================================================
|
|
501
|
+
// Step 2: Set up paths
|
|
502
|
+
// ==========================================================================
|
|
503
|
+
const statePath = join(rootPath, '.specs-fire', 'state.yaml');
|
|
504
|
+
const runsPath = join(rootPath, '.specs-fire', 'runs');
|
|
505
|
+
|
|
506
|
+
// ==========================================================================
|
|
507
|
+
// Step 3: Read and validate current state
|
|
508
|
+
// ==========================================================================
|
|
509
|
+
const state = readStateFile(statePath);
|
|
510
|
+
|
|
511
|
+
// ==========================================================================
|
|
512
|
+
// Step 4: Check for existing active run
|
|
513
|
+
// ==========================================================================
|
|
514
|
+
checkForActiveRun(state);
|
|
515
|
+
|
|
516
|
+
// ==========================================================================
|
|
517
|
+
// Step 5: Validate intent and work item exist (if intents are defined)
|
|
518
|
+
// ==========================================================================
|
|
519
|
+
validateIntentAndWorkItem(state, params.intentId, params.workItemId);
|
|
520
|
+
|
|
521
|
+
// ==========================================================================
|
|
522
|
+
// Step 6: Generate run ID (using max number, not count)
|
|
523
|
+
// ==========================================================================
|
|
524
|
+
const runId = generateRunId(runsPath);
|
|
525
|
+
const runPath = join(runsPath, runId);
|
|
526
|
+
|
|
527
|
+
// ==========================================================================
|
|
528
|
+
// Step 7: Create run folder
|
|
529
|
+
// ==========================================================================
|
|
530
|
+
createRunFolder(runPath);
|
|
531
|
+
|
|
532
|
+
// ==========================================================================
|
|
533
|
+
// Step 8: Update state with active run
|
|
534
|
+
// ==========================================================================
|
|
535
|
+
const startTime = new Date().toISOString();
|
|
536
|
+
const previousActiveRun = state.active_run; // Save for potential rollback
|
|
537
|
+
|
|
538
|
+
state.active_run = {
|
|
539
|
+
id: runId,
|
|
540
|
+
work_item: params.workItemId,
|
|
541
|
+
intent: params.intentId,
|
|
542
|
+
mode: params.mode,
|
|
543
|
+
started: startTime,
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
// ==========================================================================
|
|
547
|
+
// Step 9: Save state (with rollback on failure)
|
|
548
|
+
// ==========================================================================
|
|
549
|
+
try {
|
|
550
|
+
writeStateFile(statePath, state);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
// Rollback: remove run folder
|
|
553
|
+
rollbackRun(runPath);
|
|
554
|
+
throw error;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ==========================================================================
|
|
558
|
+
// Step 10: Create run log (with rollback on failure)
|
|
559
|
+
// ==========================================================================
|
|
560
|
+
try {
|
|
561
|
+
createRunLog(runPath, runId, params, startTime);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
// Rollback: restore previous state and remove run folder
|
|
564
|
+
state.active_run = previousActiveRun ?? null;
|
|
565
|
+
try {
|
|
566
|
+
writeStateFile(statePath, state);
|
|
567
|
+
} catch {
|
|
568
|
+
// State file write failed - log but continue with main error
|
|
569
|
+
}
|
|
570
|
+
rollbackRun(runPath);
|
|
571
|
+
throw error;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return runId;
|
|
575
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Skill: Run Status
|
|
2
|
+
|
|
3
|
+
Display current run status and progress.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Trigger
|
|
8
|
+
|
|
9
|
+
- User asks about run status
|
|
10
|
+
- During long-running execution
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Workflow
|
|
15
|
+
|
|
16
|
+
```xml
|
|
17
|
+
<skill name="run-status">
|
|
18
|
+
|
|
19
|
+
<step n="1" title="Check Active Run">
|
|
20
|
+
<action>Read state.yaml for active_run</action>
|
|
21
|
+
<check if="no active run">
|
|
22
|
+
<output>
|
|
23
|
+
No active run. Last completed run: {last-run-id}
|
|
24
|
+
</output>
|
|
25
|
+
<stop/>
|
|
26
|
+
</check>
|
|
27
|
+
</step>
|
|
28
|
+
|
|
29
|
+
<step n="2" title="Display Status">
|
|
30
|
+
<output>
|
|
31
|
+
## Run Status: {run-id}
|
|
32
|
+
|
|
33
|
+
**Work Item**: {title}
|
|
34
|
+
**Intent**: {intent-title}
|
|
35
|
+
**Mode**: {mode}
|
|
36
|
+
**Started**: {started}
|
|
37
|
+
**Duration**: {elapsed}
|
|
38
|
+
|
|
39
|
+
### Progress
|
|
40
|
+
|
|
41
|
+
- [x] Initialize run
|
|
42
|
+
- [x] Load context
|
|
43
|
+
{checkpoint status}
|
|
44
|
+
- [{status}] Execute implementation
|
|
45
|
+
- [ ] Run tests
|
|
46
|
+
- [ ] Generate walkthrough
|
|
47
|
+
|
|
48
|
+
### Files Changed So Far
|
|
49
|
+
|
|
50
|
+
Created: {created-count}
|
|
51
|
+
Modified: {modified-count}
|
|
52
|
+
|
|
53
|
+
### Recent Activity
|
|
54
|
+
|
|
55
|
+
{last 5 actions}
|
|
56
|
+
</output>
|
|
57
|
+
</step>
|
|
58
|
+
|
|
59
|
+
</skill>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Example Output
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
## Run Status: run-003
|
|
68
|
+
|
|
69
|
+
**Work Item**: Add session management
|
|
70
|
+
**Intent**: User Authentication
|
|
71
|
+
**Mode**: confirm
|
|
72
|
+
**Started**: 2026-01-19T10:30:00Z
|
|
73
|
+
**Duration**: 12 minutes
|
|
74
|
+
|
|
75
|
+
### Progress
|
|
76
|
+
|
|
77
|
+
- [x] Initialize run
|
|
78
|
+
- [x] Load context
|
|
79
|
+
- [x] Plan approved (Checkpoint 1)
|
|
80
|
+
- [~] Execute implementation
|
|
81
|
+
- [ ] Run tests
|
|
82
|
+
- [ ] Generate walkthrough
|
|
83
|
+
|
|
84
|
+
### Files Changed So Far
|
|
85
|
+
|
|
86
|
+
Created: 2
|
|
87
|
+
Modified: 1
|
|
88
|
+
|
|
89
|
+
### Recent Activity
|
|
90
|
+
|
|
91
|
+
- Created src/auth/session.ts
|
|
92
|
+
- Created src/auth/session.test.ts
|
|
93
|
+
- Modified src/auth/index.ts
|
|
94
|
+
```
|