@agile-vibe-coding/avc 0.3.4 → 0.3.5

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.
@@ -779,6 +779,37 @@ Return your response as JSON following the exact structure specified in your ins
779
779
  this.debug(`[INFO] Updated story doc.md after task extraction (${storyDocContent.length} bytes)`);
780
780
  }
781
781
 
782
+ // Compute ready status for tasks/subtasks with no pending dependencies
783
+ try {
784
+ const { checkDependenciesReady } = await import('./dependency-checker.js');
785
+ const lookup = {};
786
+ for (const task of hierarchy.tasks) {
787
+ lookup[task.id] = { id: task.id, name: task.name, type: 'task', status: 'planned', dependencies: task.dependencies || [] };
788
+ for (const sub of task.subtasks || []) {
789
+ lookup[sub.id] = { id: sub.id, name: sub.name, type: 'subtask', status: 'planned', dependencies: sub.dependencies || [] };
790
+ }
791
+ }
792
+
793
+ let readyCount = 0;
794
+ for (const id of Object.keys(lookup)) {
795
+ const result = checkDependenciesReady(id, lookup);
796
+ if (result.ready) {
797
+ const itemDir = path.join(this.projectPath, id);
798
+ const wjPath = path.join(itemDir, 'work.json');
799
+ if (fs.existsSync(wjPath)) {
800
+ const wj = JSON.parse(fs.readFileSync(wjPath, 'utf8'));
801
+ wj.status = 'ready';
802
+ fs.writeFileSync(wjPath, JSON.stringify(wj, null, 2), 'utf8');
803
+ lookup[id].status = 'ready';
804
+ readyCount++;
805
+ }
806
+ }
807
+ }
808
+ this.debug(`[INFO] Set ${readyCount} task/subtask items to 'ready' status (dependency-free)`);
809
+ } catch (err) {
810
+ this.debug(`[WARN] Ready status computation failed (non-critical): ${err.message}`);
811
+ }
812
+
782
813
  return { taskCount, subtaskCount, taskIds };
783
814
  }
784
815
 
@@ -141,16 +141,21 @@ export class WorktreeRunner {
141
141
  return { success: false, error: `Tests failed: ${testResult.summary}` };
142
142
  }
143
143
 
144
- // 5. Commit and merge
145
- progressCallback?.('Committing and merging to main...');
144
+ // 5. Commit in worktree (do NOT merge — leave for human review)
145
+ progressCallback?.('Committing changes in worktree...');
146
146
  if (cancelledCheck?.()) throw new Error('CANCELLED');
147
- this.commitAndMerge();
147
+ this.commitInWorktree();
148
148
 
149
- // 6. Cleanup
150
- progressCallback?.('Cleaning up worktree...');
151
- this.cleanup();
149
+ // 6. Leave worktree in place for review — do NOT cleanup or merge
150
+ progressCallback?.(`Code ready for review in worktree: ${this.worktreePath}`);
151
+ progressCallback?.(`Branch: ${this.branchName}`);
152
152
 
153
- return { success: true };
153
+ return {
154
+ success: true,
155
+ review: true,
156
+ worktreePath: this.worktreePath,
157
+ branchName: this.branchName,
158
+ };
154
159
  } catch (err) {
155
160
  // Always cleanup on failure
156
161
  try { this.cleanup(); } catch {}
@@ -583,28 +588,35 @@ export class WorktreeRunner {
583
588
  /**
584
589
  * Commit changes in the worktree and merge to the main branch.
585
590
  */
586
- commitAndMerge() {
587
- // Stage all changes
591
+ /**
592
+ * Commit all changes in the worktree (does NOT merge to main).
593
+ * Called after tests pass — the worktree stays in place for human review.
594
+ */
595
+ commitInWorktree() {
588
596
  git(['add', '-A'], this.worktreePath);
589
597
 
590
- // Check if there's anything to commit
591
598
  const status = git(['status', '--porcelain'], this.worktreePath);
592
599
  if (!status) {
593
600
  this.debug('No changes to commit');
594
601
  return;
595
602
  }
596
603
 
597
- // Commit
598
604
  const commitMsg = `feat(${this.taskId}): implement task\n\nGenerated by AVC WorktreeRunner`;
599
605
  git(['commit', '-m', commitMsg], this.worktreePath);
600
606
  this.debug('Committed in worktree');
607
+ }
601
608
 
609
+ /**
610
+ * Merge the worktree branch into main and clean up.
611
+ * Called AFTER human review approves the code (status: implemented → completed).
612
+ * Can be invoked via API or CLI.
613
+ */
614
+ mergeAndCleanup() {
602
615
  // Determine the main branch name
603
616
  let mainBranch = 'main';
604
617
  try {
605
618
  mainBranch = git(['symbolic-ref', '--short', 'HEAD'], this.projectRoot);
606
619
  } catch {
607
- // If HEAD is detached, try common branch names
608
620
  try { git(['rev-parse', '--verify', 'main'], this.projectRoot); mainBranch = 'main'; } catch {
609
621
  try { git(['rev-parse', '--verify', 'master'], this.projectRoot); mainBranch = 'master'; } catch {}
610
622
  }
@@ -615,10 +627,12 @@ export class WorktreeRunner {
615
627
  git(['merge', '--no-ff', this.branchName, '-m', `Merge ${this.branchName}: implement ${this.taskId}`], this.projectRoot);
616
628
  this.debug('Merged to main', { mainBranch });
617
629
  } catch (err) {
618
- // Merge conflict — abort and report
619
630
  try { git(['merge', '--abort'], this.projectRoot); } catch {}
620
631
  throw new Error(`Merge conflict: ${err.message}`);
621
632
  }
633
+
634
+ // Cleanup worktree and branch
635
+ this.cleanup();
622
636
  }
623
637
 
624
638
  /**
@@ -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);
@@ -85,11 +85,18 @@ async function run(taskId) {
85
85
  updateStatus(taskId, 'planned'); // Reset on cancel
86
86
  try { process.send({ type: 'cancelled' }); } catch {}
87
87
  } else if (result.success) {
88
- updateStatus(taskId, 'completed');
88
+ // Tests passed — move to review (not completed). Human must approve before merge.
89
+ updateStatus(taskId, 'implemented');
89
90
  try {
90
91
  process.send({
91
92
  type: 'complete',
92
- result: { success: true, taskId },
93
+ result: {
94
+ success: true,
95
+ taskId,
96
+ review: true,
97
+ worktreePath: result.worktreePath,
98
+ branchName: result.branchName,
99
+ },
93
100
  });
94
101
  } catch {}
95
102
  } 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.3.5",
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",