agentic-orchestrator 0.1.26 → 0.1.28

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 (172) hide show
  1. package/AGENTS.md +2 -2
  2. package/CLAUDE.md +2 -2
  3. package/README.md +47 -14
  4. package/agentic/orchestrator/agents.yaml +13 -0
  5. package/agentic/orchestrator/policy.yaml +3 -0
  6. package/agentic/orchestrator/schemas/agents.schema.json +76 -0
  7. package/agentic/orchestrator/schemas/policy.schema.json +16 -0
  8. package/agentic/orchestrator/schemas/policy.user.schema.json +16 -0
  9. package/agentic/orchestrator/schemas/state.schema.json +53 -0
  10. package/apps/control-plane/src/application/configuration-service.ts +181 -0
  11. package/apps/control-plane/src/application/kernel-tool-wiring.ts +292 -0
  12. package/apps/control-plane/src/application/services/checkpoint-service.ts +523 -0
  13. package/apps/control-plane/src/application/services/feature-send-message-service.ts +132 -0
  14. package/apps/control-plane/src/application/services/patch-service.ts +29 -5
  15. package/apps/control-plane/src/application/services/repo-operations-service.ts +276 -0
  16. package/apps/control-plane/src/application/services/worktree-watchdog-service.ts +156 -0
  17. package/apps/control-plane/src/cli/cli-argument-parser.ts +12 -0
  18. package/apps/control-plane/src/cli/help-command-handler.ts +17 -0
  19. package/apps/control-plane/src/cli/init-command-handler.ts +31 -0
  20. package/apps/control-plane/src/cli/resume-command-handler.ts +31 -4
  21. package/apps/control-plane/src/cli/rollback-command-handler.ts +217 -0
  22. package/apps/control-plane/src/cli/run-command-handler.ts +8 -0
  23. package/apps/control-plane/src/cli/types.ts +3 -0
  24. package/apps/control-plane/src/core/kernel-types.ts +55 -0
  25. package/apps/control-plane/src/core/kernel.ts +61 -878
  26. package/apps/control-plane/src/core/tool-caller.ts +10 -0
  27. package/apps/control-plane/src/core/utils/field-readers.ts +38 -0
  28. package/apps/control-plane/src/core/utils/index-normalizer.ts +119 -0
  29. package/apps/control-plane/src/core/utils/path-normalizers.ts +22 -0
  30. package/apps/control-plane/src/interfaces/cli/bootstrap.ts +15 -0
  31. package/apps/control-plane/src/providers/api-worker-provider.ts +14 -12
  32. package/apps/control-plane/src/providers/cli-worker-provider.ts +82 -12
  33. package/apps/control-plane/src/providers/providers.ts +45 -24
  34. package/apps/control-plane/src/providers/worker-provider-factory.ts +36 -1
  35. package/apps/control-plane/src/supervisor/run-coordinator.ts +91 -36
  36. package/apps/control-plane/src/supervisor/runtime.ts +107 -1
  37. package/apps/control-plane/src/supervisor/types.ts +9 -0
  38. package/apps/control-plane/src/supervisor/worker-decision-loop.ts +253 -14
  39. package/apps/control-plane/test/checkpoint-service.spec.ts +537 -0
  40. package/apps/control-plane/test/cli-helpers.spec.ts +28 -0
  41. package/apps/control-plane/test/cli.unit.spec.ts +52 -0
  42. package/apps/control-plane/test/configuration-service.spec.ts +466 -0
  43. package/apps/control-plane/test/dashboard-api.integration.spec.ts +537 -0
  44. package/apps/control-plane/test/dashboard-client.spec.ts +233 -0
  45. package/apps/control-plane/test/feature-send-message-service.spec.ts +314 -0
  46. package/apps/control-plane/test/init-wizard.spec.ts +35 -0
  47. package/apps/control-plane/test/path-normalizers.spec.ts +41 -0
  48. package/apps/control-plane/test/repo-operations-service.spec.ts +339 -0
  49. package/apps/control-plane/test/resume-command.spec.ts +33 -0
  50. package/apps/control-plane/test/review-workspace-logic.spec.ts +130 -0
  51. package/apps/control-plane/test/rollback-command.spec.ts +208 -0
  52. package/apps/control-plane/test/run-coordinator.spec.ts +119 -0
  53. package/apps/control-plane/test/worker-decision-loop.spec.ts +209 -0
  54. package/apps/control-plane/test/worker-provider-adapters.spec.ts +102 -0
  55. package/apps/control-plane/test/worker-provider-factory.spec.ts +14 -0
  56. package/apps/control-plane/test/worktree-watchdog-service.spec.ts +147 -0
  57. package/config/agentic/orchestrator/agents.yaml +13 -0
  58. package/dist/apps/control-plane/application/configuration-service.d.ts +19 -0
  59. package/dist/apps/control-plane/application/configuration-service.js +123 -0
  60. package/dist/apps/control-plane/application/configuration-service.js.map +1 -0
  61. package/dist/apps/control-plane/application/kernel-tool-wiring.d.ts +39 -0
  62. package/dist/apps/control-plane/application/kernel-tool-wiring.js +38 -0
  63. package/dist/apps/control-plane/application/kernel-tool-wiring.js.map +1 -0
  64. package/dist/apps/control-plane/application/services/checkpoint-service.d.ts +84 -0
  65. package/dist/apps/control-plane/application/services/checkpoint-service.js +367 -0
  66. package/dist/apps/control-plane/application/services/checkpoint-service.js.map +1 -0
  67. package/dist/apps/control-plane/application/services/feature-send-message-service.d.ts +25 -0
  68. package/dist/apps/control-plane/application/services/feature-send-message-service.js +105 -0
  69. package/dist/apps/control-plane/application/services/feature-send-message-service.js.map +1 -0
  70. package/dist/apps/control-plane/application/services/patch-service.d.ts +6 -0
  71. package/dist/apps/control-plane/application/services/patch-service.js +11 -2
  72. package/dist/apps/control-plane/application/services/patch-service.js.map +1 -1
  73. package/dist/apps/control-plane/application/services/repo-operations-service.d.ts +70 -0
  74. package/dist/apps/control-plane/application/services/repo-operations-service.js +213 -0
  75. package/dist/apps/control-plane/application/services/repo-operations-service.js.map +1 -0
  76. package/dist/apps/control-plane/application/services/worktree-watchdog-service.d.ts +23 -0
  77. package/dist/apps/control-plane/application/services/worktree-watchdog-service.js +119 -0
  78. package/dist/apps/control-plane/application/services/worktree-watchdog-service.js.map +1 -0
  79. package/dist/apps/control-plane/cli/cli-argument-parser.js +12 -0
  80. package/dist/apps/control-plane/cli/cli-argument-parser.js.map +1 -1
  81. package/dist/apps/control-plane/cli/help-command-handler.js +17 -0
  82. package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -1
  83. package/dist/apps/control-plane/cli/init-command-handler.js +23 -0
  84. package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -1
  85. package/dist/apps/control-plane/cli/resume-command-handler.js +25 -5
  86. package/dist/apps/control-plane/cli/resume-command-handler.js.map +1 -1
  87. package/dist/apps/control-plane/cli/rollback-command-handler.d.ts +6 -0
  88. package/dist/apps/control-plane/cli/rollback-command-handler.js +177 -0
  89. package/dist/apps/control-plane/cli/rollback-command-handler.js.map +1 -0
  90. package/dist/apps/control-plane/cli/run-command-handler.js +7 -1
  91. package/dist/apps/control-plane/cli/run-command-handler.js.map +1 -1
  92. package/dist/apps/control-plane/cli/types.d.ts +3 -0
  93. package/dist/apps/control-plane/cli/types.js +1 -0
  94. package/dist/apps/control-plane/cli/types.js.map +1 -1
  95. package/dist/apps/control-plane/core/configuration-service.d.ts +25 -0
  96. package/dist/apps/control-plane/core/configuration-service.js +130 -0
  97. package/dist/apps/control-plane/core/configuration-service.js.map +1 -0
  98. package/dist/apps/control-plane/core/kernel-tool-wiring.d.ts +50 -0
  99. package/dist/apps/control-plane/core/kernel-tool-wiring.js +44 -0
  100. package/dist/apps/control-plane/core/kernel-tool-wiring.js.map +1 -0
  101. package/dist/apps/control-plane/core/kernel-types.d.ts +48 -0
  102. package/dist/apps/control-plane/core/kernel-types.js +2 -0
  103. package/dist/apps/control-plane/core/kernel-types.js.map +1 -0
  104. package/dist/apps/control-plane/core/kernel.d.ts +17 -48
  105. package/dist/apps/control-plane/core/kernel.js +44 -539
  106. package/dist/apps/control-plane/core/kernel.js.map +1 -1
  107. package/dist/apps/control-plane/core/tool-caller.d.ts +10 -0
  108. package/dist/apps/control-plane/core/utils/error-normalizer.d.ts +2 -0
  109. package/dist/apps/control-plane/core/utils/error-normalizer.js +51 -0
  110. package/dist/apps/control-plane/core/utils/error-normalizer.js.map +1 -0
  111. package/dist/apps/control-plane/core/utils/field-readers.d.ts +9 -0
  112. package/dist/apps/control-plane/core/utils/field-readers.js +30 -0
  113. package/dist/apps/control-plane/core/utils/field-readers.js.map +1 -0
  114. package/dist/apps/control-plane/core/utils/index-normalizer.d.ts +7 -0
  115. package/dist/apps/control-plane/core/utils/index-normalizer.js +92 -0
  116. package/dist/apps/control-plane/core/utils/index-normalizer.js.map +1 -0
  117. package/dist/apps/control-plane/core/utils/path-normalizers.d.ts +2 -0
  118. package/dist/apps/control-plane/core/utils/path-normalizers.js +17 -0
  119. package/dist/apps/control-plane/core/utils/path-normalizers.js.map +1 -0
  120. package/dist/apps/control-plane/interfaces/cli/bootstrap.js +13 -1
  121. package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
  122. package/dist/apps/control-plane/providers/api-worker-provider.d.ts +4 -13
  123. package/dist/apps/control-plane/providers/api-worker-provider.js +10 -0
  124. package/dist/apps/control-plane/providers/api-worker-provider.js.map +1 -1
  125. package/dist/apps/control-plane/providers/cli-worker-provider.d.ts +11 -13
  126. package/dist/apps/control-plane/providers/cli-worker-provider.js +64 -0
  127. package/dist/apps/control-plane/providers/cli-worker-provider.js.map +1 -1
  128. package/dist/apps/control-plane/providers/providers.d.ts +31 -24
  129. package/dist/apps/control-plane/providers/providers.js +10 -0
  130. package/dist/apps/control-plane/providers/providers.js.map +1 -1
  131. package/dist/apps/control-plane/providers/worker-provider-factory.d.ts +11 -0
  132. package/dist/apps/control-plane/providers/worker-provider-factory.js +20 -1
  133. package/dist/apps/control-plane/providers/worker-provider-factory.js.map +1 -1
  134. package/dist/apps/control-plane/supervisor/run-coordinator.d.ts +3 -0
  135. package/dist/apps/control-plane/supervisor/run-coordinator.js +81 -33
  136. package/dist/apps/control-plane/supervisor/run-coordinator.js.map +1 -1
  137. package/dist/apps/control-plane/supervisor/runtime.d.ts +8 -1
  138. package/dist/apps/control-plane/supervisor/runtime.js +90 -0
  139. package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
  140. package/dist/apps/control-plane/supervisor/types.d.ts +11 -0
  141. package/dist/apps/control-plane/supervisor/types.js.map +1 -1
  142. package/dist/apps/control-plane/supervisor/worker-decision-loop.d.ts +21 -1
  143. package/dist/apps/control-plane/supervisor/worker-decision-loop.js +207 -13
  144. package/dist/apps/control-plane/supervisor/worker-decision-loop.js.map +1 -1
  145. package/package.json +1 -1
  146. package/packages/web-dashboard/package.json +2 -0
  147. package/packages/web-dashboard/src/app/analytics/page.tsx +83 -2
  148. package/packages/web-dashboard/src/app/api/actions/route.ts +92 -1
  149. package/packages/web-dashboard/src/app/api/analytics/route.ts +5 -2
  150. package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/[checkpointId]/diff/route.ts +43 -0
  151. package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/compare/route.ts +45 -0
  152. package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/stream/route.ts +170 -0
  153. package/packages/web-dashboard/src/app/api/features/[id]/file-diff/route.ts +144 -0
  154. package/packages/web-dashboard/src/app/api/features/[id]/log-stream/route.ts +167 -0
  155. package/packages/web-dashboard/src/app/api/features/[id]/raw-logs/[filename]/route.ts +65 -0
  156. package/packages/web-dashboard/src/app/api/features/[id]/raw-logs/route.ts +63 -0
  157. package/packages/web-dashboard/src/app/api/features/[id]/timeline/route.ts +60 -0
  158. package/packages/web-dashboard/src/app/feature/[id]/page.tsx +32 -11
  159. package/packages/web-dashboard/src/app/globals.css +2 -0
  160. package/packages/web-dashboard/src/components/detail-panel.tsx +483 -0
  161. package/packages/web-dashboard/src/components/review-workspace.tsx +1162 -0
  162. package/packages/web-dashboard/src/lib/aop-client.ts +725 -0
  163. package/packages/web-dashboard/src/lib/review-contracts.ts +182 -0
  164. package/packages/web-dashboard/src/lib/review-workspace-logic.ts +64 -0
  165. package/packages/web-dashboard/src/lib/types.ts +131 -0
  166. package/packages/web-dashboard/src/styles/dashboard.module.css +333 -0
  167. package/spec-files/completed/agentic_orchestrator_execution_mode_spec.md +1905 -0
  168. package/spec-files/outstanding/agentic_orchestrator_runtime_inspection_spec.md +940 -0
  169. package/spec-files/outstanding/execution_mode_critical_review.md +355 -0
  170. package/spec-files/outstanding/shadow_workspace_implementation_spec.md +1271 -0
  171. package/spec-files/outstanding/shadow_workspace_spec_summary.md +222 -0
  172. package/spec-files/progress.md +269 -1
