@agile-vibe-coding/avc 0.3.4 → 0.4.0

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.
@@ -289,8 +289,9 @@ export function createWorkItemsRouter(dataStore, refineService, ceremonyService)
289
289
  const VALID_TRANSITIONS = {
290
290
  'planned': ['ready', 'implementing'],
291
291
  'ready': ['implementing'],
292
- 'implementing': ['completed', 'failed'],
293
- 'failed': ['implementing'],
292
+ 'implementing': ['implemented', 'completed', 'failed'],
293
+ 'implemented': ['completed', 'failed'], // review → approve (merge) or reject
294
+ 'failed': ['implementing', 'ready'],
294
295
  'completed': [],
295
296
  };
296
297
 
@@ -328,6 +329,20 @@ export function createWorkItemsRouter(dataStore, refineService, ceremonyService)
328
329
  fsSync.writeFileSync(workJsonPath, JSON.stringify(workJson, null, 2), 'utf8');
329
330
  }
330
331
 
332
+ // When a task moves from 'implemented' to 'completed' (review approved),
333
+ // merge its worktree branch into main and clean up.
334
+ if (status === 'completed' && currentStatus === 'implemented' && (item._type === 'task' || item.type === 'task')) {
335
+ try {
336
+ const { WorktreeRunner } = await import('../../../cli/worktree-runner.js');
337
+ const runner = new WorktreeRunner(req.params.id, path.resolve(item._dirPath, '..', '..', '..', '..'));
338
+ runner.mergeAndCleanup();
339
+ console.log(`[review-merge] Merged worktree for ${req.params.id}`);
340
+ } catch (mergeErr) {
341
+ console.error(`[review-merge] Failed to merge ${req.params.id}: ${mergeErr.message}`);
342
+ // Non-fatal — the status is already set to completed
343
+ }
344
+ }
345
+
331
346
  // Reload hierarchy so in-memory state reflects the change
332
347
  await dataStore.reload();
333
348
 
@@ -376,6 +391,52 @@ export function createWorkItemsRouter(dataStore, refineService, ceremonyService)
376
391
  }
377
392
  }
378
393
 
394
+ // Upward cascade: propagate status to parent items
395
+ try {
396
+ const { items: freshItems } = dataStore.getHierarchy();
397
+ const updatedItem = freshItems.get(req.params.id);
398
+ let parentId = updatedItem?._parentId || null;
399
+ let needsReload = false;
400
+
401
+ while (parentId) {
402
+ const parent = freshItems.get(parentId);
403
+ if (!parent) break;
404
+
405
+ const children = [...freshItems.values()].filter(c => c._parentId === parentId);
406
+ if (children.length === 0) break;
407
+
408
+ let newParentStatus = null;
409
+ const allCompleted = children.every(c => c.status === 'completed');
410
+ const anyImplementing = children.some(c =>
411
+ c.status === 'implementing' || c.status === 'completed'
412
+ );
413
+
414
+ if (allCompleted && parent.status !== 'completed') {
415
+ newParentStatus = 'completed';
416
+ } else if (anyImplementing && parent.status !== 'implementing' && parent.status !== 'completed') {
417
+ newParentStatus = 'implementing';
418
+ }
419
+
420
+ if (newParentStatus) {
421
+ const parentWjPath = path.join(parent._dirPath, 'work.json');
422
+ if (fsSync.existsSync(parentWjPath)) {
423
+ const wj = JSON.parse(fsSync.readFileSync(parentWjPath, 'utf8'));
424
+ wj.status = newParentStatus;
425
+ wj.updated = new Date().toISOString();
426
+ fsSync.writeFileSync(parentWjPath, JSON.stringify(wj, null, 2), 'utf8');
427
+ needsReload = true;
428
+ console.log(`[status-cascade-up] ${parentId}: ${parent.status} → ${newParentStatus}`);
429
+ }
430
+ }
431
+
432
+ parentId = parent._parentId || null;
433
+ }
434
+
435
+ if (needsReload) await dataStore.reload();
436
+ } catch (upErr) {
437
+ console.error('Upward status cascade error (non-critical):', upErr.message);
438
+ }
439
+
379
440
  res.json({ status: 'ok', item: cleanWorkItem(item) });
380
441
  } catch (error) {
381
442
  console.error(`Error updating status for ${req.params.id}:`, error);
@@ -416,6 +477,40 @@ export function createWorkItemsRouter(dataStore, refineService, ceremonyService)
416
477
  }
417
478
  });
418
479
 
480
+ /**
481
+ * GET /api/work-items/:id/worktree-file?path=relative/path
482
+ * Read a file from the task's worktree (for reviewing generated code).
483
+ */
484
+ router.get('/:id/worktree-file', async (req, res) => {
485
+ try {
486
+ const filePath = req.query.path;
487
+ if (!filePath) return res.status(400).json({ error: 'path query parameter required' });
488
+
489
+ const projectRoot = dataStore.projectRoot;
490
+ const worktreePath = path.join(projectRoot, '.avc', 'worktrees', req.params.id);
491
+
492
+ if (!fsSync.existsSync(worktreePath)) {
493
+ return res.status(404).json({ error: 'Worktree not found — task may not have been run yet' });
494
+ }
495
+
496
+ // Safety: resolve and ensure path stays within worktree
497
+ const resolved = path.resolve(worktreePath, filePath);
498
+ if (!resolved.startsWith(path.resolve(worktreePath) + path.sep) && resolved !== path.resolve(worktreePath)) {
499
+ return res.status(403).json({ error: 'Path escapes worktree boundary' });
500
+ }
501
+
502
+ if (!fsSync.existsSync(resolved)) {
503
+ return res.status(404).json({ error: `File not found: ${filePath}` });
504
+ }
505
+
506
+ const content = fsSync.readFileSync(resolved, 'utf8');
507
+ res.json({ path: filePath, content, size: content.length });
508
+ } catch (error) {
509
+ console.error(`Error reading worktree file for ${req.params.id}:`, error);
510
+ res.status(500).json({ error: 'Failed to read file' });
511
+ }
512
+ });
513
+
419
514
  return router;
