pi-mission-control 0.0.0-dev

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 (110) hide show
  1. package/README.md +205 -0
  2. package/agents/auditor.md +45 -0
  3. package/agents/worker.md +44 -0
  4. package/dist/index.d.ts +9 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +526 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/state.d.ts +265 -0
  9. package/dist/state.d.ts.map +1 -0
  10. package/dist/state.js +474 -0
  11. package/dist/state.js.map +1 -0
  12. package/dist/tools/add-phase.d.ts +28 -0
  13. package/dist/tools/add-phase.d.ts.map +1 -0
  14. package/dist/tools/add-phase.js +69 -0
  15. package/dist/tools/add-phase.js.map +1 -0
  16. package/dist/tools/add-task.d.ts +30 -0
  17. package/dist/tools/add-task.d.ts.map +1 -0
  18. package/dist/tools/add-task.js +85 -0
  19. package/dist/tools/add-task.js.map +1 -0
  20. package/dist/tools/index.d.ts +13 -0
  21. package/dist/tools/index.d.ts.map +1 -0
  22. package/dist/tools/index.js +16 -0
  23. package/dist/tools/index.js.map +1 -0
  24. package/dist/tools/init.d.ts +34 -0
  25. package/dist/tools/init.d.ts.map +1 -0
  26. package/dist/tools/init.js +75 -0
  27. package/dist/tools/init.js.map +1 -0
  28. package/dist/tools/mission-complete.d.ts +30 -0
  29. package/dist/tools/mission-complete.d.ts.map +1 -0
  30. package/dist/tools/mission-complete.js +85 -0
  31. package/dist/tools/mission-complete.js.map +1 -0
  32. package/dist/tools/mission-resume.d.ts +35 -0
  33. package/dist/tools/mission-resume.d.ts.map +1 -0
  34. package/dist/tools/mission-resume.js +87 -0
  35. package/dist/tools/mission-resume.js.map +1 -0
  36. package/dist/tools/scaffold.d.ts +24 -0
  37. package/dist/tools/scaffold.d.ts.map +1 -0
  38. package/dist/tools/scaffold.js +129 -0
  39. package/dist/tools/scaffold.js.map +1 -0
  40. package/dist/tools/update-phase.d.ts +33 -0
  41. package/dist/tools/update-phase.d.ts.map +1 -0
  42. package/dist/tools/update-phase.js +101 -0
  43. package/dist/tools/update-phase.js.map +1 -0
  44. package/dist/tools/update-task.d.ts +34 -0
  45. package/dist/tools/update-task.d.ts.map +1 -0
  46. package/dist/tools/update-task.js +104 -0
  47. package/dist/tools/update-task.js.map +1 -0
  48. package/dist/tui/dashboard.d.ts +146 -0
  49. package/dist/tui/dashboard.d.ts.map +1 -0
  50. package/dist/tui/dashboard.js +381 -0
  51. package/dist/tui/dashboard.js.map +1 -0
  52. package/dist/tui/header.d.ts +39 -0
  53. package/dist/tui/header.d.ts.map +1 -0
  54. package/dist/tui/header.js +62 -0
  55. package/dist/tui/header.js.map +1 -0
  56. package/dist/tui/idle-view.d.ts +44 -0
  57. package/dist/tui/idle-view.d.ts.map +1 -0
  58. package/dist/tui/idle-view.js +87 -0
  59. package/dist/tui/idle-view.js.map +1 -0
  60. package/dist/tui/index.d.ts +13 -0
  61. package/dist/tui/index.d.ts.map +1 -0
  62. package/dist/tui/index.js +15 -0
  63. package/dist/tui/index.js.map +1 -0
  64. package/dist/tui/past-runs.d.ts +49 -0
  65. package/dist/tui/past-runs.d.ts.map +1 -0
  66. package/dist/tui/past-runs.js +207 -0
  67. package/dist/tui/past-runs.js.map +1 -0
  68. package/dist/tui/phases-panel.d.ts +46 -0
  69. package/dist/tui/phases-panel.d.ts.map +1 -0
  70. package/dist/tui/phases-panel.js +161 -0
  71. package/dist/tui/phases-panel.js.map +1 -0
  72. package/dist/tui/progress-bar.d.ts +37 -0
  73. package/dist/tui/progress-bar.d.ts.map +1 -0
  74. package/dist/tui/progress-bar.js +123 -0
  75. package/dist/tui/progress-bar.js.map +1 -0
  76. package/dist/tui/styles.d.ts +8 -0
  77. package/dist/tui/styles.d.ts.map +1 -0
  78. package/dist/tui/styles.js +22 -0
  79. package/dist/tui/styles.js.map +1 -0
  80. package/dist/tui/tasks-panel.d.ts +48 -0
  81. package/dist/tui/tasks-panel.d.ts.map +1 -0
  82. package/dist/tui/tasks-panel.js +191 -0
  83. package/dist/tui/tasks-panel.js.map +1 -0
  84. package/package.json +42 -0
  85. package/skills/mission-memory/SKILL.md +88 -0
  86. package/skills/mission-orchestrator/SKILL.md +167 -0
  87. package/skills/mission-pm/SKILL.md +83 -0
  88. package/skills/mission-research/SKILL.md +66 -0
  89. package/skills/mission-tech-lead/SKILL.md +68 -0
  90. package/src/index.ts +659 -0
  91. package/src/state.ts +623 -0
  92. package/src/tools/add-phase.ts +98 -0
  93. package/src/tools/add-task.ts +121 -0
  94. package/src/tools/index.ts +18 -0
  95. package/src/tools/init.ts +109 -0
  96. package/src/tools/mission-complete.ts +118 -0
  97. package/src/tools/mission-resume.ts +119 -0
  98. package/src/tools/scaffold.ts +167 -0
  99. package/src/tools/update-phase.ts +140 -0
  100. package/src/tools/update-task.ts +145 -0
  101. package/src/tui/dashboard.ts +441 -0
  102. package/src/tui/header.ts +85 -0
  103. package/src/tui/idle-view.ts +114 -0
  104. package/src/tui/index.ts +20 -0
  105. package/src/tui/past-runs.ts +261 -0
  106. package/src/tui/phases-panel.ts +199 -0
  107. package/src/tui/progress-bar.ts +152 -0
  108. package/src/tui/styles.ts +27 -0
  109. package/src/tui/tasks-panel.ts +228 -0
  110. package/templates/state.json +5 -0
