@rengler33/prov 0.1.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 (146) hide show
  1. package/README.md +314 -0
  2. package/dist/cli.d.ts +26 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +381 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/agent.d.ts +107 -0
  7. package/dist/commands/agent.d.ts.map +1 -0
  8. package/dist/commands/agent.js +197 -0
  9. package/dist/commands/agent.js.map +1 -0
  10. package/dist/commands/agent.test.d.ts +5 -0
  11. package/dist/commands/agent.test.d.ts.map +1 -0
  12. package/dist/commands/agent.test.js +199 -0
  13. package/dist/commands/agent.test.js.map +1 -0
  14. package/dist/commands/constraint.d.ts +100 -0
  15. package/dist/commands/constraint.d.ts.map +1 -0
  16. package/dist/commands/constraint.js +763 -0
  17. package/dist/commands/constraint.js.map +1 -0
  18. package/dist/commands/constraint.test.d.ts +9 -0
  19. package/dist/commands/constraint.test.d.ts.map +1 -0
  20. package/dist/commands/constraint.test.js +470 -0
  21. package/dist/commands/constraint.test.js.map +1 -0
  22. package/dist/commands/graph.d.ts +99 -0
  23. package/dist/commands/graph.d.ts.map +1 -0
  24. package/dist/commands/graph.js +552 -0
  25. package/dist/commands/graph.js.map +1 -0
  26. package/dist/commands/graph.test.d.ts +2 -0
  27. package/dist/commands/graph.test.d.ts.map +1 -0
  28. package/dist/commands/graph.test.js +258 -0
  29. package/dist/commands/graph.test.js.map +1 -0
  30. package/dist/commands/impact.d.ts +83 -0
  31. package/dist/commands/impact.d.ts.map +1 -0
  32. package/dist/commands/impact.js +319 -0
  33. package/dist/commands/impact.js.map +1 -0
  34. package/dist/commands/impact.test.d.ts +2 -0
  35. package/dist/commands/impact.test.d.ts.map +1 -0
  36. package/dist/commands/impact.test.js +234 -0
  37. package/dist/commands/impact.test.js.map +1 -0
  38. package/dist/commands/init.d.ts +45 -0
  39. package/dist/commands/init.d.ts.map +1 -0
  40. package/dist/commands/init.js +94 -0
  41. package/dist/commands/init.js.map +1 -0
  42. package/dist/commands/init.test.d.ts +7 -0
  43. package/dist/commands/init.test.d.ts.map +1 -0
  44. package/dist/commands/init.test.js +174 -0
  45. package/dist/commands/init.test.js.map +1 -0
  46. package/dist/commands/integration.test.d.ts +10 -0
  47. package/dist/commands/integration.test.d.ts.map +1 -0
  48. package/dist/commands/integration.test.js +456 -0
  49. package/dist/commands/integration.test.js.map +1 -0
  50. package/dist/commands/mcp.d.ts +21 -0
  51. package/dist/commands/mcp.d.ts.map +1 -0
  52. package/dist/commands/mcp.js +616 -0
  53. package/dist/commands/mcp.js.map +1 -0
  54. package/dist/commands/mcp.test.d.ts +7 -0
  55. package/dist/commands/mcp.test.d.ts.map +1 -0
  56. package/dist/commands/mcp.test.js +132 -0
  57. package/dist/commands/mcp.test.js.map +1 -0
  58. package/dist/commands/plan.d.ts +218 -0
  59. package/dist/commands/plan.d.ts.map +1 -0
  60. package/dist/commands/plan.js +1307 -0
  61. package/dist/commands/plan.js.map +1 -0
  62. package/dist/commands/plan.test.d.ts +9 -0
  63. package/dist/commands/plan.test.d.ts.map +1 -0
  64. package/dist/commands/plan.test.js +569 -0
  65. package/dist/commands/plan.test.js.map +1 -0
  66. package/dist/commands/spec.d.ts +94 -0
  67. package/dist/commands/spec.d.ts.map +1 -0
  68. package/dist/commands/spec.js +635 -0
  69. package/dist/commands/spec.js.map +1 -0
  70. package/dist/commands/spec.test.d.ts +9 -0
  71. package/dist/commands/spec.test.d.ts.map +1 -0
  72. package/dist/commands/spec.test.js +407 -0
  73. package/dist/commands/spec.test.js.map +1 -0
  74. package/dist/commands/trace.d.ts +157 -0
  75. package/dist/commands/trace.d.ts.map +1 -0
  76. package/dist/commands/trace.js +847 -0
  77. package/dist/commands/trace.js.map +1 -0
  78. package/dist/commands/trace.test.d.ts +9 -0
  79. package/dist/commands/trace.test.d.ts.map +1 -0
  80. package/dist/commands/trace.test.js +524 -0
  81. package/dist/commands/trace.test.js.map +1 -0
  82. package/dist/graph.d.ts +204 -0
  83. package/dist/graph.d.ts.map +1 -0
  84. package/dist/graph.js +496 -0
  85. package/dist/graph.js.map +1 -0
  86. package/dist/graph.test.d.ts +2 -0
  87. package/dist/graph.test.d.ts.map +1 -0
  88. package/dist/graph.test.js +382 -0
  89. package/dist/graph.test.js.map +1 -0
  90. package/dist/hash.d.ts +72 -0
  91. package/dist/hash.d.ts.map +1 -0
  92. package/dist/hash.js +137 -0
  93. package/dist/hash.js.map +1 -0
  94. package/dist/hash.test.d.ts +2 -0
  95. package/dist/hash.test.d.ts.map +1 -0
  96. package/dist/hash.test.js +227 -0
  97. package/dist/hash.test.js.map +1 -0
  98. package/dist/index.d.ts +18 -0
  99. package/dist/index.d.ts.map +1 -0
  100. package/dist/index.js +64 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/index.test.d.ts +2 -0
  103. package/dist/index.test.d.ts.map +1 -0
  104. package/dist/index.test.js +11 -0
  105. package/dist/index.test.js.map +1 -0
  106. package/dist/output.d.ts +84 -0
  107. package/dist/output.d.ts.map +1 -0
  108. package/dist/output.js +175 -0
  109. package/dist/output.js.map +1 -0
  110. package/dist/output.test.d.ts +7 -0
  111. package/dist/output.test.d.ts.map +1 -0
  112. package/dist/output.test.js +146 -0
  113. package/dist/output.test.js.map +1 -0
  114. package/dist/staleness.d.ts +162 -0
  115. package/dist/staleness.d.ts.map +1 -0
  116. package/dist/staleness.js +309 -0
  117. package/dist/staleness.js.map +1 -0
  118. package/dist/staleness.test.d.ts +2 -0
  119. package/dist/staleness.test.d.ts.map +1 -0
  120. package/dist/staleness.test.js +448 -0
  121. package/dist/staleness.test.js.map +1 -0
  122. package/dist/storage.d.ts +267 -0
  123. package/dist/storage.d.ts.map +1 -0
  124. package/dist/storage.js +623 -0
  125. package/dist/storage.js.map +1 -0
  126. package/dist/storage.test.d.ts +5 -0
  127. package/dist/storage.test.d.ts.map +1 -0
  128. package/dist/storage.test.js +434 -0
  129. package/dist/storage.test.js.map +1 -0
  130. package/dist/types.d.ts +270 -0
  131. package/dist/types.d.ts.map +1 -0
  132. package/dist/types.js +12 -0
  133. package/dist/types.js.map +1 -0
  134. package/dist/types.test.d.ts +2 -0
  135. package/dist/types.test.d.ts.map +1 -0
  136. package/dist/types.test.js +232 -0
  137. package/dist/types.test.js.map +1 -0
  138. package/dist/watcher.d.ts +139 -0
  139. package/dist/watcher.d.ts.map +1 -0
  140. package/dist/watcher.js +406 -0
  141. package/dist/watcher.js.map +1 -0
  142. package/dist/watcher.test.d.ts +5 -0
  143. package/dist/watcher.test.d.ts.map +1 -0
  144. package/dist/watcher.test.js +327 -0
  145. package/dist/watcher.test.js.map +1 -0
  146. package/package.json +53 -0
