selftune 0.2.0 → 0.2.2

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 (122) hide show
  1. package/.claude/agents/diagnosis-analyst.md +20 -10
  2. package/.claude/agents/evolution-reviewer.md +14 -1
  3. package/.claude/agents/integration-guide.md +18 -6
  4. package/.claude/agents/pattern-analyst.md +18 -5
  5. package/CHANGELOG.md +12 -4
  6. package/README.md +43 -35
  7. package/apps/local-dashboard/dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  8. package/apps/local-dashboard/dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  9. package/apps/local-dashboard/dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  10. package/apps/local-dashboard/dist/assets/index-C4EOTFZ2.js +15 -0
  11. package/apps/local-dashboard/dist/assets/index-bl-Webyd.css +1 -0
  12. package/apps/local-dashboard/dist/assets/vendor-react-U7zYD9Rg.js +60 -0
  13. package/apps/local-dashboard/dist/assets/vendor-table-B7VF2Ipl.js +26 -0
  14. package/apps/local-dashboard/dist/assets/vendor-ui-D7_zX_qy.js +346 -0
  15. package/apps/local-dashboard/dist/favicon.png +0 -0
  16. package/apps/local-dashboard/dist/index.html +17 -0
  17. package/apps/local-dashboard/dist/logo.png +0 -0
  18. package/apps/local-dashboard/dist/logo.svg +9 -0
  19. package/cli/selftune/badge/badge-data.ts +1 -1
  20. package/cli/selftune/badge/badge.ts +4 -8
  21. package/cli/selftune/canonical-export.ts +183 -0
  22. package/cli/selftune/constants.ts +28 -0
  23. package/cli/selftune/contribute/contribute.ts +1 -1
  24. package/cli/selftune/cron/setup.ts +17 -17
  25. package/cli/selftune/dashboard-contract.ts +202 -0
  26. package/cli/selftune/dashboard-server.ts +653 -186
  27. package/cli/selftune/dashboard.ts +41 -176
  28. package/cli/selftune/eval/baseline.ts +5 -4
  29. package/cli/selftune/eval/composability-v2.ts +273 -0
  30. package/cli/selftune/eval/hooks-to-evals.ts +34 -15
  31. package/cli/selftune/eval/unit-test-cli.ts +1 -1
  32. package/cli/selftune/evolution/evidence.ts +26 -0
  33. package/cli/selftune/evolution/evolve-body.ts +105 -11
  34. package/cli/selftune/evolution/evolve.ts +371 -25
  35. package/cli/selftune/evolution/extract-patterns.ts +87 -29
  36. package/cli/selftune/evolution/rollback.ts +2 -2
  37. package/cli/selftune/grading/auto-grade.ts +200 -0
  38. package/cli/selftune/grading/grade-session.ts +448 -97
  39. package/cli/selftune/grading/results.ts +42 -0
  40. package/cli/selftune/hooks/prompt-log.ts +172 -2
  41. package/cli/selftune/hooks/session-stop.ts +123 -3
  42. package/cli/selftune/hooks/skill-eval.ts +119 -3
  43. package/cli/selftune/index.ts +395 -116
  44. package/cli/selftune/ingestors/claude-replay.ts +140 -114
  45. package/cli/selftune/ingestors/codex-rollout.ts +345 -46
  46. package/cli/selftune/ingestors/codex-wrapper.ts +207 -39
  47. package/cli/selftune/ingestors/openclaw-ingest.ts +141 -8
  48. package/cli/selftune/ingestors/opencode-ingest.ts +193 -17
  49. package/cli/selftune/init.ts +227 -14
  50. package/cli/selftune/last.ts +14 -5
  51. package/cli/selftune/localdb/db.ts +63 -0
  52. package/cli/selftune/localdb/materialize.ts +428 -0
  53. package/cli/selftune/localdb/queries.ts +376 -0
  54. package/cli/selftune/localdb/schema.ts +204 -0
  55. package/cli/selftune/monitoring/watch.ts +66 -15
  56. package/cli/selftune/normalization.ts +682 -0
  57. package/cli/selftune/observability.ts +19 -44
  58. package/cli/selftune/orchestrate.ts +1073 -0
  59. package/cli/selftune/quickstart.ts +203 -0
  60. package/cli/selftune/repair/skill-usage.ts +576 -0
  61. package/cli/selftune/schedule.ts +561 -0
  62. package/cli/selftune/status.ts +48 -26
  63. package/cli/selftune/sync.ts +627 -0
  64. package/cli/selftune/types.ts +148 -0
  65. package/cli/selftune/utils/canonical-log.ts +45 -0
  66. package/cli/selftune/utils/hooks.ts +41 -0
  67. package/cli/selftune/utils/html.ts +27 -0
  68. package/cli/selftune/utils/llm-call.ts +78 -20
  69. package/cli/selftune/utils/math.ts +10 -0
  70. package/cli/selftune/utils/query-filter.ts +139 -0
  71. package/cli/selftune/utils/skill-discovery.ts +340 -0
  72. package/cli/selftune/utils/skill-log.ts +68 -0
  73. package/cli/selftune/utils/skill-usage-confidence.ts +18 -0
  74. package/cli/selftune/utils/transcript.ts +272 -26
  75. package/cli/selftune/workflows/discover.ts +254 -0
  76. package/cli/selftune/workflows/skill-md-writer.ts +288 -0
  77. package/cli/selftune/workflows/workflows.ts +188 -0
  78. package/package.json +21 -8
  79. package/packages/telemetry-contract/README.md +11 -0
  80. package/packages/telemetry-contract/fixtures/golden.json +87 -0
  81. package/packages/telemetry-contract/fixtures/golden.test.ts +42 -0
  82. package/packages/telemetry-contract/index.ts +1 -0
  83. package/packages/telemetry-contract/package.json +19 -0
  84. package/packages/telemetry-contract/src/index.ts +2 -0
  85. package/packages/telemetry-contract/src/types.ts +163 -0
  86. package/packages/telemetry-contract/src/validators.ts +109 -0
  87. package/skill/SKILL.md +84 -53
  88. package/skill/Workflows/AutoActivation.md +17 -16
  89. package/skill/Workflows/Badge.md +6 -0
  90. package/skill/Workflows/Baseline.md +46 -23
  91. package/skill/Workflows/Composability.md +12 -5
  92. package/skill/Workflows/Contribute.md +17 -14
  93. package/skill/Workflows/Cron.md +56 -79
  94. package/skill/Workflows/Dashboard.md +45 -34
  95. package/skill/Workflows/Doctor.md +30 -17
  96. package/skill/Workflows/Evals.md +64 -40
  97. package/skill/Workflows/EvolutionMemory.md +2 -0
  98. package/skill/Workflows/Evolve.md +102 -47
  99. package/skill/Workflows/EvolveBody.md +6 -6
  100. package/skill/Workflows/Grade.md +36 -31
  101. package/skill/Workflows/ImportSkillsBench.md +11 -5
  102. package/skill/Workflows/Ingest.md +43 -36
  103. package/skill/Workflows/Initialize.md +44 -30
  104. package/skill/Workflows/Orchestrate.md +139 -0
  105. package/skill/Workflows/Replay.md +39 -18
  106. package/skill/Workflows/Rollback.md +3 -3
  107. package/skill/Workflows/Schedule.md +61 -0
  108. package/skill/Workflows/Sync.md +88 -0
  109. package/skill/Workflows/UnitTest.md +34 -22
  110. package/skill/Workflows/Watch.md +14 -4
  111. package/skill/Workflows/Workflows.md +129 -0
  112. package/skill/assets/activation-rules-default.json +26 -0
  113. package/skill/assets/multi-skill-settings.json +63 -0
  114. package/skill/assets/single-skill-settings.json +57 -0
  115. package/skill/references/invocation-taxonomy.md +2 -2
  116. package/skill/references/logs.md +164 -2
  117. package/skill/references/setup-patterns.md +65 -0
  118. package/skill/references/version-history.md +40 -0
  119. package/skill/settings_snippet.json +1 -1
  120. package/templates/multi-skill-settings.json +7 -7
  121. package/templates/single-skill-settings.json +6 -6
  122. package/dashboard/index.html +0 -1680