package/src/state.ts ADDED
@@ -0,0 +1,623 @@
1
+ /**
2
+ * State types and file helpers for mission-control
3
+ *
4
+ * Types for state.json, run.json, and file read/write operations.
5
+ * All tool mutations go through here.
6
+ */
7
+
8
+ import * as fs from "fs";
9
+ import * as path from "path";
10
+
11
+ // ============================================================================
12
+ // Status Types
13
+ // ============================================================================
14
+
15
+ export type PhaseStatus = "pending" | "in_progress" | "done" | "removed";
16
+ export type TaskStatus = "pending" | "in_progress" | "done" | "failed" | "removed";
17
+ export type RunStatus = "in_progress" | "done" | "failed" | "paused";
18
+
19
+ // ============================================================================
20
+ // State Interfaces
21
+ // ============================================================================
22
+
23
+ /**
24
+ * Volatile UI state - stored in .pi/mission-control/state.json
25
+ * Resets on every new pi session
26
+ */
27
+ export interface State {
28
+ active_run_id: string | null;
29
+ current_phase: string;
30
+ current_status_message: string;
31
+ }
32
+
33
+ /**
34
+ * Task artifact paths
35
+ */
36
+ export interface TaskPaths {
37
+ contract: string;
38
+ worker_output: string;
39
+ auditor_report: string;
40
+ }
41
+
42
+ /**
43
+ * Task definition - stored within Phase in run.json
44
+ */
45
+ export interface Task {
46
+ id: string;
47
+ name: string;
48
+ status: TaskStatus;
49
+ started_at: string | null;
50
+ finish_at: string | null;
51
+ file: string;
52
+ paths: TaskPaths;
53
+ }
54
+
55
+ /**
56
+ * Phase definition - stored in run.json
57
+ */
58
+ export interface Phase {
59
+ id: string;
60
+ name: string;
61
+ status: PhaseStatus;
62
+ started_at: string | null;
63
+ finish_at: string | null;
64
+ file: string;
65
+ tasks: Task[];
66
+ }
67
+
68
+ /**
69
+ * Artifact references in run.json
70
+ */
71
+ export interface Artifacts {
72
+ requirements: string;
73
+ architecture: string;
74
+ validation: string;
75
+ }
76
+
77
+ /**
78
+ * Persistent run state - stored in .pi/mission-control/runs/<run-id>/run.json
79
+ */
80
+ export interface Run {
81
+ run_id: string;
82
+ started_at: string;
83
+ finish_at: string | null;
84
+ status: RunStatus;
85
+ artifacts: Artifacts;
86
+ phases: Phase[];
87
+ }
88
+
89
+ // ============================================================================
90
+ // Default State Values
91
+ // ============================================================================
92
+
93
+ export const defaultState: State = {
94
+ active_run_id: null,
95
+ current_phase: "idle",
96
+ current_status_message: ""
97
+ };
98
+
99
+ // ============================================================================
100
+ // Path Helpers
101
+ // ============================================================================
102
+
103
+ /**
104
+ * Get the project root (cwd)
105
+ */
106
+ export function getProjectRoot(): string {
107
+ return process.cwd();
108
+ }
109
+
110
+ /**
111
+ * Get the base mission-control directory path
112
+ */
113
+ export function getMissionControlDir(): string {
114
+ return path.join(getProjectRoot(), ".pi", "mission-control");
115
+ }
116
+
117
+ /**
118
+ * Get the state.json file path
119
+ */
120
+ export function getStateFilePath(): string {
121
+ return path.join(getMissionControlDir(), "state.json");
122
+ }
123
+
124
+ /**
125
+ * Get the memory directory path
126
+ */
127
+ export function getMemoryDir(): string {
128
+ return path.join(getMissionControlDir(), "memory");
129
+ }
130
+
131
+ /**
132
+ * Get the long-term memory file path
133
+ */
134
+ export function getLongTermMemoryPath(): string {
135
+ return path.join(getMemoryDir(), "long_term.md");
136
+ }
137
+
138
+ /**
139
+ * Get the runs directory path
140
+ */
141
+ export function getRunsDir(): string {
142
+ return path.join(getMissionControlDir(), "runs");
143
+ }
144
+
145
+ /**
146
+ * Get a specific run directory path
147
+ */
148
+ export function getRunDir(runId: string): string {
149
+ return path.join(getRunsDir(), runId);
150
+ }
151
+
152
+ /**
153
+ * Get the run.json file path for a specific run
154
+ */
155
+ export function getRunFilePath(runId: string): string {
156
+ return path.join(getRunDir(runId), "run.json");
157
+ }
158
+
159
+ /**
160
+ * Get the tasks directory path for a specific run
161
+ */
162
+ export function getTasksDir(runId: string): string {
163
+ return path.join(getRunDir(runId), "tasks");
164
+ }
165
+
166
+ /**
167
+ * Get a specific task directory path
168
+ */
169
+ export function getTaskDir(runId: string, taskId: string): string {
170
+ return path.join(getTasksDir(runId), taskId);
171
+ }
172
+
173
+ // ============================================================================
174
+ // File I/O Helpers
175
+ // ============================================================================
176
+
177
+ /**
178
+ * Ensure a directory exists (recursive)
179
+ */
180
+ export function ensureDir(dirPath: string): void {
181
+ if (!fs.existsSync(dirPath)) {
182
+ fs.mkdirSync(dirPath, { recursive: true });
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Read and parse JSON file
188
+ */
189
+ export function readJson<T>(filePath: string): T | null {
190
+ try {
191
+ if (!fs.existsSync(filePath)) {
192
+ return null;
193
+ }
194
+ const content = fs.readFileSync(filePath, "utf-8");
195
+ return JSON.parse(content) as T;
196
+ } catch (error) {
197
+ console.error(`Error reading JSON file ${filePath}:`, error);
198
+ return null;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Write JSON file (with pretty formatting)
204
+ */
205
+ export function writeJson<T>(filePath: string, data: T): void {
206
+ ensureDir(path.dirname(filePath));
207
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
208
+ }
209
+
210
+ /**
211
+ * Read a text file
212
+ */
213
+ export function readText(filePath: string): string | null {
214
+ try {
215
+ if (!fs.existsSync(filePath)) {
216
+ return null;
217
+ }
218
+ return fs.readFileSync(filePath, "utf-8");
219
+ } catch (error) {
220
+ console.error(`Error reading text file ${filePath}:`, error);
221
+ return null;
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Write a text file
227
+ */
228
+ export function writeText(filePath: string, content: string): void {
229
+ ensureDir(path.dirname(filePath));
230
+ fs.writeFileSync(filePath, content, "utf-8");
231
+ }
232
+
233
+ /**
234
+ * Check if a file exists
235
+ */
236
+ export function fileExists(filePath: string): boolean {
237
+ return fs.existsSync(filePath);
238
+ }
239
+
240
+ /**
241
+ * Copy a file from source to destination (skip if exists when specified)
242
+ */
243
+ export function copyFile(src: string, dest: string, skipIfExists = false): boolean {
244
+ if (skipIfExists && fs.existsSync(dest)) {
245
+ return false;
246
+ }
247
+ ensureDir(path.dirname(dest));
248
+ fs.copyFileSync(src, dest);
249
+ return true;
250
+ }
251
+
252
+ /**
253
+ * Copy a directory recursively (skip existing files when specified)
254
+ * Returns count of files copied
255
+ */
256
+ export function copyDir(src: string, dest: string, skipIfExists = false): number {
257
+ if (!fs.existsSync(src)) {
258
+ return 0;
259
+ }
260
+
261
+ ensureDir(dest);
262
+ let copiedCount = 0;
263
+
264
+ const entries = fs.readdirSync(src, { withFileTypes: true });
265
+
266
+ for (const entry of entries) {
267
+ const srcPath = path.join(src, entry.name);
268
+ const destPath = path.join(dest, entry.name);
269
+
270
+ if (entry.isDirectory()) {
271
+ copiedCount += copyDir(srcPath, destPath, skipIfExists);
272
+ } else if (entry.isFile()) {
273
+ if (copyFile(srcPath, destPath, skipIfExists)) {
274
+ copiedCount++;
275
+ }
276
+ }
277
+ }
278
+
279
+ return copiedCount;
280
+ }
281
+
282
+ // ============================================================================
283
+ // State File Operations
284
+ // ============================================================================
285
+
286
+ /**
287
+ * Read the current state.json
288
+ * Returns default state if file doesn't exist
289
+ */
290
+ export function readState(): State {
291
+ const statePath = getStateFilePath();
292
+ const state = readJson<State>(statePath);
293
+ return state ?? { ...defaultState };
294
+ }
295
+
296
+ /**
297
+ * Write state.json
298
+ */
299
+ export function writeState(state: State): void {
300
+ const statePath = getStateFilePath();
301
+ writeJson(statePath, state);
302
+ }
303
+
304
+ /**
305
+ * Update specific fields in state.json
306
+ */
307
+ export function updateState(updates: Partial<State>): State {
308
+ const state = readState();
309
+ const updatedState = { ...state, ...updates };
310
+ writeState(updatedState);
311
+ return updatedState;
312
+ }
313
+
314
+ // ============================================================================
315
+ // Run File Operations
316
+ // ============================================================================
317
+
318
+ /**
319
+ * Read a run.json file
320
+ */
321
+ export function readRun(runId: string): Run | null {
322
+ const runPath = getRunFilePath(runId);
323
+ return readJson<Run>(runPath);
324
+ }
325
+
326
+ /**
327
+ * Write a run.json file
328
+ */
329
+ export function writeRun(run: Run): void {
330
+ const runPath = getRunFilePath(run.run_id);
331
+ writeJson(runPath, run);
332
+ }
333
+
334
+ /**
335
+ * Update specific fields in a run
336
+ */
337
+ export function updateRun(runId: string, updates: Partial<Run>): Run | null {
338
+ const run = readRun(runId);
339
+ if (!run) {
340
+ return null;
341
+ }
342
+ const updatedRun = { ...run, ...updates };
343
+ writeRun(updatedRun);
344
+ return updatedRun;
345
+ }
346
+
347
+ // ============================================================================
348
+ // Timestamp Helpers
349
+ // ============================================================================
350
+
351
+ /**
352
+ * Get current ISO timestamp
353
+ */
354
+ export function getTimestamp(): string {
355
+ return new Date().toISOString();
356
+ }
357
+
358
+ /**
359
+ * Generate a run ID from current timestamp
360
+ * Format: run-YYYYMMDD-HHmmss
361
+ */
362
+ export function generateRunId(): string {
363
+ const now = new Date();
364
+ const date = now.toISOString().slice(0, 10).replace(/-/g, "");
365
+ const time = now.toTimeString().slice(0, 8).replace(/:/g, "");
366
+ return `run-${date}-${time}`;
367
+ }
368
+
369
+ // ============================================================================
370
+ // Phase Helpers
371
+ // ============================================================================
372
+
373
+ /**
374
+ * Find a phase by ID in a run
375
+ */
376
+ export function findPhase(run: Run, phaseId: string): Phase | undefined {
377
+ return run.phases.find(p => p.id === phaseId);
378
+ }
379
+
380
+ /**
381
+ * Generate the next phase ID
382
+ */
383
+ export function generatePhaseId(run: Run): string {
384
+ const phaseCount = run.phases.length;
385
+ return `phase${phaseCount + 1}`;
386
+ }
387
+
388
+ /**
389
+ * Create a new phase
390
+ */
391
+ export function createPhase(id: string, name: string, file: string): Phase {
392
+ return {
393
+ id,
394
+ name,
395
+ status: "pending",
396
+ started_at: null,
397
+ finish_at: null,
398
+ file,
399
+ tasks: []
400
+ };
401
+ }
402
+
403
+ /**
404
+ * Add a phase to a run
405
+ */
406
+ export function addPhaseToRun(run: Run, phase: Phase): Run {
407
+ return {
408
+ ...run,
409
+ phases: [...run.phases, phase]
410
+ };
411
+ }
412
+
413
+ /**
414
+ * Update a phase in a run
415
+ */
416
+ export function updatePhaseInRun(run: Run, phaseId: string, updates: Partial<Phase>): Run {
417
+ return {
418
+ ...run,
419
+ phases: run.phases.map(p =>
420
+ p.id === phaseId ? { ...p, ...updates } : p
421
+ )
422
+ };
423
+ }
424
+
425
+ // ============================================================================
426
+ // Task Helpers
427
+ // ============================================================================
428
+
429
+ /**
430
+ * Find a task by ID in a run (searches all phases)
431
+ */
432
+ export function findTask(run: Run, taskId: string): { phase: Phase; task: Task; phaseIndex: number; taskIndex: number } | null {
433
+ for (let phaseIndex = 0; phaseIndex < run.phases.length; phaseIndex++) {
434
+ const phase = run.phases[phaseIndex];
435
+ const taskIndex = phase.tasks.findIndex(t => t.id === taskId);
436
+ if (taskIndex !== -1) {
437
+ return { phase, task: phase.tasks[taskIndex], phaseIndex, taskIndex };
438
+ }
439
+ }
440
+ return null;
441
+ }
442
+
443
+ /**
444
+ * Generate the next task ID for a phase
445
+ */
446
+ export function generateTaskId(phase: Phase): string {
447
+ const taskCount = phase.tasks.length;
448
+ return `${phase.id}-task${taskCount + 1}`;
449
+ }
450
+
451
+ /**
452
+ * Create a new task
453
+ */
454
+ export function createTask(id: string, name: string, file: string): Task {
455
+ const taskDir = path.join("tasks", id);
456
+ return {
457
+ id,
458
+ name,
459
+ status: "pending",
460
+ started_at: null,
461
+ finish_at: null,
462
+ file,
463
+ paths: {
464
+ contract: path.join(taskDir, "contract.md"),
465
+ worker_output: path.join(taskDir, "worker-output.md"),
466
+ auditor_report: path.join(taskDir, "auditor-report.md")
467
+ }
468
+ };
469
+ }
470
+
471
+ /**
472
+ * Add a task to a phase within a run
473
+ */
474
+ export function addTaskToRun(run: Run, phaseId: string, task: Task): Run {
475
+ return {
476
+ ...run,
477
+ phases: run.phases.map(p =>
478
+ p.id === phaseId
479
+ ? { ...p, tasks: [...p.tasks, task] }
480
+ : p
481
+ )
482
+ };
483
+ }
484
+
485
+ /**
486
+ * Update a task in a run
487
+ */
488
+ export function updateTaskInRun(run: Run, taskId: string, updates: Partial<Task>): Run {
489
+ return {
490
+ ...run,
491
+ phases: run.phases.map(p => ({
492
+ ...p,
493
+ tasks: p.tasks.map(t =>
494
+ t.id === taskId ? { ...t, ...updates } : t
495
+ )
496
+ }))
497
+ };
498
+ }
499
+
500
+ /**
501
+ * Check if all tasks in a phase are done
502
+ */
503
+ export function areAllTasksDone(phase: Phase): boolean {
504
+ if (phase.tasks.length === 0) return false;
505
+ return phase.tasks.every(t => t.status === "done");
506
+ }
507
+
508
+ /**
509
+ * Check if all phases in a run are done
510
+ */
511
+ export function areAllPhasesDone(run: Run): boolean {
512
+ if (run.phases.length === 0) return false;
513
+ return run.phases.every(p => p.status === "done");
514
+ }
515
+
516
+ // ============================================================================
517
+ // Run Discovery
518
+ // ============================================================================
519
+
520
+ /**
521
+ * List all run IDs in the runs directory
522
+ */
523
+ export function listRunIds(): string[] {
524
+ const runsDir = getRunsDir();
525
+ if (!fs.existsSync(runsDir)) {
526
+ return [];
527
+ }
528
+
529
+ return fs.readdirSync(runsDir, { withFileTypes: true })
530
+ .filter(entry => entry.isDirectory() && entry.name.startsWith("run-"))
531
+ .map(entry => entry.name)
532
+ .sort();
533
+ }
534
+
535
+ /**
536
+ * List all runs with their basic info
537
+ */
538
+ export function listRuns(): Array<{ runId: string; run: Run | null }> {
539
+ const runIds = listRunIds();
540
+ return runIds.map(runId => ({
541
+ runId,
542
+ run: readRun(runId)
543
+ }));
544
+ }
545
+
546
+ // ============================================================================
547
+ // Scaffold Check
548
+ // ============================================================================
549
+
550
+ /**
551
+ * Check if mission-control has been scaffolded
552
+ */
553
+ export function isScaffolded(): boolean {
554
+ return fileExists(getMissionControlDir());
555
+ }
556
+
557
+ // ============================================================================
558
+ // Initialization Helpers
559
+ // ============================================================================
560
+
561
+ /**
562
+ * Create the initial run.json structure
563
+ */
564
+ export function createInitialRun(runId: string): Run {
565
+ const now = getTimestamp();
566
+ return {
567
+ run_id: runId,
568
+ started_at: now,
569
+ finish_at: null,
570
+ status: "in_progress",
571
+ artifacts: {
572
+ requirements: "00-requirements.md",
573
+ architecture: "01-architecture.md",
574
+ validation: "02-validation.md"
575
+ },
576
+ phases: []
577
+ };
578
+ }
579
+
580
+ /**
581
+ * Create the base mission-control directory structure
582
+ */
583
+ export function createBaseStructure(): void {
584
+ ensureDir(getMissionControlDir());
585
+ ensureDir(getMemoryDir());
586
+ ensureDir(getRunsDir());
587
+ }
588
+
589
+ /**
590
+ * Create a new run directory structure
591
+ */
592
+ export function createRunStructure(runId: string): void {
593
+ const runDir = getRunDir(runId);
594
+ ensureDir(runDir);
595
+ ensureDir(getTasksDir(runId));
596
+ }
597
+
598
+ // ============================================================================
599
+ // Status Transition Logic
600
+ // ============================================================================
601
+
602
+ /**
603
+ * Get the appropriate timestamp field to update based on status
604
+ */
605
+ export function getTimestampForStatus(
606
+ status: PhaseStatus | TaskStatus,
607
+ currentStatus: PhaseStatus | TaskStatus
608
+ ): { started_at?: string; finish_at?: string } | null {
609
+ const now = getTimestamp();
610
+
611
+ // Transition to in_progress sets started_at
612
+ if (status === "in_progress" && currentStatus !== "in_progress") {
613
+ return { started_at: now };
614
+ }
615
+
616
+ // Transition to terminal state sets finish_at
617
+ const terminalStatuses: Array<PhaseStatus | TaskStatus> = ["done", "failed", "removed"];
618
+ if (terminalStatuses.includes(status) && !terminalStatuses.includes(currentStatus)) {
619
+ return { finish_at: now };
620
+ }
621
+
622
+ return null;
623
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * add_phase tool
3
+ *
4
+ * Agent tool to add a new phase to the current mission run.
5
+ * Auto-generates phase_id ("phase1", "phase2", ...).
6
+ * Sets status to "pending".
7
+ *
8
+ * Used by: mission-pm skill
9
+ */
10
+
11
+ import {
12
+ readState,
13
+ readRun,
14
+ writeRun,
15
+ generatePhaseId,
16
+ createPhase,
17
+ addPhaseToRun
18
+ } from "../state.js";
19
+
20
+ export interface AddPhaseParams {
21
+ name: string;
22
+ file: string;
23
+ }
24
+
25
+ export interface AddPhaseResult {
26
+ success: boolean;
27
+ message: string;
28
+ phaseId: string | null;
29
+ errors: string[];
30
+ }
31
+
32
+ /**
33
+ * Add a phase to the active mission run
34
+ *
35
+ * @param params.name - Human-readable phase name (e.g., "Auth Backend")
36
+ * @param params.file - Path to phase definition/reference file
37
+ * @returns Result with generated phase_id
38
+ */
39
+ export function addPhase(params: AddPhaseParams): AddPhaseResult {
40
+ const result: AddPhaseResult = {
41
+ success: false,
42
+ message: "",
43
+ phaseId: null,
44
+ errors: []
45
+ };
46
+
47
+ try {
48
+ // Validate parameters
49
+ if (!params.name || params.name.trim() === "") {
50
+ result.errors.push("Phase name is required");
51
+ result.message = "Failed to add phase: name is required";
52
+ return result;
53
+ }
54
+
55
+ if (!params.file || params.file.trim() === "") {
56
+ result.errors.push("Phase file is required");
57
+ result.message = "Failed to add phase: file is required";
58
+ return result;
59
+ }
60
+
61
+ // Get active run
62
+ const state = readState();
63
+ if (!state.active_run_id) {
64
+ result.errors.push("No active run");
65
+ result.message = "Failed to add phase: no active mission run";
66
+ return result;
67
+ }
68
+
69
+ // Read run.json
70
+ const run = readRun(state.active_run_id);
71
+ if (!run) {
72
+ result.errors.push(`Run not found: ${state.active_run_id}`);
73
+ result.message = "Failed to add phase: run not found";
74
+ return result;
75
+ }
76
+
77
+ // Generate phase ID
78
+ const phaseId = generatePhaseId(run);
79
+
80
+ // Create phase
81
+ const phase = createPhase(phaseId, params.name.trim(), params.file.trim());
82
+
83
+ // Add to run
84
+ const updatedRun = addPhaseToRun(run, phase);
85
+ writeRun(updatedRun);
86
+
87
+ result.success = true;
88
+ result.phaseId = phaseId;
89
+ result.message = `Phase added: ${phaseId} - ${params.name}`;
90
+
91
+ } catch (error) {
92
+ result.success = false;
93
+ result.message = `Error adding phase: ${error instanceof Error ? error.message : String(error)}`;
94
+ result.errors.push(String(error));
95
+ }
96
+
97
+ return result;
98
+ }