@cluesmith/codev 1.6.2 → 2.0.0-rc.1

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.
Files changed (94) hide show
  1. package/bin/porch.js +7 -0
  2. package/dist/agent-farm/cli.d.ts.map +1 -1
  3. package/dist/agent-farm/cli.js +23 -0
  4. package/dist/agent-farm/cli.js.map +1 -1
  5. package/dist/agent-farm/commands/index.d.ts +1 -0
  6. package/dist/agent-farm/commands/index.d.ts.map +1 -1
  7. package/dist/agent-farm/commands/index.js +1 -0
  8. package/dist/agent-farm/commands/index.js.map +1 -1
  9. package/dist/agent-farm/commands/kickoff.d.ts +19 -0
  10. package/dist/agent-farm/commands/kickoff.d.ts.map +1 -0
  11. package/dist/agent-farm/commands/kickoff.js +269 -0
  12. package/dist/agent-farm/commands/kickoff.js.map +1 -0
  13. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  14. package/dist/agent-farm/commands/spawn.js +1 -43
  15. package/dist/agent-farm/commands/spawn.js.map +1 -1
  16. package/dist/cli.d.ts.map +1 -1
  17. package/dist/cli.js +29 -0
  18. package/dist/cli.js.map +1 -1
  19. package/dist/commands/pcheck/cache.d.ts +48 -0
  20. package/dist/commands/pcheck/cache.d.ts.map +1 -0
  21. package/dist/commands/pcheck/cache.js +170 -0
  22. package/dist/commands/pcheck/cache.js.map +1 -0
  23. package/dist/commands/pcheck/evaluator.d.ts +15 -0
  24. package/dist/commands/pcheck/evaluator.d.ts.map +1 -0
  25. package/dist/commands/pcheck/evaluator.js +246 -0
  26. package/dist/commands/pcheck/evaluator.js.map +1 -0
  27. package/dist/commands/pcheck/index.d.ts +12 -0
  28. package/dist/commands/pcheck/index.d.ts.map +1 -0
  29. package/dist/commands/pcheck/index.js +249 -0
  30. package/dist/commands/pcheck/index.js.map +1 -0
  31. package/dist/commands/pcheck/parser.d.ts +39 -0
  32. package/dist/commands/pcheck/parser.d.ts.map +1 -0
  33. package/dist/commands/pcheck/parser.js +155 -0
  34. package/dist/commands/pcheck/parser.js.map +1 -0
  35. package/dist/commands/pcheck/types.d.ts +82 -0
  36. package/dist/commands/pcheck/types.d.ts.map +1 -0
  37. package/dist/commands/pcheck/types.js +5 -0
  38. package/dist/commands/pcheck/types.js.map +1 -0
  39. package/dist/commands/porch/checks.d.ts +42 -0
  40. package/dist/commands/porch/checks.d.ts.map +1 -0
  41. package/dist/commands/porch/checks.js +195 -0
  42. package/dist/commands/porch/checks.js.map +1 -0
  43. package/dist/commands/porch/consultation.d.ts +56 -0
  44. package/dist/commands/porch/consultation.d.ts.map +1 -0
  45. package/dist/commands/porch/consultation.js +330 -0
  46. package/dist/commands/porch/consultation.js.map +1 -0
  47. package/dist/commands/porch/index.d.ts +60 -0
  48. package/dist/commands/porch/index.d.ts.map +1 -0
  49. package/dist/commands/porch/index.js +828 -0
  50. package/dist/commands/porch/index.js.map +1 -0
  51. package/dist/commands/porch/notifications.d.ts +99 -0
  52. package/dist/commands/porch/notifications.d.ts.map +1 -0
  53. package/dist/commands/porch/notifications.js +223 -0
  54. package/dist/commands/porch/notifications.js.map +1 -0
  55. package/dist/commands/porch/plan-parser.d.ts +38 -0
  56. package/dist/commands/porch/plan-parser.d.ts.map +1 -0
  57. package/dist/commands/porch/plan-parser.js +166 -0
  58. package/dist/commands/porch/plan-parser.js.map +1 -0
  59. package/dist/commands/porch/protocol-loader.d.ts +46 -0
  60. package/dist/commands/porch/protocol-loader.d.ts.map +1 -0
  61. package/dist/commands/porch/protocol-loader.js +249 -0
  62. package/dist/commands/porch/protocol-loader.js.map +1 -0
  63. package/dist/commands/porch/signal-parser.d.ts +88 -0
  64. package/dist/commands/porch/signal-parser.d.ts.map +1 -0
  65. package/dist/commands/porch/signal-parser.js +148 -0
  66. package/dist/commands/porch/signal-parser.js.map +1 -0
  67. package/dist/commands/porch/state.d.ts +133 -0
  68. package/dist/commands/porch/state.d.ts.map +1 -0
  69. package/dist/commands/porch/state.js +760 -0
  70. package/dist/commands/porch/state.js.map +1 -0
  71. package/dist/commands/porch/types.d.ts +232 -0
  72. package/dist/commands/porch/types.d.ts.map +1 -0
  73. package/dist/commands/porch/types.js +7 -0
  74. package/dist/commands/porch/types.js.map +1 -0
  75. package/package.json +2 -1
  76. package/skeleton/porch/prompts/defend.md +103 -0
  77. package/skeleton/porch/prompts/diagnose.md +70 -0
  78. package/skeleton/porch/prompts/evaluate.md +132 -0
  79. package/skeleton/porch/prompts/fix.md +59 -0
  80. package/skeleton/porch/prompts/implement.md +79 -0
  81. package/skeleton/porch/prompts/plan.md +74 -0
  82. package/skeleton/porch/prompts/pr.md +84 -0
  83. package/skeleton/porch/prompts/review.md +179 -0
  84. package/skeleton/porch/prompts/specify.md +53 -0
  85. package/skeleton/porch/prompts/test.md +63 -0
  86. package/skeleton/porch/prompts/understand.md +61 -0
  87. package/skeleton/porch/prompts/verify.md +58 -0
  88. package/skeleton/porch/protocols/bugfix.json +85 -0
  89. package/skeleton/porch/protocols/spider.json +135 -0
  90. package/skeleton/porch/protocols/tick.json +76 -0
  91. package/skeleton/protocols/bugfix/protocol.json +127 -0
  92. package/skeleton/protocols/protocol-schema.json +237 -0
  93. package/skeleton/protocols/spider/protocol.json +204 -0
  94. package/skeleton/protocols/tick/protocol.json +151 -0
