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
@@ -16,6 +16,8 @@ export type QuickFilterKey =
16
16
  | 'ready_to_merge'
17
17
  | 'ci_failing';
18
18
 
19
+ type PhaseFilterValue = Exclude<FeatureSummary['phase'], undefined>;
20
+
19
21
  export interface DashboardFilterState {
20
22
  searchText: string;
21
23
  phases: FeatureSummary['phase'][];
@@ -25,6 +27,45 @@ export interface DashboardFilterState {
25
27
  quickFilter: QuickFilterKey | null;
26
28
  }
27
29
 
30
+ export type DashboardViewMode = 'board' | 'list';
31
+
32
+ export interface DashboardRouteState {
33
+ project: string;
34
+ featureId: string | null;
35
+ viewMode: DashboardViewMode;
36
+ filters: DashboardFilterState;
37
+ }
38
+
39
+ export interface QuickLaunchAffordanceState {
40
+ available: boolean;
41
+ enabled: boolean;
42
+ authorized: boolean;
43
+ }
44
+
45
+ export interface ColumnFeatureIds {
46
+ phase: FeatureSummary['phase'];
47
+ featureIds: string[];
48
+ }
49
+
50
+ const PHASE_VALUES: PhaseFilterValue[] = [
51
+ 'planning',
52
+ 'building',
53
+ 'qa',
54
+ 'ready_to_merge',
55
+ 'merged',
56
+ 'blocked',
57
+ 'unknown',
58
+ ];
59
+
60
+ const ACTIVITY_VALUES: NonNullable<FeatureSummary['activity_state']>[] = [
61
+ 'active',
62
+ 'idle',
63
+ 'waiting_input',
64
+ 'blocked',
65
+ 'exited',
66
+ 'unknown',
67
+ ];
68
+
28
69
  export interface DashboardSummaryCounts {
29
70
  active: number;
30
71
  blocked: number;
@@ -33,6 +74,13 @@ export interface DashboardSummaryCounts {
33
74
  ciFailing: number;
34
75
  }
35
76
 
77
+ export interface SparklinePoint {
78
+ x: number;
79
+ y: number;
80
+ value: number;
81
+ index: number;
82
+ }
83
+
36
84
  export interface ParsedDiffLine {
37
85
  raw: string;
38
86
  type: 'add' | 'del' | 'context' | 'meta';
@@ -66,6 +114,262 @@ export function createDefaultFilters(): DashboardFilterState {
66
114
  };
67
115
  }
68
116
 
117
+ function parseListParam(value: string | null): string[] {
118
+ if (!value) {
119
+ return [];
120
+ }
121
+ return value
122
+ .split(',')
123
+ .map((item) => item.trim())
124
+ .filter((item) => item.length > 0);
125
+ }
126
+
127
+ function parseBooleanFlag(value: string | null): boolean {
128
+ if (!value) {
129
+ return false;
130
+ }
131
+ const normalized = value.trim().toLowerCase();
132
+ return normalized === '1' || normalized === 'true' || normalized === 'yes';
133
+ }
134
+
135
+ function isQuickFilterKey(value: string): value is QuickFilterKey {
136
+ return (
137
+ value === 'active' ||
138
+ value === 'blocked' ||
139
+ value === 'awaiting_input' ||
140
+ value === 'ready_to_merge' ||
141
+ value === 'ci_failing'
142
+ );
143
+ }
144
+
145
+ export function parseFiltersFromSearchParams(params: URLSearchParams): DashboardFilterState {
146
+ const filters = createDefaultFilters();
147
+ filters.searchText = params.get('q') ?? '';
148
+
149
+ const phases = parseListParam(params.get('phase')).filter((item): item is PhaseFilterValue =>
150
+ PHASE_VALUES.includes(item as PhaseFilterValue),
151
+ );
152
+ const activities = parseListParam(params.get('activity')).filter(
153
+ (item): item is NonNullable<FeatureSummary['activity_state']> =>
154
+ ACTIVITY_VALUES.includes(item as NonNullable<FeatureSummary['activity_state']>),
155
+ );
156
+
157
+ filters.phases = phases as FeatureSummary['phase'][];
158
+ filters.activities = activities;
159
+ filters.gateFailuresOnly = parseBooleanFlag(params.get('gate_failures'));
160
+ filters.ciFailuresOnly = parseBooleanFlag(params.get('ci_not_success'));
161
+
162
+ const quick = params.get('quick');
163
+ if (quick && isQuickFilterKey(quick)) {
164
+ filters.quickFilter = quick;
165
+ }
166
+
167
+ return filters;
168
+ }
169
+
170
+ export function applyFiltersToSearchParams(
171
+ params: URLSearchParams,
172
+ filters: DashboardFilterState,
173
+ ): void {
174
+ const searchText = filters.searchText.trim();
175
+ if (searchText.length > 0) {
176
+ params.set('q', searchText);
177
+ } else {
178
+ params.delete('q');
179
+ }
180
+
181
+ if (filters.phases.length > 0) {
182
+ params.set('phase', filters.phases.join(','));
183
+ } else {
184
+ params.delete('phase');
185
+ }
186
+
187
+ if (filters.activities.length > 0) {
188
+ params.set('activity', filters.activities.join(','));
189
+ } else {
190
+ params.delete('activity');
191
+ }
192
+
193
+ if (filters.gateFailuresOnly) {
194
+ params.set('gate_failures', '1');
195
+ } else {
196
+ params.delete('gate_failures');
197
+ }
198
+
199
+ if (filters.ciFailuresOnly) {
200
+ params.set('ci_not_success', '1');
201
+ } else {
202
+ params.delete('ci_not_success');
203
+ }
204
+
205
+ if (filters.quickFilter) {
206
+ params.set('quick', filters.quickFilter);
207
+ } else {
208
+ params.delete('quick');
209
+ }
210
+ }
211
+
212
+ function parseFeatureId(value: string | null): string | null {
213
+ if (!value) {
214
+ return null;
215
+ }
216
+ const trimmed = value.trim();
217
+ return trimmed.length > 0 ? trimmed : null;
218
+ }
219
+
220
+ export function parseDashboardRouteState(params: URLSearchParams): DashboardRouteState {
221
+ return {
222
+ project: params.get('project') ?? '',
223
+ featureId: parseFeatureId(params.get('feature')),
224
+ viewMode: params.get('view') === 'list' ? 'list' : 'board',
225
+ filters: parseFiltersFromSearchParams(params),
226
+ };
227
+ }
228
+
229
+ export function applyDashboardRouteState(
230
+ params: URLSearchParams,
231
+ state: DashboardRouteState,
232
+ ): void {
233
+ const project = state.project.trim();
234
+ const featureId = state.featureId?.trim() ?? '';
235
+
236
+ if (project.length > 0) {
237
+ params.set('project', project);
238
+ } else {
239
+ params.delete('project');
240
+ }
241
+
242
+ if (featureId.length > 0) {
243
+ params.set('feature', featureId);
244
+ } else {
245
+ params.delete('feature');
246
+ }
247
+
248
+ if (state.viewMode === 'list') {
249
+ params.set('view', 'list');
250
+ } else {
251
+ params.delete('view');
252
+ }
253
+
254
+ applyFiltersToSearchParams(params, state.filters);
255
+ }
256
+
257
+ export function buildDashboardFeatureHref(featureId: string, project: string): string {
258
+ const params = new URLSearchParams();
259
+ const nextFeature = featureId.trim();
260
+ const nextProject = project.trim();
261
+
262
+ if (nextProject.length > 0) {
263
+ params.set('project', nextProject);
264
+ }
265
+ if (nextFeature.length > 0) {
266
+ params.set('feature', nextFeature);
267
+ }
268
+
269
+ const query = params.toString();
270
+ return query.length > 0 ? `/?${query}` : '/';
271
+ }
272
+
273
+ export function buildFeatureFocusHref(featureId: string, project: string): string {
274
+ const encodedFeatureId = encodeURIComponent(featureId.trim());
275
+ const params = new URLSearchParams();
276
+ const nextProject = project.trim();
277
+ if (nextProject.length > 0) {
278
+ params.set('project', nextProject);
279
+ }
280
+ const query = params.toString();
281
+ return query.length > 0
282
+ ? `/feature/${encodedFeatureId}?${query}`
283
+ : `/feature/${encodedFeatureId}`;
284
+ }
285
+
286
+ export function isQuickLaunchAffordanceVisible(state: QuickLaunchAffordanceState): boolean {
287
+ return state.available && state.enabled && state.authorized;
288
+ }
289
+
290
+ function findCurrentColumnIndex(
291
+ columnFeatureIds: ColumnFeatureIds[],
292
+ focusedFeatureId: string | null,
293
+ ): number {
294
+ if (focusedFeatureId) {
295
+ const focusedIndex = columnFeatureIds.findIndex((column) =>
296
+ column.featureIds.includes(focusedFeatureId),
297
+ );
298
+ if (focusedIndex >= 0) {
299
+ return focusedIndex;
300
+ }
301
+ }
302
+ return columnFeatureIds.findIndex((column) => column.featureIds.length > 0);
303
+ }
304
+
305
+ export function getNextFocusedFeatureId(
306
+ columnFeatureIds: ColumnFeatureIds[],
307
+ focusedFeatureId: string | null,
308
+ key: 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight',
309
+ ): string | null {
310
+ if (columnFeatureIds.length === 0) {
311
+ return null;
312
+ }
313
+
314
+ const currentColumnIndex = findCurrentColumnIndex(columnFeatureIds, focusedFeatureId);
315
+ if (currentColumnIndex < 0) {
316
+ return null;
317
+ }
318
+
319
+ const currentColumn = columnFeatureIds[currentColumnIndex];
320
+ if (currentColumn.featureIds.length === 0) {
321
+ return null;
322
+ }
323
+
324
+ const existingRow = focusedFeatureId ? currentColumn.featureIds.indexOf(focusedFeatureId) : -1;
325
+ const currentRow = Math.max(0, existingRow);
326
+ const currentFeatureId = currentColumn.featureIds[currentRow] ?? null;
327
+
328
+ if (key === 'ArrowUp' || key === 'ArrowDown') {
329
+ const offset = key === 'ArrowUp' ? -1 : 1;
330
+ const nextRow = Math.min(currentColumn.featureIds.length - 1, Math.max(0, currentRow + offset));
331
+ return currentColumn.featureIds[nextRow] ?? currentFeatureId;
332
+ }
333
+
334
+ const direction = key === 'ArrowLeft' ? -1 : 1;
335
+ let cursor = currentColumnIndex;
336
+ while (cursor >= 0 && cursor < columnFeatureIds.length) {
337
+ cursor += direction;
338
+ if (cursor < 0 || cursor >= columnFeatureIds.length) {
339
+ break;
340
+ }
341
+ const candidate = columnFeatureIds[cursor];
342
+ if (candidate.featureIds.length === 0) {
343
+ continue;
344
+ }
345
+ const nextRow = Math.min(currentRow, candidate.featureIds.length - 1);
346
+ return candidate.featureIds[nextRow] ?? currentFeatureId;
347
+ }
348
+
349
+ return currentFeatureId;
350
+ }
351
+
352
+ export function sparklinePath(values: number[], width: number, height: number): string {
353
+ const points = sparklinePoints(values, width, height);
354
+ return points
355
+ .map((point, index) => `${index === 0 ? 'M' : 'L'}${point.x.toFixed(2)} ${point.y.toFixed(2)}`)
356
+ .join(' ');
357
+ }
358
+
359
+ export function sparklinePoints(values: number[], width: number, height: number): SparklinePoint[] {
360
+ if (values.length === 0) {
361
+ return [];
362
+ }
363
+ const max = Math.max(1, ...values);
364
+ const step = values.length > 1 ? width / (values.length - 1) : width;
365
+ return values.map((value, index) => ({
366
+ x: index * step,
367
+ y: height - (value / max) * height,
368
+ value,
369
+ index,
370
+ }));
371
+ }
372
+
69
373
  export function applyQuickFilter(key: QuickFilterKey): DashboardFilterState {
70
374
  const next = createDefaultFilters();
71
375
  next.quickFilter = key;
@@ -0,0 +1,218 @@
1
+ import type { DashboardStatusPayload, FeedEvent, FeatureSummary, LockLease } from '@/lib/types.js';
2
+
3
+ const FEED_DEDUP_WINDOW_MS = 10_000;
4
+ const FEED_MAX_EVENTS = 100;
5
+
6
+ export interface LiveFeedState {
7
+ events: FeedEvent[];
8
+ paused: boolean;
9
+ }
10
+
11
+ export type LiveFeedAction = { type: 'toggle_pause' } | { type: 'clear' };
12
+
13
+ function gateHasFailure(feature: FeatureSummary): boolean {
14
+ const gates = feature.gates;
15
+ if (!gates || typeof gates !== 'object') {
16
+ return false;
17
+ }
18
+ return [gates.fast, gates.full, gates.merge].some((value) => {
19
+ if (typeof value !== 'string') {
20
+ return false;
21
+ }
22
+ const normalized = value.toLowerCase();
23
+ return normalized === 'fail' || normalized === 'failed' || normalized === 'error';
24
+ });
25
+ }
26
+
27
+ function lockMapByResource(locks: LockLease[] | undefined): Map<string, LockLease> {
28
+ const map = new Map<string, LockLease>();
29
+ for (const lock of locks ?? []) {
30
+ map.set(lock.resource, lock);
31
+ }
32
+ return map;
33
+ }
34
+
35
+ function buildEvent(
36
+ index: number,
37
+ timestamp: string,
38
+ featureId: string,
39
+ type: FeedEvent['type'],
40
+ message: string,
41
+ severity: FeedEvent['severity'],
42
+ ): FeedEvent {
43
+ return {
44
+ id: `${type}:${featureId}:${Date.parse(timestamp)}:${index}`,
45
+ timestamp,
46
+ feature_id: featureId,
47
+ type,
48
+ message,
49
+ severity,
50
+ };
51
+ }
52
+
53
+ function eventSignature(event: FeedEvent): string {
54
+ return `${event.feature_id}:${event.type}:${event.message}`;
55
+ }
56
+
57
+ export function deriveFeedEventsFromSnapshotDiff(
58
+ previous: DashboardStatusPayload | null,
59
+ next: DashboardStatusPayload,
60
+ nowIso = new Date().toISOString(),
61
+ ): FeedEvent[] {
62
+ if (!previous) {
63
+ return [];
64
+ }
65
+
66
+ const events: FeedEvent[] = [];
67
+ const prevById = new Map(previous.features.map((feature) => [feature.feature_id, feature]));
68
+ const nextById = new Map(next.features.map((feature) => [feature.feature_id, feature]));
69
+ const allFeatureIds = new Set([...prevById.keys(), ...nextById.keys()]);
70
+ let index = 0;
71
+
72
+ for (const featureId of allFeatureIds) {
73
+ const prevFeature = prevById.get(featureId);
74
+ const nextFeature = nextById.get(featureId);
75
+ if (!prevFeature || !nextFeature) {
76
+ continue;
77
+ }
78
+
79
+ if (prevFeature.phase !== nextFeature.phase) {
80
+ const severity: FeedEvent['severity'] =
81
+ nextFeature.phase === 'blocked'
82
+ ? 'error'
83
+ : nextFeature.phase === 'merged'
84
+ ? 'success'
85
+ : 'info';
86
+ events.push(
87
+ buildEvent(
88
+ index++,
89
+ nowIso,
90
+ featureId,
91
+ 'phase_change',
92
+ `${featureId} moved ${prevFeature.phase ?? 'unknown'} -> ${nextFeature.phase ?? 'unknown'}`,
93
+ severity,
94
+ ),
95
+ );
96
+ }
97
+
98
+ if (JSON.stringify(prevFeature.gates ?? {}) !== JSON.stringify(nextFeature.gates ?? {})) {
99
+ events.push(
100
+ buildEvent(
101
+ index++,
102
+ nowIso,
103
+ featureId,
104
+ 'gate_update',
105
+ `${featureId} gate status updated`,
106
+ gateHasFailure(nextFeature) ? 'warning' : 'info',
107
+ ),
108
+ );
109
+ }
110
+
111
+ if (prevFeature.activity_state !== nextFeature.activity_state) {
112
+ events.push(
113
+ buildEvent(
114
+ index++,
115
+ nowIso,
116
+ featureId,
117
+ 'activity_change',
118
+ `${featureId} activity ${prevFeature.activity_state ?? 'unknown'} -> ${nextFeature.activity_state ?? 'unknown'}`,
119
+ nextFeature.activity_state === 'blocked' ? 'warning' : 'info',
120
+ ),
121
+ );
122
+ }
123
+
124
+ const prevPr = prevFeature.pr ?? null;
125
+ const nextPr = nextFeature.pr ?? null;
126
+ if (JSON.stringify(prevPr) !== JSON.stringify(nextPr)) {
127
+ const ciStatus =
128
+ nextPr && typeof nextPr.ci_status === 'string' ? `CI ${nextPr.ci_status}` : 'PR updated';
129
+ events.push(
130
+ buildEvent(index++, nowIso, featureId, 'pr_update', `${featureId} ${ciStatus}`, 'info'),
131
+ );
132
+ }
133
+ }
134
+
135
+ const prevLocks = lockMapByResource(previous.lock_map);
136
+ const nextLocks = lockMapByResource(next.lock_map);
137
+ const allResources = new Set([...prevLocks.keys(), ...nextLocks.keys()]);
138
+ for (const resource of allResources) {
139
+ const prevLock = prevLocks.get(resource);
140
+ const nextLock = nextLocks.get(resource);
141
+ if (!nextLock || !prevLock) {
142
+ continue;
143
+ }
144
+ if (
145
+ prevLock.holder !== nextLock.holder ||
146
+ prevLock.is_stale !== nextLock.is_stale ||
147
+ prevLock.expires_at !== nextLock.expires_at
148
+ ) {
149
+ events.push(
150
+ buildEvent(
151
+ index++,
152
+ nowIso,
153
+ nextLock.holder ?? 'system',
154
+ 'lock_change',
155
+ `${resource} lock holder ${prevLock.holder ?? 'none'} -> ${nextLock.holder ?? 'none'}`,
156
+ nextLock.is_stale ? 'warning' : 'info',
157
+ ),
158
+ );
159
+ }
160
+ }
161
+
162
+ return events;
163
+ }
164
+
165
+ export function appendFeedEvents(existing: FeedEvent[], incoming: FeedEvent[]): FeedEvent[] {
166
+ if (incoming.length === 0) {
167
+ return existing;
168
+ }
169
+
170
+ const latestSeenBySignature = new Map<string, number>();
171
+ for (const event of existing) {
172
+ const parsed = Date.parse(event.timestamp);
173
+ if (Number.isNaN(parsed)) {
174
+ continue;
175
+ }
176
+ const signature = eventSignature(event);
177
+ const current = latestSeenBySignature.get(signature) ?? Number.MIN_SAFE_INTEGER;
178
+ if (parsed > current) {
179
+ latestSeenBySignature.set(signature, parsed);
180
+ }
181
+ }
182
+
183
+ const accepted: FeedEvent[] = [];
184
+ for (const event of incoming) {
185
+ const signature = eventSignature(event);
186
+ const eventTs = Date.parse(event.timestamp);
187
+ if (Number.isNaN(eventTs)) {
188
+ accepted.push(event);
189
+ continue;
190
+ }
191
+ const latestTs = latestSeenBySignature.get(signature);
192
+ if (latestTs != null && eventTs - latestTs < FEED_DEDUP_WINDOW_MS) {
193
+ continue;
194
+ }
195
+ latestSeenBySignature.set(signature, eventTs);
196
+ accepted.push(event);
197
+ }
198
+
199
+ if (accepted.length === 0) {
200
+ return existing;
201
+ }
202
+
203
+ return [...accepted.reverse(), ...existing].slice(0, FEED_MAX_EVENTS);
204
+ }
205
+
206
+ export function reduceLiveFeedState(state: LiveFeedState, action: LiveFeedAction): LiveFeedState {
207
+ if (action.type === 'toggle_pause') {
208
+ return {
209
+ ...state,
210
+ paused: !state.paused,
211
+ };
212
+ }
213
+
214
+ return {
215
+ ...state,
216
+ events: [],
217
+ };
218
+ }
@@ -0,0 +1,22 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import YAML from 'yaml';
4
+
5
+ export async function readPolicyYaml(repoRoot: string): Promise<Record<string, unknown> | null> {
6
+ const candidates = [
7
+ path.join(repoRoot, 'config', 'agentic', 'orchestrator', 'policy.yaml'),
8
+ path.join(repoRoot, 'agentic', 'orchestrator', 'policy.yaml'),
9
+ ];
10
+ for (const candidate of candidates) {
11
+ try {
12
+ const raw = await readFile(candidate, 'utf-8');
13
+ const parsed = YAML.parse(raw) as Record<string, unknown>;
14
+ if (parsed && typeof parsed === 'object') {
15
+ return parsed;
16
+ }
17
+ } catch {
18
+ // continue to next path
19
+ }
20
+ }
21
+ return null;
22
+ }
@@ -1,9 +1,111 @@
1
+ export type RoleStatus = 'ready' | 'running' | 'blocked' | 'done' | 'unknown';
2
+
3
+ export interface AgentPipelineStatus {
4
+ planner: RoleStatus;
5
+ builder: RoleStatus;
6
+ qa: RoleStatus;
7
+ }
8
+
9
+ export interface CostSummary {
10
+ feature_id: string;
11
+ tokens_used: number;
12
+ estimated_cost_usd: number;
13
+ recorded_at: string | null;
14
+ }
15
+
16
+ export interface QaTestIndexItem {
17
+ path: string;
18
+ status: 'pending' | 'running' | 'passed' | 'failed' | 'waived';
19
+ required_tests: string[];
20
+ last_run_at?: string;
21
+ }
22
+
23
+ export interface QaTestIndex {
24
+ feature_id: string;
25
+ version: number;
26
+ items: QaTestIndexItem[];
27
+ }
28
+
29
+ export interface LockLease {
30
+ resource: string;
31
+ holder: string | null;
32
+ expires_at: string | null;
33
+ is_stale: boolean;
34
+ }
35
+
36
+ export interface ReviewBrief {
37
+ intent_summary: string;
38
+ scope_summary: string;
39
+ contract_risk_summary: string;
40
+ feasibility_score?: number;
41
+ feasibility_breakdown?: Record<string, number>;
42
+ gate_matrix: Record<string, string>;
43
+ unresolved_questions: string[];
44
+ evidence_refs: string[];
45
+ generated_at: string;
46
+ }
47
+
48
+ export interface ProviderAnalytics {
49
+ provider: string;
50
+ model: string;
51
+ total_features: number;
52
+ success_count: number;
53
+ success_rate: number;
54
+ avg_retry_count: number;
55
+ avg_duration_ms: number;
56
+ avg_cost_usd: number;
57
+ }
58
+
59
+ export interface FeedEvent {
60
+ id: string;
61
+ timestamp: string;
62
+ feature_id: string;
63
+ type: 'phase_change' | 'gate_update' | 'activity_change' | 'pr_update' | 'lock_change';
64
+ message: string;
65
+ severity: 'info' | 'success' | 'warning' | 'error';
66
+ }
67
+
68
+ export interface GateStepResult {
69
+ name: string;
70
+ command: string;
71
+ status: 'pass' | 'fail' | 'unknown';
72
+ exit_code: number | null;
73
+ duration_ms: number | null;
74
+ log_path?: string | null;
75
+ output_tail?: string[];
76
+ }
77
+
78
+ export interface GateRunEvidence {
79
+ mode: string;
80
+ profile?: string | null;
81
+ overall: string;
82
+ finished_at?: string | null;
83
+ steps: GateStepResult[];
84
+ raw?: Record<string, unknown>;
85
+ }
86
+
87
+ export interface RuntimeSession {
88
+ run_id?: string;
89
+ owner_instance_id?: string;
90
+ provider?: string | null;
91
+ model?: string | null;
92
+ started_at?: string | null;
93
+ last_heartbeat_at?: string | null;
94
+ lease_expires_at?: string | null;
95
+ orchestrator_epoch?: number;
96
+ }
97
+
1
98
  export interface FeatureSummary {
2
99
  id: string;
3
100
  feature_id: string;
4
101
  status: string;
5
102
  phase?: 'planning' | 'building' | 'qa' | 'ready_to_merge' | 'merged' | 'blocked' | 'unknown';
6
103
  plan_version?: number;
104
+ revision_of?: number | null;
105
+ revision_reason?: string | null;
106
+ role_status?: AgentPipelineStatus;
107
+ gate_retry_count?: number;
108
+ last_retry_at?: string | null;
7
109
  locks_held?: string[];
8
110
  gates?: {
9
111
  fast?: string;
@@ -46,6 +148,9 @@ export interface FeaturesIndex {
46
148
  blocked: string[];
47
149
  merged: string[];
48
150
  blocked_queue?: Array<{ feature_id: string; detected_at: string; collision_fingerprint: string }>;
151
+ lock_leases?: Record<string, { holder: string; lease_expires_at: string; stale?: boolean }>;
152
+ dep_blocked?: Array<{ feature_id: string; depends_on_unresolved: string[] }>;
153
+ runtime_sessions?: RuntimeSession;
49
154
  }
50
155
 
51
156
  export interface SSEEvent {
@@ -66,9 +171,18 @@ export interface FeatureDetail {
66
171
  plan: Record<string, unknown> | null;
67
172
  diff: string;
68
173
  evidence: EvidenceArtifact[];
174
+ gate_evidence?: GateRunEvidence | null;
175
+ cost?: CostSummary | null;
176
+ qa_test_index?: QaTestIndex | null;
177
+ review_brief?: ReviewBrief | null;
69
178
  }
70
179
 
71
180
  export interface DashboardStatusPayload {
72
181
  index: FeaturesIndex;
73
182
  features: FeatureSummary[];
183
+ runtime?: RuntimeSession;
184
+ lock_map?: LockLease[];
185
+ dep_blocked?: Array<{ feature_id: string; depends_on_unresolved: string[] }>;
186
+ flaky_suspects?: string[];
187
+ metrics?: { total_cost_today_usd?: number; merge_histogram_14d?: number[] };
74
188
  }