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.
- package/AGENTS.md +2 -2
- package/CLAUDE.md +2 -2
- package/README.md +47 -14
- package/agentic/orchestrator/agents.yaml +13 -0
- package/agentic/orchestrator/policy.yaml +3 -0
- package/agentic/orchestrator/schemas/agents.schema.json +76 -0
- package/agentic/orchestrator/schemas/policy.schema.json +16 -0
- package/agentic/orchestrator/schemas/policy.user.schema.json +16 -0
- package/agentic/orchestrator/schemas/state.schema.json +53 -0
- package/apps/control-plane/src/application/configuration-service.ts +181 -0
- package/apps/control-plane/src/application/kernel-tool-wiring.ts +292 -0
- package/apps/control-plane/src/application/services/checkpoint-service.ts +523 -0
- package/apps/control-plane/src/application/services/feature-send-message-service.ts +132 -0
- package/apps/control-plane/src/application/services/patch-service.ts +29 -5
- package/apps/control-plane/src/application/services/repo-operations-service.ts +276 -0
- package/apps/control-plane/src/application/services/worktree-watchdog-service.ts +156 -0
- package/apps/control-plane/src/cli/cli-argument-parser.ts +12 -0
- package/apps/control-plane/src/cli/help-command-handler.ts +17 -0
- package/apps/control-plane/src/cli/init-command-handler.ts +31 -0
- package/apps/control-plane/src/cli/resume-command-handler.ts +31 -4
- package/apps/control-plane/src/cli/rollback-command-handler.ts +217 -0
- package/apps/control-plane/src/cli/run-command-handler.ts +8 -0
- package/apps/control-plane/src/cli/types.ts +3 -0
- package/apps/control-plane/src/core/kernel-types.ts +55 -0
- package/apps/control-plane/src/core/kernel.ts +61 -878
- package/apps/control-plane/src/core/tool-caller.ts +10 -0
- package/apps/control-plane/src/core/utils/field-readers.ts +38 -0
- package/apps/control-plane/src/core/utils/index-normalizer.ts +119 -0
- package/apps/control-plane/src/core/utils/path-normalizers.ts +22 -0
- package/apps/control-plane/src/interfaces/cli/bootstrap.ts +15 -0
- package/apps/control-plane/src/providers/api-worker-provider.ts +14 -12
- package/apps/control-plane/src/providers/cli-worker-provider.ts +82 -12
- package/apps/control-plane/src/providers/providers.ts +45 -24
- package/apps/control-plane/src/providers/worker-provider-factory.ts +36 -1
- package/apps/control-plane/src/supervisor/run-coordinator.ts +91 -36
- package/apps/control-plane/src/supervisor/runtime.ts +107 -1
- package/apps/control-plane/src/supervisor/types.ts +9 -0
- package/apps/control-plane/src/supervisor/worker-decision-loop.ts +253 -14
- package/apps/control-plane/test/checkpoint-service.spec.ts +537 -0
- package/apps/control-plane/test/cli-helpers.spec.ts +28 -0
- package/apps/control-plane/test/cli.unit.spec.ts +52 -0
- package/apps/control-plane/test/configuration-service.spec.ts +466 -0
- package/apps/control-plane/test/dashboard-api.integration.spec.ts +537 -0
- package/apps/control-plane/test/dashboard-client.spec.ts +233 -0
- package/apps/control-plane/test/feature-send-message-service.spec.ts +314 -0
- package/apps/control-plane/test/init-wizard.spec.ts +35 -0
- package/apps/control-plane/test/path-normalizers.spec.ts +41 -0
- package/apps/control-plane/test/repo-operations-service.spec.ts +339 -0
- package/apps/control-plane/test/resume-command.spec.ts +33 -0
- package/apps/control-plane/test/review-workspace-logic.spec.ts +130 -0
- package/apps/control-plane/test/rollback-command.spec.ts +208 -0
- package/apps/control-plane/test/run-coordinator.spec.ts +119 -0
- package/apps/control-plane/test/worker-decision-loop.spec.ts +209 -0
- package/apps/control-plane/test/worker-provider-adapters.spec.ts +102 -0
- package/apps/control-plane/test/worker-provider-factory.spec.ts +14 -0
- package/apps/control-plane/test/worktree-watchdog-service.spec.ts +147 -0
- package/config/agentic/orchestrator/agents.yaml +13 -0
- package/dist/apps/control-plane/application/configuration-service.d.ts +19 -0
- package/dist/apps/control-plane/application/configuration-service.js +123 -0
- package/dist/apps/control-plane/application/configuration-service.js.map +1 -0
- package/dist/apps/control-plane/application/kernel-tool-wiring.d.ts +39 -0
- package/dist/apps/control-plane/application/kernel-tool-wiring.js +38 -0
- package/dist/apps/control-plane/application/kernel-tool-wiring.js.map +1 -0
- package/dist/apps/control-plane/application/services/checkpoint-service.d.ts +84 -0
- package/dist/apps/control-plane/application/services/checkpoint-service.js +367 -0
- package/dist/apps/control-plane/application/services/checkpoint-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/feature-send-message-service.d.ts +25 -0
- package/dist/apps/control-plane/application/services/feature-send-message-service.js +105 -0
- package/dist/apps/control-plane/application/services/feature-send-message-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/patch-service.d.ts +6 -0
- package/dist/apps/control-plane/application/services/patch-service.js +11 -2
- package/dist/apps/control-plane/application/services/patch-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/repo-operations-service.d.ts +70 -0
- package/dist/apps/control-plane/application/services/repo-operations-service.js +213 -0
- package/dist/apps/control-plane/application/services/repo-operations-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/worktree-watchdog-service.d.ts +23 -0
- package/dist/apps/control-plane/application/services/worktree-watchdog-service.js +119 -0
- package/dist/apps/control-plane/application/services/worktree-watchdog-service.js.map +1 -0
- package/dist/apps/control-plane/cli/cli-argument-parser.js +12 -0
- package/dist/apps/control-plane/cli/cli-argument-parser.js.map +1 -1
- package/dist/apps/control-plane/cli/help-command-handler.js +17 -0
- package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/init-command-handler.js +23 -0
- package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/resume-command-handler.js +25 -5
- package/dist/apps/control-plane/cli/resume-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/rollback-command-handler.d.ts +6 -0
- package/dist/apps/control-plane/cli/rollback-command-handler.js +177 -0
- package/dist/apps/control-plane/cli/rollback-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/run-command-handler.js +7 -1
- package/dist/apps/control-plane/cli/run-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/types.d.ts +3 -0
- package/dist/apps/control-plane/cli/types.js +1 -0
- package/dist/apps/control-plane/cli/types.js.map +1 -1
- package/dist/apps/control-plane/core/configuration-service.d.ts +25 -0
- package/dist/apps/control-plane/core/configuration-service.js +130 -0
- package/dist/apps/control-plane/core/configuration-service.js.map +1 -0
- package/dist/apps/control-plane/core/kernel-tool-wiring.d.ts +50 -0
- package/dist/apps/control-plane/core/kernel-tool-wiring.js +44 -0
- package/dist/apps/control-plane/core/kernel-tool-wiring.js.map +1 -0
- package/dist/apps/control-plane/core/kernel-types.d.ts +48 -0
- package/dist/apps/control-plane/core/kernel-types.js +2 -0
- package/dist/apps/control-plane/core/kernel-types.js.map +1 -0
- package/dist/apps/control-plane/core/kernel.d.ts +17 -48
- package/dist/apps/control-plane/core/kernel.js +44 -539
- package/dist/apps/control-plane/core/kernel.js.map +1 -1
- package/dist/apps/control-plane/core/tool-caller.d.ts +10 -0
- package/dist/apps/control-plane/core/utils/error-normalizer.d.ts +2 -0
- package/dist/apps/control-plane/core/utils/error-normalizer.js +51 -0
- package/dist/apps/control-plane/core/utils/error-normalizer.js.map +1 -0
- package/dist/apps/control-plane/core/utils/field-readers.d.ts +9 -0
- package/dist/apps/control-plane/core/utils/field-readers.js +30 -0
- package/dist/apps/control-plane/core/utils/field-readers.js.map +1 -0
- package/dist/apps/control-plane/core/utils/index-normalizer.d.ts +7 -0
- package/dist/apps/control-plane/core/utils/index-normalizer.js +92 -0
- package/dist/apps/control-plane/core/utils/index-normalizer.js.map +1 -0
- package/dist/apps/control-plane/core/utils/path-normalizers.d.ts +2 -0
- package/dist/apps/control-plane/core/utils/path-normalizers.js +17 -0
- package/dist/apps/control-plane/core/utils/path-normalizers.js.map +1 -0
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js +13 -1
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
- package/dist/apps/control-plane/providers/api-worker-provider.d.ts +4 -13
- package/dist/apps/control-plane/providers/api-worker-provider.js +10 -0
- package/dist/apps/control-plane/providers/api-worker-provider.js.map +1 -1
- package/dist/apps/control-plane/providers/cli-worker-provider.d.ts +11 -13
- package/dist/apps/control-plane/providers/cli-worker-provider.js +64 -0
- package/dist/apps/control-plane/providers/cli-worker-provider.js.map +1 -1
- package/dist/apps/control-plane/providers/providers.d.ts +31 -24
- package/dist/apps/control-plane/providers/providers.js +10 -0
- package/dist/apps/control-plane/providers/providers.js.map +1 -1
- package/dist/apps/control-plane/providers/worker-provider-factory.d.ts +11 -0
- package/dist/apps/control-plane/providers/worker-provider-factory.js +20 -1
- package/dist/apps/control-plane/providers/worker-provider-factory.js.map +1 -1
- package/dist/apps/control-plane/supervisor/run-coordinator.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/run-coordinator.js +81 -33
- package/dist/apps/control-plane/supervisor/run-coordinator.js.map +1 -1
- package/dist/apps/control-plane/supervisor/runtime.d.ts +8 -1
- package/dist/apps/control-plane/supervisor/runtime.js +90 -0
- package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
- package/dist/apps/control-plane/supervisor/types.d.ts +11 -0
- package/dist/apps/control-plane/supervisor/types.js.map +1 -1
- package/dist/apps/control-plane/supervisor/worker-decision-loop.d.ts +21 -1
- package/dist/apps/control-plane/supervisor/worker-decision-loop.js +207 -13
- package/dist/apps/control-plane/supervisor/worker-decision-loop.js.map +1 -1
- package/package.json +1 -1
- package/packages/web-dashboard/package.json +2 -0
- package/packages/web-dashboard/src/app/analytics/page.tsx +83 -2
- package/packages/web-dashboard/src/app/api/actions/route.ts +92 -1
- package/packages/web-dashboard/src/app/api/analytics/route.ts +5 -2
- package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/[checkpointId]/diff/route.ts +43 -0
- package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/compare/route.ts +45 -0
- package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/stream/route.ts +170 -0
- package/packages/web-dashboard/src/app/api/features/[id]/file-diff/route.ts +144 -0
- package/packages/web-dashboard/src/app/api/features/[id]/log-stream/route.ts +167 -0
- package/packages/web-dashboard/src/app/api/features/[id]/raw-logs/[filename]/route.ts +65 -0
- package/packages/web-dashboard/src/app/api/features/[id]/raw-logs/route.ts +63 -0
- package/packages/web-dashboard/src/app/api/features/[id]/timeline/route.ts +60 -0
- package/packages/web-dashboard/src/app/feature/[id]/page.tsx +32 -11
- package/packages/web-dashboard/src/app/globals.css +2 -0
- package/packages/web-dashboard/src/components/detail-panel.tsx +483 -0
- package/packages/web-dashboard/src/components/review-workspace.tsx +1162 -0
- package/packages/web-dashboard/src/lib/aop-client.ts +725 -0
- package/packages/web-dashboard/src/lib/review-contracts.ts +182 -0
- package/packages/web-dashboard/src/lib/review-workspace-logic.ts +64 -0
- package/packages/web-dashboard/src/lib/types.ts +131 -0
- package/packages/web-dashboard/src/styles/dashboard.module.css +333 -0
- package/spec-files/completed/agentic_orchestrator_execution_mode_spec.md +1905 -0
- package/spec-files/outstanding/agentic_orchestrator_runtime_inspection_spec.md +940 -0
- package/spec-files/outstanding/execution_mode_critical_review.md +355 -0
- package/spec-files/outstanding/shadow_workspace_implementation_spec.md +1271 -0
- package/spec-files/outstanding/shadow_workspace_spec_summary.md +222 -0
- package/spec-files/progress.md +269 -1
|
@@ -2,31 +2,75 @@ import { readdir, readFile, stat } from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import YAML from 'yaml';
|
|
4
4
|
import { getProjectByName, readMultiProjectConfig } from '@/lib/multi-project-config.js';
|
|
5
|
+
import { parseUnifiedDiff } from '@/lib/dashboard-utils.js';
|
|
5
6
|
import type {
|
|
7
|
+
AgentLogEntry,
|
|
8
|
+
AgentSessionCluster,
|
|
6
9
|
AgentPipelineStatus,
|
|
7
10
|
CostSummary,
|
|
8
11
|
DashboardStatusPayload,
|
|
9
12
|
EvidenceArtifact,
|
|
13
|
+
FeatureCheckpointComparison,
|
|
14
|
+
FeatureCheckpointDiff,
|
|
10
15
|
FeatureDetail,
|
|
11
16
|
FeatureLog,
|
|
12
17
|
FeatureLogEntry,
|
|
13
18
|
FeaturesIndex,
|
|
14
19
|
FeatureSummary,
|
|
20
|
+
FeatureCheckpoint,
|
|
21
|
+
FeatureExecutionMode,
|
|
15
22
|
GateRunEvidence,
|
|
23
|
+
InteractiveExecutionModeMetrics,
|
|
24
|
+
InteractivePerformanceLatestCheckpoint,
|
|
25
|
+
InteractivePerformanceMetrics,
|
|
16
26
|
LockLease,
|
|
17
27
|
QaTestIndex,
|
|
28
|
+
RawLogFileMeta,
|
|
18
29
|
ReviewBrief,
|
|
19
30
|
RoleStatus,
|
|
20
31
|
RuntimeSession,
|
|
32
|
+
WorkerEventEntry,
|
|
21
33
|
} from '@/lib/types.js';
|
|
22
34
|
|
|
23
35
|
const AOP_ROOT = process.env.AOP_ROOT ?? process.cwd();
|
|
36
|
+
const LOG_FILENAME_PATTERN = /^(planner|builder|qa)-(\d{13})\.txt$/;
|
|
37
|
+
const CHECKPOINT_METRICS_HISTORY_LIMIT = 200;
|
|
24
38
|
|
|
25
39
|
interface ParsedFrontMatter {
|
|
26
40
|
frontMatter: Record<string, unknown>;
|
|
27
41
|
body: string;
|
|
28
42
|
}
|
|
29
43
|
|
|
44
|
+
interface InteractiveCheckpointMetricRecord {
|
|
45
|
+
recorded_at: string;
|
|
46
|
+
feature_id: string;
|
|
47
|
+
checkpoint_id: string;
|
|
48
|
+
trigger: string;
|
|
49
|
+
validation_status: 'valid' | 'invalid' | 'skipped';
|
|
50
|
+
checkpoint_latency_ms: number;
|
|
51
|
+
validation_latency_ms: number;
|
|
52
|
+
diff_capture_latency_ms: number;
|
|
53
|
+
files_changed: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface InteractiveMetricsFileShape {
|
|
57
|
+
schema_version: number;
|
|
58
|
+
updated_at: string;
|
|
59
|
+
totals: {
|
|
60
|
+
checkpoint_count: number;
|
|
61
|
+
valid_count: number;
|
|
62
|
+
invalid_count: number;
|
|
63
|
+
skipped_count: number;
|
|
64
|
+
files_changed_total: number;
|
|
65
|
+
};
|
|
66
|
+
histories: {
|
|
67
|
+
checkpoint_latency_ms: number[];
|
|
68
|
+
validation_latency_ms: number[];
|
|
69
|
+
diff_capture_latency_ms: number[];
|
|
70
|
+
};
|
|
71
|
+
latest: InteractiveCheckpointMetricRecord | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
30
74
|
const STATUS_TO_PHASE: Record<string, FeatureSummary['phase']> = {
|
|
31
75
|
planning: 'planning',
|
|
32
76
|
building: 'building',
|
|
@@ -91,6 +135,39 @@ function parseRoleStatus(frontMatter: Record<string, unknown>): AgentPipelineSta
|
|
|
91
135
|
return { planner, builder, qa };
|
|
92
136
|
}
|
|
93
137
|
|
|
138
|
+
function isSessionId(value: unknown): value is string {
|
|
139
|
+
if (typeof value !== 'string') {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
const trimmed = value.trim();
|
|
143
|
+
return (
|
|
144
|
+
trimmed.length > 0 && trimmed !== 'unknown' && trimmed !== 'unassigned' && trimmed !== 'none'
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parseSessionCluster(frontMatter: Record<string, unknown>): AgentSessionCluster | null {
|
|
149
|
+
const raw = frontMatter.cluster;
|
|
150
|
+
if (!raw || typeof raw !== 'object') {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const cluster = raw as Record<string, unknown>;
|
|
154
|
+
const orchestrator = isSessionId(cluster.orchestrator_session_id)
|
|
155
|
+
? cluster.orchestrator_session_id
|
|
156
|
+
: null;
|
|
157
|
+
const planner = isSessionId(cluster.planner_session_id) ? cluster.planner_session_id : null;
|
|
158
|
+
const builder = isSessionId(cluster.builder_session_id) ? cluster.builder_session_id : null;
|
|
159
|
+
const qa = isSessionId(cluster.qa_session_id) ? cluster.qa_session_id : null;
|
|
160
|
+
if (!orchestrator && !planner && !builder && !qa) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
orchestrator_session_id: orchestrator ?? 'unknown',
|
|
165
|
+
planner_session_id: planner ?? 'unknown',
|
|
166
|
+
builder_session_id: builder ?? 'unknown',
|
|
167
|
+
qa_session_id: qa ?? 'unknown',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
94
171
|
function firstValidTimestamp(frontMatter: Record<string, unknown>): number | null {
|
|
95
172
|
const candidates = [
|
|
96
173
|
frontMatter.activity_last_event_at,
|
|
@@ -166,6 +243,133 @@ function asRecord(value: unknown): Record<string, unknown> | null {
|
|
|
166
243
|
return value && typeof value === 'object' ? (value as Record<string, unknown>) : null;
|
|
167
244
|
}
|
|
168
245
|
|
|
246
|
+
function normalizeFeatureExecutionMode(value: unknown): FeatureExecutionMode | null {
|
|
247
|
+
return value === 'deterministic' || value === 'interactive' ? value : null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function normalizeFeatureCheckpoints(frontMatter: Record<string, unknown>): FeatureCheckpoint[] {
|
|
251
|
+
const raw = frontMatter['checkpoints'];
|
|
252
|
+
if (!Array.isArray(raw)) {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
const checkpoints: FeatureCheckpoint[] = [];
|
|
256
|
+
for (const entry of raw) {
|
|
257
|
+
const record = asRecord(entry);
|
|
258
|
+
if (!record) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const checkpointId =
|
|
262
|
+
typeof record.checkpoint_id === 'string' ? record.checkpoint_id.trim() : '';
|
|
263
|
+
const timestamp = typeof record.timestamp === 'string' ? record.timestamp.trim() : '';
|
|
264
|
+
const diffSnapshot =
|
|
265
|
+
typeof record.diff_snapshot === 'string' ? record.diff_snapshot.trim() : '';
|
|
266
|
+
const validationStatus =
|
|
267
|
+
record.validation_status === 'valid' ||
|
|
268
|
+
record.validation_status === 'invalid' ||
|
|
269
|
+
record.validation_status === 'skipped'
|
|
270
|
+
? record.validation_status
|
|
271
|
+
: null;
|
|
272
|
+
if (!checkpointId || !timestamp || !diffSnapshot || !validationStatus) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const filesChanged = Array.isArray(record.files_changed)
|
|
277
|
+
? record.files_changed.filter((item): item is string => typeof item === 'string')
|
|
278
|
+
: [];
|
|
279
|
+
const violations = Array.isArray(record.violations)
|
|
280
|
+
? record.violations.filter((item): item is string => typeof item === 'string')
|
|
281
|
+
: [];
|
|
282
|
+
const severity =
|
|
283
|
+
record.severity === 'info' ||
|
|
284
|
+
record.severity === 'warning' ||
|
|
285
|
+
record.severity === 'error' ||
|
|
286
|
+
record.severity === 'critical'
|
|
287
|
+
? record.severity
|
|
288
|
+
: undefined;
|
|
289
|
+
checkpoints.push({
|
|
290
|
+
checkpoint_id: checkpointId,
|
|
291
|
+
timestamp,
|
|
292
|
+
files_changed: filesChanged,
|
|
293
|
+
validation_status: validationStatus,
|
|
294
|
+
violations,
|
|
295
|
+
severity,
|
|
296
|
+
diff_snapshot: diffSnapshot,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
checkpoints.sort((left, right) => {
|
|
301
|
+
const rightTs = Date.parse(right.timestamp);
|
|
302
|
+
const leftTs = Date.parse(left.timestamp);
|
|
303
|
+
if (Number.isNaN(rightTs) || Number.isNaN(leftTs)) {
|
|
304
|
+
return right.checkpoint_id.localeCompare(left.checkpoint_id);
|
|
305
|
+
}
|
|
306
|
+
return rightTs - leftTs;
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
return checkpoints;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function average(values: number[]): number | null {
|
|
313
|
+
if (values.length === 0) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function percentile(values: number[], percentileRank: number): number | null {
|
|
320
|
+
if (values.length === 0) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
324
|
+
const rank = Math.min(
|
|
325
|
+
sorted.length - 1,
|
|
326
|
+
Math.max(0, Math.ceil((percentileRank / 100) * sorted.length) - 1),
|
|
327
|
+
);
|
|
328
|
+
return sorted[rank] ?? null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function normalizeNumberArray(value: unknown): number[] {
|
|
332
|
+
if (!Array.isArray(value)) {
|
|
333
|
+
return [];
|
|
334
|
+
}
|
|
335
|
+
return value.filter(
|
|
336
|
+
(entry): entry is number => typeof entry === 'number' && Number.isFinite(entry),
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function truncateNumber(value: number | null, digits = 2): number | null {
|
|
341
|
+
if (value == null) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
const factor = Math.pow(10, digits);
|
|
345
|
+
return Math.round(value * factor) / factor;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function resolveRepoRelativePath(repoRoot: string, relativePath: string): string | null {
|
|
349
|
+
const resolved = path.resolve(repoRoot, relativePath);
|
|
350
|
+
const normalizedRoot = path.resolve(repoRoot);
|
|
351
|
+
if (resolved === normalizedRoot) {
|
|
352
|
+
return resolved;
|
|
353
|
+
}
|
|
354
|
+
if (!resolved.startsWith(`${normalizedRoot}${path.sep}`)) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
return resolved;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function normalizeCheckpointDiffSignature(rawDiff: string): Map<string, string> {
|
|
361
|
+
const signatures = new Map<string, string>();
|
|
362
|
+
const files = parseUnifiedDiff(rawDiff);
|
|
363
|
+
for (const file of files) {
|
|
364
|
+
const key = file.filePath.trim();
|
|
365
|
+
if (!key) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
signatures.set(key, JSON.stringify(file));
|
|
369
|
+
}
|
|
370
|
+
return signatures;
|
|
371
|
+
}
|
|
372
|
+
|
|
169
373
|
async function readJsonFile<T>(filePath: string): Promise<T | null> {
|
|
170
374
|
try {
|
|
171
375
|
const raw = await readFile(filePath, 'utf-8');
|
|
@@ -295,6 +499,7 @@ function normalizeFeatureSummary(
|
|
|
295
499
|
}
|
|
296
500
|
: undefined;
|
|
297
501
|
const roleStatus = parseRoleStatus(frontMatter);
|
|
502
|
+
const cluster = parseSessionCluster(frontMatter);
|
|
298
503
|
const rawLocks =
|
|
299
504
|
frontMatter.locks && typeof frontMatter.locks === 'object'
|
|
300
505
|
? (frontMatter.locks as Record<string, unknown>)
|
|
@@ -331,6 +536,7 @@ function normalizeFeatureSummary(
|
|
|
331
536
|
revision_reason:
|
|
332
537
|
typeof frontMatter.revision_reason === 'string' ? frontMatter.revision_reason : null,
|
|
333
538
|
role_status: roleStatus ?? undefined,
|
|
539
|
+
cluster: cluster ?? undefined,
|
|
334
540
|
gate_retry_count:
|
|
335
541
|
typeof frontMatter.gate_retry_count === 'number' ? frontMatter.gate_retry_count : undefined,
|
|
336
542
|
last_retry_at: typeof frontMatter.last_retry_at === 'string' ? frontMatter.last_retry_at : null,
|
|
@@ -469,6 +675,26 @@ export async function readFeatureState(
|
|
|
469
675
|
}
|
|
470
676
|
}
|
|
471
677
|
|
|
678
|
+
export async function readFeatureCheckpoints(
|
|
679
|
+
featureId: string,
|
|
680
|
+
repoRoot = AOP_ROOT,
|
|
681
|
+
): Promise<{
|
|
682
|
+
execution_mode: FeatureExecutionMode | null;
|
|
683
|
+
checkpoints: FeatureCheckpoint[];
|
|
684
|
+
} | null> {
|
|
685
|
+
const statePath = path.join(getFeatureRoot(featureId, repoRoot), 'state.md');
|
|
686
|
+
try {
|
|
687
|
+
const raw = await readFile(statePath, 'utf-8');
|
|
688
|
+
const { frontMatter } = parseFrontMatter(raw);
|
|
689
|
+
return {
|
|
690
|
+
execution_mode: normalizeFeatureExecutionMode(frontMatter.execution_mode),
|
|
691
|
+
checkpoints: normalizeFeatureCheckpoints(frontMatter),
|
|
692
|
+
};
|
|
693
|
+
} catch {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
472
698
|
export async function readFeaturePlan(
|
|
473
699
|
featureId: string,
|
|
474
700
|
repoRoot = AOP_ROOT,
|
|
@@ -622,6 +848,93 @@ export async function readFeatureDiff(featureId: string, repoRoot = AOP_ROOT): P
|
|
|
622
848
|
}
|
|
623
849
|
}
|
|
624
850
|
|
|
851
|
+
export async function readFeatureCheckpointDiff(
|
|
852
|
+
featureId: string,
|
|
853
|
+
checkpointId: string,
|
|
854
|
+
repoRoot = AOP_ROOT,
|
|
855
|
+
): Promise<FeatureCheckpointDiff | null> {
|
|
856
|
+
const checkpointState = await readFeatureCheckpoints(featureId, repoRoot);
|
|
857
|
+
if (!checkpointState) {
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
const checkpoint = checkpointState.checkpoints.find(
|
|
861
|
+
(entry) => entry.checkpoint_id === checkpointId,
|
|
862
|
+
);
|
|
863
|
+
if (!checkpoint) {
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
const resolvedSnapshotPath = resolveRepoRelativePath(repoRoot, checkpoint.diff_snapshot);
|
|
867
|
+
if (!resolvedSnapshotPath) {
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
try {
|
|
871
|
+
const diff = await readFile(resolvedSnapshotPath, 'utf-8');
|
|
872
|
+
return {
|
|
873
|
+
checkpoint,
|
|
874
|
+
diff,
|
|
875
|
+
};
|
|
876
|
+
} catch {
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
export async function compareFeatureCheckpoints(
|
|
882
|
+
featureId: string,
|
|
883
|
+
fromCheckpointId: string,
|
|
884
|
+
toCheckpointId: string,
|
|
885
|
+
repoRoot = AOP_ROOT,
|
|
886
|
+
): Promise<FeatureCheckpointComparison | null> {
|
|
887
|
+
if (fromCheckpointId === toCheckpointId) {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
const [fromSnapshot, toSnapshot] = await Promise.all([
|
|
891
|
+
readFeatureCheckpointDiff(featureId, fromCheckpointId, repoRoot),
|
|
892
|
+
readFeatureCheckpointDiff(featureId, toCheckpointId, repoRoot),
|
|
893
|
+
]);
|
|
894
|
+
if (!fromSnapshot || !toSnapshot) {
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const fromSignatures = normalizeCheckpointDiffSignature(fromSnapshot.diff);
|
|
899
|
+
const toSignatures = normalizeCheckpointDiffSignature(toSnapshot.diff);
|
|
900
|
+
const allPaths = new Set<string>([...fromSignatures.keys(), ...toSignatures.keys()]);
|
|
901
|
+
|
|
902
|
+
const files: FeatureCheckpointComparison['files'] = [];
|
|
903
|
+
for (const filePath of allPaths) {
|
|
904
|
+
const fromSignature = fromSignatures.get(filePath);
|
|
905
|
+
const toSignature = toSignatures.get(filePath);
|
|
906
|
+
let status: FeatureCheckpointComparison['files'][number]['status'] = 'unchanged';
|
|
907
|
+
if (fromSignature == null && toSignature != null) {
|
|
908
|
+
status = 'added';
|
|
909
|
+
} else if (fromSignature != null && toSignature == null) {
|
|
910
|
+
status = 'removed';
|
|
911
|
+
} else if (fromSignature !== toSignature) {
|
|
912
|
+
status = 'changed';
|
|
913
|
+
}
|
|
914
|
+
files.push({ path: filePath, status });
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
files.sort((left, right) => left.path.localeCompare(right.path));
|
|
918
|
+
|
|
919
|
+
const summary = {
|
|
920
|
+
added: files.filter((file) => file.status === 'added').length,
|
|
921
|
+
removed: files.filter((file) => file.status === 'removed').length,
|
|
922
|
+
changed: files.filter((file) => file.status === 'changed').length,
|
|
923
|
+
unchanged: files.filter((file) => file.status === 'unchanged').length,
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
return {
|
|
927
|
+
from_checkpoint_id: fromCheckpointId,
|
|
928
|
+
to_checkpoint_id: toCheckpointId,
|
|
929
|
+
from_timestamp: fromSnapshot.checkpoint.timestamp,
|
|
930
|
+
to_timestamp: toSnapshot.checkpoint.timestamp,
|
|
931
|
+
summary,
|
|
932
|
+
files,
|
|
933
|
+
from_diff: fromSnapshot.diff,
|
|
934
|
+
to_diff: toSnapshot.diff,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
625
938
|
export async function listEvidenceArtifacts(
|
|
626
939
|
featureId: string,
|
|
627
940
|
repoRoot = AOP_ROOT,
|
|
@@ -798,6 +1111,76 @@ function parseStateLogBody(body: string): FeatureLogEntry[] {
|
|
|
798
1111
|
return entries;
|
|
799
1112
|
}
|
|
800
1113
|
|
|
1114
|
+
function parseRoleFromActor(actor: string): AgentLogEntry['role'] {
|
|
1115
|
+
const candidate = actor.split(':')[0]?.trim().toLowerCase();
|
|
1116
|
+
if (!candidate) {
|
|
1117
|
+
return 'orchestrator';
|
|
1118
|
+
}
|
|
1119
|
+
return candidate;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function parseDecisionEntries(raw: string): AgentLogEntry[] {
|
|
1123
|
+
const lines = raw.replace(/\r\n/g, '\n').split('\n');
|
|
1124
|
+
const entries: AgentLogEntry[] = [];
|
|
1125
|
+
|
|
1126
|
+
for (const line of lines) {
|
|
1127
|
+
const trimmed = line.trim();
|
|
1128
|
+
if (trimmed.length === 0 || trimmed.startsWith('#')) {
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
const match = trimmed.match(/^-?\s*([0-9TZ:.-]{10,})\s+\[([^\]]+)\]\s+([\s\S]+)$/);
|
|
1132
|
+
if (match) {
|
|
1133
|
+
const timestampRaw = match[1];
|
|
1134
|
+
const actor = match[2].trim();
|
|
1135
|
+
const text = match[3].trim();
|
|
1136
|
+
const timestamp = Number.isNaN(Date.parse(timestampRaw))
|
|
1137
|
+
? new Date().toISOString()
|
|
1138
|
+
: new Date(timestampRaw).toISOString();
|
|
1139
|
+
entries.push({
|
|
1140
|
+
id: `${timestamp}:${entries.length}`,
|
|
1141
|
+
timestamp,
|
|
1142
|
+
actor,
|
|
1143
|
+
role: parseRoleFromActor(actor),
|
|
1144
|
+
text,
|
|
1145
|
+
});
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
entries.push({
|
|
1150
|
+
id: `line:${entries.length}`,
|
|
1151
|
+
timestamp: new Date().toISOString(),
|
|
1152
|
+
actor: 'system:unknown',
|
|
1153
|
+
role: 'orchestrator',
|
|
1154
|
+
text: trimmed,
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
return entries;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
export async function readDecisionLogEntries(
|
|
1162
|
+
featureId: string,
|
|
1163
|
+
repoRoot = AOP_ROOT,
|
|
1164
|
+
options: { order?: 'asc' | 'desc' } = {},
|
|
1165
|
+
): Promise<AgentLogEntry[]> {
|
|
1166
|
+
const logPath = path.join(getFeatureRoot(featureId, repoRoot), 'decisions.md');
|
|
1167
|
+
const order = options.order ?? 'desc';
|
|
1168
|
+
try {
|
|
1169
|
+
const raw = await readFile(logPath, 'utf8');
|
|
1170
|
+
const sorted = parseDecisionEntries(raw).sort((left, right) => {
|
|
1171
|
+
const leftMs = Date.parse(left.timestamp);
|
|
1172
|
+
const rightMs = Date.parse(right.timestamp);
|
|
1173
|
+
if (leftMs === rightMs) {
|
|
1174
|
+
return left.id.localeCompare(right.id);
|
|
1175
|
+
}
|
|
1176
|
+
return leftMs - rightMs;
|
|
1177
|
+
});
|
|
1178
|
+
return order === 'desc' ? [...sorted].reverse() : sorted;
|
|
1179
|
+
} catch {
|
|
1180
|
+
return [];
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
801
1184
|
export async function readFeatureLog(featureId: string, repoRoot = AOP_ROOT): Promise<FeatureLog> {
|
|
802
1185
|
const statePath = path.join(getFeatureRoot(featureId, repoRoot), 'state.md');
|
|
803
1186
|
const entries: FeatureLogEntry[] = [];
|
|
@@ -833,6 +1216,342 @@ export async function readFeatureLog(featureId: string, repoRoot = AOP_ROOT): Pr
|
|
|
833
1216
|
};
|
|
834
1217
|
}
|
|
835
1218
|
|
|
1219
|
+
function normalizeRawLogRole(value: unknown): RawLogFileMeta['role'] | null {
|
|
1220
|
+
return value === 'planner' || value === 'builder' || value === 'qa' ? value : null;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
export async function listRawLogFiles(
|
|
1224
|
+
featureId: string,
|
|
1225
|
+
repoRoot = AOP_ROOT,
|
|
1226
|
+
options: { role?: string | null; limit?: number } = {},
|
|
1227
|
+
): Promise<RawLogFileMeta[]> {
|
|
1228
|
+
const logsDir = path.join(getFeatureRoot(featureId, repoRoot), 'logs');
|
|
1229
|
+
const roleFilter = normalizeRawLogRole(options.role ?? null);
|
|
1230
|
+
const hardLimit = Math.min(Math.max(options.limit ?? 100, 1), 200);
|
|
1231
|
+
try {
|
|
1232
|
+
const files = await readdir(logsDir);
|
|
1233
|
+
const entries: RawLogFileMeta[] = [];
|
|
1234
|
+
for (const filename of files) {
|
|
1235
|
+
const match = filename.match(LOG_FILENAME_PATTERN);
|
|
1236
|
+
if (!match) {
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
const role = normalizeRawLogRole(match[1]);
|
|
1240
|
+
if (!role) {
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
if (roleFilter && role !== roleFilter) {
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
const unixMs = Number.parseInt(match[2], 10);
|
|
1247
|
+
const filePath = path.join(logsDir, filename);
|
|
1248
|
+
const fileStat = await stat(filePath).catch(() => null);
|
|
1249
|
+
if (!fileStat?.isFile()) {
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
entries.push({
|
|
1253
|
+
filename,
|
|
1254
|
+
role,
|
|
1255
|
+
timestamp: Number.isNaN(unixMs)
|
|
1256
|
+
? fileStat.mtime.toISOString()
|
|
1257
|
+
: new Date(unixMs).toISOString(),
|
|
1258
|
+
size_bytes: fileStat.size,
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
return entries
|
|
1262
|
+
.sort((left, right) => Date.parse(right.timestamp) - Date.parse(left.timestamp))
|
|
1263
|
+
.slice(0, hardLimit);
|
|
1264
|
+
} catch {
|
|
1265
|
+
return [];
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
export async function readRawLogFile(
|
|
1270
|
+
featureId: string,
|
|
1271
|
+
filename: string,
|
|
1272
|
+
repoRoot = AOP_ROOT,
|
|
1273
|
+
): Promise<string | null> {
|
|
1274
|
+
if (!LOG_FILENAME_PATTERN.test(filename)) {
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
1277
|
+
const logPath = path.join(getFeatureRoot(featureId, repoRoot), 'logs', filename);
|
|
1278
|
+
try {
|
|
1279
|
+
return await readFile(logPath, 'utf8');
|
|
1280
|
+
} catch {
|
|
1281
|
+
return null;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
interface WorkerTimelineReadOptions {
|
|
1286
|
+
role?: string | null;
|
|
1287
|
+
valid?: 'all' | 'valid' | 'invalid';
|
|
1288
|
+
limit?: number;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function asWorkerEventEntry(value: unknown): WorkerEventEntry | null {
|
|
1292
|
+
if (!value || typeof value !== 'object') {
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1295
|
+
const record = value as Record<string, unknown>;
|
|
1296
|
+
if (typeof record.ts !== 'string' || typeof record.role !== 'string') {
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
const outputTypes = Array.isArray(record.output_types)
|
|
1300
|
+
? record.output_types.filter((item): item is string => typeof item === 'string')
|
|
1301
|
+
: [];
|
|
1302
|
+
return {
|
|
1303
|
+
ts: record.ts,
|
|
1304
|
+
role: record.role,
|
|
1305
|
+
output_types: outputTypes,
|
|
1306
|
+
patch_count: typeof record.patch_count === 'number' ? record.patch_count : 0,
|
|
1307
|
+
plan_submission_count:
|
|
1308
|
+
typeof record.plan_submission_count === 'number' ? record.plan_submission_count : 0,
|
|
1309
|
+
request_count: typeof record.request_count === 'number' ? record.request_count : 0,
|
|
1310
|
+
note_count: typeof record.note_count === 'number' ? record.note_count : 0,
|
|
1311
|
+
valid: record.valid !== false,
|
|
1312
|
+
error_code: typeof record.error_code === 'string' ? record.error_code : null,
|
|
1313
|
+
provider: typeof record.provider === 'string' ? record.provider : 'unknown',
|
|
1314
|
+
model: typeof record.model === 'string' ? record.model : 'unknown',
|
|
1315
|
+
elapsed_ms: typeof record.elapsed_ms === 'number' ? record.elapsed_ms : undefined,
|
|
1316
|
+
feature_id: typeof record.feature_id === 'string' ? record.feature_id : undefined,
|
|
1317
|
+
event_type: typeof record.event_type === 'string' ? record.event_type : undefined,
|
|
1318
|
+
execution_mode:
|
|
1319
|
+
record.execution_mode === 'interactive' || record.execution_mode === 'deterministic'
|
|
1320
|
+
? record.execution_mode
|
|
1321
|
+
: undefined,
|
|
1322
|
+
run_id: typeof record.run_id === 'string' ? record.run_id : undefined,
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
export async function readWorkerTimelineEntries(
|
|
1327
|
+
featureId: string,
|
|
1328
|
+
repoRoot = AOP_ROOT,
|
|
1329
|
+
options: WorkerTimelineReadOptions = {},
|
|
1330
|
+
): Promise<WorkerEventEntry[]> {
|
|
1331
|
+
const eventsRoot = path.join(repoRoot, '.aop', 'runtime', 'worker-events');
|
|
1332
|
+
const hardLimit = Math.min(Math.max(options.limit ?? 100, 1), 100);
|
|
1333
|
+
const roleFilter =
|
|
1334
|
+
typeof options.role === 'string' && options.role.length > 0 ? options.role : null;
|
|
1335
|
+
const validityFilter = options.valid ?? 'all';
|
|
1336
|
+
|
|
1337
|
+
try {
|
|
1338
|
+
const files = (await readdir(eventsRoot))
|
|
1339
|
+
.filter((name) => name.endsWith('.jsonl'))
|
|
1340
|
+
.sort((left, right) => right.localeCompare(left));
|
|
1341
|
+
const entries: WorkerEventEntry[] = [];
|
|
1342
|
+
for (const file of files) {
|
|
1343
|
+
const fullPath = path.join(eventsRoot, file);
|
|
1344
|
+
const raw = await readFile(fullPath, 'utf8').catch(() => '');
|
|
1345
|
+
if (!raw) {
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
for (const line of raw.replace(/\r\n/g, '\n').split('\n')) {
|
|
1349
|
+
if (line.trim().length === 0) {
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
let parsed: unknown;
|
|
1353
|
+
try {
|
|
1354
|
+
parsed = JSON.parse(line);
|
|
1355
|
+
} catch {
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
const entry = asWorkerEventEntry(parsed);
|
|
1359
|
+
if (!entry) {
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
if (entry.feature_id !== featureId) {
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
if (roleFilter && entry.role !== roleFilter) {
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
if (validityFilter === 'valid' && !entry.valid) {
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
if (validityFilter === 'invalid' && entry.valid) {
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
entries.push(entry);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
return entries
|
|
1378
|
+
.sort((left, right) => Date.parse(right.ts) - Date.parse(left.ts))
|
|
1379
|
+
.slice(0, hardLimit);
|
|
1380
|
+
} catch {
|
|
1381
|
+
return [];
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function normalizeLatestInteractiveMetric(
|
|
1386
|
+
value: unknown,
|
|
1387
|
+
): InteractivePerformanceLatestCheckpoint | null {
|
|
1388
|
+
const record = asRecord(value);
|
|
1389
|
+
if (!record) {
|
|
1390
|
+
return null;
|
|
1391
|
+
}
|
|
1392
|
+
if (
|
|
1393
|
+
typeof record.feature_id !== 'string' ||
|
|
1394
|
+
typeof record.checkpoint_id !== 'string' ||
|
|
1395
|
+
typeof record.trigger !== 'string' ||
|
|
1396
|
+
typeof record.validation_status !== 'string' ||
|
|
1397
|
+
typeof record.recorded_at !== 'string'
|
|
1398
|
+
) {
|
|
1399
|
+
return null;
|
|
1400
|
+
}
|
|
1401
|
+
if (
|
|
1402
|
+
record.validation_status !== 'valid' &&
|
|
1403
|
+
record.validation_status !== 'invalid' &&
|
|
1404
|
+
record.validation_status !== 'skipped'
|
|
1405
|
+
) {
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
return {
|
|
1409
|
+
feature_id: record.feature_id,
|
|
1410
|
+
checkpoint_id: record.checkpoint_id,
|
|
1411
|
+
trigger: record.trigger,
|
|
1412
|
+
validation_status: record.validation_status,
|
|
1413
|
+
checkpoint_latency_ms:
|
|
1414
|
+
typeof record.checkpoint_latency_ms === 'number' ? record.checkpoint_latency_ms : 0,
|
|
1415
|
+
validation_latency_ms:
|
|
1416
|
+
typeof record.validation_latency_ms === 'number' ? record.validation_latency_ms : 0,
|
|
1417
|
+
diff_capture_latency_ms:
|
|
1418
|
+
typeof record.diff_capture_latency_ms === 'number' ? record.diff_capture_latency_ms : 0,
|
|
1419
|
+
files_changed: typeof record.files_changed === 'number' ? record.files_changed : 0,
|
|
1420
|
+
recorded_at: record.recorded_at,
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
async function readAllWorkerEvents(repoRoot = AOP_ROOT): Promise<WorkerEventEntry[]> {
|
|
1425
|
+
const eventsRoot = path.join(repoRoot, '.aop', 'runtime', 'worker-events');
|
|
1426
|
+
try {
|
|
1427
|
+
const files = (await readdir(eventsRoot))
|
|
1428
|
+
.filter((name) => name.endsWith('.jsonl'))
|
|
1429
|
+
.sort((left, right) => right.localeCompare(left));
|
|
1430
|
+
const entries: WorkerEventEntry[] = [];
|
|
1431
|
+
for (const file of files) {
|
|
1432
|
+
const fullPath = path.join(eventsRoot, file);
|
|
1433
|
+
const raw = await readFile(fullPath, 'utf8').catch(() => '');
|
|
1434
|
+
if (!raw) {
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
for (const line of raw.replace(/\r\n/g, '\n').split('\n')) {
|
|
1438
|
+
if (!line.trim()) {
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
let parsed: unknown;
|
|
1442
|
+
try {
|
|
1443
|
+
parsed = JSON.parse(line);
|
|
1444
|
+
} catch {
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
1447
|
+
const entry = asWorkerEventEntry(parsed);
|
|
1448
|
+
if (entry) {
|
|
1449
|
+
entries.push(entry);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return entries;
|
|
1454
|
+
} catch {
|
|
1455
|
+
return [];
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function computeExecutionModeMetrics(entries: WorkerEventEntry[]): InteractiveExecutionModeMetrics {
|
|
1460
|
+
const deterministic = entries.filter((entry) => entry.execution_mode === 'deterministic');
|
|
1461
|
+
const interactive = entries.filter((entry) => entry.execution_mode === 'interactive');
|
|
1462
|
+
|
|
1463
|
+
const deterministicElapsed = deterministic
|
|
1464
|
+
.map((entry) => entry.elapsed_ms)
|
|
1465
|
+
.filter((value): value is number => typeof value === 'number' && Number.isFinite(value));
|
|
1466
|
+
const interactiveElapsed = interactive
|
|
1467
|
+
.map((entry) => entry.elapsed_ms)
|
|
1468
|
+
.filter((value): value is number => typeof value === 'number' && Number.isFinite(value));
|
|
1469
|
+
const deterministicRequests = deterministic.map((entry) => entry.request_count);
|
|
1470
|
+
const interactiveRequests = interactive.map((entry) => entry.request_count);
|
|
1471
|
+
|
|
1472
|
+
const deterministicAvgRequest = average(deterministicRequests);
|
|
1473
|
+
const interactiveAvgRequest = average(interactiveRequests);
|
|
1474
|
+
const reductionRatio =
|
|
1475
|
+
deterministicAvgRequest != null && deterministicAvgRequest > 0 && interactiveAvgRequest != null
|
|
1476
|
+
? (deterministicAvgRequest - interactiveAvgRequest) / deterministicAvgRequest
|
|
1477
|
+
: null;
|
|
1478
|
+
|
|
1479
|
+
return {
|
|
1480
|
+
interactive_iterations: interactive.length,
|
|
1481
|
+
deterministic_iterations: deterministic.length,
|
|
1482
|
+
interactive_avg_elapsed_ms: truncateNumber(average(interactiveElapsed), 1),
|
|
1483
|
+
deterministic_avg_elapsed_ms: truncateNumber(average(deterministicElapsed), 1),
|
|
1484
|
+
interactive_avg_request_count: truncateNumber(interactiveAvgRequest, 2),
|
|
1485
|
+
deterministic_avg_request_count: truncateNumber(deterministicAvgRequest, 2),
|
|
1486
|
+
context_request_reduction_ratio: truncateNumber(reductionRatio, 4),
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
export async function readInteractivePerformanceMetrics(
|
|
1491
|
+
repoRoot = AOP_ROOT,
|
|
1492
|
+
): Promise<InteractivePerformanceMetrics> {
|
|
1493
|
+
const metricsPath = path.join(repoRoot, '.aop', 'analytics', 'interactive-mode.json');
|
|
1494
|
+
const parsed = await readJsonFile<InteractiveMetricsFileShape>(metricsPath);
|
|
1495
|
+
const executionModes = computeExecutionModeMetrics(await readAllWorkerEvents(repoRoot));
|
|
1496
|
+
|
|
1497
|
+
if (!parsed) {
|
|
1498
|
+
return {
|
|
1499
|
+
updated_at: null,
|
|
1500
|
+
checkpoint_count: 0,
|
|
1501
|
+
valid_count: 0,
|
|
1502
|
+
invalid_count: 0,
|
|
1503
|
+
skipped_count: 0,
|
|
1504
|
+
avg_files_changed: null,
|
|
1505
|
+
avg_checkpoint_latency_ms: null,
|
|
1506
|
+
p95_checkpoint_latency_ms: null,
|
|
1507
|
+
avg_validation_latency_ms: null,
|
|
1508
|
+
p95_validation_latency_ms: null,
|
|
1509
|
+
avg_diff_capture_latency_ms: null,
|
|
1510
|
+
latest: null,
|
|
1511
|
+
execution_modes: executionModes,
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
const checkpointLatencies = normalizeNumberArray(parsed.histories?.checkpoint_latency_ms).slice(
|
|
1516
|
+
-CHECKPOINT_METRICS_HISTORY_LIMIT,
|
|
1517
|
+
);
|
|
1518
|
+
const validationLatencies = normalizeNumberArray(parsed.histories?.validation_latency_ms).slice(
|
|
1519
|
+
-CHECKPOINT_METRICS_HISTORY_LIMIT,
|
|
1520
|
+
);
|
|
1521
|
+
const diffCaptureLatencies = normalizeNumberArray(
|
|
1522
|
+
parsed.histories?.diff_capture_latency_ms,
|
|
1523
|
+
).slice(-CHECKPOINT_METRICS_HISTORY_LIMIT);
|
|
1524
|
+
|
|
1525
|
+
const checkpointCount =
|
|
1526
|
+
typeof parsed.totals?.checkpoint_count === 'number'
|
|
1527
|
+
? parsed.totals.checkpoint_count
|
|
1528
|
+
: checkpointLatencies.length;
|
|
1529
|
+
const validCount = typeof parsed.totals?.valid_count === 'number' ? parsed.totals.valid_count : 0;
|
|
1530
|
+
const invalidCount =
|
|
1531
|
+
typeof parsed.totals?.invalid_count === 'number' ? parsed.totals.invalid_count : 0;
|
|
1532
|
+
const skippedCount =
|
|
1533
|
+
typeof parsed.totals?.skipped_count === 'number' ? parsed.totals.skipped_count : 0;
|
|
1534
|
+
const filesChangedTotal =
|
|
1535
|
+
typeof parsed.totals?.files_changed_total === 'number' ? parsed.totals.files_changed_total : 0;
|
|
1536
|
+
const averageFilesChanged = checkpointCount > 0 ? filesChangedTotal / checkpointCount : null;
|
|
1537
|
+
|
|
1538
|
+
return {
|
|
1539
|
+
updated_at: typeof parsed.updated_at === 'string' ? parsed.updated_at : null,
|
|
1540
|
+
checkpoint_count: checkpointCount,
|
|
1541
|
+
valid_count: validCount,
|
|
1542
|
+
invalid_count: invalidCount,
|
|
1543
|
+
skipped_count: skippedCount,
|
|
1544
|
+
avg_files_changed: truncateNumber(averageFilesChanged, 2),
|
|
1545
|
+
avg_checkpoint_latency_ms: truncateNumber(average(checkpointLatencies), 1),
|
|
1546
|
+
p95_checkpoint_latency_ms: truncateNumber(percentile(checkpointLatencies, 95), 1),
|
|
1547
|
+
avg_validation_latency_ms: truncateNumber(average(validationLatencies), 1),
|
|
1548
|
+
p95_validation_latency_ms: truncateNumber(percentile(validationLatencies, 95), 1),
|
|
1549
|
+
avg_diff_capture_latency_ms: truncateNumber(average(diffCaptureLatencies), 1),
|
|
1550
|
+
latest: normalizeLatestInteractiveMetric(parsed.latest),
|
|
1551
|
+
execution_modes: executionModes,
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
|
|
836
1555
|
function buildMergeHistogram14d(features: FeatureSummary[]): number[] {
|
|
837
1556
|
const days = 14;
|
|
838
1557
|
const buckets = new Array<number>(days).fill(0);
|
|
@@ -947,6 +1666,9 @@ export async function readFeatureDetail(
|
|
|
947
1666
|
if (!feature) {
|
|
948
1667
|
return null;
|
|
949
1668
|
}
|
|
1669
|
+
|
|
1670
|
+
const checkpointState = await readFeatureCheckpoints(featureId, repoRoot);
|
|
1671
|
+
|
|
950
1672
|
return {
|
|
951
1673
|
feature,
|
|
952
1674
|
plan: await readFeaturePlan(featureId, repoRoot),
|
|
@@ -956,5 +1678,8 @@ export async function readFeatureDetail(
|
|
|
956
1678
|
cost: await readFeatureCost(featureId, repoRoot),
|
|
957
1679
|
qa_test_index: await readFeatureQaTestIndex(featureId, repoRoot),
|
|
958
1680
|
review_brief: await readFeatureReviewBrief(featureId, repoRoot),
|
|
1681
|
+
log_entries: (await readDecisionLogEntries(featureId, repoRoot)).slice(0, 50),
|
|
1682
|
+
execution_mode: checkpointState?.execution_mode ?? null,
|
|
1683
|
+
checkpoints: checkpointState?.checkpoints ?? [],
|
|
959
1684
|
};
|
|
960
1685
|
}
|