vibe-coding-master 0.7.16 → 0.7.18

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 (37) hide show
  1. package/README.md +19 -2
  2. package/dist/backend/adapters/claude-adapter.js +4 -1
  3. package/dist/backend/api/harness-routes.js +21 -4
  4. package/dist/backend/api/session-routes.js +5 -0
  5. package/dist/backend/api/translation-worker-routes.js +15 -2
  6. package/dist/backend/api/usage-analytics-routes.js +31 -0
  7. package/dist/backend/cli/install-vcm-harness.js +54 -5
  8. package/dist/backend/server.js +27 -5
  9. package/dist/backend/services/app-settings-service.js +26 -2
  10. package/dist/backend/services/architect-restart-service.js +136 -0
  11. package/dist/backend/services/claude-hook-service.js +134 -54
  12. package/dist/backend/services/gate-review-service.js +92 -27
  13. package/dist/backend/services/harness-service.js +54 -7
  14. package/dist/backend/services/message-service.js +6 -1
  15. package/dist/backend/services/runtime-coordinator-service.js +10 -10
  16. package/dist/backend/services/session-service.js +78 -25
  17. package/dist/backend/services/task-close-service.js +1 -0
  18. package/dist/backend/services/terminal-interrupt-service.js +4 -1
  19. package/dist/backend/services/turn-reconciler-service.js +1 -0
  20. package/dist/backend/services/usage-analytics-service.js +346 -0
  21. package/dist/backend/templates/handoff.js +4 -0
  22. package/dist/backend/templates/harness/architect-agent.js +18 -10
  23. package/dist/backend/templates/harness/architect-scaffold-worker-agent.js +23 -0
  24. package/dist/backend/templates/harness/claude-root.js +1 -1
  25. package/dist/backend/templates/harness/gate-review.js +25 -3
  26. package/dist/backend/templates/harness/project-manager-agent.js +10 -2
  27. package/dist/backend/templates/harness/restart-architect-skill.js +75 -0
  28. package/dist/backend/templates/harness/tester-agent.js +15 -7
  29. package/dist/backend/templates/harness/vcm-architecture-interview-skill.js +31 -5
  30. package/dist/backend/templates/harness/vcm-route-message-skill.js +3 -3
  31. package/dist/shared/types/app-settings.js +14 -0
  32. package/dist/shared/types/usage-analytics.js +1 -0
  33. package/dist/shared/validation/artifact-check.js +29 -3
  34. package/dist-frontend/assets/{index-BAE_pjXJ.js → index-CDkDHrWQ.js} +43 -43
  35. package/dist-frontend/assets/{index-CiEUp9Si.css → index-Ci7z8tW3.css} +1 -1
  36. package/dist-frontend/index.html +2 -2
  37. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import path from "node:path";
2
3
  import { ROLE_NAMES, isDispatchableRole } from "../../shared/constants.js";
3
4
  import { CCR_GPT_SESSION_MODEL, isCcrSessionModel } from "../../shared/types/session.js";
@@ -17,6 +18,7 @@ const PROJECT_TRANSLATOR_SCOPE = "__project__";
17
18
  const PROJECT_HARNESS_ENGINEER_SCOPE = "__project_harness_engineer__";
18
19
  const PROJECT_TOOL_CD_ENTER_DELAY_MS = 500;
19
20
  const CLAUDE_CODE_DISABLE_AUTO_MEMORY = "1";
21
+ const CLAUDE_CODE_DISABLE_BACKGROUND_TASKS = "1";
20
22
  // Project tool sessions launch a Claude Code TUI inside a PTY. The PTY reports
21
23
  // "running" the instant it is spawned, which is earlier than the moment the TUI
22
24
  // can actually accept pasted input. These bounds drive a quiescence-based
@@ -80,9 +82,10 @@ export function createSessionService(deps) {
80
82
  ? claudeTranscriptPath(taskRepoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
81
83
  : undefined;
82
84
  const startCommand = {
83
- ...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
85
+ ...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride, input.appendSystemPrompt),
84
86
  cwd: taskRepoRoot
85
87
  };
88
+ const runtimeSessionToken = randomUUID();
86
89
  const runtimeSession = await deps.runtime.createSession({
87
90
  repoRoot,
88
91
  taskSlug,
@@ -96,8 +99,9 @@ export function createSessionService(deps) {
96
99
  VCM_TASK_REPO_ROOT: taskRepoRoot,
97
100
  VCM_TASK_SLUG: taskSlug,
98
101
  VCM_ROLE: role,
99
- VCM_SESSION_ID: claudeSessionId || undefined
100
- }, modelEnvironment),
102
+ VCM_SESSION_ID: claudeSessionId || undefined,
103
+ VCM_RUNTIME_SESSION_TOKEN: runtimeSessionToken
104
+ }, modelEnvironment, buildUsageTelemetryEnvironment(deps.apiUrl, role, model)),
101
105
  cols: input.cols,
