@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.
- package/README.md +86 -12
- package/cli/agents/code-implementer.md +33 -46
- package/cli/init.js +4 -3
- package/cli/llm-claude.js +72 -0
- package/cli/llm-gemini.js +76 -0
- package/cli/llm-local.js +52 -0
- package/cli/llm-openai.js +52 -0
- package/cli/llm-provider.js +12 -0
- package/cli/llm-xiaomi.js +51 -0
- package/cli/seed-processor.js +31 -0
- package/cli/worktree-runner.js +268 -26
- package/cli/worktree-tools.js +322 -0
- package/kanban/client/dist/assets/index-BSm2Zo5j.js +380 -0
- package/kanban/client/dist/assets/index-BevZLADh.css +1 -0
- package/kanban/client/dist/index.html +2 -2
- package/kanban/client/src/App.jsx +37 -5
- package/kanban/client/src/components/ceremony/RunModal.jsx +329 -0
- package/kanban/client/src/components/ceremony/SeedModal.jsx +2 -2
- package/kanban/client/src/components/kanban/CardDetailModal.jsx +95 -21
- package/kanban/client/src/components/kanban/RunButton.jsx +34 -153
- package/kanban/client/src/components/process/ProcessMonitorBar.jsx +4 -0
- package/kanban/client/src/lib/api.js +10 -0
- package/kanban/client/src/store/filterStore.js +10 -3
- package/kanban/client/src/store/runStore.js +103 -0
- package/kanban/server/routes/work-items.js +101 -2
- package/kanban/server/workers/run-task-worker.js +60 -11
- package/package.json +1 -1
- package/kanban/client/dist/assets/index-BfLDUxPS.js +0 -353
- package/kanban/client/dist/assets/index-C7W_e4ik.css +0 -1
|
@@ -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
|
-
'
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
let
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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: {
|
|
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