agentic-orchestrator 0.1.25 → 0.1.26

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 (177) hide show
  1. package/agentic/orchestrator/defaults/policy.defaults.yaml +1 -0
  2. package/agentic/orchestrator/policy.yaml +1 -0
  3. package/agentic/orchestrator/schemas/policy.schema.json +4 -0
  4. package/agentic/orchestrator/schemas/policy.user.schema.json +4 -0
  5. package/apps/control-plane/src/application/services/activity-monitor-service.ts +42 -0
  6. package/apps/control-plane/src/application/services/collision-queue-service.ts +48 -0
  7. package/apps/control-plane/src/application/services/cost-tracking-service.ts +80 -0
  8. package/apps/control-plane/src/application/services/feature-deletion-service.ts +57 -0
  9. package/apps/control-plane/src/application/services/feature-lifecycle-service.ts +65 -0
  10. package/apps/control-plane/src/application/services/feature-state-service.ts +87 -0
  11. package/apps/control-plane/src/application/services/gate-selection-service.ts +28 -0
  12. package/apps/control-plane/src/application/services/gate-service.ts +148 -0
  13. package/apps/control-plane/src/application/services/issue-tracker-service.ts +28 -0
  14. package/apps/control-plane/src/application/services/lock-service.ts +88 -0
  15. package/apps/control-plane/src/application/services/merge-service.ts +63 -0
  16. package/apps/control-plane/src/application/services/notifier-service.ts +70 -0
  17. package/apps/control-plane/src/application/services/patch-service.ts +95 -0
  18. package/apps/control-plane/src/application/services/performance-analytics-service.ts +90 -0
  19. package/apps/control-plane/src/application/services/plan-service.ts +184 -0
  20. package/apps/control-plane/src/application/services/pr-monitor-service.ts +58 -0
  21. package/apps/control-plane/src/application/services/qa-index-service.ts +68 -0
  22. package/apps/control-plane/src/application/services/reactions-service.ts +77 -0
  23. package/apps/control-plane/src/application/services/reporting-service.ts +79 -0
  24. package/apps/control-plane/src/application/services/run-lease-service.ts +79 -0
  25. package/apps/control-plane/src/application/tools/tool-router.ts +76 -0
  26. package/apps/control-plane/src/mcp/tool-runtime.ts +4 -0
  27. package/apps/control-plane/src/providers/providers.ts +96 -0
  28. package/apps/control-plane/src/providers/worker-provider-factory.ts +4 -0
  29. package/apps/control-plane/src/supervisor/build-wave-executor.ts +4 -0
  30. package/apps/control-plane/src/supervisor/planning-wave-executor.ts +4 -0
  31. package/apps/control-plane/src/supervisor/qa-wave-executor.ts +4 -0
  32. package/apps/control-plane/src/supervisor/run-coordinator.ts +4 -0
  33. package/apps/control-plane/src/supervisor/runtime.ts +4 -0
  34. package/apps/control-plane/src/supervisor/session-orchestrator.ts +4 -0
  35. package/apps/control-plane/test/dashboard-api.integration.spec.ts +310 -1
  36. package/apps/control-plane/test/dashboard-client.spec.ts +136 -1
  37. package/apps/control-plane/test/dashboard-live-feed.spec.ts +153 -0
  38. package/apps/control-plane/test/dashboard-ui-utils.spec.ts +132 -0
  39. package/apps/control-plane/test/mcp.spec.ts +96 -0
  40. package/apps/control-plane/test/tool-runtime.spec.ts +68 -0
  41. package/config/agentic/orchestrator/policy.yaml +1 -0
  42. package/dist/apps/control-plane/application/services/activity-monitor-service.d.ts +42 -0
  43. package/dist/apps/control-plane/application/services/activity-monitor-service.js +42 -0
  44. package/dist/apps/control-plane/application/services/activity-monitor-service.js.map +1 -1
  45. package/dist/apps/control-plane/application/services/collision-queue-service.d.ts +48 -0
  46. package/dist/apps/control-plane/application/services/collision-queue-service.js +48 -0
  47. package/dist/apps/control-plane/application/services/collision-queue-service.js.map +1 -1
  48. package/dist/apps/control-plane/application/services/cost-tracking-service.d.ts +79 -0
  49. package/dist/apps/control-plane/application/services/cost-tracking-service.js +76 -0
  50. package/dist/apps/control-plane/application/services/cost-tracking-service.js.map +1 -1
  51. package/dist/apps/control-plane/application/services/feature-deletion-service.d.ts +57 -0
  52. package/dist/apps/control-plane/application/services/feature-deletion-service.js +57 -0
  53. package/dist/apps/control-plane/application/services/feature-deletion-service.js.map +1 -1
  54. package/dist/apps/control-plane/application/services/feature-lifecycle-service.d.ts +64 -0
  55. package/dist/apps/control-plane/application/services/feature-lifecycle-service.js +61 -0
  56. package/dist/apps/control-plane/application/services/feature-lifecycle-service.js.map +1 -1
  57. package/dist/apps/control-plane/application/services/feature-state-service.d.ts +86 -0
  58. package/dist/apps/control-plane/application/services/feature-state-service.js +83 -0
  59. package/dist/apps/control-plane/application/services/feature-state-service.js.map +1 -1
  60. package/dist/apps/control-plane/application/services/gate-selection-service.d.ts +28 -0
  61. package/dist/apps/control-plane/application/services/gate-selection-service.js +28 -0
  62. package/dist/apps/control-plane/application/services/gate-selection-service.js.map +1 -1
  63. package/dist/apps/control-plane/application/services/gate-service.d.ts +148 -0
  64. package/dist/apps/control-plane/application/services/gate-service.js +120 -0
  65. package/dist/apps/control-plane/application/services/gate-service.js.map +1 -1
  66. package/dist/apps/control-plane/application/services/issue-tracker-service.d.ts +28 -0
  67. package/dist/apps/control-plane/application/services/issue-tracker-service.js +28 -0
  68. package/dist/apps/control-plane/application/services/issue-tracker-service.js.map +1 -1
  69. package/dist/apps/control-plane/application/services/lock-service.d.ts +88 -0
  70. package/dist/apps/control-plane/application/services/lock-service.js +64 -0
  71. package/dist/apps/control-plane/application/services/lock-service.js.map +1 -1
  72. package/dist/apps/control-plane/application/services/merge-service.d.ts +62 -0
  73. package/dist/apps/control-plane/application/services/merge-service.js +59 -0
  74. package/dist/apps/control-plane/application/services/merge-service.js.map +1 -1
  75. package/dist/apps/control-plane/application/services/notifier-service.d.ts +70 -0
  76. package/dist/apps/control-plane/application/services/notifier-service.js +70 -0
  77. package/dist/apps/control-plane/application/services/notifier-service.js.map +1 -1
  78. package/dist/apps/control-plane/application/services/patch-service.d.ts +94 -0
  79. package/dist/apps/control-plane/application/services/patch-service.js +91 -0
  80. package/dist/apps/control-plane/application/services/patch-service.js.map +1 -1
  81. package/dist/apps/control-plane/application/services/performance-analytics-service.d.ts +89 -0
  82. package/dist/apps/control-plane/application/services/performance-analytics-service.js +86 -0
  83. package/dist/apps/control-plane/application/services/performance-analytics-service.js.map +1 -1
  84. package/dist/apps/control-plane/application/services/plan-service.d.ts +287 -0
  85. package/dist/apps/control-plane/application/services/plan-service.js +207 -0
  86. package/dist/apps/control-plane/application/services/plan-service.js.map +1 -1
  87. package/dist/apps/control-plane/application/services/pr-monitor-service.d.ts +58 -0
  88. package/dist/apps/control-plane/application/services/pr-monitor-service.js +58 -0
  89. package/dist/apps/control-plane/application/services/pr-monitor-service.js.map +1 -1
  90. package/dist/apps/control-plane/application/services/qa-index-service.d.ts +67 -0
  91. package/dist/apps/control-plane/application/services/qa-index-service.js +64 -0
  92. package/dist/apps/control-plane/application/services/qa-index-service.js.map +1 -1
  93. package/dist/apps/control-plane/application/services/reactions-service.d.ts +76 -0
  94. package/dist/apps/control-plane/application/services/reactions-service.js +73 -0
  95. package/dist/apps/control-plane/application/services/reactions-service.js.map +1 -1
  96. package/dist/apps/control-plane/application/services/reporting-service.d.ts +78 -0
  97. package/dist/apps/control-plane/application/services/reporting-service.js +75 -0
  98. package/dist/apps/control-plane/application/services/reporting-service.js.map +1 -1
  99. package/dist/apps/control-plane/application/services/run-lease-service.d.ts +79 -0
  100. package/dist/apps/control-plane/application/services/run-lease-service.js +79 -0
  101. package/dist/apps/control-plane/application/services/run-lease-service.js.map +1 -1
  102. package/dist/apps/control-plane/application/tools/tool-router.d.ts +76 -0
  103. package/dist/apps/control-plane/application/tools/tool-router.js +62 -0
  104. package/dist/apps/control-plane/application/tools/tool-router.js.map +1 -1
  105. package/dist/apps/control-plane/mcp/tool-runtime.d.ts +3 -0
  106. package/dist/apps/control-plane/mcp/tool-runtime.js +3 -0
  107. package/dist/apps/control-plane/mcp/tool-runtime.js.map +1 -1
  108. package/dist/apps/control-plane/providers/providers.d.ts +88 -0
  109. package/dist/apps/control-plane/providers/providers.js.map +1 -1
  110. package/dist/apps/control-plane/providers/worker-provider-factory.d.ts +3 -0
  111. package/dist/apps/control-plane/providers/worker-provider-factory.js.map +1 -1
  112. package/dist/apps/control-plane/supervisor/build-wave-executor.d.ts +3 -0
  113. package/dist/apps/control-plane/supervisor/build-wave-executor.js +3 -0
  114. package/dist/apps/control-plane/supervisor/build-wave-executor.js.map +1 -1
  115. package/dist/apps/control-plane/supervisor/planning-wave-executor.d.ts +3 -0
  116. package/dist/apps/control-plane/supervisor/planning-wave-executor.js +3 -0
  117. package/dist/apps/control-plane/supervisor/planning-wave-executor.js.map +1 -1
  118. package/dist/apps/control-plane/supervisor/qa-wave-executor.d.ts +3 -0
  119. package/dist/apps/control-plane/supervisor/qa-wave-executor.js +3 -0
  120. package/dist/apps/control-plane/supervisor/qa-wave-executor.js.map +1 -1
  121. package/dist/apps/control-plane/supervisor/run-coordinator.d.ts +3 -0
  122. package/dist/apps/control-plane/supervisor/run-coordinator.js +3 -0
  123. package/dist/apps/control-plane/supervisor/run-coordinator.js.map +1 -1
  124. package/dist/apps/control-plane/supervisor/runtime.d.ts +3 -0
  125. package/dist/apps/control-plane/supervisor/runtime.js +3 -0
  126. package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
  127. package/dist/apps/control-plane/supervisor/session-orchestrator.d.ts +3 -0
  128. package/dist/apps/control-plane/supervisor/session-orchestrator.js +3 -0
  129. package/dist/apps/control-plane/supervisor/session-orchestrator.js.map +1 -1
  130. package/package.json +1 -1
  131. package/packages/web-dashboard/src/app/analytics/page.tsx +451 -0
  132. package/packages/web-dashboard/src/app/api/analytics/route.ts +62 -0
  133. package/packages/web-dashboard/src/app/api/collisions/route.ts +63 -0
  134. package/packages/web-dashboard/src/app/api/features/[id]/cost/route.ts +29 -0
  135. package/packages/web-dashboard/src/app/api/features/[id]/review-brief/route.ts +31 -0
  136. package/packages/web-dashboard/src/app/api/features/[id]/route.ts +5 -8
  137. package/packages/web-dashboard/src/app/api/features/[id]/test-index/route.ts +31 -0
  138. package/packages/web-dashboard/src/app/api/flaky/route.ts +20 -0
  139. package/packages/web-dashboard/src/app/api/policy/budget/route.ts +31 -0
  140. package/packages/web-dashboard/src/app/api/run/route.ts +162 -0
  141. package/packages/web-dashboard/src/app/api/status/route.ts +2 -5
  142. package/packages/web-dashboard/src/app/feature/[id]/page.tsx +112 -0
  143. package/packages/web-dashboard/src/app/page.tsx +379 -77
  144. package/packages/web-dashboard/src/components/agent-pipeline-stepper.tsx +100 -0
  145. package/packages/web-dashboard/src/components/dependency-chains.tsx +58 -0
  146. package/packages/web-dashboard/src/components/detail-panel.tsx +215 -5
  147. package/packages/web-dashboard/src/components/feature-card.tsx +5 -0
  148. package/packages/web-dashboard/src/components/feature-cost-panel.tsx +66 -0
  149. package/packages/web-dashboard/src/components/feature-list-view.tsx +336 -0
  150. package/packages/web-dashboard/src/components/gate-step-drilldown.tsx +92 -0
  151. package/packages/web-dashboard/src/components/kanban-board.tsx +3 -0
  152. package/packages/web-dashboard/src/components/live-event-feed.tsx +60 -0
  153. package/packages/web-dashboard/src/components/lock-resource-map.tsx +73 -0
  154. package/packages/web-dashboard/src/components/plan-revision-timeline.tsx +55 -0
  155. package/packages/web-dashboard/src/components/plan-risk-annotations.tsx +63 -0
  156. package/packages/web-dashboard/src/components/plan-scope-tree.tsx +285 -0
  157. package/packages/web-dashboard/src/components/qa-coverage-map.tsx +174 -0
  158. package/packages/web-dashboard/src/components/quick-launch-panel.tsx +70 -0
  159. package/packages/web-dashboard/src/components/review-brief-panel.tsx +75 -0
  160. package/packages/web-dashboard/src/components/run-health-panel.tsx +85 -0
  161. package/packages/web-dashboard/src/components/summary-bar.tsx +59 -2
  162. package/packages/web-dashboard/src/components/verification-signals.tsx +167 -0
  163. package/packages/web-dashboard/src/lib/analytics-utils.ts +3 -0
  164. package/packages/web-dashboard/src/lib/aop-client.ts +510 -11
  165. package/packages/web-dashboard/src/lib/api-envelope.ts +48 -0
  166. package/packages/web-dashboard/src/lib/authz-adapter.ts +33 -0
  167. package/packages/web-dashboard/src/lib/dashboard-utils.ts +304 -0
  168. package/packages/web-dashboard/src/lib/live-feed.ts +218 -0
  169. package/packages/web-dashboard/src/lib/policy-reader.ts +22 -0
  170. package/packages/web-dashboard/src/lib/types.ts +114 -0
  171. package/packages/web-dashboard/src/styles/dashboard.module.css +341 -1
  172. package/spec-files/{outstanding → completed}/agentic_orchestrator_dashboard_advanced_ux_spec.md +1 -1
  173. package/spec-files/outstanding/agentic_orchestrator_evidence_integrity_doctor_spec.md +561 -0
  174. package/spec-files/outstanding/agentic_orchestrator_kernel_simplification_spec.md +834 -0
  175. package/spec-files/outstanding/agentic_orchestrator_observability_integrity_diagnostics_spec.md +12 -5
  176. package/spec-files/outstanding/agentic_orchestrator_observability_replay_spec.md +442 -0
  177. package/spec-files/progress.md +174 -1