102
106
  rows: input.rows
103
107
  });
@@ -105,6 +109,7 @@ export function createSessionService(deps) {
105
109
  const harnessRevision = await readCurrentHarnessRevision(repoRoot);
106
110
  const record = {
107
111
  id: runtimeSession.id,
112
+ runtimeSessionToken,
108
113
  claudeSessionId,
109
114
  transcriptPath,
110
115
  taskSlug,
@@ -207,6 +212,7 @@ export function createSessionService(deps) {
207
212
  ...deps.claude.buildRoleStartCommand(TRANSLATOR_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
208
213
  cwd: launchCwd
209
214
  };
215
+ const runtimeSessionToken = randomUUID();
210
216
  const runtimeSession = await deps.runtime.createSession({
211
217
  repoRoot,
212
218
  taskSlug: PROJECT_TRANSLATOR_SCOPE,
@@ -223,8 +229,9 @@ export function createSessionService(deps) {
223
229
  // active task; VCM_TASK_REPO_ROOT remains the active worktree.
224
230
  VCM_TASK_SLUG: PROJECT_TRANSLATOR_SCOPE,
225
231
  VCM_ROLE: TRANSLATOR_ROLE,
226
- VCM_SESSION_ID: claudeSessionId || undefined
227
- }, modelEnvironment),
232
+ VCM_SESSION_ID: claudeSessionId || undefined,
233
+ VCM_RUNTIME_SESSION_TOKEN: runtimeSessionToken
234
+ }, modelEnvironment, buildUsageTelemetryEnvironment(deps.apiUrl, TRANSLATOR_ROLE, model)),
228
235
  cols: input.cols,
229
236
  rows: input.rows
230
237
  });
@@ -232,6 +239,7 @@ export function createSessionService(deps) {
232
239
  const harnessRevision = await readCurrentHarnessRevision(repoRoot);
233
240
  const record = {
234
241
  id: runtimeSession.id,
242
+ runtimeSessionToken,
235
243
  claudeSessionId,
236
244
  transcriptPath,
237
245
  taskSlug: PROJECT_TRANSLATOR_SCOPE,
@@ -320,6 +328,7 @@ export function createSessionService(deps) {
320
328
  ...deps.claude.buildRoleStartCommand(HARNESS_ENGINEER_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
321
329
  cwd: launchCwd
322
330
  };
331
+ const runtimeSessionToken = randomUUID();
323
332
  const runtimeSession = await deps.runtime.createSession({
324
333
  repoRoot,
325
334
  taskSlug: PROJECT_HARNESS_ENGINEER_SCOPE,
@@ -336,8 +345,9 @@ export function createSessionService(deps) {
336
345
  // active task; VCM_TASK_REPO_ROOT remains the active worktree.
337
346
  VCM_TASK_SLUG: PROJECT_HARNESS_ENGINEER_SCOPE,
338
347
  VCM_ROLE: HARNESS_ENGINEER_ROLE,
339
- VCM_SESSION_ID: claudeSessionId || undefined
340
- }, modelEnvironment),
348
+ VCM_SESSION_ID: claudeSessionId || undefined,
349
+ VCM_RUNTIME_SESSION_TOKEN: runtimeSessionToken
350
+ }, modelEnvironment, buildUsageTelemetryEnvironment(deps.apiUrl, HARNESS_ENGINEER_ROLE, model)),
341
351
  cols: input.cols,
342
352
  rows: input.rows
343
353
  });
