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
@@ -0,0 +1,451 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { useEffect, useMemo, useState } from 'react';
5
+ import { shouldPollAnalytics } from '@/lib/analytics-utils.js';
6
+ import { sparklinePath, sparklinePoints } from '@/lib/dashboard-utils.js';
7
+ import styles from '@/styles/dashboard.module.css';
8
+ import type { DashboardStatusPayload, ProviderAnalytics } from '@/lib/types.js';
9
+
10
+ interface AnalyticsPayload {
11
+ providers: ProviderAnalytics[];
12
+ recent_outcomes: Array<{
13
+ feature_id: string;
14
+ status: string;
15
+ gate_pass: boolean;
16
+ retry_count: number;
17
+ duration_ms: number;
18
+ cost_usd: number;
19
+ recorded_at: string;
20
+ }>;
21
+ }
22
+
23
+ interface CollisionsPayload {
24
+ mode: 'matrix' | 'ranked_pairs';
25
+ matrix?: Array<{
26
+ feature_id: string;
27
+ with_feature_id: string;
28
+ collisions?: unknown;
29
+ fingerprint?: string;
30
+ }>;
31
+ ranked_pairs?: Array<{
32
+ feature_id: string;
33
+ with_feature_id: string;
34
+ count: number;
35
+ fingerprint: string;
36
+ }>;
37
+ }
38
+
39
+ type SortColumn =
40
+ | 'provider'
41
+ | 'model'
42
+ | 'total_features'
43
+ | 'success_rate'
44
+ | 'avg_cost_usd'
45
+ | 'avg_duration_ms'
46
+ | 'avg_retry_count';
47
+ type SortDirection = 'asc' | 'desc';
48
+
49
+ function queryParam(project: string): string {
50
+ return project.length > 0 ? `?project=${encodeURIComponent(project)}` : '';
51
+ }
52
+
53
+ function mergeTooltipLabel(index: number, total: number, merges: number): string {
54
+ const dayOffset = total - index - 1;
55
+ const day = new Date(Date.now() - dayOffset * 86_400_000);
56
+ const dayLabel = day.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
57
+ return `${dayLabel}: ${merges} merges`;
58
+ }
59
+
60
+ export default function AnalyticsPage() {
61
+ const [project, setProject] = useState('');
62
+ const [analytics, setAnalytics] = useState<AnalyticsPayload>({
63
+ providers: [],
64
+ recent_outcomes: [],
65
+ });
66
+ const [collisions, setCollisions] = useState<CollisionsPayload | null>(null);
67
+ const [flaky, setFlaky] = useState<string[]>([]);
68
+ const [histogram, setHistogram] = useState<number[]>([]);
69
+ const [sortColumn, setSortColumn] = useState<SortColumn>('success_rate');
70
+ const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
71
+
72
+ useEffect(() => {
73
+ const params = new URLSearchParams(window.location.search);
74
+ setProject(params.get('project') ?? '');
75
+ }, []);
76
+
77
+ useEffect(() => {
78
+ let cancelled = false;
79
+ const refresh = async () => {
80
+ const [analyticsRes, collisionsRes, flakyRes, statusRes] = await Promise.all([
81
+ fetch(`/api/analytics${queryParam(project)}`),
82
+ fetch(`/api/collisions${queryParam(project)}`),
83
+ fetch(`/api/flaky${queryParam(project)}`),
84
+ fetch(`/api/status${queryParam(project)}`),
85
+ ]);
86
+ const analyticsBody = (await analyticsRes.json()) as {
87
+ ok: boolean;
88
+ data?: AnalyticsPayload;
89
+ };
90
+ const collisionsBody = (await collisionsRes.json()) as {
91
+ ok: boolean;
92
+ data?: CollisionsPayload;
93
+ };
94
+ const flakyBody = (await flakyRes.json()) as {
95
+ ok: boolean;
96
+ data?: { suspects?: string[] };
97
+ };
98
+ const statusBody = (await statusRes.json()) as {
99
+ ok: boolean;
100
+ data?: DashboardStatusPayload;
101
+ };
102
+
103
+ if (cancelled) {
104
+ return;
105
+ }
106
+ setAnalytics(
107
+ analyticsBody.ok && analyticsBody.data
108
+ ? analyticsBody.data
109
+ : { providers: [], recent_outcomes: [] },
110
+ );
111
+ setCollisions(collisionsBody.ok ? (collisionsBody.data ?? null) : null);
112
+ setFlaky(
113
+ flakyBody.ok && Array.isArray(flakyBody.data?.suspects) ? flakyBody.data.suspects : [],
114
+ );
115
+ setHistogram(statusBody.ok ? (statusBody.data?.metrics?.merge_histogram_14d ?? []) : []);
116
+ };
117
+
118
+ void refresh();
119
+ const interval = setInterval(() => {
120
+ if (!shouldPollAnalytics(document.hidden)) {
121
+ return;
122
+ }
123
+ void refresh();
124
+ }, 30000);
125
+
126
+ return () => {
127
+ cancelled = true;
128
+ clearInterval(interval);
129
+ };
130
+ }, [project]);
131
+
132
+ const sortedProviders = useMemo(() => {
133
+ const copy = [...analytics.providers];
134
+ copy.sort((left, right) => {
135
+ const direction = sortDirection === 'asc' ? 1 : -1;
136
+ if (sortColumn === 'provider' || sortColumn === 'model') {
137
+ return String(left[sortColumn]).localeCompare(String(right[sortColumn])) * direction;
138
+ }
139
+ return (Number(left[sortColumn]) - Number(right[sortColumn])) * direction;
140
+ });
141
+ return copy;
142
+ }, [analytics.providers, sortColumn, sortDirection]);
143
+
144
+ const updateSort = (column: SortColumn) => {
145
+ if (sortColumn === column) {
146
+ setSortDirection((current) => (current === 'asc' ? 'desc' : 'asc'));
147
+ return;
148
+ }
149
+ setSortColumn(column);
150
+ setSortDirection(column === 'provider' || column === 'model' ? 'asc' : 'desc');
151
+ };
152
+
153
+ const sortState = (column: SortColumn): 'none' | 'ascending' | 'descending' => {
154
+ if (sortColumn !== column) {
155
+ return 'none';
156
+ }
157
+ return sortDirection === 'asc' ? 'ascending' : 'descending';
158
+ };
159
+
160
+ const sparkPath = sparklinePath(histogram, 280, 64);
161
+ const sparkPoints = sparklinePoints(histogram, 280, 64);
162
+ const collisionFeatures = useMemo(() => {
163
+ if (collisions?.mode !== 'matrix') {
164
+ return [];
165
+ }
166
+ const set = new Set<string>();
167
+ for (const item of collisions.matrix ?? []) {
168
+ set.add(item.feature_id);
169
+ set.add(item.with_feature_id);
170
+ }
171
+ return Array.from(set).sort((left, right) => left.localeCompare(right));
172
+ }, [collisions]);
173
+
174
+ const collisionMatrix = useMemo(() => {
175
+ const map = new Map<
176
+ string,
177
+ {
178
+ count: number;
179
+ dominantType: string;
180
+ }
181
+ >();
182
+ if (collisions?.mode !== 'matrix') {
183
+ return map;
184
+ }
185
+ for (const item of collisions.matrix ?? []) {
186
+ const key = `${item.feature_id}::${item.with_feature_id}`;
187
+ const payload = Array.isArray(item.collisions) ? item.collisions : [];
188
+ const count = payload.length;
189
+ const first = payload[0];
190
+ const dominantType =
191
+ first &&
192
+ typeof first === 'object' &&
193
+ typeof (first as Record<string, unknown>).type === 'string'
194
+ ? ((first as Record<string, unknown>).type as string)
195
+ : 'unknown';
196
+ map.set(key, { count, dominantType });
197
+ map.set(`${item.with_feature_id}::${item.feature_id}`, { count, dominantType });
198
+ }
199
+ return map;
200
+ }, [collisions]);
201
+
202
+ const collisionCellClass = (count: number): string => {
203
+ if (count >= 4) {
204
+ return styles.badgeFail;
205
+ }
206
+ if (count >= 2) {
207
+ return styles.badgePending;
208
+ }
209
+ if (count === 1) {
210
+ return styles.badgePass;
211
+ }
212
+ return styles.badgeNeutral;
213
+ };
214
+
215
+ return (
216
+ <div className={styles.dashboardRoot}>
217
+ <header className={styles.focusHeader}>
218
+ <div className={styles.titleBlock}>
219
+ <h1 className={styles.pageTitle}>Analytics</h1>
220
+ <p className={styles.pageSubtitle}>Provider performance and collision topology</p>
221
+ </div>
222
+ <div className={styles.inlineActions}>
223
+ <Link href={`/${queryParam(project)}`} className={styles.linkButton}>
224
+ Back To Board
225
+ </Link>
226
+ </div>
227
+ </header>
228
+
229
+ <section className={styles.section}>
230
+ <h2 className={styles.sectionTitle}>Throughput (14 days)</h2>
231
+ {sparkPath.length > 0 ? (
232
+ <svg
233
+ viewBox="0 0 280 64"
234
+ width="100%"
235
+ height={80}
236
+ role="img"
237
+ aria-label="14 day merge sparkline"
238
+ >
239
+ <path d={sparkPath} fill="none" stroke="#7fc6ff" strokeWidth={2} />
240
+ {sparkPoints.map((point) => (
241
+ <circle
242
+ key={`merge-point-${point.index}`}
243
+ className={styles.sparklinePoint}
244
+ cx={point.x}
245
+ cy={point.y}
246
+ r={4}
247
+ >
248
+ <title>{mergeTooltipLabel(point.index, sparkPoints.length, point.value)}</title>
249
+ </circle>
250
+ ))}
251
+ </svg>
252
+ ) : (
253
+ <p className={styles.metaText}>No merge data in the last 14 days.</p>
254
+ )}
255
+ </section>
256
+
257
+ <section className={styles.section}>
258
+ <h2 className={styles.sectionTitle}>Provider Performance</h2>
259
+ <div className={styles.tableWrapper}>
260
+ <table className={styles.table}>
261
+ <thead>
262
+ <tr>
263
+ <th aria-sort={sortState('provider')}>
264
+ <button
265
+ type="button"
266
+ className={styles.listHeaderButton}
267
+ onClick={() => updateSort('provider')}
268
+ >
269
+ Provider
270
+ </button>
271
+ </th>
272
+ <th aria-sort={sortState('model')}>
273
+ <button
274
+ type="button"
275
+ className={styles.listHeaderButton}
276
+ onClick={() => updateSort('model')}
277
+ >
278
+ Model
279
+ </button>
280
+ </th>
281
+ <th aria-sort={sortState('total_features')}>
282
+ <button
283
+ type="button"
284
+ className={styles.listHeaderButton}
285
+ onClick={() => updateSort('total_features')}
286
+ >
287
+ Features
288
+ </button>
289
+ </th>
290
+ <th aria-sort={sortState('success_rate')}>
291
+ <button
292
+ type="button"
293
+ className={styles.listHeaderButton}
294
+ onClick={() => updateSort('success_rate')}
295
+ >
296
+ Success
297
+ </button>
298
+ </th>
299
+ <th aria-sort={sortState('avg_cost_usd')}>
300
+ <button
301
+ type="button"
302
+ className={styles.listHeaderButton}
303
+ onClick={() => updateSort('avg_cost_usd')}
304
+ >
305
+ Avg Cost
306
+ </button>
307
+ </th>
308
+ <th aria-sort={sortState('avg_duration_ms')}>
309
+ <button
310
+ type="button"
311
+ className={styles.listHeaderButton}
312
+ onClick={() => updateSort('avg_duration_ms')}
313
+ >
314
+ Avg Duration
315
+ </button>
316
+ </th>
317
+ <th aria-sort={sortState('avg_retry_count')}>
318
+ <button
319
+ type="button"
320
+ className={styles.listHeaderButton}
321
+ onClick={() => updateSort('avg_retry_count')}
322
+ >
323
+ Avg Retries
324
+ </button>
325
+ </th>
326
+ </tr>
327
+ </thead>
328
+ <tbody>
329
+ {sortedProviders.length === 0 ? (
330
+ <tr>
331
+ <td colSpan={7} className={styles.metaText}>
332
+ No analytics outcomes recorded yet.
333
+ </td>
334
+ </tr>
335
+ ) : (
336
+ sortedProviders.map((row) => (
337
+ <tr key={`${row.provider}:${row.model}`}>
338
+ <td>{row.provider}</td>
339
+ <td>{row.model}</td>
340
+ <td>{row.total_features}</td>
341
+ <td>{(row.success_rate * 100).toFixed(1)}%</td>
342
+ <td>${row.avg_cost_usd.toFixed(3)}</td>
343
+ <td>{Math.round(row.avg_duration_ms)}ms</td>
344
+ <td>{row.avg_retry_count.toFixed(2)}</td>
345
+ </tr>
346
+ ))
347
+ )}
348
+ </tbody>
349
+ </table>
350
+ </div>
351
+ </section>
352
+
353
+ <section className={styles.section}>
354
+ <h2 className={styles.sectionTitle}>Recent Outcomes</h2>
355
+ <ul className={styles.planList}>
356
+ {analytics.recent_outcomes.map((outcome) => (
357
+ <li
358
+ key={`${outcome.feature_id}-${outcome.recorded_at}`}
359
+ className={styles.planListItem}
360
+ >
361
+ {outcome.feature_id} • {outcome.status} • retries {outcome.retry_count} • $
362
+ {outcome.cost_usd.toFixed(3)}
363
+ </li>
364
+ ))}
365
+ </ul>
366
+ </section>
367
+
368
+ <section className={styles.section}>
369
+ <h2 className={styles.sectionTitle}>Collision Topology</h2>
370
+ {collisions?.mode === 'matrix' ? (
371
+ <div className={styles.tableWrapper}>
372
+ <div className={styles.badgeRow}>
373
+ <span className={`${styles.badge} ${styles.badgeNeutral}`}>Legend: 0 none</span>
374
+ <span className={`${styles.badge} ${styles.badgePass}`}>1 low</span>
375
+ <span className={`${styles.badge} ${styles.badgePending}`}>2-3 medium</span>
376
+ <span className={`${styles.badge} ${styles.badgeFail}`}>4+ high</span>
377
+ </div>
378
+ <p className={styles.metaText}>Hover a cell to view collision type and count.</p>
379
+ <table className={styles.table}>
380
+ <thead>
381
+ <tr>
382
+ <th>Feature</th>
383
+ {collisionFeatures.map((feature) => (
384
+ <th key={`col-${feature}`}>{feature}</th>
385
+ ))}
386
+ </tr>
387
+ </thead>
388
+ <tbody>
389
+ {collisionFeatures.map((rowFeature) => (
390
+ <tr key={`row-${rowFeature}`}>
391
+ <th>{rowFeature}</th>
392
+ {collisionFeatures.map((colFeature) => {
393
+ if (rowFeature === colFeature) {
394
+ return (
395
+ <td key={`${rowFeature}:${colFeature}`}>
396
+ <span className={`${styles.badge} ${styles.badgeNeutral}`}>-</span>
397
+ </td>
398
+ );
399
+ }
400
+ const pair = collisionMatrix.get(`${rowFeature}::${colFeature}`) ?? {
401
+ count: 0,
402
+ dominantType: 'none',
403
+ };
404
+ return (
405
+ <td key={`${rowFeature}:${colFeature}`}>
406
+ <span
407
+ className={`${styles.badge} ${collisionCellClass(pair.count)}`}
408
+ title={`Count: ${pair.count}, type: ${pair.dominantType}`}
409
+ >
410
+ {pair.count}
411
+ </span>
412
+ </td>
413
+ );
414
+ })}
415
+ </tr>
416
+ ))}
417
+ </tbody>
418
+ </table>
419
+ </div>
420
+ ) : (
421
+ <ul className={styles.planList}>
422
+ {(collisions?.ranked_pairs ?? []).map((item) => (
423
+ <li
424
+ key={`${item.feature_id}:${item.with_feature_id}`}
425
+ className={styles.planListItem}
426
+ >
427
+ {item.feature_id} ↔ {item.with_feature_id} ({item.count})
428
+ </li>
429
+ ))}
430
+ </ul>
431
+ )}
432
+ </section>
433
+
434
+ {flaky.length > 0 ? (
435
+ <section className={styles.section}>
436
+ <h2 className={styles.sectionTitle}>Flaky Suspects</h2>
437
+ <p className={styles.metaText}>
438
+ Probabilistic signal only. Treat this as triage guidance, not root-cause proof.
439
+ </p>
440
+ <ul className={styles.planList}>
441
+ {flaky.map((name) => (
442
+ <li key={name} className={styles.planListItem}>
443
+ {name}
444
+ </li>
445
+ ))}
446
+ </ul>
447
+ </section>
448
+ ) : null}
449
+ </div>
450
+ );
451
+ }
@@ -0,0 +1,62 @@
1
+ import { errorEnvelope, okEnvelope } from '@/lib/api-envelope.js';
2
+ import { resolveProjectRoot } from '@/lib/aop-client.js';
3
+ import { callOrchestratorTool } from '@/lib/orchestrator-tools.js';
4
+ import type { ProviderAnalytics } from '@/lib/types.js';
5
+
6
+ export const dynamic = 'force-dynamic';
7
+
8
+ interface AnalyticsOutcome {
9
+ feature_id: string;
10
+ status: string;
11
+ gate_pass: boolean;
12
+ retry_count: number;
13
+ duration_ms: number;
14
+ cost_usd: number;
15
+ recorded_at: string;
16
+ }
17
+
18
+ export async function GET(req: Request): Promise<Response> {
19
+ const project = new URL(req.url).searchParams.get('project');
20
+ const repoRoot = await resolveProjectRoot(project);
21
+
22
+ const result = await callOrchestratorTool<{
23
+ aggregates?: ProviderAnalytics[];
24
+ outcomes?: AnalyticsOutcome[];
25
+ }>('performance.get_analytics', {}, { repoRoot });
26
+
27
+ if (!result.ok) {
28
+ return Response.json(
29
+ errorEnvelope(
30
+ result.error?.code ?? 'tool_unavailable',
31
+ result.error?.message ?? 'Unable to load analytics',
32
+ { retryable: result.error?.code === 'tool_timeout' },
33
+ ),
34
+ { status: 503 },
35
+ );
36
+ }
37
+
38
+ const aggregates = Array.isArray(result.data?.aggregates) ? result.data.aggregates : [];
39
+ const outcomes = Array.isArray(result.data?.outcomes) ? result.data.outcomes : [];
40
+ const recentOutcomes = outcomes
41
+ .slice(-20)
42
+ .reverse()
43
+ .map((item) => ({
44
+ feature_id: item.feature_id,
45
+ status: item.status,
46
+ gate_pass: item.gate_pass,
47
+ retry_count: item.retry_count,
48
+ duration_ms: item.duration_ms,
49
+ cost_usd: item.cost_usd,
50
+ recorded_at: item.recorded_at,
51
+ }));
52
+
53
+ return Response.json(
54
+ okEnvelope(
55
+ {
56
+ providers: aggregates,
57
+ recent_outcomes: recentOutcomes,
58
+ },
59
+ { source: 'tool' },
60
+ ),
61
+ );
62
+ }
@@ -0,0 +1,63 @@
1
+ import { errorEnvelope, okEnvelope } from '@/lib/api-envelope.js';
2
+ import { resolveProjectRoot } from '@/lib/aop-client.js';
3
+ import { callOrchestratorTool } from '@/lib/orchestrator-tools.js';
4
+
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ interface CollisionItem {
8
+ feature_id: string;
9
+ with_feature_id: string;
10
+ collisions?: unknown;
11
+ fingerprint?: string;
12
+ }
13
+
14
+ function collisionCount(collisions: unknown): number {
15
+ if (!Array.isArray(collisions)) {
16
+ return 0;
17
+ }
18
+ return collisions.length;
19
+ }
20
+
21
+ export async function GET(req: Request): Promise<Response> {
22
+ const project = new URL(req.url).searchParams.get('project');
23
+ const repoRoot = await resolveProjectRoot(project);
24
+ const result = await callOrchestratorTool<{ collisions?: CollisionItem[] }>(
25
+ 'collisions.scan',
26
+ {},
27
+ { repoRoot },
28
+ );
29
+
30
+ if (!result.ok) {
31
+ return Response.json(
32
+ errorEnvelope(
33
+ result.error?.code ?? 'tool_unavailable',
34
+ result.error?.message ?? 'Unable to load collisions',
35
+ { retryable: result.error?.code === 'tool_timeout' },
36
+ ),
37
+ { status: 503 },
38
+ );
39
+ }
40
+
41
+ const collisions = Array.isArray(result.data?.collisions) ? result.data.collisions : [];
42
+ const featureSet = new Set<string>();
43
+ for (const item of collisions) {
44
+ featureSet.add(item.feature_id);
45
+ featureSet.add(item.with_feature_id);
46
+ }
47
+
48
+ if (featureSet.size > 50) {
49
+ const rankedPairs = collisions
50
+ .map((item) => ({
51
+ feature_id: item.feature_id,
52
+ with_feature_id: item.with_feature_id,
53
+ count: collisionCount(item.collisions),
54
+ fingerprint: item.fingerprint ?? '',
55
+ }))
56
+ .sort((left, right) => right.count - left.count);
57
+ return Response.json(
58
+ okEnvelope({ mode: 'ranked_pairs', ranked_pairs: rankedPairs }, { source: 'tool' }),
59
+ );
60
+ }
61
+
62
+ return Response.json(okEnvelope({ mode: 'matrix', matrix: collisions }, { source: 'tool' }));
63
+ }
@@ -0,0 +1,29 @@
1
+ import { errorEnvelope, okEnvelope } from '@/lib/api-envelope.js';
2
+ import { readFeatureCost, readFeatureState, resolveProjectRoot } from '@/lib/aop-client.js';
3
+
4
+ export const dynamic = 'force-dynamic';
5
+
6
+ export async function GET(
7
+ req: Request,
8
+ { params }: { params: Promise<{ id: string }> },
9
+ ): Promise<Response> {
10
+ const { id } = await params;
11
+ const project = new URL(req.url).searchParams.get('project');
12
+ const repoRoot = await resolveProjectRoot(project);
13
+ const feature = await readFeatureState(id, repoRoot);
14
+ if (!feature) {
15
+ return Response.json(errorEnvelope('feature_not_found', `Feature ${id} was not found`), {
16
+ status: 404,
17
+ });
18
+ }
19
+
20
+ const cost = await readFeatureCost(id, repoRoot);
21
+ if (!cost) {
22
+ return Response.json(
23
+ errorEnvelope('artifact_missing', `No cost data recorded yet for feature ${id}`),
24
+ { status: 404 },
25
+ );
26
+ }
27
+
28
+ return Response.json(okEnvelope(cost, { source: 'artifact' }));
29
+ }
@@ -0,0 +1,31 @@
1
+ import { errorEnvelope, okEnvelope } from '@/lib/api-envelope.js';
2
+ import { readFeatureReviewBrief, readFeatureState, resolveProjectRoot } from '@/lib/aop-client.js';
3
+
4
+ export const dynamic = 'force-dynamic';
5
+
6
+ export async function GET(
7
+ req: Request,
8
+ { params }: { params: Promise<{ id: string }> },
9
+ ): Promise<Response> {
10
+ const { id } = await params;
11
+ const project = new URL(req.url).searchParams.get('project');
12
+ const repoRoot = await resolveProjectRoot(project);
13
+ const feature = await readFeatureState(id, repoRoot);
14
+ if (!feature) {
15
+ return Response.json(errorEnvelope('feature_not_found', `Feature ${id} was not found`), {
16
+ status: 404,
17
+ });
18
+ }
19
+
20
+ const brief = await readFeatureReviewBrief(id, repoRoot);
21
+ if (!brief) {
22
+ return Response.json(
23
+ errorEnvelope('artifact_missing', `Review brief not generated yet for feature ${id}`),
24
+ {
25
+ status: 404,
26
+ },
27
+ );
28
+ }
29
+
30
+ return Response.json(okEnvelope(brief, { source: 'artifact' }));
31
+ }
@@ -1,4 +1,5 @@
1
1
  import { readFeatureDetail, resolveProjectRoot } from '@/lib/aop-client.js';