@@ -17,6 +17,38 @@ const readFeatureLogMock = vi.hoisted(() =>
17
17
  total: 1,
18
18
  })),
19
19
  );
20
+ const readFeatureCostMock = vi.hoisted(() =>
21
+ vi.fn(async () => ({
22
+ feature_id: 'feature_checkout',
23
+ tokens_used: 120,
24
+ estimated_cost_usd: 1.23,
25
+ recorded_at: '2026-03-05T00:00:00Z',
26
+ })),
27
+ );
28
+ const readFeatureQaTestIndexMock = vi.hoisted(() =>
29
+ vi.fn(async () => ({
30
+ feature_id: 'feature_checkout',
31
+ version: 1,
32
+ items: [
33
+ {
34
+ path: 'src/a.ts',
35
+ status: 'passed',
36
+ required_tests: ['a.spec.ts'],
37
+ },
38
+ ],
39
+ })),
40
+ );
41
+ const readFeatureReviewBriefMock = vi.hoisted(() =>
42
+ vi.fn(async () => ({
43
+ intent_summary: 'Intent',
44
+ scope_summary: 'Scope',
45
+ contract_risk_summary: 'Risk',
46
+ gate_matrix: { fast: 'pass' },
47
+ unresolved_questions: [],
48
+ evidence_refs: [],
49
+ generated_at: '2026-03-05T00:00:00Z',
50
+ })),
51
+ );
20
52
  const getAopRootMock = vi.hoisted(() => vi.fn(() => process.cwd()));