@@ -345,6 +355,7 @@ export function createSessionService(deps) {
345
355
  const harnessRevision = await readCurrentHarnessRevision(repoRoot);
346
356
  const record = {
347
357
  id: runtimeSession.id,
358
+ runtimeSessionToken,
348
359
  claudeSessionId,
349
360
  transcriptPath,
350
361
  taskSlug: PROJECT_HARNESS_ENGINEER_SCOPE,
@@ -515,6 +526,7 @@ export function createSessionService(deps) {
515
526
  ...deps.claude.buildRoleStartCommand(session.role, config.claudeCommand, permissionMode, session.claudeSessionId, true, model, effort, modelSettingsOverride),
516
527
  cwd: launchCwd
517
528
  };
529
+ const runtimeSessionToken = randomUUID();
518
530
  const runtimeSession = await deps.runtime.createSession({
519
531
  repoRoot,
520
532
  taskSlug: normalizeProjectScopedRecordForPersistence(session).taskSlug,
@@ -528,14 +540,16 @@ export function createSessionService(deps) {
528
540
  VCM_TASK_REPO_ROOT: targetCwd,
529
541
  VCM_TASK_SLUG: normalizeProjectScopedRecordForPersistence(session).taskSlug,
530
542
  VCM_ROLE: session.role,
531
- VCM_SESSION_ID: session.claudeSessionId
532
- }, modelEnvironment)
543
+ VCM_SESSION_ID: session.claudeSessionId,
544
+ VCM_RUNTIME_SESSION_TOKEN: runtimeSessionToken
545
+ }, modelEnvironment, buildUsageTelemetryEnvironment(deps.apiUrl, session.role, model))
533
546
  });
