@agile-vibe-coding/avc 0.3.5 → 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.
@@ -169,12 +169,12 @@ export function CardDetailModal({ workItem, open, onOpenChange, onNavigate, onIt
169
169
  loadFullDetails();
170
170
  };
171
171
 
172
- // Load full details when modal opens
172
+ // Load full details when modal opens or work item status changes
173
173
  useEffect(() => {
174
174
  if (open && workItem) {
175
175
  loadFullDetails();
176
176
  }
177
- }, [open, workItem?.id]);
177
+ }, [open, workItem?.id, workItem?.status]);
178
178
 
179
179
  // Fall back to 'details' tab if no doc.md available
180
180
  useEffect(() => {
@@ -304,7 +304,7 @@ export function CardDetailModal({ workItem, open, onOpenChange, onNavigate, onIt
304
304
  <FileText className="w-4 h-4 mr-2" />
305
305
  Details
306
306
  </TabsTrigger>
307
- {fullDetails?.children && fullDetails.children.length > 0 && (
307
+ {fullDetails?.children && fullDetails.children.length > 0 && workItem.type !== 'task' && (
308
308
  <TabsTrigger value="children">
309
309
  <Users className="w-4 h-4 mr-2" />
310
310
  Children ({fullDetails.children.length})
@@ -355,7 +355,7 @@ export function CardDetailModal({ workItem, open, onOpenChange, onNavigate, onIt
355
355
  )}
356
356
  {/* Run button — tasks that are planned, ready, or failed */}
357
357
  {workItem.type === 'task' && (workItem.status === 'planned' || workItem.status === 'ready' || workItem.status === 'failed') && (
358
- <RunButton taskId={workItem.id} onStarted={loadFullDetails} />
358
+ <RunButton taskId={workItem.id} taskName={workItem.name} />
359
359
  )}
360
360
  <button
361
361
  onClick={() => setRefineOpen(true)}
@@ -482,33 +482,107 @@ export function CardDetailModal({ workItem, open, onOpenChange, onNavigate, onIt
482
482
  ? fullDetails?.features
483
483
  : fullDetails?.acceptance;
484
484
  if (!items || items.length === 0) return null;
485
+ const acStatus = fullDetails?.acceptanceStatus;
486
+ const hasPassed = acStatus?.some(s => s.passed);
485
487
  return (
486
488
  <div>
487
489
  <div className="flex items-center gap-2 text-sm font-semibold text-slate-700 mb-2">
488
490
  <ListChecks className="w-4 h-4" />
489
491
  <span>Acceptance Criteria</span>
490
- <span className="ml-auto flex items-center gap-1 text-xs font-normal text-slate-400">
491
- <Lock className="w-3 h-3" />
492
- updated by tests
493
- </span>
492
+ {hasPassed ? (
493
+ <span className="ml-auto text-xs font-normal text-green-600">verified by tests</span>
494
+ ) : (
495
+ <span className="ml-auto flex items-center gap-1 text-xs font-normal text-slate-400">
496
+ <Lock className="w-3 h-3" />
497
+ updated by tests
498
+ </span>
499
+ )}
494
500
  </div>
495
501
  <ul className="space-y-1.5">
496
- {items.map((ac, idx) => (
497
- <li key={idx} className="flex items-start gap-2.5 text-sm text-slate-700">
498
- <input
499
- type="checkbox"
500
- readOnly
501
- disabled
502
- className="mt-0.5 h-4 w-4 flex-shrink-0 rounded border-slate-300 text-indigo-600 cursor-not-allowed opacity-60"
503
- />
504
- <span className="leading-snug">{ac}</span>
505
- </li>
506
- ))}
502
+ {items.map((ac, idx) => {
503
+ const passed = acStatus?.[idx]?.passed;
504
+ return (
505
+ <li key={idx} className="flex items-start gap-2.5 text-sm text-slate-700">
506
+ <input
507
+ type="checkbox"
508
+ checked={!!passed}
509
+ readOnly
510
+ disabled
511
+ className={`mt-0.5 h-4 w-4 flex-shrink-0 rounded border-slate-300 cursor-not-allowed ${passed ? 'text-green-600 opacity-100' : 'text-indigo-600 opacity-60'}`}
512
+ />
513
+ <span className={`leading-snug ${passed ? 'text-green-700' : ''}`}>{ac}</span>
514
+ </li>
515
+ );
516
+ })}
507
517
  </ul>
508
518
  </div>
509
519
  );
510
520
  })()}
511
521
 
522
+ {/* Test Results (shown after Run ceremony for review) */}
523
+ {fullDetails?.testResults && (
524
+ <div>
525
+ <div className="flex items-center gap-2 text-sm font-semibold text-slate-700 mb-2">
526
+ <span>{fullDetails.testResults.passed ? '✅' : '❌'}</span>
527
+ <span>Test Results</span>
528
+ <span className={`ml-auto text-xs font-normal ${fullDetails.testResults.passed ? 'text-green-600' : 'text-red-600'}`}>
529
+ {fullDetails.testResults.passed ? 'All tests passed' : 'Tests failed'}
530
+ </span>
531
+ </div>
532
+ {fullDetails.testResults.command && (
533
+ <p className="text-xs text-slate-500 mb-1 font-mono">$ {fullDetails.testResults.command}</p>
534
+ )}
535
+ <pre className="bg-slate-900 text-slate-300 text-xs font-mono rounded-lg p-3 max-h-40 overflow-y-auto whitespace-pre-wrap">
536
+ {fullDetails.testResults.output || '(no output)'}
537
+ </pre>
538
+ </div>
539
+ )}
540
+
541
+ {/* Subtasks (inline checklist for task cards) */}
542
+ {workItem.type === 'task' && fullDetails?.children && fullDetails.children.length > 0 && (
543
+ <div>
544
+ <div className="flex items-center gap-2 text-sm font-semibold text-slate-700 mb-2">
545
+ <ListChecks className="w-4 h-4" />
546
+ <span>Subtasks</span>
547
+ <span className="ml-auto text-xs font-normal text-slate-400">
548
+ {fullDetails.children.filter(c => c.status === 'completed').length}/{fullDetails.children.length} done
549
+ </span>
550
+ </div>
551
+ <div className="space-y-2">
552
+ {fullDetails.children.map((child) => {
553
+ const done = child.status === 'completed';
554
+ const fullChild = allItems?.find((i) => i.id === child.id);
555
+ return (
556
+ <button
557
+ key={child.id}
558
+ onClick={() => fullChild && onItemClick?.(fullChild)}
559
+ className={`w-full text-left border rounded-lg p-3 transition-colors cursor-pointer ${done ? 'border-green-200 bg-green-50/50 hover:border-green-300' : 'border-slate-200 hover:border-blue-300 hover:bg-blue-50'}`}
560
+ >
561
+ <div className="flex items-start gap-2.5">
562
+ <input type="checkbox" checked={done} readOnly tabIndex={-1}
563
+ className={`mt-0.5 h-4 w-4 flex-shrink-0 rounded pointer-events-none ${done ? 'text-green-600 opacity-100' : 'text-slate-400 opacity-60'}`}
564
+ />
565
+ <div className="flex-1 min-w-0">
566
+ <p className={`text-sm font-medium ${done ? 'text-green-700' : 'text-slate-700'}`}>{child.name}</p>
567
+ {child.acceptance && child.acceptance.length > 0 && (
568
+ <ul className="mt-1 space-y-0.5">
569
+ {child.acceptance.map((ac, i) => (
570
+ <li key={i} className="flex items-start gap-1.5 text-xs text-slate-500">
571
+ <span className="flex-shrink-0 mt-0.5">{done ? '✓' : '·'}</span>
572
+ <span>{ac}</span>
573
+ </li>
574
+ ))}
575
+ </ul>
576
+ )}
577
+ </div>
578
+ </div>
579
+ </button>
580
+ );
581
+ })}
582
+ </div>
583
+ </div>
584
+ )}
585
+
512
586
  {/* Dependencies */}
513
587
  {fullDetails?.dependencies && fullDetails.dependencies.length > 0 && (
514
588
  <div>
@@ -527,8 +601,8 @@ export function CardDetailModal({ workItem, open, onOpenChange, onNavigate, onIt
527
601
  </div>
528
602
  </TabsContent>
529
603
 
530
- {/* Children Tab */}
531
- {fullDetails?.children && fullDetails.children.length > 0 && (
604
+ {/* Children Tab (epics/stories only — subtasks shown inline for tasks) */}
605
+ {fullDetails?.children && fullDetails.children.length > 0 && workItem.type !== 'task' && (
532
606
  <TabsContent value="children">
533
607
  <div className="space-y-2">
534
608
  {fullDetails.children.map((child) => {
@@ -1,162 +1,43 @@
1
- import { useState, useEffect, useRef } from 'react';
2
- import { Play, Loader2, AlertTriangle, X, ChevronDown, ChevronUp } from 'lucide-react';
3
- import { getDependencyStatus, startRunTask } from '../../lib/api';
4
- import { cn } from '../../lib/utils';
1
+ import { Play, Loader2 } from 'lucide-react';
2
+ import { useRunStore } from '../../store/runStore';
5
3
 
6
4
  /**
7
- * Run Button — runs a task in a git worktree (implement + test + commit).
8
- * Shows dependency check, and streaming progress log.
5
+ * Run Button — thin trigger that opens the RunModal.
6
+ * If a run is already active for this task, shows "View Run" instead.
9
7
  */
10
- export function RunButton({ taskId, onStarted }) {
11
- const [state, setState] = useState('idle'); // idle | checking | blocked | running | complete | error
12
- const [blockers, setBlockers] = useState([]);
13
- const [error, setError] = useState(null);
14
- const [progress, setProgress] = useState([]);
15
- const [showLog, setShowLog] = useState(false);
16
- const [processId, setProcessId] = useState(null);
17
- const logRef = useRef(null);
18
-
19
- useEffect(() => {
20
- if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight;
21
- }, [progress]);
22
-
23
- // Listen for WebSocket run-task events dispatched by App.jsx
24
- useEffect(() => {
25
- if (!processId) return;
26
-
27
- const handler = (event) => {
28
- const msg = event.detail;
29
- if (!msg || msg.processId !== processId) return;
30
-
31
- if (msg.type === 'run-task:progress') {
32
- setProgress(prev => [...prev, msg.message]);
33
- } else if (msg.type === 'run-task:complete') {
34
- setState('complete');
35
- setProgress(prev => [...prev, 'Task complete — code committed.']);
36
- onStarted?.();
37
- } else if (msg.type === 'run-task:error') {
38
- setState('error');
39
- setError(msg.error || 'Run failed');
40
- setProgress(prev => [...prev, `Error: ${msg.error}`]);
41
- }
42
- };
43
-
44
- window.addEventListener('avc-ws-message', handler);
45
- return () => window.removeEventListener('avc-ws-message', handler);
46
- }, [processId, onStarted]);
47
-
48
- const handleClick = async () => {
49
- if (state === 'checking' || state === 'running') return;
50
-
51
- setState('checking');
52
- setError(null);
53
- setBlockers([]);
54
- setProgress([]);
55
-
56
- try {
57
- const depStatus = await getDependencyStatus(taskId);
58
- if (!depStatus.ready) {
59
- setState('blocked');
60
- setBlockers(depStatus.blockers || []);
61
- return;
62
- }
63
-
64
- setState('running');
65
- setShowLog(true);
66
- setProgress(['Starting task implementation...']);
67
- const result = await startRunTask(taskId);
68
- setProcessId(result.processId);
69
- setProgress(prev => [...prev, `Process started: ${result.processId}`]);
70
- } catch (err) {
71
- setError(err.message);
72
- setState('error');
8
+ export function RunButton({ taskId, taskName }) {
9
+ const { sessions, openModal, reopenModal } = useRunStore();
10
+ const session = sessions[taskId];
11
+ const isActive = session && (session.status === 'running' || session.status === 'checking');
12
+
13
+ const handleClick = () => {
14
+ if (isActive) {
15
+ reopenModal(taskId);
16
+ } else {
17
+ openModal(taskId, taskName || taskId);
73
18
  }
74
19
  };
75
20
 
76
- const dismiss = () => {
77
- setState('idle');
78
- setBlockers([]);
79
- setError(null);
80
- setProgress([]);
81
- setShowLog(false);
82
- setProcessId(null);
83
- };
84
-
85
- if (state === 'blocked') {
86
- return (
87
- <div className="space-y-2">
88
- <div className="flex items-center gap-2 text-amber-700 text-sm bg-amber-50 border border-amber-200 rounded-lg px-3 py-2">
89
- <AlertTriangle className="w-4 h-4 flex-shrink-0" />
90
- <div>
91
- <div className="font-medium">Dependencies not met</div>
92
- <ul className="mt-1 text-xs space-y-0.5">
93
- {blockers.map((b) => (<li key={b.id}>{b.id}: {b.name} ({b.status})</li>))}
94
- </ul>
95
- </div>
96
- </div>
97
- <button onClick={dismiss} className="text-xs text-slate-500 hover:text-slate-700">Dismiss</button>
98
- </div>
99
- );
100
- }
101
-
102
- if ((state === 'running' || state === 'complete' || state === 'error') && showLog) {
103
- return (
104
- <div className="space-y-2">
105
- <div className="flex items-center justify-between">
106
- <div className="flex items-center gap-2 text-sm font-medium">
107
- {state === 'running' && <Loader2 className="w-4 h-4 animate-spin text-blue-600" />}
108
- {state === 'complete' && <Play className="w-4 h-4 text-green-600" />}
109
- {state === 'error' && <AlertTriangle className="w-4 h-4 text-red-600" />}
110
- <span className={cn(
111
- state === 'running' && 'text-blue-700',
112
- state === 'complete' && 'text-green-700',
113
- state === 'error' && 'text-red-700',
114
- )}>
115
- {state === 'running' ? 'Implementing...' : state === 'complete' ? 'Complete' : 'Failed'}
116
- </span>
117
- </div>
118
- <div className="flex items-center gap-1">
119
- <button onClick={() => setShowLog(!showLog)} className="p-1 text-slate-400 hover:text-slate-600">
120
- {showLog ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
121
- </button>
122
- {state !== 'running' && (
123
- <button onClick={dismiss} className="p-1 text-slate-400 hover:text-slate-600">
124
- <X className="w-3.5 h-3.5" />
125
- </button>
126
- )}
127
- </div>
128
- </div>
129
- {showLog && (
130
- <div ref={logRef} className="max-h-32 overflow-y-auto bg-slate-900 text-slate-300 text-xs font-mono rounded-md p-2 space-y-0.5">
131
- {progress.map((msg, i) => (
132
- <div key={i} className={cn(
133
- msg.startsWith('Error') && 'text-red-400',
134
- msg.includes('complete') && 'text-green-400',
135
- )}>{msg}</div>
136
- ))}
137
- {state === 'running' && <div className="text-slate-500 animate-pulse">...</div>}
138
- </div>
139
- )}
140
- </div>
141
- );
142
- }
143
-
144
21
  return (
145
- <div className="space-y-1">
146
- <button
147
- onClick={handleClick}
148
- disabled={state === 'checking'}
149
- className={cn(
150
- 'flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors',
151
- state === 'checking'
152
- ? 'bg-blue-100 text-blue-700 cursor-wait'
153
- : 'bg-blue-600 text-white hover:bg-blue-700'
154
- )}
155
- >
156
- {state === 'checking' ? <Loader2 className="w-4 h-4 animate-spin" /> : <Play className="w-4 h-4" />}
157
- {state === 'checking' ? 'Checking...' : 'Run'}
158
- </button>
159
- {error && <p className="text-xs text-red-600">{error}</p>}
160
- </div>
22
+ <button
23
+ onClick={handleClick}
24
+ className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
25
+ isActive
26
+ ? 'bg-blue-100 text-blue-700 hover:bg-blue-200'
27
+ : 'bg-blue-600 text-white hover:bg-blue-700'
28
+ }`}
29
+ >
30
+ {isActive ? (
31
+ <>
32
+ <Loader2 className="w-4 h-4 animate-spin" />
33
+ View Run
34
+ </>
35
+ ) : (
36
+ <>
37
+ <Play className="w-4 h-4" />
38
+ Run
39
+ </>
40
+ )}
41
+ </button>
161
42
  );
162
43
  }
@@ -3,6 +3,7 @@ import { useProcessStore } from '../../store/processStore';
3
3
  import { useSprintPlanningStore } from '../../store/sprintPlanningStore';
4
4
  import { useCeremonyStore } from '../../store/ceremonyStore';
5
5
  import { useSeedStore } from '../../store/seedStore';
6
+ import { useRunStore } from '../../store/runStore';
6
7
 
7
8
  const STATUS_PILL = {
8
9
  running: 'bg-blue-50 text-blue-700 border-blue-200 hover:bg-blue-100',
@@ -51,6 +52,9 @@ export function ProcessMonitorBar() {
51
52
  } else if (p.type === 'seed') {
52
53
  const storyId = useSeedStore.getState().getStoryIdByProcessId(p.id);
53
54
  if (storyId) useSeedStore.getState().reopenModal(storyId);
55
+ } else if (p.type === 'run-task') {
56
+ const taskId = useRunStore.getState().getTaskIdByProcessId(p.id);
57
+ if (taskId) useRunStore.getState().reopenModal(taskId);
54
58
  }
55
59
  };
56
60
 
@@ -95,6 +95,16 @@ export async function getWorkItem(id) {
95
95
  return apiFetch(`/work-items/${id}`);
96
96
  }
97
97
 
98
+ /**
99
+ * Get file content from a task's worktree
100
+ * @param {string} taskId - Task ID
101
+ * @param {string} filePath - Relative file path
102
+ * @returns {Promise<{path, content, size}>}
103
+ */
104
+ export async function getWorktreeFile(taskId, filePath) {
105
+ return apiFetch(`/work-items/${taskId}/worktree-file?path=${encodeURIComponent(filePath)}`);
106
+ }
107
+
98
108
  /**
99
109
  * Get rendered documentation (doc.md) as HTML
100
110
  * @param {string} id - Work item ID
@@ -14,7 +14,7 @@ export const useFilterStore = create(
14
14
  epic: true,
15
15
  story: true,
16
16
  task: true,
17
- subtask: true,
17
+ subtask: false,
18
18
  },
19
19
  columnVisibility: {
20
20
  Backlog: true,
@@ -174,7 +174,7 @@ export const useFilterStore = create(
174
174
  epic: true,
175
175
  story: true,
176
176
  task: true,
177
- subtask: true,
177
+ subtask: false,
178
178
  },
179
179
  columnVisibility: {
180
180
  Backlog: true,
@@ -190,12 +190,19 @@ export const useFilterStore = create(
190
190
  }),
191
191
  {
192
192
  name: 'avc-kanban-filters', // localStorage key
193
+ version: 2, // bump to reset persisted subtask:true → false
193
194
  partialize: (state) => ({
194
- // Only persist these fields
195
195
  typeFilters: state.typeFilters,
196
196
  columnVisibility: state.columnVisibility,
197
197
  groupBy: state.groupBy,
198
198
  }),
199
+ migrate: (persisted, version) => {
200
+ if (version < 2) {
201
+ // Force subtask filter to false on upgrade
202
+ return { ...persisted, typeFilters: { ...persisted.typeFilters, subtask: false } };
203
+ }
204
+ return persisted;
205
+ },
199
206
  }
200
207
  )
201
208
  );
@@ -0,0 +1,103 @@
1
+ import { create } from 'zustand';
2
+
3
+ /**
4
+ * Run Ceremony Store — manages multiple concurrent run sessions.
5
+ * Each session is keyed by taskId and tracks its own progress/status.
6
+ */
7
+ export const useRunStore = create((set, get) => ({
8
+ sessions: {}, // { [taskId]: { status, progressLog, processId, taskName, error, reviewInfo, startedAt } }
9
+ activeTaskId: null, // which run modal is showing
10
+ isOpen: false, // modal visibility
11
+
12
+ openModal: (taskId, taskName) => set((s) => ({
13
+ isOpen: true,
14
+ activeTaskId: taskId,
15
+ sessions: {
16
+ ...s.sessions,
17
+ [taskId]: s.sessions[taskId] || {
18
+ status: 'idle',
19
+ progressLog: [],
20
+ processId: null,
21
+ taskName: taskName || taskId,
22
+ error: null,
23
+ reviewInfo: null,
24
+ startedAt: null,
25
+ },
26
+ },
27
+ })),
28
+
29
+ closeModal: () => set({ isOpen: false }),
30
+
31
+ reopenModal: (taskId) => set({ isOpen: true, activeTaskId: taskId }),
32
+
33
+ setStatus: (taskId, status) => set((s) => {
34
+ const session = s.sessions[taskId];
35
+ if (!session) return {};
36
+ return {
37
+ sessions: {
38
+ ...s.sessions,
39
+ [taskId]: { ...session, status, ...(status === 'running' ? { startedAt: Date.now() } : {}) },
40
+ },
41
+ };
42
+ }),
43
+
44
+ appendProgress: (taskId, message) => set((s) => {
45
+ const session = s.sessions[taskId];
46
+ if (!session) return {};
47
+ return {
48
+ sessions: {
49
+ ...s.sessions,
50
+ [taskId]: { ...session, progressLog: [...session.progressLog, message] },
51
+ },
52
+ };
53
+ }),
54
+
55
+ setProcessId: (taskId, processId) => set((s) => {
56
+ const session = s.sessions[taskId];
57
+ if (!session) return {};
58
+ return {
59
+ sessions: {
60
+ ...s.sessions,
61
+ [taskId]: { ...session, processId },
62
+ },
63
+ };
64
+ }),
65
+
66
+ setError: (taskId, error) => set((s) => {
67
+ const session = s.sessions[taskId];
68
+ if (!session) return {};
69
+ return {
70
+ sessions: {
71
+ ...s.sessions,
72
+ [taskId]: { ...session, error, status: 'error' },
73
+ },
74
+ };
75
+ }),
76
+
77
+ setReviewInfo: (taskId, reviewInfo) => set((s) => {
78
+ const session = s.sessions[taskId];
79
+ if (!session) return {};
80
+ return {
81
+ sessions: {
82
+ ...s.sessions,
83
+ [taskId]: { ...session, reviewInfo },
84
+ },
85
+ };
86
+ }),
87
+
88
+ removeSession: (taskId) => set((s) => {
89
+ const { [taskId]: _, ...rest } = s.sessions;
90
+ return {
91
+ sessions: rest,
92
+ ...(s.activeTaskId === taskId ? { activeTaskId: null, isOpen: false } : {}),
93
+ };
94
+ }),
95
+
96
+ getTaskIdByProcessId: (processId) => {
97
+ const sessions = get().sessions;
98
+ for (const [taskId, session] of Object.entries(sessions)) {
99
+ if (session.processId === processId) return taskId;
100
+ }
101
+ return null;
102
+ },
103
+ }));
@@ -477,6 +477,40 @@ export function createWorkItemsRouter(dataStore, refineService, ceremonyService)
477
477
  }
478
478
  });
479
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
+
480
514
  return router;
481
515
  }
482
516
 
@@ -504,6 +538,9 @@ function cleanWorkItem(item, includeFullDetails = false) {
504
538
  userType: item.userType, // stories
505
539
  domain: item.domain, // epics
506
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
507
544
  };
508
545
 
509
546
  // Add parent reference (ID only, not full object)
@@ -519,6 +556,7 @@ function cleanWorkItem(item, includeFullDetails = false) {
519
556
  name: child.name,
520
557
  type: child._type,
521
558
  status: child.status,
559
+ acceptance: child.acceptance,
522
560
  }));
523
561
  }
524
562