@@ -0,0 +1,60 @@
1
+ import {
2
+ readFeatureState,
3
+ readWorkerTimelineEntries,
4
+ resolveProjectRoot,
5
+ } from '@/lib/aop-client.js';
6
+ import { errorEnvelope, okEnvelope } from '@/lib/api-envelope.js';
7
+ import { FEATURE_ID_PATTERN, normalizeLimit } from '@/lib/review-contracts.js';
8
+
9
+ export const dynamic = 'force-dynamic';
10
+
11
+ export async function GET(
12
+ req: Request,
13
+ { params }: { params: Promise<{ id: string }> },
14
+ ): Promise<Response> {
15
+ const { id } = await params;
16
+ if (!FEATURE_ID_PATTERN.test(id)) {
17
+ return Response.json(errorEnvelope('invalid_input', 'Invalid feature id'), { status: 400 });
18
+ }
19
+
20
+ const url = new URL(req.url);
21
+ const project = url.searchParams.get('project');
22
+ const role = url.searchParams.get('role');
23
+ const validityRaw = url.searchParams.get('valid');
24
+ const validity = validityRaw === 'valid' || validityRaw === 'invalid' ? validityRaw : 'all';
25
+ const limit = normalizeLimit(url.searchParams.get('limit'), 100, 100);
26
+ const repoRoot = await resolveProjectRoot(project);
27
+
28
+ const state = await readFeatureState(id, repoRoot);
29
+ if (!state) {
30
+ return Response.json(errorEnvelope('feature_not_found', `Feature ${id} not found`), {
31
+ status: 404,
32
+ });
33
+ }
34
+
35
+ const entries = await readWorkerTimelineEntries(id, repoRoot, {
36
+ role,
37
+ valid: validity,
38
+ limit,
39
+ });
40
+
41
+ const summary = {
42
+ total: entries.length,
43
+ valid: entries.filter((entry) => entry.valid).length,
44
+ invalid: entries.filter((entry) => !entry.valid).length,
45
+ roles: entries.reduce<Record<string, number>>((acc, entry) => {
46
+ acc[entry.role] = (acc[entry.role] ?? 0) + 1;
47
+ return acc;
48
+ }, {}),
49
+ };
50
+
51
+ return Response.json(
52
+ okEnvelope(
53
+ {
54
+ entries,
55
+ summary,
56
+ },
57
+ { source: 'artifact' },
58
+ ),
59
+ );
60
+ }
@@ -3,6 +3,7 @@
3
3
  import { useCallback, useEffect, useMemo, useState } from 'react';