@@ -25,36 +25,62 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
25
25
  import { homedir } from "node:os";
26
26
  import { basename, join } from "node:path";
27
27
  import { parseArgs } from "node:util";
28
- import { QUERY_LOG, SKILL_LOG, TELEMETRY_LOG } from "../constants.js";
29
- import type { QueryLogRecord, SessionTelemetryRecord, SkillUsageRecord } from "../types.js";
28
+ import { CANONICAL_LOG, QUERY_LOG, SKILL_LOG, TELEMETRY_LOG } from "../constants.js";
29
+ import {
30
+ appendCanonicalRecords,
31
+ buildCanonicalExecutionFact,
32
+ buildCanonicalPrompt,
33
+ buildCanonicalSession,
34
+ buildCanonicalSkillInvocation,
35
+ type CanonicalBaseInput,
36
+ deriveInvocationMode,
37
+ derivePromptId,
38
+ deriveSkillInvocationId,
39
+ } from "../normalization.js";
40
+ import type {
41
+ CanonicalRecord,
42
+ QueryLogRecord,
43
+ SessionTelemetryRecord,
44
+ SkillUsageRecord,
45
+ } from "../types.js";
30
46
  import { appendJsonl, loadMarker, saveMarker } from "../utils/jsonl.js";
47
+ import { extractActionableQueryText } from "../utils/query-filter.js";
48
+ import {
49
+ classifySkillPath,
50
+ containsWholeSkillMention,
51
+ extractExplicitSkillMentions,
52
+ extractSkillNamesFromInstructions,
53
+ extractSkillNamesFromPathReferences,
54
+ findInstalledSkillNames,
55
+ findInstalledSkillPath,
56
+ findRepositorySkillDirs,
57
+ } from "../utils/skill-discovery.js";
31
58
 
