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,336 @@
1
+ import Link from 'next/link';
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { buildFeatureFocusHref, formatRelativeTime, getGateMap } from '@/lib/dashboard-utils.js';
4
+ import type { FeatureSummary } from '@/lib/types.js';
5
+ import styles from '@/styles/dashboard.module.css';
6
+
7
+ interface FeatureListViewProps {
8
+ features: FeatureSummary[];
9
+ project: string;
10
+ selectedFeatureId: string | null;
11
+ focusedFeatureId: string | null;
12
+ onSelectFeature: (featureId: string) => void;
13
+ onFocusFeature: (featureId: string) => void;
14
+ }
15
+
16
+ type SortColumn = 'feature_id' | 'phase' | 'status' | 'updated';
17
+ type SortDirection = 'asc' | 'desc';
18
+
19
+ const LIST_PAGINATION_THRESHOLD = 150;
20
+ const LIST_PAGE_SIZE = 100;
21
+
22
+ function compareFeatures(
23
+ left: FeatureSummary,
24
+ right: FeatureSummary,
25
+ column: SortColumn,
26
+ direction: SortDirection,
27
+ ): number {
28
+ const multiplier = direction === 'asc' ? 1 : -1;
29
+ if (column === 'feature_id') {
30
+ return left.feature_id.localeCompare(right.feature_id) * multiplier;
31
+ }
32
+ if (column === 'phase') {
33
+ return (
34
+ String(left.phase ?? 'unknown').localeCompare(String(right.phase ?? 'unknown')) * multiplier
35
+ );
36
+ }
37
+ if (column === 'status') {
38
+ return left.status.localeCompare(right.status) * multiplier;
39
+ }
40
+ const leftMs = left.last_updated ? Date.parse(left.last_updated) : 0;
41
+ const rightMs = right.last_updated ? Date.parse(right.last_updated) : 0;
42
+ return (leftMs - rightMs) * multiplier;
43
+ }
44
+
45
+ function pageForIndex(index: number): number {
46
+ return Math.floor(index / LIST_PAGE_SIZE);
47
+ }
48
+
49
+ export function FeatureListView({
50
+ features,
51
+ project,
52
+ selectedFeatureId,
53
+ focusedFeatureId,
54
+ onSelectFeature,
55
+ onFocusFeature,
56
+ }: FeatureListViewProps) {
57
+ const [sortColumn, setSortColumn] = useState<SortColumn>('updated');
58
+ const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
59
+ const [page, setPage] = useState(0);
60
+ const [openActionMenuFeatureId, setOpenActionMenuFeatureId] = useState<string | null>(null);
61
+
62
+ const sorted = useMemo(() => {
63
+ return [...features].sort((left, right) =>
64
+ compareFeatures(left, right, sortColumn, sortDirection),
65
+ );
66
+ }, [features, sortColumn, sortDirection]);
67
+
68
+ const paginationEnabled = sorted.length > LIST_PAGINATION_THRESHOLD;
69
+ const totalPages = paginationEnabled ? Math.ceil(sorted.length / LIST_PAGE_SIZE) : 1;
70
+ const currentPage = Math.min(page, totalPages - 1);
71
+ const pageStart = paginationEnabled ? currentPage * LIST_PAGE_SIZE : 0;
72
+ const pageEnd = paginationEnabled
73
+ ? Math.min(sorted.length, pageStart + LIST_PAGE_SIZE)
74
+ : sorted.length;
75
+ const visibleRows = sorted
76
+ .slice(pageStart, pageEnd)
77
+ .map((feature, localIndex) => ({ feature, globalIndex: pageStart + localIndex }));
78
+
79
+ const updateSort = (column: SortColumn) => {
80
+ if (sortColumn === column) {
81
+ setSortDirection((current) => (current === 'asc' ? 'desc' : 'asc'));
82
+ return;
83
+ }
84
+ setSortColumn(column);
85
+ setSortDirection(column === 'updated' ? 'desc' : 'asc');
86
+ };
87
+
88
+ const sortStateLabel = (column: SortColumn): 'none' | 'ascending' | 'descending' => {
89
+ if (sortColumn !== column) {
90
+ return 'none';
91
+ }
92
+ return sortDirection === 'asc' ? 'ascending' : 'descending';
93
+ };
94
+
95
+ useEffect(() => {
96
+ setPage((current) => Math.min(current, Math.max(0, totalPages - 1)));
97
+ }, [totalPages]);
98
+
99
+ useEffect(() => {
100
+ setPage(0);
101
+ }, [sortColumn, sortDirection]);
102
+
103
+ useEffect(() => {
104
+ if (!paginationEnabled) {
105
+ return;
106
+ }
107
+ const trackedFeatureId = focusedFeatureId ?? selectedFeatureId;
108
+ if (!trackedFeatureId) {
109
+ return;
110
+ }
111
+ const index = sorted.findIndex((feature) => feature.feature_id === trackedFeatureId);
112
+ if (index < 0) {
113
+ return;
114
+ }
115
+ const nextPage = pageForIndex(index);
116
+ setPage((current) => (current === nextPage ? current : nextPage));
117
+ }, [focusedFeatureId, paginationEnabled, selectedFeatureId, sorted]);
118
+
119
+ if (sorted.length === 0) {
120
+ return (
121
+ <section className={styles.section} aria-label="Feature list view">
122
+ <h2 className={styles.sectionTitle}>List View</h2>
123
+ <p className={styles.metaText}>No features match current filters.</p>
124
+ </section>
125
+ );
126
+ }
127
+
128
+ return (
129
+ <section className={styles.section} aria-label="Feature list view">
130
+ <h2 className={styles.sectionTitle}>List View</h2>
131
+ {paginationEnabled ? (
132
+ <div className={styles.listPagination}>
133
+ <span className={styles.metaText}>
134
+ Showing {pageStart + 1}-{pageEnd} of {sorted.length} features
135
+ </span>
136
+ <div className={styles.inlineActions}>
137
+ <button
138
+ type="button"
139
+ className={styles.button}
140
+ onClick={() => setPage((current) => Math.max(0, current - 1))}
141
+ disabled={currentPage === 0}
142
+ >
143
+ Previous
144
+ </button>
145
+ <span className={styles.metaText}>
146
+ Page {currentPage + 1} of {totalPages}
147
+ </span>
148
+ <button
149
+ type="button"
150
+ className={styles.button}
151
+ onClick={() => setPage((current) => Math.min(totalPages - 1, current + 1))}
152
+ disabled={currentPage >= totalPages - 1}
153
+ >
154
+ Next
155
+ </button>
156
+ </div>
157
+ </div>
158
+ ) : null}
159
+ <div className={styles.tableWrapper}>
160
+ <table className={styles.listTable}>
161
+ <thead>
162
+ <tr>
163
+ <th aria-sort={sortStateLabel('feature_id')}>
164
+ <button
165
+ type="button"
166
+ className={styles.listHeaderButton}
167
+ onClick={() => updateSort('feature_id')}
168
+ >
169
+ Feature
170
+ </button>
171
+ </th>
172
+ <th aria-sort={sortStateLabel('phase')}>
173
+ <button
174
+ type="button"
175
+ className={styles.listHeaderButton}
176
+ onClick={() => updateSort('phase')}
177
+ >
178
+ Phase
179
+ </button>
180
+ </th>
181
+ <th aria-sort={sortStateLabel('status')}>
182
+ <button
183
+ type="button"
184
+ className={styles.listHeaderButton}
185
+ onClick={() => updateSort('status')}
186
+ >
187
+ Status
188
+ </button>
189
+ </th>
190
+ <th aria-sort={sortStateLabel('updated')}>
191
+ <button
192
+ type="button"
193
+ className={styles.listHeaderButton}
194
+ onClick={() => updateSort('updated')}
195
+ >
196
+ Updated
197
+ </button>
198
+ </th>
199
+ <th>Gates</th>
200
+ <th>Actions</th>
201
+ </tr>
202
+ </thead>
203
+ <tbody>
204
+ {visibleRows.map(({ feature, globalIndex }) => {
205
+ const gates = getGateMap(feature);
206
+ const selected = selectedFeatureId === feature.feature_id;
207
+ const focused = focusedFeatureId === feature.feature_id;
208
+ const actionMenuOpen = openActionMenuFeatureId === feature.feature_id;
209
+ return (
210
+ <tr
211
+ key={feature.feature_id}
212
+ tabIndex={focused ? 0 : -1}
213
+ className={[
214
+ styles.listRow,
215
+ selected ? styles.listRowSelected : '',
216
+ focused ? styles.listRowFocused : '',
217
+ ].join(' ')}
218
+ onFocus={() => onFocusFeature(feature.feature_id)}
219
+ onClick={() => onSelectFeature(feature.feature_id)}
220
+ onKeyDown={(event) => {
221
+ if (event.key === 'Enter') {
222
+ event.preventDefault();
223
+ onSelectFeature(feature.feature_id);
224
+ return;
225
+ }
226
+ if (event.key === 'ArrowDown') {
227
+ event.preventDefault();
228
+ const nextIndex = Math.min(globalIndex + 1, sorted.length - 1);
229
+ const next = sorted[nextIndex];
230
+ if (next) {
231
+ if (paginationEnabled) {
232
+ setPage(pageForIndex(nextIndex));
233
+ }
234
+ onFocusFeature(next.feature_id);
235
+ }
236
+ return;
237
+ }
238
+ if (event.key === 'ArrowUp') {
239
+ event.preventDefault();
240
+ const nextIndex = Math.max(globalIndex - 1, 0);
241
+ const next = sorted[nextIndex];
242
+ if (next) {
243
+ if (paginationEnabled) {
244
+ setPage(pageForIndex(nextIndex));
245
+ }
246
+ onFocusFeature(next.feature_id);
247
+ }
248
+ }
249
+ }}
250
+ >
251
+ <td>{feature.feature_id}</td>
252
+ <td>{feature.phase ?? 'unknown'}</td>
253
+ <td>{feature.status}</td>
254
+ <td>{formatRelativeTime(feature.last_updated)}</td>
255
+ <td>
256
+ <span className={styles.metaText}>
257
+ fast:{gates.fast} full:{gates.full} merge:{gates.merge}
258
+ </span>
259
+ </td>
260
+ <td>
261
+ <div className={styles.listActions}>
262
+ <button
263
+ type="button"
264
+ className={styles.button}
265
+ onClick={(event) => {
266
+ event.stopPropagation();
267
+ onSelectFeature(feature.feature_id);
268
+ }}
269
+ >
270
+ Open
271
+ </button>
272
+ <div
273
+ className={styles.listActionMenuWrap}
274
+ onBlur={(event) => {
275
+ const nextFocus = event.relatedTarget as Node | null;
276
+ if (!event.currentTarget.contains(nextFocus)) {
277
+ setOpenActionMenuFeatureId((current) =>
278
+ current === feature.feature_id ? null : current,
279
+ );
280
+ }
281
+ }}
282
+ >
283
+ <button
284
+ type="button"
285
+ className={`${styles.button} ${styles.buttonGhost}`}
286
+ aria-haspopup="menu"
287
+ aria-expanded={actionMenuOpen}
288
+ onClick={(event) => {
289
+ event.stopPropagation();
290
+ setOpenActionMenuFeatureId((current) =>
291
+ current === feature.feature_id ? null : feature.feature_id,
292
+ );
293
+ }}
294
+ >
295
+ Actions
296
+ </button>
297
+ {actionMenuOpen ? (
298
+ <div
299
+ className={styles.listActionMenu}
300
+ role="menu"
301
+ aria-label="Feature actions"
302
+ >
303
+ <button
304
+ type="button"
305
+ className={styles.listActionMenuItem}
306
+ role="menuitem"
307
+ onClick={(event) => {
308
+ event.stopPropagation();
309
+ onSelectFeature(feature.feature_id);
310
+ setOpenActionMenuFeatureId(null);
311
+ }}
312
+ >
313
+ Open In Board
314
+ </button>
315
+ <Link
316
+ href={buildFeatureFocusHref(feature.feature_id, project)}
317
+ className={styles.listActionMenuItem}
318
+ role="menuitem"
319
+ onClick={() => setOpenActionMenuFeatureId(null)}
320
+ >
321
+ Open Focus View
322
+ </Link>
323
+ </div>
324
+ ) : null}
325
+ </div>
326
+ </div>
327
+ </td>
328
+ </tr>
329
+ );
330
+ })}
331
+ </tbody>
332
+ </table>
333
+ </div>
334
+ </section>
335
+ );
336
+ }
@@ -0,0 +1,92 @@
1
+ import type { GateRunEvidence } from '@/lib/types.js';
2
+ import styles from '@/styles/dashboard.module.css';
3
+
4
+ interface GateStepDrilldownProps {
5
+ evidence: GateRunEvidence | null | undefined;
6
+ }
7
+
8
+ function stepBadgeClass(status: string): string {
9
+ const normalized = status.toLowerCase();
10
+ if (normalized === 'pass') {
11
+ return styles.badgePass;
12
+ }
13
+ if (normalized === 'fail') {
14
+ return styles.badgeFail;
15
+ }
16
+ return styles.badgeNeutral;
17
+ }
18
+
19
+ function overallClass(status: string): string {
20
+ const normalized = status.toLowerCase();
21
+ if (normalized === 'pass' || normalized === 'success') {
22
+ return styles.badgePass;
23
+ }
24
+ if (normalized === 'fail' || normalized === 'error') {
25
+ return styles.badgeFail;
26
+ }
27
+ return styles.badgeNeutral;
28
+ }
29
+
30
+ export function GateStepDrilldown({ evidence }: GateStepDrilldownProps) {
31
+ if (!evidence) {
32
+ return (
33
+ <div className={styles.section}>
34
+ <h3 className={styles.sectionTitle}>Gate Step Drill-Down</h3>
35
+ <p className={styles.metaText}>No gate step evidence recorded yet.</p>
36
+ </div>
37
+ );
38
+ }
39
+
40
+ if (!Array.isArray(evidence.steps) || evidence.steps.length === 0) {
41
+ return (
42
+ <details className={styles.section}>
43
+ <summary className={styles.sectionSummary}>Gate Step Drill-Down</summary>
44
+ <p className={styles.metaText}>Unknown gate artifact shape. Rendering raw evidence.</p>
45
+ <pre className={styles.preContent}>{JSON.stringify(evidence.raw ?? evidence, null, 2)}</pre>
46
+ </details>
47
+ );
48
+ }
49
+
50
+ const firstFailedIndex = evidence.steps.findIndex((step) => step.status === 'fail');
51
+
52
+ return (
53
+ <details className={styles.section} open>
54
+ <summary className={styles.sectionSummary}>Gate Step Drill-Down</summary>
55
+ <div className={styles.badgeRow}>
56
+ <span className={`${styles.badge} ${overallClass(evidence.overall)}`}>
57
+ Overall {evidence.overall}
58
+ </span>
59
+ <span className={`${styles.badge} ${styles.badgeNeutral}`}>Mode {evidence.mode}</span>
60
+ <span className={`${styles.badge} ${styles.badgeNeutral}`}>
61
+ Profile {evidence.profile ?? 'unknown'}
62
+ </span>
63
+ </div>
64
+
65
+ {evidence.steps.map((step, index) => (
66
+ <details
67
+ key={`${step.name}-${index}`}
68
+ className={styles.section}
69
+ open={index === firstFailedIndex}
70
+ >
71
+ <summary className={styles.sectionSummary}>
72
+ <span className={`${styles.badge} ${stepBadgeClass(step.status)}`}>{step.status}</span>{' '}
73
+ {step.name}
74
+ </summary>
75
+ <p className={styles.metaText}>Command: {step.command || '(not recorded)'}</p>
76
+ <p className={styles.metaText}>
77
+ Exit code: {step.exit_code ?? 'unknown'} • Duration:{' '}
78
+ {typeof step.duration_ms === 'number' ? `${step.duration_ms}ms` : 'unknown'}
79
+ </p>
80
+ {step.output_tail && step.output_tail.length > 0 ? (
81
+ <details>
82
+ <summary className={styles.sectionSummary}>
83
+ Output tail ({step.output_tail.length} lines)
84
+ </summary>
85
+ <pre className={styles.preContent}>{step.output_tail.join('\n')}</pre>
86
+ </details>
87
+ ) : null}
88
+ </details>
89
+ ))}
90
+ </details>
91
+ );
92
+ }
@@ -8,6 +8,7 @@ interface KanbanBoardProps {
8
8
  hasActiveFilters: boolean;
9
9
  selectedFeatureId: string | null;
10
10
  focusedFeatureId: string | null;
11
+ dependencyBlockedFeatureIds: Set<string>;
11
12
  onSelectFeature: (featureId: string) => void;
12
13
  onFocusFeature: (featureId: string) => void;
13
14
  }