@@ -0,0 +1,1307 @@
1
+ /**
2
+ * prov plan commands implementation.
3
+ *
4
+ * Commands for managing implementation plans:
5
+ * - plan create: Create a new plan from specs and constraints
6
+ * - plan show: Display plan details
7
+ * - plan validate: Validate plan completeness
8
+ *
9
+ * @see req:cli:plan-create
10
+ * @see req:cli:plan-show
11
+ * @see req:cli:plan-validate
12
+ */
13
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
14
+ import { join, resolve, relative, extname, dirname } from 'node:path';
15
+ import { isInitialized, loadGraph, saveGraph } from '../storage.js';
16
+ import { addPlanToGraph } from '../graph.js';
17
+ import { parseYaml, computeHash, toYaml } from '../hash.js';
18
+ import { output, error, success, warn, resolveFormat } from '../output.js';
19
+ // ============================================================================
20
+ // Validation Helpers
21
+ // ============================================================================
22
+ /**
23
+ * Validate plan ID format: plan:{name}:v{number}
24
+ */
25
+ function isValidPlanId(id) {
26
+ if (typeof id !== 'string')
27
+ return false;
28
+ return /^plan:[a-z0-9-]+:v\d+$/.test(id);
29
+ }
30
+ /**
31
+ * Validate decision ID format: dec:{plan}:{name}
32
+ */
33
+ function isValidDecisionId(id) {
34
+ if (typeof id !== 'string')
35
+ return false;
36
+ return /^dec:[a-z0-9-]+:[a-z0-9-]+$/.test(id);
37
+ }
38
+ /**
39
+ * Validate step ID format: step:{plan}:{number}
40
+ */
41
+ function isValidStepId(id) {
42
+ if (typeof id !== 'string')
43
+ return false;
44
+ return /^step:[a-z0-9-]+:\d+$/.test(id);
45
+ }
46
+ /**
47
+ * Validate spec ID format.
48
+ */
49
+ function isValidSpecId(id) {
50
+ if (typeof id !== 'string')
51
+ return false;
52
+ return /^spec:[a-z0-9-]+:v\d+$/.test(id);
53
+ }
54
+ /**
55
+ * Validate constraint ID format.
56
+ */
57
+ function isValidConstraintId(id) {
58
+ if (typeof id !== 'string')
59
+ return false;
60
+ return /^constraint:[a-z0-9-]+:v\d+$/.test(id);
61
+ }
62
+ /**
63
+ * Validate requirement ID format.
64
+ */
65
+ function isValidRequirementId(id) {
66
+ if (typeof id !== 'string')
67
+ return false;
68
+ return /^req:[a-z0-9-]+(:[a-z0-9-]+)?$/.test(id);
69
+ }
70
+ /**
71
+ * Validate invariant ID format.
72
+ */
73
+ function isValidInvariantId(id) {
74
+ if (typeof id !== 'string')
75
+ return false;
76
+ return /^inv:[a-z0-9-]+:[a-z0-9-]+$/.test(id);
77
+ }
78
+ /**
79
+ * Validate source reference ID (spec or constraint).
80
+ */
81
+ function isValidSourceId(id) {
82
+ return isValidSpecId(id) || isValidConstraintId(id);
83
+ }
84
+ /**
85
+ * Validate hash format.
86
+ */
87
+ function isValidHash(hash) {
88
+ if (typeof hash !== 'string')
89
+ return false;
90
+ return /^sha256:[a-f0-9]+$/.test(hash);
91
+ }
92
+ /**
93
+ * Validate entity status.
94
+ */
95
+ function isValidStatus(status) {
96
+ return status === 'draft' || status === 'active' || status === 'deprecated' || status === 'archived';
97
+ }
98
+ /**
99
+ * Validate step status.
100
+ */
101
+ function isValidStepStatus(status) {
102
+ return status === 'pending' || status === 'in_progress' || status === 'completed' || status === 'blocked';
103
+ }
104
+ /**
105
+ * Validate a raw plan object and convert to typed Plan.
106
+ */
107
+ function validatePlan(raw, _filePath) {
108
+ const errors = [];
109
+ // Required fields
110
+ if (raw.id === undefined || raw.id === null) {
111
+ errors.push('Missing required field: id');
112
+ }
113
+ else if (!isValidPlanId(raw.id)) {
114
+ errors.push(`Invalid plan ID format: ${String(raw.id)} (expected plan:{name}:v{number})`);
115
+ }
116
+ if (raw.version === undefined || raw.version === null) {
117
+ errors.push('Missing required field: version');
118
+ }
119
+ else if (typeof raw.version !== 'string') {
120
+ errors.push(`Invalid version type: expected string, got ${typeof raw.version}`);
121
+ }
122
+ if (raw.title === undefined || raw.title === null) {
123
+ errors.push('Missing required field: title');
124
+ }
125
+ else if (typeof raw.title !== 'string') {
126
+ errors.push(`Invalid title type: expected string, got ${typeof raw.title}`);
127
+ }
128
+ if (raw.status !== undefined && !isValidStatus(raw.status)) {
129
+ errors.push(`Invalid status: ${String(raw.status)} (expected draft|active|deprecated|archived)`);
130
+ }
131
+ if (raw.sources === undefined || raw.sources === null) {
132
+ errors.push('Missing required field: sources');
133
+ }
134
+ else if (!Array.isArray(raw.sources)) {
135
+ errors.push(`Invalid sources type: expected array, got ${typeof raw.sources}`);
136
+ }
137
+ if (raw.decisions !== undefined && !Array.isArray(raw.decisions)) {
138
+ errors.push(`Invalid decisions type: expected array, got ${typeof raw.decisions}`);
139
+ }
140
+ if (raw.steps === undefined || raw.steps === null) {
141
+ errors.push('Missing required field: steps');
142
+ }
143
+ else if (!Array.isArray(raw.steps)) {
144
+ errors.push(`Invalid steps type: expected array, got ${typeof raw.steps}`);
145
+ }
146
+ if (errors.length > 0) {
147
+ return { errors };
148
+ }
149
+ // Validate sources
150
+ const sources = [];
151
+ for (let i = 0; i < raw.sources.length; i++) {
152
+ const rawSource = raw.sources[i];
153
+ if (!isValidSourceId(rawSource.id)) {
154
+ errors.push(`Source ${i}: invalid ID format: ${String(rawSource.id)}`);
155
+ continue;
156
+ }
157
+ if (!isValidHash(rawSource.hash)) {
158
+ errors.push(`Source ${i}: invalid hash format: ${String(rawSource.hash)}`);
159
+ continue;
160
+ }
161
+ sources.push({
162
+ id: rawSource.id,
163
+ hash: rawSource.hash,
164
+ });
165
+ }
166
+ // Validate decisions
167
+ const decisions = [];
168
+ const decisionIds = new Set();
169
+ if (raw.decisions !== undefined) {
170
+ for (let i = 0; i < raw.decisions.length; i++) {
171
+ const rawDec = raw.decisions[i];
172
+ if (rawDec.id === undefined || rawDec.id === null) {
173
+ errors.push(`Decision ${i}: missing required field: id`);
174
+ continue;
175
+ }
176
+ if (!isValidDecisionId(rawDec.id)) {
177
+ errors.push(`Decision ${i}: invalid ID format: ${String(rawDec.id)} (expected dec:{plan}:{name})`);
178
+ continue;
179
+ }
180
+ const decIdStr = rawDec.id;
181
+ if (decisionIds.has(decIdStr)) {
182
+ errors.push(`Decision ${i}: duplicate ID: ${decIdStr}`);
183
+ continue;
184
+ }
185
+ decisionIds.add(decIdStr);
186
+ if (rawDec.question === undefined || typeof rawDec.question !== 'string') {
187
+ errors.push(`Decision ${decIdStr}: missing or invalid question`);
188
+ continue;
189
+ }
190
+ if (rawDec.choice === undefined || typeof rawDec.choice !== 'string') {
191
+ errors.push(`Decision ${decIdStr}: missing or invalid choice`);
192
+ continue;
193
+ }
194
+ if (rawDec.rationale === undefined || typeof rawDec.rationale !== 'string') {
195
+ errors.push(`Decision ${decIdStr}: missing or invalid rationale`);
196
+ continue;
197
+ }
198
+ // Validate traces_to
199
+ const tracesTo = [];
200
+ if (rawDec.traces_to !== undefined) {
201
+ if (!Array.isArray(rawDec.traces_to)) {
202
+ errors.push(`Decision ${decIdStr}: traces_to must be an array`);
203
+ }
204
+ else {
205
+ for (const target of rawDec.traces_to) {
206
+ if (!isValidRequirementId(target) && !isValidInvariantId(target)) {
207
+ errors.push(`Decision ${decIdStr}: invalid traces_to target: ${String(target)}`);
208
+ }
209
+ else {
210
+ tracesTo.push(target);
211
+ }
212
+ }
213
+ }
214
+ }
215
+ decisions.push({
216
+ id: decIdStr,
217
+ question: rawDec.question,
218
+ choice: rawDec.choice,
219
+ rationale: rawDec.rationale,
220
+ tracesTo,
221
+ });
222
+ }
223
+ }
224
+ // Validate steps
225
+ const steps = [];
226
+ const stepIds = new Set();
227
+ for (let i = 0; i < raw.steps.length; i++) {
228
+ const rawStep = raw.steps[i];
229
+ if (rawStep.id === undefined || rawStep.id === null) {
230
+ errors.push(`Step ${i}: missing required field: id`);
231
+ continue;
232
+ }
233
+ if (!isValidStepId(rawStep.id)) {
234
+ errors.push(`Step ${i}: invalid ID format: ${String(rawStep.id)} (expected step:{plan}:{number})`);
235
+ continue;
236
+ }
237
+ const stepIdStr = rawStep.id;
238
+ if (stepIds.has(stepIdStr)) {
239
+ errors.push(`Step ${i}: duplicate ID: ${stepIdStr}`);
240
+ continue;
241
+ }
242
+ stepIds.add(stepIdStr);
243
+ if (rawStep.number === undefined || typeof rawStep.number !== 'number') {
244
+ errors.push(`Step ${stepIdStr}: missing or invalid number`);
245
+ continue;
246
+ }
247
+ if (rawStep.action === undefined || typeof rawStep.action !== 'string') {
248
+ errors.push(`Step ${stepIdStr}: missing or invalid action`);
249
+ continue;
250
+ }
251
+ if (rawStep.status !== undefined && !isValidStepStatus(rawStep.status)) {
252
+ errors.push(`Step ${stepIdStr}: invalid status: ${String(rawStep.status)}`);
253
+ continue;
254
+ }
255
+ // Validate traces_to
256
+ // Steps can trace to decisions, requirements, or invariants they implement
257
+ const tracesTo = [];
258
+ if (rawStep.traces_to !== undefined) {
259
+ if (!Array.isArray(rawStep.traces_to)) {
260
+ errors.push(`Step ${stepIdStr}: traces_to must be an array`);
261
+ }
262
+ else {
263
+ for (const target of rawStep.traces_to) {
264
+ if (!isValidDecisionId(target) && !isValidRequirementId(target) && !isValidInvariantId(target)) {
265
+ errors.push(`Step ${stepIdStr}: invalid traces_to target: ${String(target)}`);
266
+ }
267
+ else {
268
+ tracesTo.push(target);
269
+ }
270
+ }
271
+ }
272
+ }
273
+ // Validate dependencies
274
+ let dependencies;
275
+ if (rawStep.dependencies !== undefined) {
276
+ if (!Array.isArray(rawStep.dependencies)) {
277
+ errors.push(`Step ${stepIdStr}: dependencies must be an array`);
278
+ }
279
+ else {
280
+ dependencies = [];
281
+ for (const dep of rawStep.dependencies) {
282
+ if (!isValidStepId(dep)) {
283
+ errors.push(`Step ${stepIdStr}: invalid dependency: ${String(dep)}`);
284
+ }
285
+ else {
286
+ dependencies.push(dep);
287
+ }
288
+ }
289
+ }
290
+ }
291
+ // Validate artifacts
292
+ let artifacts;
293
+ if (rawStep.artifacts !== undefined) {
294
+ if (!Array.isArray(rawStep.artifacts)) {
295
+ errors.push(`Step ${stepIdStr}: artifacts must be an array`);
296
+ }
297
+ else {
298
+ artifacts = [];
299
+ for (const artifact of rawStep.artifacts) {
300
+ if (typeof artifact !== 'string') {
301
+ errors.push(`Step ${stepIdStr}: artifact must be a string`);
302
+ }
303
+ else {
304
+ artifacts.push(artifact);
305
+ }
306
+ }
307
+ }
308
+ }
309
+ // Build step object - use Object.assign to avoid exactOptionalPropertyTypes issues
310
+ const step = Object.assign({
311
+ id: stepIdStr,
312
+ number: rawStep.number,
313
+ action: rawStep.action,
314
+ tracesTo,
315
+ }, rawStep.description !== undefined ? { description: rawStep.description } : null, dependencies !== undefined && dependencies.length > 0 ? { dependencies } : null, artifacts !== undefined && artifacts.length > 0 ? { artifacts } : null, rawStep.status !== undefined ? { status: rawStep.status } : null);
316
+ steps.push(step);
317
+ }
318
+ if (errors.length > 0) {
319
+ return { errors };
320
+ }
321
+ const plan = {
322
+ id: raw.id,
323
+ version: raw.version,
324
+ title: raw.title,
325
+ status: raw.status ?? 'draft',
326
+ sources,
327
+ decisions,
328
+ steps,
329
+ };
330
+ return { plan, errors: [] };
331
+ }
332
+ // ============================================================================
333
+ // File Discovery
334
+ // ============================================================================
335
+ /**
336
+ * Find all plan files in the plan directory.
337
+ */
338
+ function findPlanFiles(projectRoot) {
339
+ const planDir = join(projectRoot, 'plan');
340
+ const files = [];
341
+ if (!existsSync(planDir)) {
342
+ return files;
343
+ }
344
+ function walkDir(dir) {
345
+ const entries = readdirSync(dir);
346
+ for (const entry of entries) {
347
+ const fullPath = join(dir, entry);
348
+ const stat = statSync(fullPath);
349
+ if (stat.isDirectory()) {
350
+ walkDir(fullPath);
351
+ }
352
+ else {
353
+ const ext = extname(entry).toLowerCase();
354
+ if (ext === '.yaml' || ext === '.yml') {
355
+ // Include files with .plan. in the name or in plan directory
356
+ if (entry.includes('.plan.') || dir.includes('/plan')) {
357
+ files.push(fullPath);
358
+ }
359
+ }
360
+ }
361
+ }
362
+ }
363
+ walkDir(planDir);
364
+ return files.sort();
365
+ }
366
+ /**
367
+ * Load and parse a plan file.
368
+ */
369
+ function loadPlanFile(filePath) {
370
+ try {
371
+ const content = readFileSync(filePath, 'utf8');
372
+ const raw = parseYaml(content);
373
+ if (raw === null || typeof raw !== 'object') {
374
+ return { errors: ['File does not contain a valid YAML object'] };
375
+ }
376
+ return validatePlan(raw, filePath);
377
+ }
378
+ catch (err) {
379
+ return {
380
+ errors: [`Failed to parse YAML: ${err instanceof Error ? err.message : String(err)}`],
381
+ };
382
+ }
383
+ }
384
+ // ============================================================================
385
+ // plan create Command
386
+ // ============================================================================
387
+ /**
388
+ * Execute the plan create command.
389
+ *
390
+ * Creates a new plan scaffold based on the specified specs and constraints.
391
+ *
392
+ * @see req:cli:plan-create
393
+ */
394
+ export function runPlanCreate(globalOpts, options) {
395
+ const projectRoot = globalOpts.dir ?? process.cwd();
396
+ const fmt = resolveFormat({ format: globalOpts.format });
397
+ // Check if prov is initialized
398
+ if (!isInitialized(projectRoot)) {
399
+ const result = {
400
+ success: false,
401
+ error: 'prov is not initialized. Run "prov init" first.',
402
+ };
403
+ if (fmt === 'json') {
404
+ output(result, { format: 'json' });
405
+ }
406
+ else if (fmt === 'yaml') {
407
+ output(result, { format: 'yaml' });
408
+ }
409
+ else {
410
+ error(result.error);
411
+ }
412
+ process.exit(1);
413
+ }
414
+ // Load graph to get specs and constraints
415
+ const loadResult = loadGraph(projectRoot);
416
+ if (!loadResult.success || loadResult.data === undefined) {
417
+ const result = {
418
+ success: false,
419
+ error: loadResult.error ?? 'Failed to load graph',
420
+ };
421
+ if (fmt === 'json') {
422
+ output(result, { format: 'json' });
423
+ }
424
+ else if (fmt === 'yaml') {
425
+ output(result, { format: 'yaml' });
426
+ }
427
+ else {
428
+ error(result.error);
429
+ }
430
+ process.exit(1);
431
+ }
432
+ const graph = loadResult.data;
433
+ // Resolve source references
434
+ const sources = [];
435
+ const allRequirements = [];
436
+ const allInvariants = [];
437
+ // Process --from specs
438
+ for (const specIdStr of options.from) {
439
+ const specId = specIdStr;
440
+ const node = graph.getNode(specId);
441
+ if (node === undefined || node.type !== 'spec') {
442
+ const result = {
443
+ success: false,
444
+ error: `Spec not found: ${specIdStr}`,
445
+ };
446
+ if (fmt === 'json') {
447
+ output(result, { format: 'json' });
448
+ }
449
+ else if (fmt === 'yaml') {
450
+ output(result, { format: 'yaml' });
451
+ }
452
+ else {
453
+ error(result.error);
454
+ }
455
+ process.exit(1);
456
+ }
457
+ sources.push({
458
+ id: specId,
459
+ hash: node.hash,
460
+ });
461
+ // Collect requirements for tracing
462
+ const spec = node.data;
463
+ for (const req of spec.requirements) {
464
+ allRequirements.push(req.id);
465
+ }
466
+ }
467
+ // Process --with constraints
468
+ if (options.with !== undefined) {
469
+ for (const constraintIdStr of options.with) {
470
+ const constraintId = constraintIdStr;
471
+ const node = graph.getNode(constraintId);
472
+ if (node === undefined || node.type !== 'constraint') {
473
+ const result = {
474
+ success: false,
475
+ error: `Constraint not found: ${constraintIdStr}`,
476
+ };
477
+ if (fmt === 'json') {
478
+ output(result, { format: 'json' });
479
+ }
480
+ else if (fmt === 'yaml') {
481
+ output(result, { format: 'yaml' });
482
+ }
483
+ else {
484
+ error(result.error);
485
+ }
486
+ process.exit(1);
487
+ }
488
+ sources.push({
489
+ id: constraintId,
490
+ hash: node.hash,
491
+ });
492
+ // Collect invariants for tracing
493
+ const constraint = node.data;
494
+ for (const inv of constraint.invariants) {
495
+ allInvariants.push(inv.id);
496
+ }
497
+ }
498
+ }
499
+ // Generate plan name from first spec
500
+ const firstSpecId = options.from[0];
501
+ const planName = firstSpecId.replace(/^spec:/, '').replace(/:v\d+$/, '');
502
+ const planId = `plan:${planName}:v1`;
503
+ // Create scaffold plan with placeholder steps
504
+ const plan = {
505
+ id: planId,
506
+ version: '1.0.0',
507
+ title: `Implementation plan for ${planName}`,
508
+ status: 'draft',
509
+ sources,
510
+ decisions: [],
511
+ steps: allRequirements.map((reqId, index) => ({
512
+ id: `step:${planName}:${index + 1}`,
513
+ number: index + 1,
514
+ action: `Implement ${reqId}`,
515
+ tracesTo: [reqId],
516
+ status: 'pending',
517
+ })),
518
+ };
519
+ // Compute hash
520
+ const hash = computeHash(plan);
521
+ const planWithHash = { ...plan, hash };
522
+ // Determine output file
523
+ let outputFile = options.output;
524
+ if (outputFile === undefined) {
525
+ const planDir = join(projectRoot, 'plan');
526
+ if (!existsSync(planDir)) {
527
+ mkdirSync(planDir, { recursive: true });
528
+ }
529
+ outputFile = join(planDir, `${planName}.plan.yaml`);
530
+ }
531
+ else {
532
+ outputFile = resolve(projectRoot, outputFile);
533
+ const dir = dirname(outputFile);
534
+ if (!existsSync(dir)) {
535
+ mkdirSync(dir, { recursive: true });
536
+ }
537
+ }
538
+ // Convert plan to YAML and write to file
539
+ const yamlContent = toYaml({
540
+ id: plan.id,
541
+ version: plan.version,
542
+ title: plan.title,
543
+ status: plan.status,
544
+ sources: plan.sources.map((s) => ({ id: s.id, hash: s.hash })),
545
+ decisions: plan.decisions.map((d) => ({
546
+ id: d.id,
547
+ question: d.question,
548
+ choice: d.choice,
549
+ rationale: d.rationale,
550
+ traces_to: d.tracesTo,
551
+ })),
552
+ steps: plan.steps.map((s) => ({
553
+ id: s.id,
554
+ number: s.number,
555
+ action: s.action,
556
+ ...(s.description !== undefined ? { description: s.description } : {}),
557
+ traces_to: s.tracesTo,
558
+ ...(s.dependencies !== undefined ? { dependencies: s.dependencies } : {}),
559
+ ...(s.artifacts !== undefined ? { artifacts: s.artifacts } : {}),
560
+ ...(s.status !== undefined ? { status: s.status } : {}),
561
+ })),
562
+ });
563
+ writeFileSync(outputFile, yamlContent, 'utf8');
564
+ // Add plan to graph
565
+ addPlanToGraph(graph, planWithHash);
566
+ // Save graph
567
+ const saveResult = saveGraph(graph, projectRoot);
568
+ if (!saveResult.success) {
569
+ const result = {
570
+ success: false,
571
+ error: saveResult.error ?? 'Failed to save graph',
572
+ };
573
+ if (fmt === 'json') {
574
+ output(result, { format: 'json' });
575
+ }
576
+ else if (fmt === 'yaml') {
577
+ output(result, { format: 'yaml' });
578
+ }
579
+ else {
580
+ error(result.error);
581
+ }
582
+ process.exit(1);
583
+ }
584
+ const result = {
585
+ success: true,
586
+ planId: plan.id,
587
+ hash,
588
+ stepCount: plan.steps.length,
589
+ decisionCount: plan.decisions.length,
590
+ outputFile: relative(projectRoot, outputFile),
591
+ };
592
+ if (fmt === 'json') {
593
+ output(result, { format: 'json' });
594
+ }
595
+ else if (fmt === 'yaml') {
596
+ output(result, { format: 'yaml' });
597
+ }
598
+ else {
599
+ success(`Created plan ${plan.id} (${plan.steps.length} steps)`);
600
+ process.stdout.write(` Output: ${relative(projectRoot, outputFile)}\n`);
601
+ }
602
+ }
603
+ // ============================================================================
604
+ // plan show Command
605
+ // ============================================================================
606
+ /**
607
+ * Execute the plan show command.
608
+ *
609
+ * @see req:cli:plan-show
610
+ */
611
+ export function runPlanShow(globalOpts, planIdStr, _options) {
612
+ const projectRoot = globalOpts.dir ?? process.cwd();
613
+ const fmt = resolveFormat({ format: globalOpts.format });
614
+ // Check if prov is initialized
615
+ if (!isInitialized(projectRoot)) {
616
+ const result = {
617
+ success: false,
618
+ error: 'prov is not initialized. Run "prov init" first.',
619
+ };
620
+ if (fmt === 'json') {
621
+ output(result, { format: 'json' });
622
+ }
623
+ else if (fmt === 'yaml') {
624
+ output(result, { format: 'yaml' });
625
+ }
626
+ else {
627
+ error(result.error);
628
+ }
629
+ process.exit(1);
630
+ }
631
+ // Load graph
632
+ const loadResult = loadGraph(projectRoot);
633
+ if (!loadResult.success || loadResult.data === undefined) {
634
+ const result = {
635
+ success: false,
636
+ error: loadResult.error ?? 'Failed to load graph',
637
+ };
638
+ if (fmt === 'json') {
639
+ output(result, { format: 'json' });
640
+ }
641
+ else if (fmt === 'yaml') {
642
+ output(result, { format: 'yaml' });
643
+ }
644
+ else {
645
+ error(result.error);
646
+ }
647
+ process.exit(1);
648
+ }
649
+ const graph = loadResult.data;
650
+ // Find the plan
651
+ const planId = planIdStr;
652
+ const node = graph.getNode(planId);
653
+ if (node === undefined || node.type !== 'plan') {
654
+ const result = {
655
+ success: false,
656
+ error: `Plan not found: ${planIdStr}`,
657
+ };
658
+ if (fmt === 'json') {
659
+ output(result, { format: 'json' });
660
+ }
661
+ else if (fmt === 'yaml') {
662
+ output(result, { format: 'yaml' });
663
+ }
664
+ else {
665
+ error(result.error);
666
+ }
667
+ process.exit(1);
668
+ }
669
+ const plan = node.data;
670
+ const planData = {
671
+ id: plan.id,
672
+ version: plan.version,
673
+ title: plan.title,
674
+ status: plan.status,
675
+ sources: [...plan.sources],
676
+ decisions: plan.decisions.map((d) => ({
677
+ id: d.id,
678
+ question: d.question,
679
+ choice: d.choice,
680
+ })),
681
+ steps: plan.steps.map((s) => ({
682
+ id: s.id,
683
+ number: s.number,
684
+ action: s.action,
685
+ status: s.status ?? 'pending',
686
+ tracesTo: [...s.tracesTo],
687
+ })),
688
+ };
689
+ if (node.hash !== undefined) {
690
+ planData.hash = node.hash;
691
+ }
692
+ const result = {
693
+ success: true,
694
+ plan: planData,
695
+ };
696
+ if (fmt === 'json') {
697
+ output(result, { format: 'json' });
698
+ }
699
+ else if (fmt === 'yaml') {
700
+ output(result, { format: 'yaml' });
701
+ }
702
+ else {
703
+ // Table format for terminal
704
+ process.stdout.write(`\n${plan.title}\n`);
705
+ process.stdout.write(`${'─'.repeat(plan.title.length)}\n`);
706
+ process.stdout.write(`ID: ${plan.id}\n`);
707
+ process.stdout.write(`Version: ${plan.version}\n`);
708
+ process.stdout.write(`Status: ${plan.status}\n`);
709
+ process.stdout.write(`Hash: ${node.hash ?? 'N/A'}\n`);
710
+ if (plan.sources.length > 0) {
711
+ process.stdout.write(`\nSources:\n`);
712
+ for (const source of plan.sources) {
713
+ process.stdout.write(` - ${source.id} (${source.hash})\n`);
714
+ }
715
+ }
716
+ if (plan.decisions.length > 0) {
717
+ process.stdout.write(`\nDecisions:\n`);
718
+ for (const dec of plan.decisions) {
719
+ process.stdout.write(` ${dec.id}:\n`);
720
+ process.stdout.write(` Q: ${dec.question}\n`);
721
+ process.stdout.write(` A: ${dec.choice}\n`);
722
+ }
723
+ }
724
+ if (plan.steps.length > 0) {
725
+ process.stdout.write(`\nSteps:\n`);
726
+ const columns = [
727
+ { header: '#', key: 'number', minWidth: 3, align: 'right' },
728
+ { header: 'Action', key: 'action', maxWidth: 50 },
729
+ { header: 'Status', key: 'status', minWidth: 10 },
730
+ { header: 'Traces To', key: 'tracesTo', maxWidth: 30 },
731
+ ];
732
+ const rows = plan.steps.map((s) => ({
733
+ number: s.number,
734
+ action: s.action,
735
+ status: s.status ?? 'pending',
736
+ tracesTo: s.tracesTo.join(', '),
737
+ }));
738
+ output(rows, { format: 'table', columns });
739
+ }
740
+ }
741
+ }
742
+ // ============================================================================
743
+ // plan validate Command
744
+ // ============================================================================
745
+ /**
746
+ * Execute the plan validate command.
747
+ *
748
+ * Validates plan completeness and consistency.
749
+ *
750
+ * @see req:cli:plan-validate
751
+ */
752
+ export function runPlanValidate(globalOpts, _options) {
753
+ const projectRoot = globalOpts.dir ?? process.cwd();
754
+ const fmt = resolveFormat({ format: globalOpts.format });
755
+ // Check if prov is initialized
756
+ if (!isInitialized(projectRoot)) {
757
+ const result = {
758
+ valid: false,
759
+ issues: [{ severity: 'error', message: 'prov is not initialized. Run "prov init" first.' }],
760
+ plansChecked: 0,
761
+ };
762
+ if (fmt === 'json') {
763
+ output(result, { format: 'json' });
764
+ }
765
+ else if (fmt === 'yaml') {
766
+ output(result, { format: 'yaml' });
767
+ }
768
+ else {
769
+ error(result.issues[0].message);
770
+ }
771
+ process.exit(1);
772
+ }
773
+ // Load graph
774
+ const loadResult = loadGraph(projectRoot);
775
+ if (!loadResult.success || loadResult.data === undefined) {
776
+ const result = {
777
+ valid: false,
778
+ issues: [{ severity: 'error', message: loadResult.error ?? 'Failed to load graph' }],
779
+ plansChecked: 0,
780
+ };
781
+ if (fmt === 'json') {
782
+ output(result, { format: 'json' });
783
+ }
784
+ else if (fmt === 'yaml') {
785
+ output(result, { format: 'yaml' });
786
+ }
787
+ else {
788
+ error(result.issues[0].message);
789
+ }
790
+ process.exit(1);
791
+ }
792
+ const graph = loadResult.data;
793
+ // Find all plan files and validate them
794
+ const planFiles = findPlanFiles(projectRoot);
795
+ const issues = [];
796
+ const validPlans = [];
797
+ // Validate each plan file
798
+ for (const filePath of planFiles) {
799
+ const relPath = relative(projectRoot, filePath);
800
+ const { plan, errors } = loadPlanFile(filePath);
801
+ if (errors.length > 0) {
802
+ for (const err of errors) {
803
+ issues.push({
804
+ severity: 'error',
805
+ message: `${relPath}: ${err}`,
806
+ });
807
+ }
808
+ continue;
809
+ }
810
+ if (plan !== undefined) {
811
+ validPlans.push(plan);
812
+ }
813
+ }
814
+ // Cross-validate plans against graph
815
+ const planNodes = graph.getNodesByType('plan');
816
+ const graphPlanIds = new Set(planNodes.map((n) => n.id));
817
+ const filePlanIds = new Set(validPlans.map((p) => p.id));
818
+ // Check for plans in graph but missing files
819
+ for (const node of planNodes) {
820
+ if (!filePlanIds.has(node.id)) {
821
+ issues.push({
822
+ severity: 'warning',
823
+ planId: node.id,
824
+ message: `Plan ${node.id} is in graph but has no corresponding file`,
825
+ });
826
+ }
827
+ }
828
+ // Check for plans in files but not in graph
829
+ for (const plan of validPlans) {
830
+ if (!graphPlanIds.has(plan.id)) {
831
+ issues.push({
832
+ severity: 'warning',
833
+ planId: plan.id,
834
+ message: `Plan ${plan.id} has a file but is not tracked in graph`,
835
+ });
836
+ }
837
+ }
838
+ // Validate plan completeness
839
+ for (const plan of validPlans) {
840
+ // Check that all sources exist in graph
841
+ for (const source of plan.sources) {
842
+ const sourceNode = graph.getNode(source.id);
843
+ if (sourceNode === undefined) {
844
+ issues.push({
845
+ severity: 'error',
846
+ planId: plan.id,
847
+ message: `Source ${source.id} not found in graph`,
848
+ });
849
+ }
850
+ else if (sourceNode.hash !== source.hash) {
851
+ issues.push({
852
+ severity: 'warning',
853
+ planId: plan.id,
854
+ message: `Source ${source.id} has changed since plan creation (hash mismatch)`,
855
+ });
856
+ }
857
+ }
858
+ // Check that all requirements are covered by steps
859
+ const coveredReqs = new Set();
860
+ for (const step of plan.steps) {
861
+ for (const target of step.tracesTo) {
862
+ if (target.startsWith('req:')) {
863
+ coveredReqs.add(target);
864
+ }
865
+ }
866
+ }
867
+ // Get all requirements from sources
868
+ for (const source of plan.sources) {
869
+ if (source.id.startsWith('spec:')) {
870
+ const specNode = graph.getNode(source.id);
871
+ if (specNode !== undefined && specNode.type === 'spec') {
872
+ const spec = specNode.data;
873
+ for (const req of spec.requirements) {
874
+ if (!coveredReqs.has(req.id)) {
875
+ issues.push({
876
+ severity: 'warning',
877
+ planId: plan.id,
878
+ message: `Requirement ${req.id} is not covered by any step`,
879
+ });
880
+ }
881
+ }
882
+ }
883
+ }
884
+ }
885
+ // Check step dependencies
886
+ const stepIds = new Set(plan.steps.map((s) => s.id));
887
+ for (const step of plan.steps) {
888
+ if (step.dependencies !== undefined) {
889
+ for (const dep of step.dependencies) {
890
+ if (!stepIds.has(dep)) {
891
+ issues.push({
892
+ severity: 'error',
893
+ planId: plan.id,
894
+ message: `Step ${step.id} depends on non-existent step ${dep}`,
895
+ });
896
+ }
897
+ }
898
+ }
899
+ }
900
+ }
901
+ // Sort issues by severity then plan
902
+ issues.sort((a, b) => {
903
+ if (a.severity !== b.severity) {
904
+ return a.severity === 'error' ? -1 : 1;
905
+ }
906
+ return (a.planId ?? '').localeCompare(b.planId ?? '');
907
+ });
908
+ const hasErrors = issues.some((i) => i.severity === 'error');
909
+ const result = {
910
+ valid: !hasErrors,
911
+ issues,
912
+ plansChecked: validPlans.length,
913
+ };
914
+ if (fmt === 'json') {
915
+ output(result, { format: 'json' });
916
+ }
917
+ else if (fmt === 'yaml') {
918
+ output(result, { format: 'yaml' });
919
+ }
920
+ else {
921
+ if (issues.length === 0) {
922
+ success(`Validated ${validPlans.length} plan(s) - no issues found`);
923
+ return;
924
+ }
925
+ // Display issues
926
+ for (const issue of issues) {
927
+ const prefix = issue.severity === 'error' ? 'ERROR' : 'WARN';
928
+ const planPart = issue.planId !== undefined ? ` [${issue.planId}]` : '';
929
+ process.stdout.write(`${prefix}${planPart}: ${issue.message}\n`);
930
+ }
931
+ process.stdout.write('\n');
932
+ const errorCount = issues.filter((i) => i.severity === 'error').length;
933
+ const warnCount = issues.filter((i) => i.severity === 'warning').length;
934
+ if (errorCount > 0) {
935
+ error(`Found ${errorCount} error(s) and ${warnCount} warning(s)`);
936
+ }
937
+ else {
938
+ warn(`Found ${warnCount} warning(s)`);
939
+ }
940
+ }
941
+ if (hasErrors) {
942
+ process.exit(1);
943
+ }
944
+ }
945
+ /**
946
+ * Execute the plan next command.
947
+ *
948
+ * Returns the next unimplemented step in the plan.
949
+ *
950
+ * @see req:agent:step-execution
951
+ */
952
+ export function runPlanNext(globalOpts, planIdStr, _options) {
953
+ const projectRoot = globalOpts.dir ?? process.cwd();
954
+ const fmt = resolveFormat({ format: globalOpts.format });
955
+ // Check if prov is initialized
956
+ if (!isInitialized(projectRoot)) {
957
+ const result = {
958
+ success: false,
959
+ error: 'prov is not initialized. Run "prov init" first.',
960
+ };
961
+ if (fmt === 'json') {
962
+ output(result, { format: 'json' });
963
+ }
964
+ else if (fmt === 'yaml') {
965
+ output(result, { format: 'yaml' });
966
+ }
967
+ else {
968
+ error(result.error);
969
+ }
970
+ process.exit(1);
971
+ }
972
+ // Load graph
973
+ const loadResult = loadGraph(projectRoot);
974
+ if (!loadResult.success || loadResult.data === undefined) {
975
+ const result = {
976
+ success: false,
977
+ error: loadResult.error ?? 'Failed to load graph',
978
+ };
979
+ if (fmt === 'json') {
980
+ output(result, { format: 'json' });
981
+ }
982
+ else if (fmt === 'yaml') {
983
+ output(result, { format: 'yaml' });
984
+ }
985
+ else {
986
+ error(result.error);
987
+ }
988
+ process.exit(1);
989
+ }
990
+ const graph = loadResult.data;
991
+ // Find the plan
992
+ const planId = planIdStr;
993
+ const node = graph.getNode(planId);
994
+ if (node === undefined || node.type !== 'plan') {
995
+ const result = {
996
+ success: false,
997
+ error: `Plan not found: ${planIdStr}`,
998
+ };
999
+ if (fmt === 'json') {
1000
+ output(result, { format: 'json' });
1001
+ }
1002
+ else if (fmt === 'yaml') {
1003
+ output(result, { format: 'yaml' });
1004
+ }
1005
+ else {
1006
+ error(result.error);
1007
+ }
1008
+ process.exit(1);
1009
+ }
1010
+ const plan = node.data;
1011
+ // Find next step (first non-completed step that's not blocked)
1012
+ const completedSteps = new Set(plan.steps.filter((s) => s.status === 'completed').map((s) => s.id));
1013
+ let nextStep;
1014
+ for (const step of plan.steps) {
1015
+ if (step.status === 'completed')
1016
+ continue;
1017
+ if (step.status === 'blocked')
1018
+ continue;
1019
+ // Check dependencies
1020
+ if (step.dependencies !== undefined) {
1021
+ const hasUnmetDeps = step.dependencies.some((depId) => !completedSteps.has(depId));
1022
+ if (hasUnmetDeps)
1023
+ continue;
1024
+ }
1025
+ // This is the next available step
1026
+ nextStep = step;
1027
+ break;
1028
+ }
1029
+ if (nextStep === undefined) {
1030
+ const result = {
1031
+ success: true,
1032
+ planId,
1033
+ message: 'All steps completed or blocked',
1034
+ };
1035
+ if (fmt === 'json') {
1036
+ output(result, { format: 'json' });
1037
+ }
1038
+ else if (fmt === 'yaml') {
1039
+ output(result, { format: 'yaml' });
1040
+ }
1041
+ else {
1042
+ success('All steps are completed or blocked');
1043
+ }
1044
+ return;
1045
+ }
1046
+ const stepEntry = {
1047
+ id: nextStep.id,
1048
+ number: nextStep.number,
1049
+ action: nextStep.action,
1050
+ status: nextStep.status ?? 'pending',
1051
+ tracesTo: [...nextStep.tracesTo],
1052
+ };
1053
+ const result = {
1054
+ success: true,
1055
+ planId,
1056
+ step: stepEntry,
1057
+ };
1058
+ if (fmt === 'json') {
1059
+ output(result, { format: 'json' });
1060
+ }
1061
+ else if (fmt === 'yaml') {
1062
+ output(result, { format: 'yaml' });
1063
+ }
1064
+ else {
1065
+ process.stdout.write(`\nNext step for ${planId}\n`);
1066
+ process.stdout.write(`${'─'.repeat(40)}\n`);
1067
+ process.stdout.write(`Step ${nextStep.number}: ${nextStep.action}\n`);
1068
+ if (nextStep.description !== undefined) {
1069
+ process.stdout.write(`\n${nextStep.description}\n`);
1070
+ }
1071
+ process.stdout.write(`\nTraces to: ${nextStep.tracesTo.join(', ')}\n`);
1072
+ process.stdout.write('\n');
1073
+ }
1074
+ }
1075
+ /**
1076
+ * Execute the plan remaining command.
1077
+ *
1078
+ * Lists all unimplemented steps in the plan.
1079
+ *
1080
+ * @see req:cli:plan-remaining
1081
+ */
1082
+ export function runPlanRemaining(globalOpts, planIdStr, _options) {
1083
+ const projectRoot = globalOpts.dir ?? process.cwd();
1084
+ const fmt = resolveFormat({ format: globalOpts.format });
1085
+ // Check if prov is initialized
1086
+ if (!isInitialized(projectRoot)) {
1087
+ const result = {
1088
+ success: false,
1089
+ error: 'prov is not initialized. Run "prov init" first.',
1090
+ };
1091
+ if (fmt === 'json') {
1092
+ output(result, { format: 'json' });
1093
+ }
1094
+ else if (fmt === 'yaml') {
1095
+ output(result, { format: 'yaml' });
1096
+ }
1097
+ else {
1098
+ error(result.error);
1099
+ }
1100
+ process.exit(1);
1101
+ }
1102
+ // Load graph
1103
+ const loadResult = loadGraph(projectRoot);
1104
+ if (!loadResult.success || loadResult.data === undefined) {
1105
+ const result = {
1106
+ success: false,
1107
+ error: loadResult.error ?? 'Failed to load graph',
1108
+ };
1109
+ if (fmt === 'json') {
1110
+ output(result, { format: 'json' });
1111
+ }
1112
+ else if (fmt === 'yaml') {
1113
+ output(result, { format: 'yaml' });
1114
+ }
1115
+ else {
1116
+ error(result.error);
1117
+ }
1118
+ process.exit(1);
1119
+ }
1120
+ const graph = loadResult.data;
1121
+ // Find the plan
1122
+ const planId = planIdStr;
1123
+ const node = graph.getNode(planId);
1124
+ if (node === undefined || node.type !== 'plan') {
1125
+ const result = {
1126
+ success: false,
1127
+ error: `Plan not found: ${planIdStr}`,
1128
+ };
1129
+ if (fmt === 'json') {
1130
+ output(result, { format: 'json' });
1131
+ }
1132
+ else if (fmt === 'yaml') {
1133
+ output(result, { format: 'yaml' });
1134
+ }
1135
+ else {
1136
+ error(result.error);
1137
+ }
1138
+ process.exit(1);
1139
+ }
1140
+ const plan = node.data;
1141
+ // Filter to non-completed steps
1142
+ const remainingSteps = plan.steps
1143
+ .filter((s) => s.status !== 'completed')
1144
+ .map((s) => ({
1145
+ id: s.id,
1146
+ number: s.number,
1147
+ action: s.action,
1148
+ status: s.status ?? 'pending',
1149
+ tracesTo: [...s.tracesTo],
1150
+ }));
1151
+ const completedCount = plan.steps.filter((s) => s.status === 'completed').length;
1152
+ const result = {
1153
+ success: true,
1154
+ planId,
1155
+ remainingSteps,
1156
+ totalSteps: plan.steps.length,
1157
+ completedSteps: completedCount,
1158
+ };
1159
+ if (fmt === 'json') {
1160
+ output(result, { format: 'json' });
1161
+ }
1162
+ else if (fmt === 'yaml') {
1163
+ output(result, { format: 'yaml' });
1164
+ }
1165
+ else {
1166
+ process.stdout.write(`\nRemaining steps for ${planId}\n`);
1167
+ process.stdout.write(`${'─'.repeat(40)}\n`);
1168
+ process.stdout.write(`Progress: ${completedCount}/${plan.steps.length} completed\n\n`);
1169
+ if (remainingSteps.length === 0) {
1170
+ success('All steps completed!');
1171
+ return;
1172
+ }
1173
+ const columns = [
1174
+ { header: '#', key: 'number', minWidth: 3, align: 'right' },
1175
+ { header: 'Action', key: 'action', maxWidth: 50 },
1176
+ { header: 'Status', key: 'status', minWidth: 12 },
1177
+ ];
1178
+ const rows = remainingSteps.map((s) => ({
1179
+ number: s.number,
1180
+ action: s.action,
1181
+ status: s.status,
1182
+ }));
1183
+ output(rows, { format: 'table', columns });
1184
+ process.stdout.write('\n');
1185
+ }
1186
+ }
1187
+ /**
1188
+ * Execute the plan progress command.
1189
+ *
1190
+ * Shows completion percentage and breakdown for a plan.
1191
+ *
1192
+ * @see req:agent:step-execution
1193
+ */
1194
+ export function runPlanProgress(globalOpts, planIdStr, _options) {
1195
+ const projectRoot = globalOpts.dir ?? process.cwd();
1196
+ const fmt = resolveFormat({ format: globalOpts.format });
1197
+ // Check if prov is initialized
1198
+ if (!isInitialized(projectRoot)) {
1199
+ const result = {
1200
+ success: false,
1201
+ error: 'prov is not initialized. Run "prov init" first.',
1202
+ };
1203
+ if (fmt === 'json') {
1204
+ output(result, { format: 'json' });
1205
+ }
1206
+ else if (fmt === 'yaml') {
1207
+ output(result, { format: 'yaml' });
1208
+ }
1209
+ else {
1210
+ error(result.error);
1211
+ }
1212
+ process.exit(1);
1213
+ }
1214
+ // Load graph
1215
+ const loadResult = loadGraph(projectRoot);
1216
+ if (!loadResult.success || loadResult.data === undefined) {
1217
+ const result = {
1218
+ success: false,
1219
+ error: loadResult.error ?? 'Failed to load graph',
1220
+ };
1221
+ if (fmt === 'json') {
1222
+ output(result, { format: 'json' });
1223
+ }
1224
+ else if (fmt === 'yaml') {
1225
+ output(result, { format: 'yaml' });
1226
+ }
1227
+ else {
1228
+ error(result.error);
1229
+ }
1230
+ process.exit(1);
1231
+ }
1232
+ const graph = loadResult.data;
1233
+ // Find the plan
1234
+ const planId = planIdStr;
1235
+ const node = graph.getNode(planId);
1236
+ if (node === undefined || node.type !== 'plan') {
1237
+ const result = {
1238
+ success: false,
1239
+ error: `Plan not found: ${planIdStr}`,
1240
+ };
1241
+ if (fmt === 'json') {
1242
+ output(result, { format: 'json' });
1243
+ }
1244
+ else if (fmt === 'yaml') {
1245
+ output(result, { format: 'yaml' });
1246
+ }
1247
+ else {
1248
+ error(result.error);
1249
+ }
1250
+ process.exit(1);
1251
+ }
1252
+ const plan = node.data;
1253
+ // Count by status
1254
+ const breakdown = {
1255
+ pending: 0,
1256
+ inProgress: 0,
1257
+ completed: 0,
1258
+ blocked: 0,
1259
+ };
1260
+ for (const step of plan.steps) {
1261
+ const status = step.status ?? 'pending';
1262
+ if (status === 'pending')
1263
+ breakdown.pending++;
1264
+ else if (status === 'in_progress')
1265
+ breakdown.inProgress++;
1266
+ else if (status === 'completed')
1267
+ breakdown.completed++;
1268
+ else if (status === 'blocked')
1269
+ breakdown.blocked++;
1270
+ }
1271
+ const totalSteps = plan.steps.length;
1272
+ const percentComplete = totalSteps > 0 ? Math.round((breakdown.completed / totalSteps) * 100) : 100;
1273
+ const result = {
1274
+ success: true,
1275
+ planId,
1276
+ progress: {
1277
+ totalSteps,
1278
+ completedSteps: breakdown.completed,
1279
+ percentComplete,
1280
+ breakdown,
1281
+ },
1282
+ };
1283
+ if (fmt === 'json') {
1284
+ output(result, { format: 'json' });
1285
+ }
1286
+ else if (fmt === 'yaml') {
1287
+ output(result, { format: 'yaml' });
1288
+ }
1289
+ else {
1290
+ process.stdout.write(`\nProgress for ${planId}\n`);
1291
+ process.stdout.write(`${'─'.repeat(40)}\n\n`);
1292
+ // Progress bar
1293
+ const barWidth = 30;
1294
+ const filledWidth = Math.round((percentComplete / 100) * barWidth);
1295
+ const emptyWidth = barWidth - filledWidth;
1296
+ const bar = '\u2588'.repeat(filledWidth) + '\u2591'.repeat(emptyWidth);
1297
+ process.stdout.write(`[${bar}] ${percentComplete}%\n\n`);
1298
+ // Breakdown
1299
+ process.stdout.write(`Completed: ${breakdown.completed}\n`);
1300
+ process.stdout.write(`In Progress: ${breakdown.inProgress}\n`);
1301
+ process.stdout.write(`Pending: ${breakdown.pending}\n`);
1302
+ process.stdout.write(`Blocked: ${breakdown.blocked}\n`);
1303
+ process.stdout.write(`${'─'.repeat(20)}\n`);
1304
+ process.stdout.write(`Total: ${totalSteps}\n\n`);
1305
+ }
1306
+ }
1307
+ //# sourceMappingURL=plan.js.map