32
59
  const MARKER_FILE = join(homedir(), ".claude", "codex_ingested_rollouts.json");
33
60
 
34
- const DEFAULT_CODEX_HOME = process.env.CODEX_HOME ?? join(homedir(), ".codex");
35
-
36
- const CODEX_SKILLS_DIRS = [
37
- join(process.cwd(), ".codex", "skills"),
38
- join(homedir(), ".codex", "skills"),
39
- ];
40
-
41
- /** Return skill names from Codex skill directories. */
42
- export function findSkillNames(dirs: string[] = CODEX_SKILLS_DIRS): Set<string> {
43
- const names = new Set<string>();
44
- for (const dir of dirs) {
45
- if (!existsSync(dir)) continue;
46
- for (const entry of readdirSync(dir)) {
47
- const skillDir = join(dir, entry);
48
- try {
49
- if (statSync(skillDir).isDirectory() && existsSync(join(skillDir, "SKILL.md"))) {
50
- names.add(entry);
51
- }
52
- } catch {
53
- // skip entries that can't be stat'd (broken symlinks, permission errors, etc.)
54
- }
55
- }
56
- }
57
- return names;
61
+ export const DEFAULT_CODEX_HOME = process.env.CODEX_HOME ?? join(homedir(), ".codex");
62
+ const SKILL_NAME_CACHE = new Map<string, Set<string>>();
63
+
64
+ /** Return skill names from Codex and agent skill directories for the given workspace. */
65
+ export function findSkillNames(
66
+ cwd: string = process.cwd(),
67
+ homeDir: string = homedir(),
68
+ adminDir: string = "/etc/codex/skills",
69
+ codexHome: string = process.env.CODEX_HOME ?? join(homeDir, ".codex"),
70
+ ): Set<string> {
71
+ const cacheKey = [cwd, homeDir, adminDir, codexHome].join("\u0000");
72
+ const cached = SKILL_NAME_CACHE.get(cacheKey);
73
+ if (cached) return new Set(cached);
74
+
75
+ const names = findInstalledSkillNames([
76
+ ...findRepositorySkillDirs(cwd),
77
+ join(homeDir, ".agents", "skills"),
78
+ adminDir,
79
+ join(codexHome, "skills"),
80
+ join(codexHome, "skills", ".system"),
81
+ ]);
82
+ SKILL_NAME_CACHE.set(cacheKey, names);
83
+ return new Set(names);
58
84
  }
59
85
 
60
86
  /**
@@ -124,6 +150,8 @@ export interface ParsedRollout {
124
150
  total_tool_calls: number;
125
151
  bash_commands: string[];
126
152
  skills_triggered: string[];
153
+ skills_invoked: string[];
154
+ skill_evidence: Record<string, "explicit" | "inferred">;
127
155
  assistant_turns: number;
128
156
  errors_encountered: number;
129
157
  input_tokens: number;
@@ -132,6 +160,19 @@ export interface ParsedRollout {
132
160
  cwd: string;
133
161
  transcript_path: string;
134
162
  last_user_query: string;
163
+ /** Observed-format metadata (populated when session_meta/event_msg records are found). */
164
+ observed_meta?: {
165
+ model_provider?: string;
166
+ model?: string;
167
+ approval_policy?: string;
168
+ sandbox_policy?: string;
169
+ originator?: string;
170
+ git?: { branch?: string; remote?: string; commit?: string };
171
+ };
172
+ }
173
+
174
+ function optionalString(value: unknown): string | undefined {
175
+ return typeof value === "string" && value.trim() ? value : undefined;
135
176
  }