@@ -31,6 +32,7 @@ export function KanbanBoard({
31
32
  hasActiveFilters,
32
33
  selectedFeatureId,
33
34
  focusedFeatureId,
35
+ dependencyBlockedFeatureIds,
34
36
  onSelectFeature,
35
37
  onFocusFeature,
36
38
  }: KanbanBoardProps) {
@@ -58,6 +60,7 @@ export function KanbanBoard({
58
60
  feature={feature}
59
61
  isSelected={selectedFeatureId === feature.feature_id}
60
62
  isFocused={focusedFeatureId === feature.feature_id}
63
+ awaitingDependencies={dependencyBlockedFeatureIds.has(feature.feature_id)}
61
64
  onSelect={onSelectFeature}
62
65
  onFocus={onFocusFeature}
63
66
  />
@@ -0,0 +1,60 @@
1
+ import type { FeedEvent } from '@/lib/types.js';
2
+ import styles from '@/styles/dashboard.module.css';
3
+
4
+ interface LiveEventFeedProps {
5
+ events: FeedEvent[];
6
+ paused: boolean;
7
+ onTogglePause: () => void;
8
+ onClear: () => void;
9
+ }
10
+
11
+ function severityClass(severity: FeedEvent['severity']): string {
12
+ if (severity === 'success') {
13
+ return styles.badgePass;
14
+ }
15
+ if (severity === 'warning') {
16
+ return styles.badgePending;
17
+ }
18
+ if (severity === 'error') {
19
+ return styles.badgeFail;
20
+ }
21
+ return styles.badgeNeutral;
22
+ }
23
+
24
+ export function LiveEventFeed({ events, paused, onTogglePause, onClear }: LiveEventFeedProps) {
25
+ return (
26
+ <details className={styles.section}>
27
+ <summary className={styles.sectionSummary}>Live Event Feed ({events.length})</summary>
28
+ <div className={styles.inlineActions}>
29
+ <button type="button" className={styles.button} onClick={onTogglePause}>
30
+ {paused ? 'Resume' : 'Pause'}
31
+ </button>
32
+ <button
33
+ type="button"
34
+ className={`${styles.button} ${styles.buttonGhost}`}
35
+ onClick={onClear}
36
+ >
37
+ Clear
38
+ </button>
39
+ </div>
40
+ {events.length === 0 ? (
41
+ <p className={styles.metaText}>No recent cross-feature events.</p>
42
+ ) : (
43
+ <ul className={styles.logList}>
44
+ {events.map((event) => (
45
+ <li key={event.id} className={styles.logListItem}>
46
+ <div className={styles.badgeRow}>
47
+ <span className={`${styles.badge} ${severityClass(event.severity)}`}>
48
+ {event.severity}
49
+ </span>
50
+ <span className={`${styles.badge} ${styles.badgeNeutral}`}>{event.type}</span>
51
+ <span className={styles.metaText}>{event.timestamp}</span>
52
+ </div>
53
+ <p className={styles.metaText}>{event.message}</p>
54
+ </li>
55
+ ))}
56
+ </ul>
57
+ )}
58
+ </details>
59
+ );
60
+ }
@@ -0,0 +1,73 @@
1
+ import type { LockLease } from '@/lib/types.js';
2
+ import styles from '@/styles/dashboard.module.css';
3
+
4
+ interface LockResourceMapProps {
5
+ locks: LockLease[];
6
+ nowMs: number;
7
+ onSelectFeature: (featureId: string) => void;
8
+ }
9
+
10
+ function remainingText(expiresAt: string | null, nowMs: number): string {
11
+ if (!expiresAt) {
12
+ return '-';
13
+ }
14
+ const parsed = Date.parse(expiresAt);
15
+ if (Number.isNaN(parsed)) {
16
+ return '-';
17
+ }
18
+ const seconds = Math.max(0, Math.floor((parsed - nowMs) / 1000));
19
+ const minutes = Math.floor(seconds / 60);
20
+ return `${minutes}m ${seconds % 60}s`;
21
+ }
22
+
23
+ export function LockResourceMap({ locks, nowMs, onSelectFeature }: LockResourceMapProps) {
24
+ return (
25
+ <section className={styles.section}>
26
+ <h2 className={styles.sectionTitle}>Lock Resource Map</h2>
27
+ {locks.length === 0 ? (
28
+ <p className={styles.metaText}>No active lock leases.</p>
29
+ ) : (
30
+ <div className={styles.tableWrapper}>
31
+ <table className={styles.table}>
32
+ <thead>
33
+ <tr>
34
+ <th>Resource</th>
35
+ <th>Holder</th>
36
+ <th>Expires in</th>
37
+ <th>Status</th>
38
+ </tr>
39
+ </thead>
40
+ <tbody>
41
+ {locks.map((lock) => (
42
+ <tr key={lock.resource}>
43
+ <td>{lock.resource}</td>
44
+ <td>
45
+ {lock.holder ? (
46
+ <button
47
+ type="button"
48
+ className={styles.listHeaderButton}
49
+ onClick={() => onSelectFeature(lock.holder!)}
50
+ >
51
+ {lock.holder}
52
+ </button>
53
+ ) : (
54
+ <span className={styles.metaText}>-</span>
55
+ )}
56
+ </td>
57
+ <td>{remainingText(lock.expires_at, nowMs)}</td>
58
+ <td>
59
+ {lock.is_stale ? (
60
+ <span className={`${styles.badge} ${styles.badgeFail}`}>Stale lease</span>
61
+ ) : (
62
+ <span className={`${styles.badge} ${styles.badgePass}`}>Active</span>
63
+ )}
64
+ </td>
65
+ </tr>
66
+ ))}
67
+ </tbody>
68
+ </table>
69
+ </div>
70
+ )}
71
+ </section>
72
+ );
73
+ }
@@ -0,0 +1,55 @@
1
+ import styles from '@/styles/dashboard.module.css';
2
+
3
+ interface PlanRevisionTimelineProps {
4
+ plan: Record<string, unknown> | null;
5
+ featurePlanVersion?: number;
6
+ featureRevisionOf?: number | null;
7
+ featureRevisionReason?: string | null;
8
+ }
9
+
10
+ function asText(value: unknown, fallback = '-'): string {
11
+ if (typeof value === 'string' && value.trim().length > 0) {
12
+ return value;
13
+ }
14
+ if (typeof value === 'number') {
15
+ return String(value);
16
+ }
17
+ return fallback;
18
+ }
19
+
20
+ export function PlanRevisionTimeline({
21
+ plan,
22
+ featurePlanVersion,
23
+ featureRevisionOf,
24
+ featureRevisionReason,
25
+ }: PlanRevisionTimelineProps) {
26
+ const planVersion =
27
+ typeof plan?.plan_version === 'number' ? plan.plan_version : (featurePlanVersion ?? null);
28
+ const revisionOf =
29
+ typeof plan?.revision_of === 'number' ? plan.revision_of : (featureRevisionOf ?? null);
30
+ const revisionReason =
31
+ typeof plan?.revision_reason === 'string'
32
+ ? plan.revision_reason
33
+ : (featureRevisionReason ?? null);
34
+
35
+ return (
36
+ <details className={styles.section} open>
37
+ <summary className={styles.sectionSummary}>Plan Revision History</summary>
38
+ <ul className={styles.planList}>
39
+ <li className={styles.planListItem}>
40
+ <strong>Current plan version:</strong> {asText(planVersion)}
41
+ </li>
42
+ <li className={styles.planListItem}>
43
+ <strong>Revision of:</strong>{' '}
44
+ {revisionOf == null ? 'No prior revision metadata' : String(revisionOf)}
45
+ </li>
46
+ <li className={styles.planListItem}>
47
+ <strong>Revision reason:</strong>{' '}
48
+ {revisionReason && revisionReason.trim().length > 0
49
+ ? revisionReason
50
+ : 'Only current plan metadata is retained'}
51
+ </li>
52
+ </ul>
53
+ </details>
54
+ );
55
+ }