4
4
  import Link from 'next/link';
5
5
  import { DetailPanel } from '@/components/detail-panel';
6
+ import { ReviewWorkspace } from '@/components/review-workspace';
6
7
  import { ToastStack, useToast } from '@/components/toast';
7
8
  import type { DashboardStatusPayload, FeatureDetail } from '@/lib/types.js';
8
9
  import styles from '@/styles/dashboard.module.css';
@@ -14,6 +15,10 @@ function emptyPayload(): DashboardStatusPayload {
14
15
  };
15
16
  }
16
17
 
18
+ function hasActiveRuntimeRunId(runId: string | null | undefined): boolean {
19
+ return Boolean(runId) && runId !== 'unknown' && runId !== 'unassigned' && runId !== 'none';
20
+ }
21
+
17
22
  async function jsonFetch<T>(input: string, init?: RequestInit): Promise<T> {
18
23
  const response = await fetch(input, init);
19
24
  return (await response.json()) as T;
@@ -77,6 +82,11 @@ export default function FeatureFocusPage({ params }: { params: Promise<{ id: str
77
82
  payload.features.find((feature) => feature.feature_id === featureId)?.status ??
78
83
  detail?.feature.status ??
79
84
  'unknown';
85
+ const showReviewWorkspace =
86
+ detail?.feature.phase === 'ready_to_merge' || detail?.feature.phase === 'qa';
87
+ const hasActiveRuntimeSession =
88
+ hasActiveRuntimeRunId(payload.runtime?.run_id) ||
89
+ hasActiveRuntimeRunId(payload.index.runtime_sessions?.run_id);
80
90
 
81
91
  return (
82
92
  <div className={styles.dashboardRoot}>
@@ -93,17 +103,28 @@ export default function FeatureFocusPage({ params }: { params: Promise<{ id: str
93
103
  </header>
94
104
 
95
105
  <div className={styles.focusLayout}>
96
- <DetailPanel
97
- detail={detail}
98
- project={project}
99
- isFocusView
100
- onClose={() => {
101
- window.location.href = `/${query.length > 0 ? query : ''}`;
102
- }}
103
- refreshDetail={refreshDetail}
104
- refreshStatus={refreshStatus}
105
- addToast={addToast}
106
- />
106
+ {detail && showReviewWorkspace ? (
107
+ <ReviewWorkspace
108
+ detail={detail}
109
+ project={project}
110
+ hasActiveRuntimeSession={hasActiveRuntimeSession}
111
+ refreshDetail={refreshDetail}
112
+ refreshStatus={refreshStatus}
113
+ addToast={addToast}
114
+ />
115
+ ) : (
116
+ <DetailPanel
117
+ detail={detail}
118
+ project={project}
119
+ isFocusView
120
+ onClose={() => {
121
+ window.location.href = `/${query.length > 0 ? query : ''}`;
122
+ }}
123
+ refreshDetail={refreshDetail}
124
+ refreshStatus={refreshStatus}
125
+ addToast={addToast}
126
+ />
127
+ )}
107
128
  </div>
108
129
 
109
130
  <ToastStack notifications={notifications} onDismiss={dismissToast} />
@@ -1,3 +1,5 @@
1
+ @import 'diff2html/bundles/css/diff2html.min.css';
2
+
1
3
  * {
2
4
  box-sizing: border-box;
3
5
  margin: 0;
@@ -6,6 +6,8 @@ import {
6
6
  } from '@/lib/dashboard-utils.js';
7
7
  import type {
8
8
  CostSummary,
9
+ FeatureCheckpointComparison,
10
+ FeatureCheckpoint,
9
11
  FeatureDetail,
10
12
  FeatureLog,
11
13
  FeatureLogEntry,
@@ -80,6 +82,14 @@ interface ApiEnvelope<TData> {
80
82
  error?: { code?: string; message?: string; retryable?: boolean };
81
83
  }
82
84
 
85
+ interface CheckpointStreamPayload {
86
+ type: 'snapshot' | 'append' | 'heartbeat' | 'error';
87
+ cursor: string;
88
+ execution_mode: 'deterministic' | 'interactive' | null;
89
+ checkpoints: FeatureCheckpoint[];
90
+ message?: string;
91
+ }
92
+
83
93
  async function callAction(
84
94
  action: ReviewAction,
85
95
  featureId: string,
@@ -94,6 +104,59 @@ async function callAction(
94
104
  return (await response.json()) as ActionResponse;
95
105
  }
96
106
 
107
+ function formatCheckpointTimestamp(value: string): string {
108
+ const parsed = Date.parse(value);
109
+ if (Number.isNaN(parsed)) {
110
+ return value;
111
+ }
112
+ return new Date(parsed).toLocaleString([], {
113
+ month: 'short',
114
+ day: '2-digit',
115
+ hour: '2-digit',
116
+ minute: '2-digit',
117
+ second: '2-digit',
118
+ });
119
+ }
120
+
121
+ function checkpointStatusBadge(status: FeatureCheckpoint['validation_status']): string {
122
+ if (status === 'valid') {
123
+ return `${styles.badge} ${styles.badgePass}`;
124
+ }
125
+ if (status === 'invalid') {
126
+ return `${styles.badge} ${styles.badgeFail}`;
127
+ }
128
+ return `${styles.badge} ${styles.badgePending}`;
129
+ }
130
+
131
+ function checkpointSeverityBadge(severity: FeatureCheckpoint['severity']): string {
132
+ if (severity === 'error' || severity === 'critical') {
133
+ return `${styles.badge} ${styles.badgeFail}`;
134
+ }
135
+ if (severity === 'warning') {
136
+ return `${styles.badge} ${styles.badgePending}`;
137
+ }
138
+ if (severity === 'info') {
139
+ return `${styles.badge} ${styles.badgeNeutral}`;
140
+ }
141
+ return `${styles.badge} ${styles.badgeNeutral}`;
142
+ }
143
+
144
+ function mergeCheckpointTimeline(
145
+ current: FeatureCheckpoint[],
146
+ incoming: FeatureCheckpoint[],
147
+ ): FeatureCheckpoint[] {
148
+ const byId = new Map<string, FeatureCheckpoint>();
149
+ for (const item of current) {
150
+ byId.set(item.checkpoint_id, item);
151
+ }
152
+ for (const item of incoming) {
153
+ byId.set(item.checkpoint_id, item);
154
+ }
155
+ return [...byId.values()].sort(
156
+ (left, right) => Date.parse(right.timestamp) - Date.parse(left.timestamp),
157
+ );
158
+ }
159
+
97
160
  export function DetailPanel({
98
161
  detail,
99
162
  project,
@@ -117,6 +180,25 @@ export function DetailPanel({
117
180
  const [cost, setCost] = useState<CostSummary | null>(detail?.cost ?? null);
118
181
  const [qaIndex, setQaIndex] = useState<QaTestIndex | null>(detail?.qa_test_index ?? null);
119
182
  const [reviewBrief, setReviewBrief] = useState<ReviewBrief | null>(detail?.review_brief ?? null);
183
+ const [runtimeInspectorView, setRuntimeInspectorView] = useState<
184
+ 'timeline' | 'snapshot' | 'compare'
185
+ >('timeline');
186
+ const [checkpointTimeline, setCheckpointTimeline] = useState<FeatureCheckpoint[]>(
187
+ detail?.checkpoints ?? [],
188
+ );
189
+ const [checkpointStreamError, setCheckpointStreamError] = useState<string | null>(null);
190
+ const [selectedCheckpointId, setSelectedCheckpointId] = useState<string | null>(null);
191
+ const [selectedCheckpointDiff, setSelectedCheckpointDiff] = useState<string>('');
192
+ const [selectedCheckpointDiffLoading, setSelectedCheckpointDiffLoading] = useState(false);
193
+ const [selectedCheckpointDiffError, setSelectedCheckpointDiffError] = useState<string | null>(
194
+ null,
195
+ );
196
+ const [compareFromCheckpointId, setCompareFromCheckpointId] = useState<string>('');
197
+ const [compareToCheckpointId, setCompareToCheckpointId] = useState<string>('');
198
+ const [checkpointComparison, setCheckpointComparison] =
199
+ useState<FeatureCheckpointComparison | null>(null);
200
+ const [checkpointComparisonLoading, setCheckpointComparisonLoading] = useState(false);
201
+ const [checkpointComparisonError, setCheckpointComparisonError] = useState<string | null>(null);
120
202
  const [budgetLimitUsd, setBudgetLimitUsd] = useState<number | null>(null);
121
203
  const costCacheRef = useRef<Map<string, { expiresAt: number; value: CostSummary | null }>>(
122
204
  new Map(),
@@ -301,12 +383,217 @@ export function DetailPanel({
301
383
  };
302
384
  }, [detail, project]);
303
385
 
386
+ useEffect(() => {
387
+ setCheckpointTimeline(detail?.checkpoints ?? []);
388
+ setCheckpointStreamError(null);
389
+ }, [detail?.feature.feature_id, detail?.checkpoints]);
390
+
391
+ useEffect(() => {
392
+ if (checkpointTimeline.length === 0) {
393
+ setSelectedCheckpointId(null);
394
+ setCompareFromCheckpointId('');
395
+ setCompareToCheckpointId('');
396
+ setCheckpointComparison(null);
397
+ return;
398
+ }
399
+
400
+ setSelectedCheckpointId((current) => {
401
+ if (
402
+ current &&
403
+ checkpointTimeline.some((checkpoint) => checkpoint.checkpoint_id === current)
404
+ ) {
405
+ return current;
406
+ }
407
+ return checkpointTimeline[0].checkpoint_id;
408
+ });
409
+
410
+ setCompareFromCheckpointId((current) => {
411
+ if (
412
+ current &&
413
+ checkpointTimeline.some((checkpoint) => checkpoint.checkpoint_id === current)
414
+ ) {
415
+ return current;
416
+ }
417
+ return checkpointTimeline[0].checkpoint_id;
418
+ });
419
+
420
+ setCompareToCheckpointId((current) => {
421
+ if (
422
+ current &&
423
+ checkpointTimeline.some((checkpoint) => checkpoint.checkpoint_id === current)
424
+ ) {
425
+ return current;
426
+ }
427
+ return checkpointTimeline[1]?.checkpoint_id ?? '';
428
+ });
429
+ }, [checkpointTimeline]);
430
+
431
+ useEffect(() => {
432
+ if (!detail) {
433
+ return;
434
+ }
435
+ const shouldStream =
436
+ detail.execution_mode === 'interactive' || (detail.checkpoints?.length ?? 0) > 0;
437
+ if (!shouldStream) {
438
+ return;
439
+ }
440
+ const params = new URLSearchParams();
441
+ if (project.length > 0) {
442
+ params.set('project', project);
443
+ }
444
+ const eventSource = new EventSource(
445
+ `/api/features/${encodeURIComponent(detail.feature.feature_id)}/checkpoints/stream?${params.toString()}`,
446
+ );
447
+
448
+ const onStreamEvent = (event: MessageEvent<string>) => {
449
+ let payload: CheckpointStreamPayload;
450
+ try {
451
+ payload = JSON.parse(event.data) as CheckpointStreamPayload;
452
+ } catch {
453
+ return;
454
+ }
455
+ if (payload.type === 'error') {
456
+ setCheckpointStreamError(payload.message ?? 'Checkpoint stream warning');
457
+ return;
458
+ }
459
+ if (payload.checkpoints.length === 0) {
460
+ return;
461
+ }
462
+ setCheckpointStreamError(null);
463
+ if (payload.type === 'snapshot') {
464
+ setCheckpointTimeline(payload.checkpoints);
465
+ return;
466
+ }
467
+ if (payload.type === 'append') {
468
+ setCheckpointTimeline((current) => mergeCheckpointTimeline(current, payload.checkpoints));
469
+ }
470
+ };
471
+
472
+ eventSource.addEventListener('snapshot', onStreamEvent as EventListener);
473
+ eventSource.addEventListener('append', onStreamEvent as EventListener);
474
+ eventSource.addEventListener('error', () => {
475
+ setCheckpointStreamError('Disconnected from checkpoint stream');
476
+ });
477
+
478
+ return () => {
479
+ eventSource.close();
480
+ };
481
+ }, [detail?.feature.feature_id, detail?.execution_mode, detail?.checkpoints?.length, project]);
482
+
483
+ useEffect(() => {
484
+ if (!detail || !selectedCheckpointId) {
485
+ setSelectedCheckpointDiff('');
486
+ setSelectedCheckpointDiffError(null);
487
+ return;
488
+ }
489
+ const controller = new AbortController();
490
+ let cancelled = false;
491
+
492
+ void (async () => {
493
+ setSelectedCheckpointDiffLoading(true);
494
+ setSelectedCheckpointDiffError(null);
495
+ try {
496
+ const response = await fetch(
497
+ `/api/features/${encodeURIComponent(
498
+ detail.feature.feature_id,
499
+ )}/checkpoints/${encodeURIComponent(selectedCheckpointId)}/diff${projectQuery(project)}`,
500
+ { signal: controller.signal },
501
+ );
502
+ const body = (await response.json()) as ApiEnvelope<{
503
+ checkpoint: FeatureCheckpoint;
504
+ diff: string;
505
+ }>;
506
+ if (cancelled) {
507
+ return;
508
+ }
509
+ if (!body.ok || !body.data) {
510
+ setSelectedCheckpointDiff('');
511
+ setSelectedCheckpointDiffError(body.error?.message ?? 'Unable to load checkpoint diff');
512
+ return;
513
+ }
514
+ setSelectedCheckpointDiff(body.data.diff);
515
+ } catch {
516
+ if (!cancelled) {
517
+ setSelectedCheckpointDiff('');
518
+ setSelectedCheckpointDiffError('Unable to load checkpoint diff');
519
+ }
520
+ } finally {
521
+ if (!cancelled) {
522
+ setSelectedCheckpointDiffLoading(false);
523
+ }
524
+ }
525
+ })();
526
+
527
+ return () => {
528
+ cancelled = true;
529
+ controller.abort();
530
+ };
531
+ }, [detail, selectedCheckpointId, project]);
532
+
533
+ useEffect(() => {
534
+ if (!detail || !compareFromCheckpointId || !compareToCheckpointId) {
535
+ setCheckpointComparison(null);
536
+ setCheckpointComparisonError(null);
537
+ return;
538
+ }
539
+ if (compareFromCheckpointId === compareToCheckpointId) {
540
+ setCheckpointComparison(null);
541
+ setCheckpointComparisonError('Select two different checkpoints to compare.');
542
+ return;
543
+ }
544
+
545
+ const controller = new AbortController();
546
+ let cancelled = false;
547
+ void (async () => {
548
+ setCheckpointComparisonLoading(true);
549
+ setCheckpointComparisonError(null);
550
+ try {
551
+ const query = new URLSearchParams();
552
+ query.set('from', compareFromCheckpointId);
553
+ query.set('to', compareToCheckpointId);
554
+ if (project.length > 0) {
555
+ query.set('project', project);
556
+ }
557
+ const response = await fetch(
558
+ `/api/features/${encodeURIComponent(detail.feature.feature_id)}/checkpoints/compare?${query.toString()}`,
559
+ { signal: controller.signal },
560
+ );
561
+ const body = (await response.json()) as ApiEnvelope<FeatureCheckpointComparison>;
562
+ if (cancelled) {
563
+ return;
564
+ }
565
+ if (!body.ok || !body.data) {
566
+ setCheckpointComparison(null);
567
+ setCheckpointComparisonError(body.error?.message ?? 'Unable to compare checkpoints');
568
+ return;
569
+ }
570
+ setCheckpointComparison(body.data);
571
+ } catch {
572
+ if (!cancelled) {
573
+ setCheckpointComparison(null);
574
+ setCheckpointComparisonError('Unable to compare checkpoints');
575
+ }
576
+ } finally {
577
+ if (!cancelled) {
578
+ setCheckpointComparisonLoading(false);
579
+ }
580
+ }
581
+ })();
582
+
583
+ return () => {
584
+ cancelled = true;
585
+ controller.abort();
586
+ };
587
+ }, [detail, compareFromCheckpointId, compareToCheckpointId, project]);
588
+
304
589
  const phase = detail?.feature.phase ?? 'unknown';
305
590
 
306
591
  const shouldShowPlan = phase === 'planning' || phase === 'building';
307
592
  const shouldShowDiff = ['building', 'qa', 'ready_to_merge', 'blocked'].includes(phase);
308
593
  const shouldShowGates = ['qa', 'ready_to_merge', 'blocked'].includes(phase);
309
594
  const shouldShowEvidence = ['qa', 'ready_to_merge', 'blocked'].includes(phase);
595
+ const checkpoints = checkpointTimeline;
596
+ const showCheckpointTimeline = detail?.execution_mode === 'interactive' || checkpoints.length > 0;
310
597
 
311
598
  const timeInPhase = useMemo(
312
599
  () => formatElapsedTime(detail?.feature.last_updated, nowMs),
@@ -466,6 +753,202 @@ export function DetailPanel({
466
753
 
467
754
  {detail.feature.pr ? <PrStatusCard pr={detail.feature.pr} /> : null}
468
755
 
756
+ {showCheckpointTimeline ? (
757
+ <details className={styles.section} open={checkpoints.length > 0}>
758
+ <summary className={styles.sectionSummary}>Runtime Inspector · Checkpoints</summary>
759
+ <div className={styles.badgeRow}>
760
+ <span className={`${styles.badge} ${styles.badgeNeutral}`}>
761
+ mode: {detail.execution_mode ?? 'deterministic'}
762
+ </span>
763
+ <span className={`${styles.badge} ${styles.badgeNeutral}`}>
764
+ {checkpoints.length} checkpoints
765
+ </span>
766
+ {checkpointStreamError ? (
767
+ <span className={`${styles.badge} ${styles.badgeFail}`}>{checkpointStreamError}</span>
768
+ ) : (
769
+ <span className={`${styles.badge} ${styles.badgePass}`}>streaming</span>
770
+ )}
771
+ </div>
772
+ <div className={styles.runtimeInspectorTabs}>
773
+ <button
774
+ type="button"
775
+ className={`${styles.runtimeInspectorTab} ${
776
+ runtimeInspectorView === 'timeline' ? styles.runtimeInspectorTabActive : ''
777
+ }`}
778
+ onClick={() => setRuntimeInspectorView('timeline')}
779
+ >
780
+ Timeline
781
+ </button>
782
+ <button
783
+ type="button"
784
+ className={`${styles.runtimeInspectorTab} ${
785
+ runtimeInspectorView === 'snapshot' ? styles.runtimeInspectorTabActive : ''
786
+ }`}
787
+ onClick={() => setRuntimeInspectorView('snapshot')}
788
+ disabled={checkpoints.length === 0}
789
+ >
790
+ Snapshot Diff
791
+ </button>
792
+ <button
793
+ type="button"
794
+ className={`${styles.runtimeInspectorTab} ${
795
+ runtimeInspectorView === 'compare' ? styles.runtimeInspectorTabActive : ''
796
+ }`}
797
+ onClick={() => setRuntimeInspectorView('compare')}
798
+ disabled={checkpoints.length < 2}
799
+ >
800
+ Compare
801
+ </button>
802
+ </div>
803
+ {runtimeInspectorView === 'timeline' ? (
804
+ checkpoints.length === 0 ? (
805
+ <p className={styles.metaText}>No checkpoints recorded for this feature yet.</p>
806
+ ) : (
807
+ <div className={styles.reviewTimelineList}>
808
+ {checkpoints.map((checkpoint) => (
809
+ <div
810
+ key={`${checkpoint.checkpoint_id}:${checkpoint.timestamp}`}
811
+ className={styles.reviewTimelineEntry}
812
+ >
813
+ <div className={styles.badgeRow}>
814
+ <span className={checkpointStatusBadge(checkpoint.validation_status)}>
815
+ {checkpoint.validation_status}
816
+ </span>
817
+ {checkpoint.severity ? (
818
+ <span className={checkpointSeverityBadge(checkpoint.severity)}>
819
+ {checkpoint.severity}
820
+ </span>
821
+ ) : null}
822
+ </div>
823
+ <span>{formatCheckpointTimestamp(checkpoint.timestamp)}</span>
824
+ <span>{checkpoint.files_changed.length} files</span>
825
+ <span>{checkpoint.checkpoint_id}</span>
826
+ {checkpoint.violations.length > 0 ? (
827
+ <span>{checkpoint.violations[0]}</span>
828
+ ) : (
829
+ <span>no violations</span>
830
+ )}
831
+ </div>
832
+ ))}
833
+ </div>
834
+ )
835
+ ) : null}
836
+ {runtimeInspectorView === 'snapshot' ? (
837
+ <div className={styles.runtimeInspectorPanel}>
838
+ <label className={styles.label} htmlFor="checkpoint-select">
839
+ Checkpoint
840
+ </label>
841
+ <select
842
+ id="checkpoint-select"
843
+ className={styles.actionSelect}
844
+ value={selectedCheckpointId ?? ''}
845
+ onChange={(event) => setSelectedCheckpointId(event.target.value)}
846
+ >
847
+ {checkpoints.map((checkpoint) => (
848
+ <option key={checkpoint.checkpoint_id} value={checkpoint.checkpoint_id}>
849
+ {checkpoint.checkpoint_id} · {formatCheckpointTimestamp(checkpoint.timestamp)}
850
+ </option>
851
+ ))}
852
+ </select>
853
+ {selectedCheckpointDiffLoading ? (
854
+ <p className={styles.metaText}>Loading checkpoint diff...</p>
855
+ ) : null}
856
+ {selectedCheckpointDiffError ? (
857
+ <p className={styles.metaText}>{selectedCheckpointDiffError}</p>
858
+ ) : null}
859
+ {!selectedCheckpointDiffLoading && !selectedCheckpointDiffError ? (
860
+ <DiffViewer diff={selectedCheckpointDiff} />
861
+ ) : null}
862
+ </div>
863
+ ) : null}
864
+ {runtimeInspectorView === 'compare' ? (
865
+ <div className={styles.runtimeInspectorPanel}>
866
+ <div className={styles.runtimeInspectorCompareGrid}>
867
+ <div>
868
+ <label className={styles.label} htmlFor="compare-from-checkpoint">
869
+ From checkpoint
870
+ </label>
871
+ <select
872
+ id="compare-from-checkpoint"
873
+ className={styles.actionSelect}
874
+ value={compareFromCheckpointId}
875
+ onChange={(event) => setCompareFromCheckpointId(event.target.value)}
876
+ >
877
+ {checkpoints.map((checkpoint) => (
878
+ <option
879
+ key={`from-${checkpoint.checkpoint_id}`}
880
+ value={checkpoint.checkpoint_id}
881
+ >
882
+ {checkpoint.checkpoint_id}
883
+ </option>
884
+ ))}
885
+ </select>
886
+ </div>
887
+ <div>
888
+ <label className={styles.label} htmlFor="compare-to-checkpoint">
889
+ To checkpoint
890
+ </label>
891
+ <select
892
+ id="compare-to-checkpoint"
893
+ className={styles.actionSelect}
894
+ value={compareToCheckpointId}
895
+ onChange={(event) => setCompareToCheckpointId(event.target.value)}
896
+ >
897
+ {checkpoints.map((checkpoint) => (
898
+ <option
899
+ key={`to-${checkpoint.checkpoint_id}`}
900
+ value={checkpoint.checkpoint_id}
901
+ >
902
+ {checkpoint.checkpoint_id}
903
+ </option>
904
+ ))}
905
+ </select>
906
+ </div>
907
+ </div>
908
+ {checkpointComparisonLoading ? (
909
+ <p className={styles.metaText}>Comparing checkpoints...</p>
910
+ ) : null}
911
+ {checkpointComparisonError ? (
912
+ <p className={styles.metaText}>{checkpointComparisonError}</p>
913
+ ) : null}
914
+ {checkpointComparison ? (
915
+ <>
916
+ <div className={styles.badgeRow}>
917
+ <span className={`${styles.badge} ${styles.badgePass}`}>
918
+ added {checkpointComparison.summary.added}
919
+ </span>
920
+ <span className={`${styles.badge} ${styles.badgePending}`}>
921
+ changed {checkpointComparison.summary.changed}
922
+ </span>
923
+ <span className={`${styles.badge} ${styles.badgeFail}`}>
924
+ removed {checkpointComparison.summary.removed}
925
+ </span>
926
+ <span className={`${styles.badge} ${styles.badgeNeutral}`}>
927
+ unchanged {checkpointComparison.summary.unchanged}
928
+ </span>
929
+ </div>
930
+ <p className={styles.metaText}>
931
+ {checkpointComparison.files.length} files compared between{' '}
932
+ {checkpointComparison.from_checkpoint_id} and{' '}
933
+ {checkpointComparison.to_checkpoint_id}.
934
+ </p>
935
+ <div className={styles.runtimeInspectorCompareGrid}>
936
+ <div>
937
+ <h4 className={styles.sectionTitle}>From Snapshot</h4>
938
+ <DiffViewer diff={checkpointComparison.from_diff} />
939
+ </div>
940
+ <div>
941
+ <h4 className={styles.sectionTitle}>To Snapshot</h4>
942
+ <DiffViewer diff={checkpointComparison.to_diff} />
943
+ </div>
944
+ </div>
945
+ </>
946
+ ) : null}
947
+ </div>
948
+ ) : null}
949
+ </details>
950
+ ) : null}
951
+
469
952
  {shouldShowPlan ? <PlanViewer plan={detail.plan} /> : null}
470
953
  {shouldShowGates ? <GateResults feature={detail.feature} /> : null}
471
954
  {shouldShowGates ? <GateStepDrilldown evidence={detail.gate_evidence} /> : null}