420
515
  }
421
516
 
@@ -443,6 +538,9 @@ function cleanWorkItem(item, includeFullDetails = false) {
443
538
  userType: item.userType, // stories
444
539
  domain: item.domain, // epics
445
540
  functions: item.functions, // code traceability registry
541
+ files: item.files, // files created/edited/deleted by Run ceremony
542
+ testResults: item.testResults, // test output from Run ceremony
543
+ acceptanceStatus: item.acceptanceStatus, // per-AC pass/fail from Run ceremony
446
544
  };
447
545
 
448
546
  // Add parent reference (ID only, not full object)
@@ -458,6 +556,7 @@ function cleanWorkItem(item, includeFullDetails = false) {
458
556
  name: child.name,
459
557
  type: child._type,
460
558
  status: child.status,
559
+ acceptance: child.acceptance,
461
560
  }));
462
561
  }
463
562
 
@@ -38,16 +38,22 @@ process.on('message', async (msg) => {
38
38
  */
39
39
  function updateStatus(taskId, status) {
40
40
  const projectRoot = process.cwd();
41
- // Find the work.json by walking the hierarchy
42
- // Task ID: context-0001-0001-0001 → path: .avc/project/context-0001/context-0001-0001/context-0001-0001-0001/work.json
43
- const idParts = taskId.replace('context-', '').split('-');
44
- let dir = join(projectRoot, '.avc', 'project');
45
- let current = 'context';
46
- for (const part of idParts) {
47
- current += `-${part}`;
48
- dir = join(dir, current);
41
+ const projectDir = join(projectRoot, '.avc', 'project');
42
+
43
+ // Try flat path first (tasks/subtasks written by seed-processor are flat)
44
+ let workJsonPath = join(projectDir, taskId, 'work.json');
45
+
46
+ // Fall back to nested path (epics/stories are nested)
47
+ if (!existsSync(workJsonPath)) {
48
+ const idParts = taskId.replace('context-', '').split('-');
49
+ let dir = projectDir;
50
+ let current = 'context';
51
+ for (const part of idParts) {
52
+ current += `-${part}`;
53
+ dir = join(dir, current);
54
+ }
55
+ workJsonPath = join(dir, 'work.json');
49
56
  }
50
- const workJsonPath = join(dir, 'work.json');
51
57
 
52
58
  if (existsSync(workJsonPath)) {
53
59
  try {
@@ -59,6 +65,39 @@ function updateStatus(taskId, status) {
59
65
  }
60
66
  }
61
67
 
68
+ /**
69
+ * Auto-complete all subtasks of a task.
70
+ * Subtasks are documentation artifacts — the task's agent loop implements everything.
71
+ */
72
+ function autoCompleteSubtasks(taskId) {
73
+ const projectDir = join(process.cwd(), '.avc', 'project');
74
+
75
+ // Read the task's work.json to get children
76
+ const taskWorkJsonPath = join(projectDir, taskId, 'work.json');
77
+ if (!existsSync(taskWorkJsonPath)) return;
78
+
79
+ try {
80
+ const taskWork = JSON.parse(readFileSync(taskWorkJsonPath, 'utf8'));
81
+ const children = taskWork.children || [];
82
+
83
+ for (const childId of children) {
84
+ // Try flat path first
85
+ let childPath = join(projectDir, childId, 'work.json');
86
+ if (!existsSync(childPath)) continue;
87
+
88
+ try {
89
+ const childWork = JSON.parse(readFileSync(childPath, 'utf8'));
90
+ if (childWork.type === 'subtask' && childWork.status !== 'completed') {
91
+ childWork.status = 'completed';
92
+ childWork.updated = new Date().toISOString();
93
+ childWork.metadata = { ...childWork.metadata, completedBy: taskId };
94
+ writeFileSync(childPath, JSON.stringify(childWork, null, 2), 'utf8');
95
+ }
96
+ } catch {}
97
+ }
98
+ } catch {}
99
+ }
100
+
62
101
  async function run(taskId) {
63
102
  const logger = new CommandLogger('run-task', process.cwd());
64
103
  logger.start();
@@ -85,11 +124,21 @@ async function run(taskId) {
85
124
  updateStatus(taskId, 'planned'); // Reset on cancel
86
125
  try { process.send({ type: 'cancelled' }); } catch {}
87
126
  } else if (result.success) {
88
- updateStatus(taskId, 'completed');
127
+ // Tests passed — move to review (not completed). Human must approve before merge.
128
+ updateStatus(taskId, 'implemented');
129
+ // Auto-complete subtasks — they are documentation artifacts, not separate implementation units.
130
+ // The task's agent loop implements everything including subtask ACs.
131
+ autoCompleteSubtasks(taskId);
89
132
  try {
90
133
  process.send({
91
134
  type: 'complete',
92
- result: { success: true, taskId },
135
+ result: {
136
+ success: true,
137
+ taskId,
138
+ review: true,
139
+ worktreePath: result.worktreePath,
140
+ branchName: result.branchName,
141
+ },
93
142
  });
94
143
  } catch {}
95
144
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agile-vibe-coding/avc",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Agile Vibe Coding (AVC) - Framework for managing AI agent-based software development projects",
5
5
  "type": "module",
6
6
  "main": "cli/index.js",