21
53
  const approveFeatureReviewMock = vi.hoisted(() =>
22
54
  vi.fn(async () => ({ ok: true, data: { merged: true } })),
@@ -33,6 +65,9 @@ const sendFeatureMessageMock = vi.hoisted(() =>
33
65
  const retryFeatureMock = vi.hoisted(() =>
34
66
  vi.fn(async () => ({ ok: true, data: { retry_mode: 'full' } })),
35
67
  );
68
+ const callOrchestratorToolMock = vi.hoisted(() =>
69
+ vi.fn(async () => ({ ok: true, data: {} as Record<string, unknown> })),
70
+ );
36
71
  const execFileMock = vi.hoisted(() => vi.fn());
37
72
 
38
73
  vi.mock('../../../packages/web-dashboard/src/lib/aop-client.js', () => ({
@@ -40,6 +75,9 @@ vi.mock('../../../packages/web-dashboard/src/lib/aop-client.js', () => ({
40
75
  readDashboardStatus: readDashboardStatusMock,
41
76
  readFeatureState: readFeatureStateMock,
42
77
  readFeatureLog: readFeatureLogMock,
78
+ readFeatureCost: readFeatureCostMock,
79
+ readFeatureQaTestIndex: readFeatureQaTestIndexMock,
80
+ readFeatureReviewBrief: readFeatureReviewBriefMock,
43
81
  getAopRoot: getAopRootMock,
44
82
  }));
45
83
 
@@ -49,6 +87,7 @@ vi.mock('../../../packages/web-dashboard/src/lib/orchestrator-tools.js', () => (
49
87
  requestFeatureChanges: requestFeatureChangesMock,
50
88
  sendFeatureMessage: sendFeatureMessageMock,
51
89
  retryFeature: retryFeatureMock,
90
+ callOrchestratorTool: callOrchestratorToolMock,
52
91
  }));
53
92
 
54
93
  vi.mock('node:child_process', () => ({
@@ -67,9 +106,17 @@ vi.mock('node:child_process', () => ({
67
106
 
68
107
  import { POST as actionsPost } from '../../../packages/web-dashboard/src/app/api/actions/route.js';
69
108
  import { POST as checkoutPost } from '../../../packages/web-dashboard/src/app/api/features/[id]/checkout/route.js';
109
+ import { GET as collisionsGet } from '../../../packages/web-dashboard/src/app/api/collisions/route.js';
110
+ import { GET as costGet } from '../../../packages/web-dashboard/src/app/api/features/[id]/cost/route.js';
70
111
  import { GET as logGet } from '../../../packages/web-dashboard/src/app/api/features/[id]/log/route.js';
112
+ import { GET as reviewBriefGet } from '../../../packages/web-dashboard/src/app/api/features/[id]/review-brief/route.js';
71
113
  import { GET as statusGet } from '../../../packages/web-dashboard/src/app/api/status/route.js';
114
+ import { GET as testIndexGet } from '../../../packages/web-dashboard/src/app/api/features/[id]/test-index/route.js';
72
115
  import { GET as projectsGet } from '../../../packages/web-dashboard/src/app/api/projects/route.js';
116
+ import {
117
+ GET as runGet,
118
+ POST as runPost,
119
+ } from '../../../packages/web-dashboard/src/app/api/run/route.js';
73
120
 
74
121
  describe('dashboard api integration', () => {
75
122
  let repoRoot: string;
@@ -84,12 +131,16 @@ describe('dashboard api integration', () => {
84
131
  readDashboardStatusMock.mockReset();
85
132
  readFeatureStateMock.mockReset();
86
133
  readFeatureLogMock.mockReset();
134
+ readFeatureCostMock.mockReset();
135
+ readFeatureQaTestIndexMock.mockReset();
136
+ readFeatureReviewBriefMock.mockReset();
87
137
  getAopRootMock.mockReset();
88
138
  approveFeatureReviewMock.mockReset();
89
139
  denyFeatureReviewMock.mockReset();
90
140
  requestFeatureChangesMock.mockReset();
91
141
  sendFeatureMessageMock.mockReset();
92
142
  retryFeatureMock.mockReset();
143
+ callOrchestratorToolMock.mockReset();
93
144
  execFileMock.mockReset();
94
145
 
95
146
  resolveProjectRootMock.mockResolvedValue(repoRoot);
@@ -112,6 +163,31 @@ describe('dashboard api integration', () => {
112
163
  entries: [{ timestamp: '2026-03-05T00:00:00Z', message: 'log message', type: 'log' }],
113
164
  total: 1,
114
165
  });
166
+ readFeatureCostMock.mockResolvedValue({
167
+ feature_id: 'feature_checkout',
168
+ tokens_used: 120,
169
+ estimated_cost_usd: 1.23,
170
+ recorded_at: '2026-03-05T00:00:00Z',
171
+ });
172
+ readFeatureQaTestIndexMock.mockResolvedValue({
173
+ feature_id: 'feature_checkout',
174
+ version: 1,
175
+ items: [{ path: 'src/a.ts', status: 'passed', required_tests: ['a.spec.ts'] }],
176
+ });
177
+ readFeatureReviewBriefMock.mockResolvedValue({
178
+ intent_summary: 'Intent',
179
+ scope_summary: 'Scope',
180
+ contract_risk_summary: 'Risk',
181
+ gate_matrix: { fast: 'pass' },
182
+ unresolved_questions: [],
183
+ evidence_refs: [],
184
+ generated_at: '2026-03-05T00:00:00Z',
185
+ });
186
+ callOrchestratorToolMock.mockResolvedValue({
187
+ ok: true,
188
+ data: { feature_id: 'feature_checkout' } as Record<string, unknown>,
189
+ });
190
+ delete process.env.AOP_DASHBOARD_QUICK_LAUNCH;
115
191
  });
116
192
 
117
193
  afterEach(async () => {
@@ -299,14 +375,113 @@ describe('dashboard api integration', () => {
299
375
  resolveProjectRootMock.mockResolvedValue(path.join(repoRoot, 'project-beta'));
300
376
 
301
377
  const response = await statusGet(new Request('http://localhost/api/status?project=beta'));
302
- const body = (await response.json()) as { ok: boolean; data: { features: unknown[] } };
378
+ const body = (await response.json()) as {
379
+ ok: boolean;
380
+ data: { features: unknown[] };
381
+ meta: { source: string };
382
+ };
303
383
 
304
384
  expect(response.status).toBe(200);
305
385
  expect(body.ok).toBe(true);
386
+ expect(body.meta.source).toBe('derived');
306
387
  expect(resolveProjectRootMock).toHaveBeenCalledWith('beta');
307
388
  expect(readDashboardStatusMock).toHaveBeenCalledWith(path.join(repoRoot, 'project-beta'));
308
389
  });
309
390
 
391
+ it('GIVEN_feature_cost_route_WHEN_artifact_present_THEN_returns_uniform_envelope', async () => {
392
+ readFeatureStateMock.mockResolvedValue({
393
+ id: 'feature_checkout',
394
+ feature_id: 'feature_checkout',
395
+ status: 'ready_to_merge',
396
+ phase: 'ready_to_merge',
397
+ });
398
+ const response = await costGet(
399
+ new Request('http://localhost/api/features/feature_checkout/cost'),
400
+ {
401
+ params: Promise.resolve({ id: 'feature_checkout' }),
402
+ },
403
+ );
404
+ const body = (await response.json()) as {
405
+ ok: boolean;
406
+ data?: { estimated_cost_usd: number };
407
+ meta?: { source: string };
408
+ };
409
+
410
+ expect(response.status).toBe(200);
411
+ expect(body.ok).toBe(true);
412
+ expect(body.data?.estimated_cost_usd).toBe(1.23);
413
+ expect(body.meta?.source).toBe('artifact');
414
+ });
415
+
416
+ it('GIVEN_feature_cost_route_WHEN_artifact_missing_THEN_returns_artifact_missing', async () => {
417
+ readFeatureStateMock.mockResolvedValue({
418
+ id: 'feature_checkout',
419
+ feature_id: 'feature_checkout',
420
+ status: 'ready_to_merge',
421
+ phase: 'ready_to_merge',
422
+ });
423
+ readFeatureCostMock.mockResolvedValue(null);
424
+
425
+ const response = await costGet(
426
+ new Request('http://localhost/api/features/feature_checkout/cost'),
427
+ {
428
+ params: Promise.resolve({ id: 'feature_checkout' }),
429
+ },
430
+ );
431
+ const body = (await response.json()) as { ok: boolean; error: { code: string } };
432
+
433
+ expect(response.status).toBe(404);
434
+ expect(body.ok).toBe(false);
435
+ expect(body.error.code).toBe('artifact_missing');
436
+ });
437
+
438
+ it('GIVEN_feature_test_index_route_WHEN_requested_THEN_returns_qa_index', async () => {
439
+ readFeatureStateMock.mockResolvedValue({
440
+ id: 'feature_checkout',
441
+ feature_id: 'feature_checkout',
442
+ status: 'qa',
443
+ phase: 'qa',
444
+ });
445
+ const response = await testIndexGet(
446
+ new Request('http://localhost/api/features/feature_checkout/test-index'),
447
+ { params: Promise.resolve({ id: 'feature_checkout' }) },
448
+ );
449
+ const body = (await response.json()) as {
450
+ ok: boolean;
451
+ data?: { feature_id: string; items: unknown[] };
452
+ };
453
+
454
+ expect(response.status).toBe(200);
455
+ expect(body.ok).toBe(true);
456
+ expect(body.data?.feature_id).toBe('feature_checkout');
457
+ expect(body.data?.items).toHaveLength(1);
458
+ });
459
+
460
+ it('GIVEN_feature_review_brief_route_WHEN_requested_THEN_returns_review_brief', async () => {
461
+ readFeatureStateMock.mockResolvedValue({
462
+ id: 'feature_checkout',
463
+ feature_id: 'feature_checkout',
464
+ status: 'ready_to_merge',
465
+ phase: 'ready_to_merge',
466
+ });
467
+ const response = await reviewBriefGet(
468
+ new Request('http://localhost/api/features/feature_checkout/review-brief'),
469
+ {
470
+ params: Promise.resolve({ id: 'feature_checkout' }),
471
+ },
472
+ );
473
+ const body = (await response.json()) as {
474
+ ok: boolean;
475
+ data?: { intent_summary: string };
476
+ meta?: { source: string };
477
+ };
478
+
479
+ expect(response.status).toBe(200);
480
+ expect(body.ok).toBe(true);
481
+ expect(body.data?.intent_summary).toBe('Intent');
482
+ expect(body.meta?.source).toBe('artifact');
483
+ });
484
+
310
485
  it('GIVEN_multi_project_config_WHEN_projects_endpoint_called_THEN_returns_switchable_projects', async () => {
311
486
  const projectAlpha = path.join(repoRoot, 'project-alpha');
312
487
  const projectBeta = path.join(repoRoot, 'project-beta');
@@ -352,6 +527,140 @@ describe('dashboard api integration', () => {
352
527
  expect(body.data.projects.map((project) => project.path)).toEqual([projectAlpha, projectBeta]);
353
528
  });
354
529
 
530
+ it('GIVEN_quick_launch_disabled_policy_WHEN_run_get_called_THEN_returns_unavailable_state', async () => {
531
+ const policyPath = path.join(repoRoot, 'config', 'agentic', 'orchestrator');
532
+ await fs.mkdir(policyPath, { recursive: true });
533
+ await fs.writeFile(
534
+ path.join(policyPath, 'policy.yaml'),
535
+ ['dashboard:', ' enabled: true', ' port: 3000', ' quick_launch: false'].join('\n'),
536
+ 'utf8',
537
+ );
538
+
539
+ const response = await runGet(new Request('http://localhost/api/run?project=alpha'));
540
+ const body = (await response.json()) as {
541
+ ok: boolean;
542
+ data?: { available: boolean; enabled: boolean; authorized: boolean; reason: string | null };
543
+ };
544
+
545
+ expect(response.status).toBe(200);
546
+ expect(body.ok).toBe(true);
547
+ expect(body.data).toEqual({
548
+ available: false,
549
+ enabled: false,
550
+ authorized: false,
551
+ reason:
552
+ 'Quick launch is disabled by policy. Enable dashboard.quick_launch or AOP_DASHBOARD_QUICK_LAUNCH=true.',
553
+ });
554
+ });
555
+
556
+ it('GIVEN_quick_launch_enabled_WHEN_run_post_called_THEN_calls_feature_init_tool', async () => {
557
+ const policyPath = path.join(repoRoot, 'config', 'agentic', 'orchestrator');
558
+ await fs.mkdir(policyPath, { recursive: true });
559
+ await fs.writeFile(
560
+ path.join(policyPath, 'policy.yaml'),
561
+ ['dashboard:', ' enabled: true', ' port: 3000', ' quick_launch: true'].join('\n'),
562
+ 'utf8',
563
+ );
564
+
565
+ const response = await runPost(
566
+ new Request('http://localhost/api/run?project=alpha', {
567
+ method: 'POST',
568
+ body: JSON.stringify({
569
+ feature_id: 'feature_checkout',
570
+ goal: 'Implement checkout flow updates',
571
+ confirm_launch: true,
572
+ }),
573
+ }),
574
+ );
575
+ const body = (await response.json()) as {
576
+ ok: boolean;
577
+ data?: { feature_id: string; status: string };
578
+ };
579
+
580
+ expect(response.status).toBe(201);
581
+ expect(body.ok).toBe(true);
582
+ expect(body.data?.feature_id).toBe('feature_checkout');
583
+ expect(callOrchestratorToolMock).toHaveBeenCalledWith(
584
+ 'feature.init',
585
+ {
586
+ feature_id: 'feature_checkout',
587
+ goal: 'Implement checkout flow updates',
588
+ },
589
+ {
590
+ mutating: true,
591
+ featureId: 'feature_checkout',
592
+ repoRoot,
593
+ },
594
+ );
595
+ });
596
+
597
+ it('GIVEN_collision_count_with_50_or_fewer_features_WHEN_collisions_route_called_THEN_returns_matrix_mode', async () => {
598
+ callOrchestratorToolMock.mockResolvedValue({
599
+ ok: true,
600
+ data: {
601
+ collisions: [
602
+ {
603
+ feature_id: 'feature_a',
604
+ with_feature_id: 'feature_b',
605
+ collisions: [{ type: 'same_file' }],
606
+ fingerprint: 'ab',
607
+ },
608
+ {
609
+ feature_id: 'feature_b',
610
+ with_feature_id: 'feature_c',
611
+ collisions: [{ type: 'same_file' }, { type: 'same_hunk' }],
612
+ fingerprint: 'bc',
613
+ },
614
+ ],
615
+ },
616
+ });
617
+
618
+ const response = await collisionsGet(
619
+ new Request('http://localhost/api/collisions?project=alpha'),
620
+ );
621
+ const body = (await response.json()) as {
622
+ ok: boolean;
623
+ data?: { mode: string; matrix?: unknown[] };
624
+ };
625
+
626
+ expect(response.status).toBe(200);
627
+ expect(body.ok).toBe(true);
628
+ expect(body.data?.mode).toBe('matrix');
629
+ expect(body.data?.matrix).toHaveLength(2);
630
+ });
631
+
632
+ it('GIVEN_collision_count_with_more_than_50_features_WHEN_collisions_route_called_THEN_returns_ranked_pairs_mode', async () => {
633
+ const collisions = Array.from({ length: 51 }, (_, index) => ({
634
+ feature_id: `feature_${index}`,
635
+ with_feature_id: `feature_${index + 1}`,
636
+ collisions: Array.from({ length: (index % 3) + 1 }, () => ({ type: 'same_file' })),
637
+ fingerprint: `${index}`,
638
+ }));
639
+ callOrchestratorToolMock.mockResolvedValue({
640
+ ok: true,
641
+ data: { collisions },
642
+ });
643
+
644
+ const response = await collisionsGet(
645
+ new Request('http://localhost/api/collisions?project=alpha'),
646
+ );
647
+ const body = (await response.json()) as {
648
+ ok: boolean;
649
+ data?: {
650
+ mode: string;
651
+ ranked_pairs?: Array<{ feature_id: string; with_feature_id: string; count: number }>;
652
+ };
653
+ };
654
+
655
+ expect(response.status).toBe(200);
656
+ expect(body.ok).toBe(true);
657
+ expect(body.data?.mode).toBe('ranked_pairs');
658
+ expect(body.data?.ranked_pairs?.length).toBe(51);
659
+ expect(body.data?.ranked_pairs?.[0].count).toBeGreaterThanOrEqual(
660
+ body.data?.ranked_pairs?.[1].count ?? 0,
661
+ );
662
+ });
663
+
355
664
  it('GIVEN_feature_history_route_WHEN_requested_THEN_returns_log_entries', async () => {
356
665
  const response = await logGet(
357
666
  new Request('http://localhost/api/features/feature_checkout/log?project=alpha'),
@@ -2,7 +2,13 @@ import fs from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { afterEach, describe, expect, it } from 'vitest';
5
- import { readDashboardStatus, readFeatureLog } from '@/lib/aop-client.js';
5
+ import {
6
+ readDashboardStatus,
7
+ readFeatureCost,
8
+ readFeatureLog,
9
+ readFeatureQaTestIndex,
10
+ readFeatureReviewBrief,
11
+ } from '@/lib/aop-client.js';
6
12
 
7
13
  async function writeState(repoRoot: string, featureId: string, frontMatter: string): Promise<void> {
8
14
  const featureDir = path.join(repoRoot, '.aop', 'features', featureId);
@@ -224,4 +230,133 @@ describe('dashboard aop client mapping', () => {
224
230
  expect(log.entries.some((entry) => entry.type === 'evidence')).toBe(true);
225
231
  expect(log.entries.some((entry) => entry.type === 'state_change')).toBe(true);
226
232
  });
233
+
234
+ it('GIVEN_index_with_runtime_locks_and_dep_blocked_WHEN_readDashboardStatus_THEN_maps_operator_payload_fields', async () => {
235
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-dash-client-'));
236
+ tempRoots.push(repoRoot);
237
+ await fs.mkdir(path.join(repoRoot, '.aop', 'features', 'feature_lock'), { recursive: true });
238
+ await fs.writeFile(
239
+ path.join(repoRoot, '.aop', 'features', 'index.json'),
240
+ JSON.stringify({
241
+ active: ['feature_lock'],
242
+ blocked: [],
243
+ merged: [],
244
+ blocked_queue: [],
245
+ dep_blocked: [{ feature_id: 'feature_waiting', depends_on_unresolved: ['dep_a'] }],
246
+ lock_leases: {
247
+ 'repo:write': {
248
+ holder: 'feature_lock',
249
+ lease_id: 'lease-1',
250
+ expires_at: '2099-01-01T00:00:00Z',
251
+ },
252
+ },
253
+ runtime_sessions: {
254
+ run_id: 'run:1',
255
+ orchestrator_session_id: 'session-1',
256
+ provider: 'codex',
257
+ model: 'gpt-5',
258
+ provider_config_ref_hash: 'hash',
259
+ owner_instance_id: 'instance-1',
260
+ lease_id: 'lease',
261
+ started_at: '2026-03-05T00:00:00Z',
262
+ last_heartbeat_at: '2026-03-05T00:01:00Z',
263
+ lease_expires_at: '2026-03-05T00:02:00Z',
264
+ orchestrator_epoch: 1,
265
+ },
266
+ }),
267
+ 'utf8',
268
+ );
269
+ await writeState(
270
+ repoRoot,
271
+ 'feature_lock',
272
+ ['feature_id: feature_lock', 'status: building', 'last_updated: 2026-03-05T00:00:00Z'].join(
273
+ '\n',
274
+ ),
275
+ );
276
+
277
+ const payload = await readDashboardStatus(repoRoot);
278
+ expect(payload.runtime?.run_id).toBe('run:1');
279
+ expect(payload.lock_map).toEqual([
280
+ {
281
+ resource: 'repo:write',
282
+ holder: 'feature_lock',
283
+ expires_at: '2099-01-01T00:00:00Z',
284
+ is_stale: false,
285
+ },
286
+ ]);
287
+ expect(payload.dep_blocked).toEqual([
288
+ { feature_id: 'feature_waiting', depends_on_unresolved: ['dep_a'] },
289
+ ]);
290
+ expect(payload.metrics?.merge_histogram_14d).toHaveLength(14);
291
+ });
292
+
293
+ it('GIVEN_feature_artifacts_WHEN_readers_called_THEN_return_normalized_cost_qa_and_review_brief', async () => {
294
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-dash-client-'));
295
+ tempRoots.push(repoRoot);
296
+ const featureRoot = path.join(repoRoot, '.aop', 'features', 'feature_artifacts');
297
+ await fs.mkdir(featureRoot, { recursive: true });
298
+ await fs.writeFile(
299
+ path.join(featureRoot, 'cost.json'),
300
+ JSON.stringify({
301
+ feature_id: 'feature_artifacts',
302
+ tokens_used: 50,
303
+ estimated_cost_usd: 0.52,
304
+ last_updated: '2026-03-05T00:00:00Z',
305
+ }),
306
+ 'utf8',
307
+ );
308
+ await fs.writeFile(
309
+ path.join(featureRoot, 'qa_test_index.json'),
310
+ JSON.stringify({
311
+ feature_id: 'feature_artifacts',
312
+ version: 3,
313
+ source_diff_ref: 'abc',
314
+ items: [
315
+ {
316
+ path: 'src/a.ts',
317
+ hunks: [],
318
+ required_tests: ['a.spec.ts'],
319
+ status: 'passed',
320
+ last_run_at: '2026-03-05T00:00:00Z',
321
+ },
322
+ ],
323
+ }),
324
+ 'utf8',
325
+ );
326
+ await fs.writeFile(
327
+ path.join(featureRoot, 'review_brief.json'),
328
+ JSON.stringify({
329
+ intent_summary: 'Intent',
330
+ scope_summary: 'Scope',
331
+ contract_risk_summary: 'Risk',
332
+ gate_matrix: { fast: 'pass' },
333
+ unresolved_questions: ['Q1'],
334
+ evidence_refs: ['evidence/qa.txt'],
335
+ generated_at: '2026-03-05T00:00:00Z',
336
+ }),
337
+ 'utf8',
338
+ );
339
+
340
+ await expect(readFeatureCost('feature_artifacts', repoRoot)).resolves.toMatchObject({
341
+ feature_id: 'feature_artifacts',
342
+ tokens_used: 50,
343
+ estimated_cost_usd: 0.52,
344
+ });
345
+ await expect(readFeatureQaTestIndex('feature_artifacts', repoRoot)).resolves.toMatchObject({
346
+ feature_id: 'feature_artifacts',
347
+ version: 3,
348
+ items: [
349
+ {
350
+ path: 'src/a.ts',
351
+ status: 'passed',
352
+ required_tests: ['a.spec.ts'],
353
+ },
354
+ ],
355
+ });
356
+ await expect(readFeatureReviewBrief('feature_artifacts', repoRoot)).resolves.toMatchObject({
357
+ intent_summary: 'Intent',
358
+ scope_summary: 'Scope',
359
+ contract_risk_summary: 'Risk',
360
+ });
361
+ });
227
362
  });
@@ -0,0 +1,153 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ appendFeedEvents,
4
+ deriveFeedEventsFromSnapshotDiff,
5
+ reduceLiveFeedState,
6
+ } from '../../../packages/web-dashboard/src/lib/live-feed.js';
7
+ import type { DashboardStatusPayload } from '../../../packages/web-dashboard/src/lib/types.js';
8
+
9
+ function payload(overrides: Partial<DashboardStatusPayload>): DashboardStatusPayload {
10
+ return {
11
+ index: { active: [], blocked: [], merged: [], blocked_queue: [] },
12
+ features: [],
13
+ ...overrides,
14
+ };
15
+ }
16
+
17
+ describe('dashboard live feed utils', () => {
18
+ it('derives phase and gate events from snapshot diffs', () => {
19
+ const previous = payload({
20
+ features: [
21
+ {
22
+ id: 'feature_a',
23
+ feature_id: 'feature_a',
24
+ status: 'building',
25
+ phase: 'building',
26
+ gates: { fast: 'na' },
27
+ activity_state: 'active',
28
+ pr: null,
29
+ },
30
+ ],
31
+ lock_map: [
32
+ {
33
+ resource: 'repo:write',
34
+ holder: 'feature_a',
35
+ expires_at: '2099-01-01T00:00:00Z',
36
+ is_stale: false,
37
+ },
38
+ ],
39
+ });
40
+ const next = payload({
41
+ features: [
42
+ {
43
+ id: 'feature_a',
44
+ feature_id: 'feature_a',
45
+ status: 'qa',
46
+ phase: 'qa',
47
+ gates: { fast: 'pass' },
48
+ activity_state: 'idle',
49
+ pr: null,
50
+ },
51
+ ],
52
+ lock_map: [
53
+ {
54
+ resource: 'repo:write',
55
+ holder: 'feature_b',
56
+ expires_at: '2099-01-01T00:00:00Z',
57
+ is_stale: false,
58
+ },
59
+ ],
60
+ });
61
+
62
+ const events = deriveFeedEventsFromSnapshotDiff(previous, next, '2026-03-05T00:00:00Z');
63
+ expect(events.some((event) => event.type === 'phase_change')).toBe(true);
64
+ expect(events.some((event) => event.type === 'gate_update')).toBe(true);
65
+ expect(events.some((event) => event.type === 'activity_change')).toBe(true);
66
+ expect(events.some((event) => event.type === 'lock_change')).toBe(true);
67
+ });
68
+
69
+ it('de-duplicates identical events within ten seconds and keeps bounded length', () => {
70
+ const seed = [
71
+ {
72
+ id: 'a',
73
+ timestamp: '2026-03-05T00:00:00Z',
74
+ feature_id: 'feature_a',
75
+ type: 'phase_change' as const,
76
+ message: 'feature_a moved planning -> building',
77
+ severity: 'info' as const,
78
+ },
79
+ ];
80
+ const duplicate = [
81
+ {
82
+ id: 'b',
83
+ timestamp: '2026-03-05T00:00:05Z',
84
+ feature_id: 'feature_a',
85
+ type: 'phase_change' as const,
86
+ message: 'feature_a moved planning -> building',
87
+ severity: 'info' as const,
88
+ },
89
+ ];
90
+
91
+ const unchanged = appendFeedEvents(seed, duplicate);
92
+ expect(unchanged).toHaveLength(1);
93
+ });
94
+
95
+ it('caps retained feed events to last 100 entries', () => {
96
+ const baseTimestamp = Date.parse('2026-03-05T00:00:00Z');
97
+ const existing = Array.from({ length: 99 }, (_, index) => ({
98
+ id: `existing-${index}`,
99
+ timestamp: new Date(baseTimestamp + index * 1000).toISOString(),
100
+ feature_id: 'feature_a',
101
+ type: 'activity_change' as const,
102
+ message: `message-${index}`,
103
+ severity: 'info' as const,
104
+ }));
105
+ const incoming = [
106
+ {
107
+ id: 'incoming-1',
108
+ timestamp: '2026-03-05T00:02:00Z',
109
+ feature_id: 'feature_a',
110
+ type: 'activity_change' as const,
111
+ message: 'message-new-1',
112
+ severity: 'info' as const,
113
+ },
114
+ {
115
+ id: 'incoming-2',
116
+ timestamp: '2026-03-05T00:02:01Z',
117
+ feature_id: 'feature_a',
118
+ type: 'activity_change' as const,
119
+ message: 'message-new-2',
120
+ severity: 'info' as const,
121
+ },
122
+ ];
123
+
124
+ const updated = appendFeedEvents(existing, incoming);
125
+ expect(updated).toHaveLength(100);
126
+ expect(updated[0].id).toBe('incoming-2');
127
+ expect(updated[1].id).toBe('incoming-1');
128
+ });
129
+
130
+ it('applies live-feed pause and clear controls deterministically', () => {
131
+ const base = {
132
+ paused: false,
133
+ events: [
134
+ {
135
+ id: 'event-1',
136
+ timestamp: '2026-03-05T00:00:00Z',
137
+ feature_id: 'feature_a',
138
+ type: 'phase_change' as const,
139
+ message: 'feature_a moved planning -> building',
140
+ severity: 'info' as const,
141
+ },
142
+ ],
143
+ };
144
+
145
+ const paused = reduceLiveFeedState(base, { type: 'toggle_pause' });
146
+ expect(paused.paused).toBe(true);
147
+ expect(paused.events).toHaveLength(1);
148
+
149
+ const cleared = reduceLiveFeedState(paused, { type: 'clear' });
150
+ expect(cleared.paused).toBe(true);
151
+ expect(cleared.events).toEqual([]);
152
+ });
153
+ });