534
547
  if ((await waitForSessionInputReady(runtimeSession.id)) === "exited") {
535
548
  deps.registry.remove(runtimeSession.id);
536
549
  return markProjectToolRuntimeUnavailable(repoRoot, {
537
550
  ...session,
538
551
  id: runtimeSession.id,
552
+ runtimeSessionToken,
539
553
  status: "crashed",
540
554
  activityStatus: "idle",
541
555
  command: startCommand.display,
@@ -553,6 +567,7 @@ export function createSessionService(deps) {
553
567
  const resumed = {
554
568
  ...session,
555
569
  id: runtimeSession.id,
570
+ runtimeSessionToken,
556
571
  status: runtimeSession.status,
557
572
  activityStatus: "idle",
558
573
  command: startCommand.display,
@@ -679,9 +694,9 @@ export function createSessionService(deps) {
679
694
  const view = toRoleSessionRecordView(record, deps.runtime);
680
695
  return view ? withHarnessRevisionView(repoRoot, view) : undefined;
681
696
  }
682
- async function markTaskRoleActivityIdle(repoRoot, taskSlug, role) {
697
+ async function markTaskRoleActivityIdle(repoRoot, taskSlug, role, expectedSessionId) {
683
698
  const current = await getTaskRoleSessionView(repoRoot, taskSlug, role);
684
- if (!current) {
699
+ if (!current || (expectedSessionId && current.id !== expectedSessionId)) {
685
700
  return undefined;
686
701
  }
687
702
  const timestamp = now();
@@ -794,7 +809,7 @@ export function createSessionService(deps) {
794
809
  },
795
810
  async recordProjectTranslatorHookEvent(repoRoot, input) {
796
811
  const current = await this.getProjectTranslatorSession(repoRoot);
797
- if (!current) {
812
+ if (!current || !matchesRoleHookSession(current, input)) {
798
813
  return undefined;
799
814
  }
800
815
  const timestamp = now();
@@ -920,7 +935,7 @@ export function createSessionService(deps) {
920
935
  },
921
936
  async recordProjectHarnessEngineerHookEvent(repoRoot, input) {
922
937
  const current = await this.getProjectHarnessEngineerSession(repoRoot);
923
- if (!current) {
938
+ if (!current || !matchesRoleHookSession(current, input)) {
924
939
  return undefined;
925
940
  }
926
941
  const timestamp = now();
@@ -1039,7 +1054,7 @@ export function createSessionService(deps) {
1039
1054
  },
1040
1055
  async recordRoleHookEvent(repoRoot, input) {
1041
1056
  const current = await this.getRoleSession(repoRoot, input.taskSlug, input.role);
1042
- if (!current || (!input.allowSessionMismatch && !matchesRoleHookSession(current, input))) {
1057
+ if (!current || !matchesRoleHookSession(current, input)) {
1043
1058
  return undefined;
1044
1059
  }
1045
1060
  const timestamp = now();
@@ -1071,7 +1086,9 @@ export function createSessionService(deps) {
1071
1086
  eventName: input.eventName,
1072
1087
  sessionId: input.claudeSessionId,
1073
1088
  transcriptPath: input.transcriptPath,
1074
- cwd: input.cwd
1089
+ cwd: input.cwd,
1090
+ runtimeSessionId: input.runtimeSessionId,
1091
+ runtimeSessionToken: input.runtimeSessionToken
1075
1092
  });
1076
1093
  },
1077
1094
  async markTerminalSessionActivityIdle(repoRoot, sessionId) {
@@ -1094,11 +1111,11 @@ export function createSessionService(deps) {
1094
1111
  ? markProjectToolActivityIdle(repoRoot, current, persistHarnessEngineerSession)
1095
1112
  : undefined;
1096
1113
  }
1097
- return markTaskRoleActivityIdle(repoRoot, taskSlug, role);
1114
+ return markTaskRoleActivityIdle(repoRoot, taskSlug, role, sessionId);
1098
1115
  },
1099
- async markRoleActivityRunning(repoRoot, taskSlug, role) {
1116
+ async markRoleActivityRunning(repoRoot, taskSlug, role, expectedSessionId) {
1100
1117
  const current = await this.getRoleSession(repoRoot, taskSlug, role);
1101
- if (!current) {
1118
+ if (!current || (expectedSessionId && current.id !== expectedSessionId)) {
1102
1119
  return undefined;
1103
1120
  }
1104
1121
  const timestamp = now();
@@ -1154,7 +1171,18 @@ function toRoleSessionRecordView(record, runtime) {
1154
1171
  exitCode: runtimeSession.exitCode
1155
1172
  };
1156
1173
  }
1157
- function matchesRoleHookSession(record, input) {
1174
+ export function matchesRoleHookSession(record, input) {
1175
+ if (!record.claudeSessionId
1176
+ && !record.transcriptPath
1177
+ && input.eventName !== "UserPromptSubmit") {
1178
+ return false;
1179
+ }
1180
+ if (input.runtimeSessionId) {
1181
+ return record.id === input.runtimeSessionId;
1182
+ }
1183
+ if (record.runtimeSessionToken) {
1184
+ return record.runtimeSessionToken === input.runtimeSessionToken;
1185
+ }
1158
1186
  if (!record.claudeSessionId && !record.transcriptPath) {
1159
1187
  return input.eventName === "UserPromptSubmit";
1160
1188
  }
@@ -1164,9 +1192,6 @@ function matchesRoleHookSession(record, input) {
1164
1192
  if (input.transcriptPath && record.transcriptPath === input.transcriptPath) {
1165
1193
  return true;
1166
1194
  }
1167
- if (!input.sessionId && !input.transcriptPath) {
1168
- return true;
1169
- }
1170
1195
  return false;
1171
1196
  }
1172
1197
  function nextHookSessionIdentity(current, input) {
@@ -1208,7 +1233,7 @@ function isSessionMissingError(error) {
1208
1233
  error.code === "SESSION_MISSING";
1209
1234
  }
1210
1235
  function withoutRuntimeOnlySessionFields(session) {
1211
- const { pid: _pid, ...persisted } = session;
1236
+ const { pid: _pid, runtimeSessionToken: _runtimeSessionToken, ...persisted } = session;
1212
1237
  return persisted;
1213
1238
  }
1214
1239
  function defaultIsProcessAlive(pid) {
@@ -1536,11 +1561,39 @@ function formatClaudeCdCommand(targetCwd) {
1536
1561
  function isExitedStatus(status) {
1537
1562
  return status === "exited" || status === "crashed" || status === "missing";
1538
1563
  }
1539
- function withClaudeCodeRuntimeEnv(env, modelEnvironment = {}) {
1564
+ function withClaudeCodeRuntimeEnv(env, modelEnvironment = {}, telemetryEnvironment = {}) {
1540
1565
  return {
1541
1566
  ...env,
1542
1567
  ...modelEnvironment,
1543
- CLAUDE_CODE_DISABLE_AUTO_MEMORY
1568
+ ...telemetryEnvironment,
1569
+ CLAUDE_CODE_DISABLE_AUTO_MEMORY,
1570
+ CLAUDE_CODE_DISABLE_BACKGROUND_TASKS
1571
+ };
1572
+ }
1573
+ function buildUsageTelemetryEnvironment(apiUrl, role, model) {
1574
+ const disabled = {
1575
+ CLAUDE_CODE_ENABLE_TELEMETRY: undefined,
1576
+ OTEL_LOGS_EXPORTER: "none",
1577
+ OTEL_METRICS_EXPORTER: "none",
1578
+ OTEL_TRACES_EXPORTER: "none",
1579
+ OTEL_EXPORTER_OTLP_LOGS_PROTOCOL: undefined,
1580
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: undefined,
1581
+ OTEL_RESOURCE_ATTRIBUTES: undefined,
1582
+ OTEL_LOG_USER_PROMPTS: "0",
1583
+ OTEL_LOG_ASSISTANT_RESPONSES: "0",
1584
+ OTEL_LOG_TOOL_DETAILS: "0",
1585
+ OTEL_LOG_RAW_API_BODIES: "0"
1586
+ };
1587
+ if (!apiUrl || isCcrSessionModel(model)) {
1588
+ return disabled;
1589
+ }
1590
+ return {
1591
+ ...disabled,
1592
+ CLAUDE_CODE_ENABLE_TELEMETRY: "1",
1593
+ OTEL_LOGS_EXPORTER: "otlp",
1594
+ OTEL_EXPORTER_OTLP_LOGS_PROTOCOL: "http/json",
1595
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: `${apiUrl.replace(/\/+$/, "")}/api/telemetry/v1/logs`,
1596
+ OTEL_RESOURCE_ATTRIBUTES: `vcm.role=${role},vcm.launch_id=${randomUUID()}`
1544
1597
  };
1545
1598
  }
1546
1599
  function delay(ms) {
@@ -5,6 +5,7 @@ export function createTaskCloseService(deps) {
5
5
  async closeTask(repoRoot, taskSlug) {
6
6
  const task = await deps.taskService.markTaskCleaned(repoRoot, taskSlug);
7
7
  const warnings = [];
8
+ deps.architectRestartService?.clear(repoRoot, taskSlug);
8
9
  await stopTaskRoleSessions(repoRoot, taskSlug, warnings);
9
10
  await bestEffort("Unable to stop task translation runtime", () => deps.translationService.stopTask(getTaskRuntimeRepoRoot(task), taskSlug, { clearCache: true }), warnings);
10
11
  await bestEffort("Unable to clear task round runtime", () => deps.roundService.stopTask(taskSlug), warnings);
@@ -11,7 +11,10 @@ export function createTerminalInterruptService(deps) {
11
11
  if (!repoRoot) {
12
12
  return;
13
13
  }
14
- await deps.sessionService.markTerminalSessionActivityIdle(repoRoot, sessionId);
14
+ const session = await deps.sessionService.markTerminalSessionActivityIdle(repoRoot, sessionId);
15
+ if (!session) {
16
+ return;
17
+ }
15
18
  if (!isVcmRoleName(terminalSession.role)) {
16
19
  return;
17
20
  }
@@ -96,6 +96,7 @@ function buildReconciledHook(taskSlug, role, session, eventName, evidence) {
96
96
  ...(session?.claudeSessionId ? { session_id: session.claudeSessionId } : {}),
97
97
  ...(session?.transcriptPath ? { transcript_path: session.transcriptPath } : {}),
98
98
  ...(session?.cwd ? { cwd: session.cwd } : {}),
99
+ ...(session?.id ? { vcm_runtime_session_id: session.id } : {}),
99
100
  vcm_reconciled: true,
100
101
  ...evidence
101
102
  }
@@ -0,0 +1,346 @@
1
+ import path from "node:path";
2
+ import { ROLE_NAMES, isRoleName } from "../../shared/constants.js";
3
+ const USAGE_FILE = path.join("telemetry", "usage.json");
4
+ export function createUsageAnalyticsService(deps) {
5
+ const now = deps.now ?? (() => new Date().toISOString());
6
+ const writeQueues = new Map();
7
+ return {
8
+ async ingest(taskRepoRoot, stateRoot, payload) {
9
+ const events = extractApiRequestEvents(payload);
10
+ if (events.length === 0) {
11
+ return;
12
+ }
13
+ const usagePath = getUsagePath(taskRepoRoot, stateRoot);
14
+ await enqueueWrite(writeQueues, usagePath, async () => {
15
+ const state = await loadState(deps.fs, usagePath);
16
+ const seen = new Set(state.seenEvents);
17
+ let changed = false;
18
+ for (const event of events) {
19
+ if (seen.has(event.eventKey)) {
20
+ continue;
21
+ }
22
+ seen.add(event.eventKey);
23
+ state.seenEvents.push(event.eventKey);
24
+ addEvent(state, event);
25
+ changed = true;
26
+ }
27
+ if (!changed) {
28
+ return;
29
+ }
30
+ state.updatedAt = now();
31
+ await deps.fs.writeJsonAtomic(usagePath, state);
32
+ });
33
+ },
34
+ async getReport(taskRepoRoot, stateRoot, taskSlug) {
35
+ const usagePath = getUsagePath(taskRepoRoot, stateRoot);
36
+ await writeQueues.get(usagePath);
37
+ if (!(await deps.fs.pathExists(usagePath))) {
38
+ return emptyReport(taskSlug);
39
+ }
40
+ const state = normalizeStoredState(await deps.fs.readJson(usagePath));
41
+ return toReport(taskSlug, state);
42
+ }
43
+ };
44
+ }
45
+ function extractApiRequestEvents(payload) {
46
+ if (!isObject(payload) || !Array.isArray(payload.resourceLogs)) {
47
+ return [];
48
+ }
49
+ const events = [];
50
+ for (const resourceLog of payload.resourceLogs) {
51
+ if (!isObject(resourceLog)) {
52
+ continue;
53
+ }
54
+ const resourceAttributes = readAttributes(isObject(resourceLog.resource) ? resourceLog.resource.attributes : undefined);
55
+ if (!Array.isArray(resourceLog.scopeLogs)) {
56
+ continue;
57
+ }
58
+ for (const scopeLog of resourceLog.scopeLogs) {
59
+ if (!isObject(scopeLog) || !Array.isArray(scopeLog.logRecords)) {
60
+ continue;
61
+ }
62
+ for (const logRecord of scopeLog.logRecords) {
63
+ if (!isObject(logRecord)) {
64
+ continue;
65
+ }
66
+ const attributes = {
67
+ ...resourceAttributes,
68
+ ...readAttributes(logRecord.attributes)
69
+ };
70
+ if (readString(attributes["event.name"]) !== "api_request") {
71
+ continue;
72
+ }
73
+ const roleValue = readString(attributes["vcm.role"]);
74
+ const launchId = readString(attributes["vcm.launch_id"]);
75
+ if (!roleValue || !isRoleName(roleValue) || !launchId) {
76
+ continue;
77
+ }
78
+ const model = normalizeModel(readString(attributes.model));
79
+ if (isCcrModel(model)) {
80
+ continue;
81
+ }
82
+ const sequence = readInteger(attributes["event.sequence"]);
83
+ const sessionId = readString(attributes["session.id"]) || launchId;
84
+ const requestId = readString(attributes.request_id);
85
+ const timestamp = readString(logRecord.timeUnixNano) || readString(logRecord.observedTimeUnixNano);
86
+ const fallbackIdentity = [sessionId, requestId, timestamp, model].join(":");
87
+ const eventKey = `${launchId}:${sequence ?? fallbackIdentity}`;
88
+ const costUsdMicros = readNonNegativeInteger(attributes.cost_usd_micros)
89
+ ?? Math.round((readNonNegativeNumber(attributes.cost_usd) ?? 0) * 1_000_000);
90
+ events.push({
91
+ eventKey,
92
+ role: roleValue,
93
+ model,
94
+ sessionId,
95
+ inputTokens: readNonNegativeInteger(attributes.input_tokens) ?? 0,
96
+ outputTokens: readNonNegativeInteger(attributes.output_tokens) ?? 0,
97
+ cacheReadTokens: readNonNegativeInteger(attributes.cache_read_tokens) ?? 0,
98
+ cacheCreationTokens: readNonNegativeInteger(attributes.cache_creation_tokens) ?? 0,
99
+ costUsdMicros
100
+ });
101
+ }
102
+ }
103
+ }
104
+ return events;
105
+ }
106
+ function addEvent(state, event) {
107
+ addTotals(state.totals, event);
108
+ state.byRole[event.role] ??= emptyStoredTotals();
109
+ addTotals(state.byRole[event.role], event);
110
+ state.byModel[event.model] ??= emptyStoredTotals();
111
+ addTotals(state.byModel[event.model], event);
112
+ addUnique(state.sessionsByRole, event.role, event.sessionId);
113
+ addUnique(state.sessionsByModel, event.model, event.sessionId);
114
+ }
115
+ function addTotals(target, event) {
116
+ target.inputTokens += event.inputTokens;
117
+ target.outputTokens += event.outputTokens;
118
+ target.cacheReadTokens += event.cacheReadTokens;
119
+ target.cacheCreationTokens += event.cacheCreationTokens;
120
+ target.costUsdMicros += event.costUsdMicros;
121
+ target.requestCount += 1;
122
+ }
123
+ function addUnique(target, key, value) {
124
+ const values = target[key] ?? [];
125
+ if (!values.includes(value)) {
126
+ values.push(value);
127
+ }
128
+ target[key] = values;
129
+ }
130
+ async function loadState(fs, usagePath) {
131
+ if (!(await fs.pathExists(usagePath))) {
132
+ return emptyStoredState();
133
+ }
134
+ return normalizeStoredState(await fs.readJson(usagePath));
135
+ }
136
+ function normalizeStoredState(input) {
137
+ return {
138
+ version: 1,
139
+ updatedAt: typeof input.updatedAt === "string" ? input.updatedAt : new Date(0).toISOString(),
140
+ seenEvents: Array.isArray(input.seenEvents) ? input.seenEvents.filter((value) => typeof value === "string") : [],
141
+ sessionsByRole: normalizeRoleStringLists(input.sessionsByRole),
142
+ sessionsByModel: normalizeStringLists(input.sessionsByModel),
143
+ totals: normalizeStoredTotals(input.totals),
144
+ byRole: normalizeRoleTotals(input.byRole),
145
+ byModel: normalizeModelTotals(input.byModel)
146
+ };
147
+ }
148
+ function normalizeRoleStringLists(input) {
149
+ if (!isObject(input)) {
150
+ return {};
151
+ }
152
+ const result = {};
153
+ for (const role of ROLE_NAMES) {
154
+ const values = input[role];
155
+ if (Array.isArray(values)) {
156
+ result[role] = values.filter((value) => typeof value === "string");
157
+ }
158
+ }
159
+ return result;
160
+ }
161
+ function normalizeStringLists(input) {
162
+ const result = {};
163
+ if (!isObject(input)) {
164
+ return result;
165
+ }
166
+ for (const [key, values] of Object.entries(input)) {
167
+ if (isSafeGroupKey(key) && Array.isArray(values)) {
168
+ result[key] = values.filter((value) => typeof value === "string");
169
+ }
170
+ }
171
+ return result;
172
+ }
173
+ function normalizeRoleTotals(input) {
174
+ if (!isObject(input)) {
175
+ return {};
176
+ }
177
+ const result = {};
178
+ for (const role of ROLE_NAMES) {
179
+ if (isObject(input[role])) {
180
+ result[role] = normalizeStoredTotals(input[role]);
181
+ }
182
+ }
183
+ return result;
184
+ }
185
+ function normalizeModelTotals(input) {
186
+ const result = {};
187
+ if (!isObject(input)) {
188
+ return result;
189
+ }
190
+ for (const [key, totals] of Object.entries(input)) {
191
+ if (isSafeGroupKey(key) && isObject(totals)) {
192
+ result[key] = normalizeStoredTotals(totals);
193
+ }
194
+ }
195
+ return result;
196
+ }
197
+ function normalizeStoredTotals(input) {
198
+ const value = isObject(input) ? input : {};
199
+ return {
200
+ inputTokens: readNonNegativeInteger(value.inputTokens) ?? 0,
201
+ outputTokens: readNonNegativeInteger(value.outputTokens) ?? 0,
202
+ cacheReadTokens: readNonNegativeInteger(value.cacheReadTokens) ?? 0,
203
+ cacheCreationTokens: readNonNegativeInteger(value.cacheCreationTokens) ?? 0,
204
+ costUsdMicros: readNonNegativeInteger(value.costUsdMicros) ?? 0,
205
+ requestCount: readNonNegativeInteger(value.requestCount) ?? 0
206
+ };
207
+ }
208
+ function toReport(taskSlug, state) {
209
+ const allSessions = new Set(Object.values(state.sessionsByRole).flatMap((values) => values ?? []));
210
+ const byRole = ROLE_NAMES.map((role) => ({
211
+ role,
212
+ ...toPublicTotals(state.byRole[role] ?? emptyStoredTotals(), state.sessionsByRole[role]?.length ?? 0)
213
+ }));
214
+ const byModel = Object.keys(state.byModel)
215
+ .sort((left, right) => left.localeCompare(right))
216
+ .map((model) => ({
217
+ model,
218
+ ...toPublicTotals(state.byModel[model], state.sessionsByModel[model]?.length ?? 0)
219
+ }));
220
+ return {
221
+ version: 1,
222
+ taskSlug,
223
+ updatedAt: state.updatedAt,
224
+ totals: toPublicTotals(state.totals, allSessions.size),
225
+ byRole,
226
+ byModel
227
+ };
228
+ }
229
+ function emptyReport(taskSlug) {
230
+ return {
231
+ version: 1,
232
+ taskSlug,
233
+ updatedAt: null,
234
+ totals: toPublicTotals(emptyStoredTotals(), 0),
235
+ byRole: ROLE_NAMES.map((role) => ({ role, ...toPublicTotals(emptyStoredTotals(), 0) })),
236
+ byModel: []
237
+ };
238
+ }
239
+ function toPublicTotals(totals, sessionCount) {
240
+ return {
241
+ inputTokens: totals.inputTokens,
242
+ outputTokens: totals.outputTokens,
243
+ cacheReadTokens: totals.cacheReadTokens,
244
+ cacheCreationTokens: totals.cacheCreationTokens,
245
+ costUsd: totals.costUsdMicros / 1_000_000,
246
+ requestCount: totals.requestCount,
247
+ sessionCount
248
+ };
249
+ }
250
+ function emptyStoredState() {
251
+ return {
252
+ version: 1,
253
+ updatedAt: new Date(0).toISOString(),
254
+ seenEvents: [],
255
+ sessionsByRole: {},
256
+ sessionsByModel: {},
257
+ totals: emptyStoredTotals(),
258
+ byRole: {},
259
+ byModel: {}
260
+ };
261
+ }
262
+ function emptyStoredTotals() {
263
+ return {
264
+ inputTokens: 0,
265
+ outputTokens: 0,
266
+ cacheReadTokens: 0,
267
+ cacheCreationTokens: 0,
268
+ costUsdMicros: 0,
269
+ requestCount: 0
270
+ };
271
+ }
272
+ function getUsagePath(taskRepoRoot, stateRoot) {
273
+ return path.join(taskRepoRoot, stateRoot, USAGE_FILE);
274
+ }
275
+ async function enqueueWrite(queues, key, operation) {
276
+ const previous = queues.get(key) ?? Promise.resolve();
277
+ const current = previous.catch(() => undefined).then(operation);
278
+ queues.set(key, current);
279
+ try {
280
+ await current;
281
+ }
282
+ finally {
283
+ if (queues.get(key) === current) {
284
+ queues.delete(key);
285
+ }
286
+ }
287
+ }
288
+ function readAttributes(input) {
289
+ const result = {};
290
+ if (!Array.isArray(input)) {
291
+ return result;
292
+ }
293
+ for (const entry of input) {
294
+ if (!isObject(entry) || typeof entry.key !== "string") {
295
+ continue;
296
+ }
297
+ result[entry.key] = readAnyValue(entry.value);
298
+ }
299
+ return result;
300
+ }
301
+ function readAnyValue(input) {
302
+ if (!isObject(input)) {
303
+ return input;
304
+ }
305
+ for (const key of ["stringValue", "intValue", "doubleValue", "boolValue"]) {
306
+ if (key in input) {
307
+ return input[key];
308
+ }
309
+ }
310
+ return undefined;
311
+ }
312
+ function readString(input) {
313
+ if (typeof input === "string") {
314
+ return input.trim() || undefined;
315
+ }
316
+ if (typeof input === "number" || typeof input === "bigint") {
317
+ return String(input);
318
+ }
319
+ return undefined;
320
+ }
321
+ function readInteger(input) {
322
+ const value = typeof input === "number" ? input : typeof input === "string" ? Number(input) : Number.NaN;
323
+ return Number.isSafeInteger(value) ? value : undefined;
324
+ }
325
+ function readNonNegativeInteger(input) {
326
+ const value = readInteger(input);
327
+ return value !== undefined && value >= 0 ? value : undefined;
328
+ }
329
+ function readNonNegativeNumber(input) {
330
+ const value = typeof input === "number" ? input : typeof input === "string" ? Number(input) : Number.NaN;
331
+ return Number.isFinite(value) && value >= 0 ? value : undefined;
332
+ }
333
+ function normalizeModel(input) {
334
+ const model = input?.slice(0, 200).trim();
335
+ return model && isSafeGroupKey(model) ? model : "unknown";
336
+ }
337
+ function isCcrModel(model) {
338
+ const normalized = model.toLowerCase();
339
+ return normalized.startsWith("gpt-") || normalized.startsWith("ccr:") || normalized.includes("codex api/");
340
+ }
341
+ function isSafeGroupKey(value) {
342
+ return value !== "__proto__" && value !== "prototype" && value !== "constructor";
343
+ }
344
+ function isObject(input) {
345
+ return typeof input === "object" && input !== null && !Array.isArray(input);
346
+ }
@@ -183,6 +183,10 @@ TBD
183
183
 
184
184
  ## Blocking Validation Issues
185
185
 
186
+ TBD
187
+
188
+ ## User Approval Evidence
189
+
186
190
  TBD
187
191
  `;
188
192
  }