136
177
 
137
178
  /**
@@ -155,14 +196,89 @@ export function parseRolloutFile(path: string, skillNames: Set<string>): ParsedR
155
196
 
156
197
  const threadId = basename(path, ".jsonl").replace("rollout-", "");
157
198
  let prompt = "";
199
+ let lastUserQuery = "";
158
200
  const toolCalls: Record<string, number> = {};
159
201
  const bashCommands: string[] = [];
160
202
  const skillsTriggered: string[] = [];
203
+ const skillEvidence = new Map<string, "explicit" | "inferred">();
161
204
  let errors = 0;
162
205
  let turns = 0;
163
206
  let inputTokens = 0;
164
207
  let outputTokens = 0;
165
208
 
209
+ // Observed-format metadata (session_meta/turn_context/event_msg records)
210
+ let observedMeta:
211
+ | {
212
+ model_provider?: string;
213
+ model?: string;
214
+ approval_policy?: string;
215
+ sandbox_policy?: string;
216
+ originator?: string;
217
+ git?: { branch?: string; remote?: string; commit?: string };
218
+ }
219
+ | undefined;
220
+ let observedSessionId: string | undefined;
221
+ let observedCwd: string | undefined;
222
+ const sessionSkillNames = new Set(skillNames);
223
+ let hasActionablePrompt = false;
224
+ const rememberSessionSkillNames = (text: unknown): void => {
225
+ if (typeof text !== "string" || !text) return;
226
+ for (const skillName of extractSkillNamesFromInstructions(text, sessionSkillNames)) {
227
+ sessionSkillNames.add(skillName);
228
+ }
229
+ };
230
+ const rememberWorkspaceSkills = (cwd: unknown): void => {
231
+ if (typeof cwd !== "string" || !cwd.trim()) return;
232
+ for (const skillName of findSkillNames(cwd)) {
233
+ sessionSkillNames.add(skillName);
234
+ }
235
+ };
236
+ const detectTriggeredSkills = (text: unknown): void => {
237
+ if (typeof text !== "string" || !text) return;
238
+ for (const skillName of sessionSkillNames) {
239
+ if (containsWholeSkillMention(text, skillName) && !skillsTriggered.includes(skillName)) {
240
+ skillsTriggered.push(skillName);
241
+ }
242
+ if (containsWholeSkillMention(text, skillName) && !skillEvidence.has(skillName)) {
243
+ skillEvidence.set(skillName, "inferred");
244
+ }
245
+ }
246
+ };
247
+ const detectExplicitPromptSkillMentions = (text: unknown): void => {
248
+ if (typeof text !== "string" || !text) return;
249
+ for (const skillName of extractExplicitSkillMentions(text, sessionSkillNames)) {
250
+ if (!skillsTriggered.includes(skillName)) {
251
+ skillsTriggered.push(skillName);
252
+ }
253
+ skillEvidence.set(skillName, "explicit");
254
+ }
255
+ };
256
+ const detectExplicitSkillReads = (text: unknown): void => {
257
+ if (typeof text !== "string" || !text) return;
258
+ for (const skillName of extractSkillNamesFromPathReferences(text, sessionSkillNames)) {
259
+ if (!skillsTriggered.includes(skillName)) {
260
+ skillsTriggered.push(skillName);
261
+ }
262
+ skillEvidence.set(skillName, "explicit");
263
+ }
264
+ };
265
+ const rememberPromptCandidate = (value: unknown): void => {
266
+ const message = typeof value === "string" ? value.trim() : "";
267
+ if (!message) return;
268
+ lastUserQuery = message;
269
+ const actionableMessage = extractActionableQueryText(message);
270
+ if (actionableMessage) {
271
+ if (!hasActionablePrompt) {
272
+ prompt = actionableMessage;
273
+ hasActionablePrompt = true;
274
+ }
275
+ return;
276
+ }
277
+ if (!prompt) {
278
+ prompt = message;
279
+ }
280
+ };
281
+
166
282
  for (const line of lines) {
167
283
  let event: Record<string, unknown>;
168
284
  try {
@@ -173,15 +289,93 @@ export function parseRolloutFile(path: string, skillNames: Set<string>): ParsedR
173
289
 
174
290
  const etype = (event.type as string) ?? "";
175
291
 
176
- if (etype === "turn.started") {
292
+ // --- Observed local rollout format (session_meta, event_msg, turn_context, response_item) ---
293
+ if (etype === "session_meta") {
294
+ const payload = (event.payload as Record<string, unknown>) ?? {};
295
+ const observedId = optionalString(payload.id);
296
+ const observedWorkspace = optionalString(payload.cwd);
297
+ const modelProvider = optionalString(payload.model_provider);
298
+ const model = optionalString(payload.model);
299
+ const originator = optionalString(payload.originator);
300
+ if (observedId) observedSessionId = observedId;
301
+ if (observedWorkspace) observedCwd = observedWorkspace;
302
+ rememberWorkspaceSkills(observedWorkspace);
303
+ rememberSessionSkillNames(payload.instructions);
304
+ rememberSessionSkillNames(
305
+ (payload.base_instructions as Record<string, unknown> | undefined)?.text,
306
+ );
307
+ if (!observedMeta) observedMeta = {};
308
+ if (modelProvider) observedMeta.model_provider = modelProvider;
309
+ if (model) observedMeta.model = model;
310
+ if (originator) observedMeta.originator = originator;
311
+ } else if (etype === "turn_context") {
312
+ const payload = (event.payload as Record<string, unknown>) ?? {};
313
+ const approvalPolicy = optionalString(payload.approval_policy);
314
+ const sandboxPolicy = optionalString(payload.sandbox_policy);
315
+ const model = optionalString(payload.model);
316
+ const gitPayload = payload.git as Record<string, unknown> | undefined;
317
+ if (!observedMeta) observedMeta = {};
318
+ if (approvalPolicy) observedMeta.approval_policy = approvalPolicy;
319
+ if (sandboxPolicy) observedMeta.sandbox_policy = sandboxPolicy;
320
+ if (model) observedMeta.model = model;
321
+ if (gitPayload) {
322
+ observedMeta.git = {
323
+ branch: optionalString(gitPayload.branch),
324
+ remote: optionalString(gitPayload.remote),
325
+ commit: optionalString(gitPayload.commit) ?? optionalString(gitPayload.sha),
326
+ };
327
+ }
328
+ turns += 1;
329
+ } else if (etype === "event_msg") {
330
+ const payload = (event.payload as Record<string, unknown>) ?? {};
331
+ const msgType = (payload.type as string) ?? "";
332
+ if (msgType === "user_message") {
333
+ rememberPromptCandidate(payload.message);
334
+ detectExplicitPromptSkillMentions(payload.message);
335
+ }
336
+ // Token usage in event_msg payloads
337
+ const tokenCount = payload.token_count as Record<string, number> | undefined;
338
+ if (tokenCount) {
339
+ inputTokens += tokenCount.input_tokens ?? tokenCount.input ?? 0;
340
+ outputTokens += tokenCount.output_tokens ?? tokenCount.output ?? 0;
341
+ }
342
+ } else if (etype === "response_item") {
343
+ const payload = (event.payload as Record<string, unknown>) ?? {};
344
+ const itemType = (payload.type as string) ?? "";
345
+ if (itemType === "function_call") {
346
+ const fnName = (payload.name as string) ?? "function_call";
347
+ toolCalls[fnName] = (toolCalls[fnName] ?? 0) + 1;
348
+ // Check for skill mentions in function arguments
349
+ detectExplicitSkillReads(payload.arguments);
350
+ detectTriggeredSkills(payload.arguments);
351
+ } else if (itemType === "agent_reasoning") {
352
+ toolCalls.reasoning = (toolCalls.reasoning ?? 0) + 1;
353
+ detectTriggeredSkills(payload.text);
354
+ } else if (itemType === "message") {
355
+ const content = Array.isArray(payload.content)
356
+ ? payload.content
357
+ .map((part) =>
358
+ typeof part === "object" && part
359
+ ? (((part as Record<string, unknown>).text as string | undefined) ?? "")
360
+ : "",
361
+ )
362
+ .join("\n")
363
+ : "";
364
+ rememberSessionSkillNames(content);
365
+ if ((payload.role as string) === "assistant") {
366
+ detectTriggeredSkills(content);
367
+ } else if ((payload.role as string) === "user") {
368
+ detectExplicitPromptSkillMentions(content);
369
+ }
370
+ }
371
+ } else if (etype === "turn.started") {
372
+ // --- Documented Codex event format ---
177
373
  turns += 1;
178
374
  } else if (etype === "turn.completed") {
179
375
  const usage = (event.usage as Record<string, number>) ?? {};
180
376
  inputTokens += usage.input_tokens ?? 0;
181
377
  outputTokens += usage.output_tokens ?? 0;
182
- if (!prompt) {
183
- prompt = (event.user_message as string) ?? "";
184
- }
378
+ rememberPromptCandidate(event.user_message);
185
379
  } else if (etype === "turn.failed") {
186
380
  errors += 1;
187
381
  } else if (etype === "item.completed" || etype === "item.started" || etype === "item.updated") {
@@ -193,6 +387,7 @@ export function parseRolloutFile(path: string, skillNames: Set<string>): ParsedR
193
387
  toolCalls.command_execution = (toolCalls.command_execution ?? 0) + 1;
194
388
  const cmd = ((item.command as string) ?? "").trim();
195
389
  if (cmd) bashCommands.push(cmd);
390
+ detectExplicitSkillReads(cmd);
196
391
  if ((item.exit_code as number) !== 0 && item.exit_code !== undefined) {
197
392
  errors += 1;
198
393
  }
@@ -209,23 +404,16 @@ export function parseRolloutFile(path: string, skillNames: Set<string>): ParsedR
209
404
 
210
405
  // Detect skill names in text content on completed events
211
406
  const textContent = ((item.text as string) ?? "") + ((item.command as string) ?? "");
212
- for (const skillName of skillNames) {
213
- if (
214
- textContent.includes(skillName) &&
215
- !skillsTriggered.includes(skillName) &&
216
- etype === "item.completed"
217
- ) {
218
- skillsTriggered.push(skillName);
219
- }
407
+ detectExplicitSkillReads(textContent);
408
+ if (etype === "item.completed") {
409
+ detectTriggeredSkills(textContent);
220
410
  }
221
411
  } else if (etype === "error") {
222
412
  errors += 1;
223
413
  }
224
414
 
225
415
  // Some rollout formats embed the original prompt
226
- if (!prompt && (event.prompt as string)) {
227
- prompt = event.prompt as string;
228
- }
416
+ rememberPromptCandidate(event.prompt);
229
417
  }
230
418
 
231
419
  // Infer file date from path structure: .../YYYY/MM/DD/rollout-*.jsonl
@@ -249,7 +437,7 @@ export function parseRolloutFile(path: string, skillNames: Set<string>): ParsedR
249
437
 
250
438
  return {
251
439
  timestamp: fileDate,
252
- session_id: threadId,
440
+ session_id: observedSessionId ?? threadId,
253
441
  source: "codex_rollout",
254
442
  rollout_path: path,
255
443
  query: prompt,
@@ -257,14 +445,19 @@ export function parseRolloutFile(path: string, skillNames: Set<string>): ParsedR
257
445
  total_tool_calls: Object.values(toolCalls).reduce((a, b) => a + b, 0),
258
446
  bash_commands: bashCommands,
259
447
  skills_triggered: skillsTriggered,
448
+ skills_invoked: skillsTriggered.filter(
449
+ (skillName) => skillEvidence.get(skillName) === "explicit",
450
+ ),
451
+ skill_evidence: Object.fromEntries(skillEvidence),
260
452
  assistant_turns: turns,
261
453
  errors_encountered: errors,
262
454
  input_tokens: inputTokens,
263
455
  output_tokens: outputTokens,
264
456
  transcript_chars: lines.reduce((sum, l) => sum + l.length, 0),
265
- cwd: "",
457
+ cwd: observedCwd ?? "",
266
458
  transcript_path: path,
267
- last_user_query: prompt,
459
+ last_user_query: lastUserQuery || prompt,
460
+ observed_meta: observedMeta,
268
461
  };
269
462
  }
270
463
 
@@ -275,6 +468,7 @@ export function ingestFile(
275
468
  queryLogPath: string = QUERY_LOG,
276
469
  telemetryLogPath: string = TELEMETRY_LOG,
277
470
  skillLogPath: string = SKILL_LOG,
471
+ canonicalLogPath: string = CANONICAL_LOG,
278
472
  ): boolean {
279
473
  const { query: prompt, session_id: sessionId, skills_triggered: skills } = parsed;
280
474
 
@@ -308,6 +502,7 @@ export function ingestFile(
308
502
  total_tool_calls: parsed.total_tool_calls,
309
503
  bash_commands: parsed.bash_commands,
310
504
  skills_triggered: skills,
505
+ skills_invoked: parsed.skills_invoked,
311
506
  assistant_turns: parsed.assistant_turns,
312
507
  errors_encountered: parsed.errors_encountered,
313
508
  transcript_chars: parsed.transcript_chars,
@@ -321,21 +516,125 @@ export function ingestFile(
321
516
 
322
517
  // Write skill triggers
323
518
  for (const skillName of skills) {
519
+ const isExplicit = parsed.skill_evidence[skillName] === "explicit";
520
+ const skillPath = isExplicit
521
+ ? (findInstalledSkillPath(skillName, [
522
+ ...findRepositorySkillDirs(parsed.cwd || process.cwd()),
523
+ join(homedir(), ".agents", "skills"),
524
+ "/etc/codex/skills",
525
+ join(DEFAULT_CODEX_HOME, "skills"),
526
+ join(DEFAULT_CODEX_HOME, "skills", ".system"),
527
+ ]) ?? `(codex:${skillName})`)
528
+ : `(codex:${skillName})`;
324
529
  const skillRecord: SkillUsageRecord = {
325
530
  timestamp: parsed.timestamp,
326
531
  session_id: sessionId,
327
532
  skill_name: skillName,
328
- skill_path: `(codex:${skillName})`,
533
+ skill_path: skillPath,
534
+ ...classifySkillPath(skillPath),
329
535
  query: prompt,
330
536
  triggered: true,
331
- source: "codex_rollout",
537
+ source: isExplicit ? "codex_rollout_explicit" : "codex_rollout",
332
538
  };
333
539
  appendJsonl(skillLogPath, skillRecord, "skill_usage");
334
540
  }
335
541
 
542
+ // --- Canonical normalization records (additive) ---
543
+ const canonicalRecords = buildCanonicalRecordsFromRollout(parsed);
544
+ appendCanonicalRecords(canonicalRecords, canonicalLogPath);
545
+
336
546
  return true;
337
547
  }
338
548
 
549
+ /** Build canonical records from a parsed rollout. */
550
+ export function buildCanonicalRecordsFromRollout(parsed: ParsedRollout): CanonicalRecord[] {
551
+ const records: CanonicalRecord[] = [];
552
+ const baseInput: CanonicalBaseInput = {
553
+ platform: "codex",
554
+ capture_mode: "batch_ingest",
555
+ source_session_kind: "replayed",
556
+ session_id: parsed.session_id,
557
+ raw_source_ref: {
558
+ path: parsed.rollout_path,
559
+ event_type: "codex_rollout",
560
+ },
561
+ };
562
+
563
+ // Session record
564
+ const meta = parsed.observed_meta;
565
+ records.push(
566
+ buildCanonicalSession({
567
+ ...baseInput,
568
+ started_at: parsed.timestamp,
569
+ workspace_path: parsed.cwd || undefined,
570
+ provider: meta?.model_provider,
571
+ model: meta?.model,
572
+ approval_policy: meta?.approval_policy,
573
+ sandbox_policy: meta?.sandbox_policy,
574
+ agent_id: meta?.originator,
575
+ branch: meta?.git?.branch,
576
+ repo_remote: meta?.git?.remote,
577
+ commit_sha: meta?.git?.commit,
578
+ }),
579
+ );
580
+
581
+ // Prompt record
582
+ const promptEmitted = Boolean(parsed.query && parsed.query.length >= 4);
583
+ const promptId = promptEmitted ? derivePromptId(parsed.session_id, 0) : undefined;
584
+
585
+ if (promptId) {
586
+ records.push(
587
+ buildCanonicalPrompt({
588
+ ...baseInput,
589
+ prompt_id: promptId,
590
+ occurred_at: parsed.timestamp,
591
+ prompt_text: parsed.query,
592
+ prompt_index: 0,
593
+ }),
594
+ );
595
+ }
596
+
597
+ // Skill invocation records
598
+ for (let i = 0; i < parsed.skills_triggered.length; i++) {
599
+ const skillName = parsed.skills_triggered[i];
600
+ const isExplicit = parsed.skill_evidence[skillName] === "explicit";
601
+ const { invocation_mode, confidence } = deriveInvocationMode(
602
+ isExplicit ? { has_skill_md_read: true } : { is_text_mention_only: true },
603
+ );
604
+ records.push(
605
+ buildCanonicalSkillInvocation({
606
+ ...baseInput,
607
+ skill_invocation_id: deriveSkillInvocationId(parsed.session_id, skillName, i),
608
+ occurred_at: parsed.timestamp,
609
+ matched_prompt_id: promptId,
610
+ skill_name: skillName,
611
+ skill_path: `(codex:${skillName})`,
612
+ invocation_mode,
613
+ triggered: true,
614
+ confidence,
615
+ }),
616
+ );
617
+ }
618
+
619
+ // Execution fact record
620
+ records.push(
621
+ buildCanonicalExecutionFact({
622
+ ...baseInput,
623
+ occurred_at: parsed.timestamp,
624
+ prompt_id: promptId,
625
+ tool_calls_json: parsed.tool_calls,
626
+ total_tool_calls: parsed.total_tool_calls,
627
+ bash_commands_redacted: parsed.bash_commands,
628
+ assistant_turns: parsed.assistant_turns,
629
+ errors_encountered: parsed.errors_encountered,
630
+ input_tokens: parsed.input_tokens ?? undefined,
631
+ output_tokens: parsed.output_tokens ?? undefined,
632
+ }),
633
+ );
634
+
635
+ return records;
636
+ }
637
+
339
638
  // --- CLI main ---
340
639
  export function cliMain(): void {
341
640
  const { values } = parseArgs({