@@ -0,0 +1,760 @@
1
+ /**
2
+ * Porch State Management
3
+ *
4
+ * Handles project state persistence with:
5
+ * - Pure YAML format (no markdown frontmatter)
6
+ * - Atomic writes (tmp file + fsync + rename)
7
+ * - File locking (flock advisory locking)
8
+ * - Crash recovery
9
+ */
10
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+ import { openSync, closeSync, fsyncSync, writeFileSync, renameSync, unlinkSync, readFileSync } from 'node:fs';
13
+ // ============================================================================
14
+ // Constants
15
+ // ============================================================================
16
+ /** Directory for SPIDER project state (relative to project root) */
17
+ export const PROJECTS_DIR = 'codev/projects';
18
+ /** Directory for TICK/BUGFIX execution state (relative to project root) */
19
+ export const EXECUTIONS_DIR = 'codev/executions';
20
+ /** Lock timeout in milliseconds */
21
+ const LOCK_TIMEOUT_MS = 5000;
22
+ /** Lock retry interval in milliseconds */
23
+ const LOCK_RETRY_MS = 100;
24
+ // ============================================================================
25
+ // Path Utilities
26
+ // ============================================================================
27
+ /**
28
+ * Get the status file path for a SPIDER project
29
+ */
30
+ export function getProjectStatusPath(projectRoot, projectId, name) {
31
+ const projectDir = name
32
+ ? path.join(projectRoot, PROJECTS_DIR, `${projectId}-${name}`)
33
+ : path.join(projectRoot, PROJECTS_DIR, projectId);
34
+ return path.join(projectDir, 'status.yaml');
35
+ }
36
+ /**
37
+ * Get the status file path for a TICK/BUGFIX execution
38
+ */
39
+ export function getExecutionStatusPath(projectRoot, protocol, id, name) {
40
+ const dirName = name ? `${protocol}_${id}_${name}` : `${protocol}_${id}`;
41
+ return path.join(projectRoot, EXECUTIONS_DIR, dirName, 'status.yaml');
42
+ }
43
+ /**
44
+ * Get the project directory for a SPIDER project
45
+ */
46
+ export function getProjectDir(projectRoot, projectId, name) {
47
+ return name
48
+ ? path.join(projectRoot, PROJECTS_DIR, `${projectId}-${name}`)
49
+ : path.join(projectRoot, PROJECTS_DIR, projectId);
50
+ }
51
+ /**
52
+ * Get the worktree path for a protocol execution
53
+ */
54
+ export function getWorktreePath(projectRoot, protocol, id, name) {
55
+ const dirName = name ? `${protocol}_${id}_${name}` : `${protocol}_${id}`;
56
+ return path.join(projectRoot, 'worktrees', dirName);
57
+ }
58
+ /**
59
+ * Acquire an advisory lock on a file
60
+ * Creates a .lock file to indicate lock ownership
61
+ */
62
+ export async function acquireLock(filePath) {
63
+ const lockFile = `${filePath}.lock`;
64
+ const startTime = Date.now();
65
+ while (Date.now() - startTime < LOCK_TIMEOUT_MS) {
66
+ try {
67
+ // Try to create lock file exclusively
68
+ const fd = openSync(lockFile, 'wx');
69
+ // Write our PID for debugging
70
+ writeFileSync(lockFile, `${process.pid}\n`);
71
+ return { fd, lockFile };
72
+ }
73
+ catch (err) {
74
+ if (err.code === 'EEXIST') {
75
+ // Lock file exists, check if stale
76
+ try {
77
+ const stat = fs.statSync(lockFile);
78
+ // If lock is older than 60 seconds, consider it stale
79
+ if (Date.now() - stat.mtimeMs > 60000) {
80
+ unlinkSync(lockFile);
81
+ continue;
82
+ }
83
+ }
84
+ catch {
85
+ // Lock file disappeared, retry
86
+ continue;
87
+ }
88
+ // Wait and retry
89
+ await new Promise(r => setTimeout(r, LOCK_RETRY_MS));
90
+ }
91
+ else {
92
+ throw err;
93
+ }
94
+ }
95
+ }
96
+ throw new Error(`Failed to acquire lock on ${filePath} after ${LOCK_TIMEOUT_MS}ms`);
97
+ }
98
+ /**
99
+ * Release an advisory lock
100
+ */
101
+ export function releaseLock(lock) {
102
+ try {
103
+ closeSync(lock.fd);
104
+ unlinkSync(lock.lockFile);
105
+ }
106
+ catch {
107
+ // Ignore errors during cleanup
108
+ }
109
+ }
110
+ // ============================================================================
111
+ // YAML Serialization
112
+ // ============================================================================
113
+ /**
114
+ * Simple YAML serializer for project state
115
+ * Handles our specific data structures without external dependencies
116
+ */
117
+ export function serializeState(state) {
118
+ const lines = [];
119
+ // Basic fields
120
+ lines.push(`id: "${state.id}"`);
121
+ lines.push(`title: "${state.title}"`);
122
+ lines.push(`protocol: "${state.protocol}"`);
123
+ lines.push(`state: "${state.current_state}"`);
124
+ if (state.worktree) {
125
+ lines.push(`worktree: "${state.worktree}"`);
126
+ }
127
+ lines.push('');
128
+ // Gates
129
+ lines.push('gates:');
130
+ if (state.gates && Object.keys(state.gates).length > 0) {
131
+ for (const [gateId, gateStatus] of Object.entries(state.gates)) {
132
+ const status = gateStatus.status || 'pending';
133
+ const requestedAt = gateStatus.requested_at ? `, requested_at: "${gateStatus.requested_at}"` : '';
134
+ const approvedAt = gateStatus.approved_at ? `, approved_at: "${gateStatus.approved_at}"` : '';
135
+ lines.push(` ${gateId}: { status: ${status}${requestedAt}${approvedAt} }`);
136
+ }
137
+ }
138
+ else {
139
+ lines.push(' # No gates defined');
140
+ }
141
+ lines.push('');
142
+ // Phases (for phased implementation)
143
+ lines.push('phases:');
144
+ if (state.phases && Object.keys(state.phases).length > 0) {
145
+ for (const [phaseId, phaseStatus] of Object.entries(state.phases)) {
146
+ if (typeof phaseStatus === 'object' && phaseStatus !== null) {
147
+ const ps = phaseStatus;
148
+ const status = ps.status || 'pending';
149
+ const title = ps.title ? `, title: "${ps.title}"` : '';
150
+ lines.push(` ${phaseId}: { status: ${status}${title} }`);
151
+ }
152
+ }
153
+ }
154
+ else {
155
+ lines.push(' # No phases extracted yet');
156
+ }
157
+ lines.push('');
158
+ // Plan phases (extracted from plan.md)
159
+ if (state.plan_phases && state.plan_phases.length > 0) {
160
+ lines.push('plan_phases:');
161
+ for (const phase of state.plan_phases) {
162
+ lines.push(` - id: "${phase.id}"`);
163
+ lines.push(` title: "${phase.title}"`);
164
+ if (phase.description) {
165
+ lines.push(` description: "${phase.description.replace(/"/g, '\\"')}"`);
166
+ }
167
+ }
168
+ lines.push('');
169
+ }
170
+ // Consultation attempts (for tracking retries across iterations)
171
+ if (state.consultation_attempts && Object.keys(state.consultation_attempts).length > 0) {
172
+ lines.push('consultation_attempts:');
173
+ for (const [stateKey, count] of Object.entries(state.consultation_attempts)) {
174
+ lines.push(` "${stateKey}": ${count}`);
175
+ }
176
+ lines.push('');
177
+ }
178
+ // Metadata
179
+ lines.push(`iteration: ${state.iteration || 0}`);
180
+ lines.push(`started_at: "${state.started_at || new Date().toISOString()}"`);
181
+ lines.push(`last_updated: "${new Date().toISOString()}"`);
182
+ lines.push('');
183
+ // Log
184
+ lines.push('log:');
185
+ if (state.log && state.log.length > 0) {
186
+ for (const entry of state.log) {
187
+ if (typeof entry === 'string') {
188
+ lines.push(` - "${entry}"`);
189
+ }
190
+ else if (typeof entry === 'object' && entry !== null) {
191
+ const logEntry = entry;
192
+ const ts = logEntry.ts || new Date().toISOString();
193
+ const event = logEntry.event || 'unknown';
194
+ let entryLine = ` - ts: "${ts}"`;
195
+ lines.push(entryLine);
196
+ lines.push(` event: "${event}"`);
197
+ if (logEntry.from)
198
+ lines.push(` from: "${logEntry.from}"`);
199
+ if (logEntry.to)
200
+ lines.push(` to: "${logEntry.to}"`);
201
+ if (logEntry.signal)
202
+ lines.push(` signal: "${logEntry.signal}"`);
203
+ }
204
+ }
205
+ }
206
+ return lines.join('\n') + '\n';
207
+ }
208
+ /**
209
+ * Parse YAML status file into ProjectState
210
+ */
211
+ export function parseState(content) {
212
+ const state = {
213
+ gates: {},
214
+ phases: {},
215
+ log: [],
216
+ consultation_attempts: {},
217
+ };
218
+ const lines = content.split('\n');
219
+ let currentSection = '';
220
+ let currentArrayItem = null;
221
+ for (const line of lines) {
222
+ // Skip comments and empty lines
223
+ if (line.trim().startsWith('#') || line.trim() === '') {
224
+ continue;
225
+ }
226
+ // Detect section headers
227
+ if (line.match(/^gates:\s*$/)) {
228
+ currentSection = 'gates';
229
+ continue;
230
+ }
231
+ if (line.match(/^phases:\s*$/)) {
232
+ currentSection = 'phases';
233
+ continue;
234
+ }
235
+ if (line.match(/^plan_phases:\s*$/)) {
236
+ currentSection = 'plan_phases';
237
+ state.plan_phases = [];
238
+ continue;
239
+ }
240
+ if (line.match(/^log:\s*$/)) {
241
+ currentSection = 'log';
242
+ continue;
243
+ }
244
+ if (line.match(/^consultation_attempts:\s*$/)) {
245
+ currentSection = 'consultation_attempts';
246
+ continue;
247
+ }
248
+ // Parse based on section
249
+ if (currentSection === 'gates') {
250
+ // Parse: gate_id: { status: pending, requested_at: "..." }
251
+ const match = line.match(/^\s+(\w+):\s*\{\s*status:\s*(\w+)(?:,\s*requested_at:\s*"([^"]*)")?(?:,\s*approved_at:\s*"([^"]*)")?\s*\}/);
252
+ if (match) {
253
+ const [, gateId, status, requestedAt, approvedAt] = match;
254
+ state.gates[gateId] = {
255
+ status: status,
256
+ ...(requestedAt && { requested_at: requestedAt }),
257
+ ...(approvedAt && { approved_at: approvedAt }),
258
+ };
259
+ }
260
+ continue;
261
+ }
262
+ if (currentSection === 'phases') {
263
+ // Parse: phase_id: { status: pending, title: "..." }
264
+ const match = line.match(/^\s+(\w+):\s*\{\s*status:\s*(\w+)(?:,\s*title:\s*"([^"]*)")?\s*\}/);
265
+ if (match) {
266
+ const [, phaseId, status, title] = match;
267
+ state.phases[phaseId] = {
268
+ status: status,
269
+ ...(title && { title }),
270
+ };
271
+ }
272
+ continue;
273
+ }
274
+ if (currentSection === 'plan_phases') {
275
+ // Parse array items
276
+ if (line.match(/^\s+-\s+id:/)) {
277
+ if (currentArrayItem) {
278
+ state.plan_phases.push(currentArrayItem);
279
+ }
280
+ currentArrayItem = {};
281
+ const idMatch = line.match(/id:\s*"([^"]*)"/);
282
+ if (idMatch)
283
+ currentArrayItem.id = idMatch[1];
284
+ }
285
+ else if (line.match(/^\s+title:/)) {
286
+ const titleMatch = line.match(/title:\s*"([^"]*)"/);
287
+ if (titleMatch && currentArrayItem)
288
+ currentArrayItem.title = titleMatch[1];
289
+ }
290
+ else if (line.match(/^\s+description:/)) {
291
+ const descMatch = line.match(/description:\s*"([^"]*)"/);
292
+ if (descMatch && currentArrayItem)
293
+ currentArrayItem.description = descMatch[1];
294
+ }
295
+ continue;
296
+ }
297
+ if (currentSection === 'consultation_attempts') {
298
+ // Parse: "state:key": count
299
+ const match = line.match(/^\s+"([^"]+)":\s*(\d+)/);
300
+ if (match) {
301
+ const [, stateKey, count] = match;
302
+ state.consultation_attempts[stateKey] = parseInt(count, 10);
303
+ }
304
+ continue;
305
+ }
306
+ if (currentSection === 'log') {
307
+ // Parse log entries - simplified
308
+ if (line.match(/^\s+-\s+ts:/)) {
309
+ if (currentArrayItem) {
310
+ state.log.push(currentArrayItem);
311
+ }
312
+ currentArrayItem = {};
313
+ const tsMatch = line.match(/ts:\s*"([^"]*)"/);
314
+ if (tsMatch)
315
+ currentArrayItem.ts = tsMatch[1];
316
+ }
317
+ else if (line.match(/^\s+event:/)) {
318
+ const eventMatch = line.match(/event:\s*"([^"]*)"/);
319
+ if (eventMatch && currentArrayItem)
320
+ currentArrayItem.event = eventMatch[1];
321
+ }
322
+ else if (line.match(/^\s+from:/)) {
323
+ const fromMatch = line.match(/from:\s*"([^"]*)"/);
324
+ if (fromMatch && currentArrayItem)
325
+ currentArrayItem.from = fromMatch[1];
326
+ }
327
+ else if (line.match(/^\s+to:/)) {
328
+ const toMatch = line.match(/to:\s*"([^"]*)"/);
329
+ if (toMatch && currentArrayItem)
330
+ currentArrayItem.to = toMatch[1];
331
+ }
332
+ else if (line.match(/^\s+signal:/)) {
333
+ const signalMatch = line.match(/signal:\s*"([^"]*)"/);
334
+ if (signalMatch && currentArrayItem)
335
+ currentArrayItem.signal = signalMatch[1];
336
+ }
337
+ else if (line.match(/^\s+-\s*"[^"]*"/)) {
338
+ // Simple string log entry
339
+ const strMatch = line.match(/^\s+-\s*"([^"]*)"/);
340
+ if (strMatch)
341
+ state.log.push(strMatch[1]);
342
+ }
343
+ continue;
344
+ }
345
+ // Top-level fields
346
+ const kvMatch = line.match(/^(\w+):\s*"?([^"\n]*)"?$/);
347
+ if (kvMatch) {
348
+ const [, key, value] = kvMatch;
349
+ switch (key) {
350
+ case 'id':
351
+ state.id = value;
352
+ break;
353
+ case 'title':
354
+ state.title = value;
355
+ break;
356
+ case 'protocol':
357
+ state.protocol = value;
358
+ break;
359
+ case 'state':
360
+ state.current_state = value;
361
+ break;
362
+ case 'worktree':
363
+ state.worktree = value;
364
+ break;
365
+ case 'iteration':
366
+ state.iteration = parseInt(value, 10);
367
+ break;
368
+ case 'started_at':
369
+ state.started_at = value;
370
+ break;
371
+ case 'last_updated':
372
+ state.last_updated = value;
373
+ break;
374
+ }
375
+ }
376
+ }
377
+ // Push final array item if exists
378
+ if (currentArrayItem) {
379
+ if (currentSection === 'plan_phases') {
380
+ state.plan_phases.push(currentArrayItem);
381
+ }
382
+ else if (currentSection === 'log') {
383
+ state.log.push(currentArrayItem);
384
+ }
385
+ }
386
+ return state;
387
+ }
388
+ // ============================================================================
389
+ // State Operations
390
+ // ============================================================================
391
+ /**
392
+ * Read project state from status file
393
+ */
394
+ export function readState(statusFilePath) {
395
+ // Check for crash recovery - .tmp file exists
396
+ const tmpPath = `${statusFilePath}.tmp`;
397
+ if (fs.existsSync(tmpPath)) {
398
+ try {
399
+ const tmpContent = readFileSync(tmpPath, 'utf-8');
400
+ const tmpState = parseState(tmpContent);
401
+ // tmp file is valid, use it and clean up
402
+ renameSync(tmpPath, statusFilePath);
403
+ console.log('[porch] Recovered state from interrupted write');
404
+ return tmpState;
405
+ }
406
+ catch {
407
+ // tmp file is corrupt, delete it
408
+ unlinkSync(tmpPath);
409
+ }
410
+ }
411
+ if (!fs.existsSync(statusFilePath)) {
412
+ return null;
413
+ }
414
+ const content = readFileSync(statusFilePath, 'utf-8');
415
+ return parseState(content);
416
+ }
417
+ /**
418
+ * Write project state atomically
419
+ * Uses tmp file + fsync + rename for crash safety
420
+ */
421
+ export async function writeState(statusFilePath, state) {
422
+ const lock = await acquireLock(statusFilePath);
423
+ try {
424
+ const content = serializeState(state);
425
+ const tmpPath = `${statusFilePath}.tmp`;
426
+ const dir = path.dirname(statusFilePath);
427
+ // Ensure directory exists
428
+ fs.mkdirSync(dir, { recursive: true });
429
+ // Write to temp file
430
+ const fd = openSync(tmpPath, 'w');
431
+ writeFileSync(fd, content);
432
+ fsyncSync(fd);
433
+ closeSync(fd);
434
+ // Atomic rename
435
+ renameSync(tmpPath, statusFilePath);
436
+ }
437
+ finally {
438
+ releaseLock(lock);
439
+ }
440
+ }
441
+ /**
442
+ * Create initial project state
443
+ */
444
+ export function createInitialState(protocol, projectId, title, worktreePath) {
445
+ const now = new Date().toISOString();
446
+ // Extract gates from protocol
447
+ const gates = {};
448
+ for (const phase of protocol.phases) {
449
+ if (phase.gate) {
450
+ const gateId = `${phase.id}_approval`;
451
+ gates[gateId] = { status: 'pending' };
452
+ }
453
+ }
454
+ return {
455
+ id: projectId,
456
+ title,
457
+ protocol: protocol.name,
458
+ current_state: protocol.initial || `${protocol.phases[0]?.id}:draft`,
459
+ worktree: worktreePath,
460
+ gates,
461
+ phases: {},
462
+ plan_phases: [],
463
+ iteration: 0,
464
+ started_at: now,
465
+ last_updated: now,
466
+ log: [{
467
+ ts: now,
468
+ event: 'state_change',
469
+ from: null,
470
+ to: protocol.initial || `${protocol.phases[0]?.id}:draft`,
471
+ }],
472
+ };
473
+ }
474
+ /**
475
+ * Update state with a new current state
476
+ */
477
+ export function updateState(state, newState, options = {}) {
478
+ const now = new Date().toISOString();
479
+ const logEntry = {
480
+ ts: now,
481
+ event: 'state_change',
482
+ from: state.current_state,
483
+ to: newState,
484
+ };
485
+ if (options.signal) {
486
+ logEntry.signal = options.signal;
487
+ }
488
+ return {
489
+ ...state,
490
+ current_state: newState,
491
+ iteration: state.iteration + 1,
492
+ last_updated: now,
493
+ log: [...state.log, logEntry],
494
+ };
495
+ }
496
+ /**
497
+ * Approve a gate in state
498
+ */
499
+ export function approveGate(state, gateId) {
500
+ const now = new Date().toISOString();
501
+ return {
502
+ ...state,
503
+ gates: {
504
+ ...state.gates,
505
+ [gateId]: {
506
+ ...state.gates[gateId],
507
+ status: 'passed',
508
+ approved_at: now,
509
+ },
510
+ },
511
+ last_updated: now,
512
+ log: [...state.log, {
513
+ ts: now,
514
+ event: 'gate_approved',
515
+ gate: gateId,
516
+ }],
517
+ };
518
+ }
519
+ /**
520
+ * Request a gate approval (mark as pending with timestamp)
521
+ */
522
+ export function requestGateApproval(state, gateId) {
523
+ const now = new Date().toISOString();
524
+ return {
525
+ ...state,
526
+ gates: {
527
+ ...state.gates,
528
+ [gateId]: {
529
+ status: 'pending',
530
+ requested_at: now,
531
+ },
532
+ },
533
+ last_updated: now,
534
+ log: [...state.log, {
535
+ ts: now,
536
+ event: 'gate_requested',
537
+ gate: gateId,
538
+ }],
539
+ };
540
+ }
541
+ /**
542
+ * Update phase status
543
+ */
544
+ export function updatePhaseStatus(state, phaseId, status) {
545
+ const now = new Date().toISOString();
546
+ return {
547
+ ...state,
548
+ phases: {
549
+ ...state.phases,
550
+ [phaseId]: {
551
+ ...state.phases[phaseId],
552
+ status,
553
+ },
554
+ },
555
+ last_updated: now,
556
+ log: [...state.log, {
557
+ ts: now,
558
+ event: 'phase_status_change',
559
+ phase: phaseId,
560
+ status,
561
+ }],
562
+ };
563
+ }
564
+ /**
565
+ * Set plan phases extracted from plan.md
566
+ */
567
+ export function setPlanPhases(state, phases) {
568
+ const now = new Date().toISOString();
569
+ // Initialize phase status for each extracted phase
570
+ const phaseStatus = {};
571
+ for (const phase of phases) {
572
+ phaseStatus[phase.id] = { status: 'pending', title: phase.title };
573
+ }
574
+ return {
575
+ ...state,
576
+ plan_phases: phases,
577
+ phases: phaseStatus,
578
+ last_updated: now,
579
+ log: [...state.log, {
580
+ ts: now,
581
+ event: 'plan_phases_extracted',
582
+ count: phases.length,
583
+ }],
584
+ };
585
+ }
586
+ // ============================================================================
587
+ // Discovery
588
+ // ============================================================================
589
+ /**
590
+ * Find all SPIDER projects
591
+ */
592
+ export function findProjects(projectRoot) {
593
+ const projectsDir = path.join(projectRoot, PROJECTS_DIR);
594
+ if (!fs.existsSync(projectsDir)) {
595
+ return [];
596
+ }
597
+ const entries = fs.readdirSync(projectsDir, { withFileTypes: true });
598
+ const projects = [];
599
+ for (const entry of entries) {
600
+ if (entry.isDirectory()) {
601
+ const statusPath = path.join(projectsDir, entry.name, 'status.yaml');
602
+ if (fs.existsSync(statusPath)) {
603
+ // Extract project ID from directory name (format: NNNN-name or just NNNN)
604
+ const idMatch = entry.name.match(/^(\d+)/);
605
+ if (idMatch) {
606
+ projects.push({
607
+ id: idMatch[1],
608
+ path: statusPath,
609
+ });
610
+ }
611
+ }
612
+ }
613
+ }
614
+ return projects;
615
+ }
616
+ /**
617
+ * Find all executions (TICK, BUGFIX, etc.)
618
+ */
619
+ export function findExecutions(projectRoot) {
620
+ const executionsDir = path.join(projectRoot, EXECUTIONS_DIR);
621
+ if (!fs.existsSync(executionsDir)) {
622
+ return [];
623
+ }
624
+ const entries = fs.readdirSync(executionsDir, { withFileTypes: true });
625
+ const executions = [];
626
+ for (const entry of entries) {
627
+ if (entry.isDirectory()) {
628
+ const statusPath = path.join(executionsDir, entry.name, 'status.yaml');
629
+ if (fs.existsSync(statusPath)) {
630
+ // Parse directory name (format: protocol_id_name)
631
+ const match = entry.name.match(/^(\w+)_(\w+)/);
632
+ if (match) {
633
+ executions.push({
634
+ protocol: match[1],
635
+ id: match[2],
636
+ path: statusPath,
637
+ });
638
+ }
639
+ }
640
+ }
641
+ }
642
+ return executions;
643
+ }
644
+ /**
645
+ * Find status file for a project by ID
646
+ */
647
+ export function findStatusFile(projectRoot, projectId) {
648
+ // Check projects directory first
649
+ const projectsDir = path.join(projectRoot, PROJECTS_DIR);
650
+ if (fs.existsSync(projectsDir)) {
651
+ const entries = fs.readdirSync(projectsDir);
652
+ for (const entry of entries) {
653
+ if (entry.startsWith(projectId)) {
654
+ const statusPath = path.join(projectsDir, entry, 'status.yaml');
655
+ if (fs.existsSync(statusPath)) {
656
+ return statusPath;
657
+ }
658
+ }
659
+ }
660
+ }
661
+ // Check executions directory
662
+ const executionsDir = path.join(projectRoot, EXECUTIONS_DIR);
663
+ if (fs.existsSync(executionsDir)) {
664
+ const entries = fs.readdirSync(executionsDir);
665
+ for (const entry of entries) {
666
+ if (entry.includes(`_${projectId}`)) {
667
+ const statusPath = path.join(executionsDir, entry, 'status.yaml');
668
+ if (fs.existsSync(statusPath)) {
669
+ return statusPath;
670
+ }
671
+ }
672
+ }
673
+ }
674
+ return null;
675
+ }
676
+ // ============================================================================
677
+ // Consultation Attempt Tracking
678
+ // ============================================================================
679
+ /**
680
+ * Get the number of consultation attempts for a given state
681
+ */
682
+ export function getConsultationAttempts(state, stateKey) {
683
+ return state.consultation_attempts?.[stateKey] ?? 0;
684
+ }
685
+ /**
686
+ * Increment consultation attempts for a given state
687
+ */
688
+ export function incrementConsultationAttempts(state, stateKey) {
689
+ const now = new Date().toISOString();
690
+ const currentAttempts = getConsultationAttempts(state, stateKey);
691
+ return {
692
+ ...state,
693
+ consultation_attempts: {
694
+ ...state.consultation_attempts,
695
+ [stateKey]: currentAttempts + 1,
696
+ },
697
+ last_updated: now,
698
+ log: [...state.log, {
699
+ ts: now,
700
+ event: 'consultation_attempt',
701
+ phase: stateKey,
702
+ count: currentAttempts + 1,
703
+ }],
704
+ };
705
+ }
706
+ /**
707
+ * Reset consultation attempts for a given state (e.g., after gate approval)
708
+ */
709
+ export function resetConsultationAttempts(state, stateKey) {
710
+ const newAttempts = { ...state.consultation_attempts };
711
+ delete newAttempts[stateKey];
712
+ return {
713
+ ...state,
714
+ consultation_attempts: newAttempts,
715
+ last_updated: new Date().toISOString(),
716
+ };
717
+ }
718
+ // ============================================================================
719
+ // Discovery
720
+ // ============================================================================
721
+ /**
722
+ * Find all status files with pending gates
723
+ */
724
+ export function findPendingGates(projectRoot) {
725
+ const pending = [];
726
+ // Check projects
727
+ for (const { id, path: statusPath } of findProjects(projectRoot)) {
728
+ const state = readState(statusPath);
729
+ if (state && state.gates) {
730
+ for (const [gateId, gateStatus] of Object.entries(state.gates)) {
731
+ if (gateStatus.status === 'pending' && gateStatus.requested_at) {
732
+ pending.push({
733
+ projectId: id,
734
+ gateId,
735
+ requestedAt: gateStatus.requested_at,
736
+ statusPath,
737
+ });
738
+ }
739
+ }
740
+ }
741
+ }
742
+ // Check executions
743
+ for (const { id, path: statusPath } of findExecutions(projectRoot)) {
744
+ const state = readState(statusPath);
745
+ if (state && state.gates) {
746
+ for (const [gateId, gateStatus] of Object.entries(state.gates)) {
747
+ if (gateStatus.status === 'pending' && gateStatus.requested_at) {
748
+ pending.push({
749
+ projectId: id,
750
+ gateId,
751
+ requestedAt: gateStatus.requested_at,
752
+ statusPath,
753
+ });
754
+ }
755
+ }
756
+ }
757
+ }
758
+ return pending;
759
+ }
760
+ //# sourceMappingURL=state.js.map