2
+ import { errorEnvelope, okEnvelope } from '@/lib/api-envelope.js';
2
3
 
3
4
  export const dynamic = 'force-dynamic';
4
5
 
@@ -11,13 +12,9 @@ export async function GET(
11
12
  const repoRoot = await resolveProjectRoot(project);
12
13
  const detail = await readFeatureDetail(id, repoRoot);
13
14
  if (!detail) {
14
- return Response.json(
15
- {
16
- ok: false,
17
- error: { code: 'feature_not_found', message: `Feature ${id} not found` },
18
- },
19
- { status: 404 },
20
- );
15
+ return Response.json(errorEnvelope('feature_not_found', `Feature ${id} not found`), {
16
+ status: 404,
17
+ });
21
18
  }
22
- return Response.json({ ok: true, data: detail });
19
+ return Response.json(okEnvelope(detail, { source: 'artifact' }));
23
20
  }
@@ -0,0 +1,31 @@
1
+ import { errorEnvelope, okEnvelope } from '@/lib/api-envelope.js';
2
+ import { readFeatureQaTestIndex, readFeatureState, resolveProjectRoot } from '@/lib/aop-client.js';
3
+
4
+ export const dynamic = 'force-dynamic';
5
+
6
+ export async function GET(
7
+ req: Request,
8
+ { params }: { params: Promise<{ id: string }> },
9
+ ): Promise<Response> {
10
+ const { id } = await params;
11
+ const project = new URL(req.url).searchParams.get('project');
12
+ const repoRoot = await resolveProjectRoot(project);
13
+ const feature = await readFeatureState(id, repoRoot);
14
+ if (!feature) {
15
+ return Response.json(errorEnvelope('feature_not_found', `Feature ${id} was not found`), {
16
+ status: 404,
17
+ });
18
+ }
19
+
20
+ const qaIndex = await readFeatureQaTestIndex(id, repoRoot);
21
+ if (!qaIndex) {
22
+ return Response.json(
23
+ errorEnvelope('artifact_missing', `qa_test_index.json not found for feature ${id}`),
24
+ {
25
+ status: 404,
26
+ },
27
+ );
28
+ }
29
+
30
+ return Response.json(okEnvelope(qaIndex, { source: 'artifact' }));
31
+ }