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.
- package/.claude/agents/diagnosis-analyst.md +20 -10
- package/.claude/agents/evolution-reviewer.md +14 -1
- package/.claude/agents/integration-guide.md +18 -6
- package/.claude/agents/pattern-analyst.md +18 -5
- package/CHANGELOG.md +12 -4
- package/README.md +43 -35
- package/apps/local-dashboard/dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/index-C4EOTFZ2.js +15 -0
- package/apps/local-dashboard/dist/assets/index-bl-Webyd.css +1 -0
- package/apps/local-dashboard/dist/assets/vendor-react-U7zYD9Rg.js +60 -0
- package/apps/local-dashboard/dist/assets/vendor-table-B7VF2Ipl.js +26 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-D7_zX_qy.js +346 -0
- package/apps/local-dashboard/dist/favicon.png +0 -0
- package/apps/local-dashboard/dist/index.html +17 -0
- package/apps/local-dashboard/dist/logo.png +0 -0
- package/apps/local-dashboard/dist/logo.svg +9 -0
- package/cli/selftune/badge/badge-data.ts +1 -1
- package/cli/selftune/badge/badge.ts +4 -8
- package/cli/selftune/canonical-export.ts +183 -0
- package/cli/selftune/constants.ts +28 -0
- package/cli/selftune/contribute/contribute.ts +1 -1
- package/cli/selftune/cron/setup.ts +17 -17
- package/cli/selftune/dashboard-contract.ts +202 -0
- package/cli/selftune/dashboard-server.ts +653 -186
- package/cli/selftune/dashboard.ts +41 -176
- package/cli/selftune/eval/baseline.ts +5 -4
- package/cli/selftune/eval/composability-v2.ts +273 -0
- package/cli/selftune/eval/hooks-to-evals.ts +34 -15
- package/cli/selftune/eval/unit-test-cli.ts +1 -1
- package/cli/selftune/evolution/evidence.ts +26 -0
- package/cli/selftune/evolution/evolve-body.ts +105 -11
- package/cli/selftune/evolution/evolve.ts +371 -25
- package/cli/selftune/evolution/extract-patterns.ts +87 -29
- package/cli/selftune/evolution/rollback.ts +2 -2
- package/cli/selftune/grading/auto-grade.ts +200 -0
- package/cli/selftune/grading/grade-session.ts +448 -97
- package/cli/selftune/grading/results.ts +42 -0
- package/cli/selftune/hooks/prompt-log.ts +172 -2
- package/cli/selftune/hooks/session-stop.ts +123 -3
- package/cli/selftune/hooks/skill-eval.ts +119 -3
- package/cli/selftune/index.ts +395 -116
- package/cli/selftune/ingestors/claude-replay.ts +140 -114
- package/cli/selftune/ingestors/codex-rollout.ts +345 -46
- package/cli/selftune/ingestors/codex-wrapper.ts +207 -39
- package/cli/selftune/ingestors/openclaw-ingest.ts +141 -8
- package/cli/selftune/ingestors/opencode-ingest.ts +193 -17
- package/cli/selftune/init.ts +227 -14
- package/cli/selftune/last.ts +14 -5
- package/cli/selftune/localdb/db.ts +63 -0
- package/cli/selftune/localdb/materialize.ts +428 -0
- package/cli/selftune/localdb/queries.ts +376 -0
- package/cli/selftune/localdb/schema.ts +204 -0
- package/cli/selftune/monitoring/watch.ts +66 -15
- package/cli/selftune/normalization.ts +682 -0
- package/cli/selftune/observability.ts +19 -44
- package/cli/selftune/orchestrate.ts +1073 -0
- package/cli/selftune/quickstart.ts +203 -0
- package/cli/selftune/repair/skill-usage.ts +576 -0
- package/cli/selftune/schedule.ts +561 -0
- package/cli/selftune/status.ts +48 -26
- package/cli/selftune/sync.ts +627 -0
- package/cli/selftune/types.ts +148 -0
- package/cli/selftune/utils/canonical-log.ts +45 -0
- package/cli/selftune/utils/hooks.ts +41 -0
- package/cli/selftune/utils/html.ts +27 -0
- package/cli/selftune/utils/llm-call.ts +78 -20
- package/cli/selftune/utils/math.ts +10 -0
- package/cli/selftune/utils/query-filter.ts +139 -0
- package/cli/selftune/utils/skill-discovery.ts +340 -0
- package/cli/selftune/utils/skill-log.ts +68 -0
- package/cli/selftune/utils/skill-usage-confidence.ts +18 -0
- package/cli/selftune/utils/transcript.ts +272 -26
- package/cli/selftune/workflows/discover.ts +254 -0
- package/cli/selftune/workflows/skill-md-writer.ts +288 -0
- package/cli/selftune/workflows/workflows.ts +188 -0
- package/package.json +21 -8
- package/packages/telemetry-contract/README.md +11 -0
- package/packages/telemetry-contract/fixtures/golden.json +87 -0
- package/packages/telemetry-contract/fixtures/golden.test.ts +42 -0
- package/packages/telemetry-contract/index.ts +1 -0
- package/packages/telemetry-contract/package.json +19 -0
- package/packages/telemetry-contract/src/index.ts +2 -0
- package/packages/telemetry-contract/src/types.ts +163 -0
- package/packages/telemetry-contract/src/validators.ts +109 -0
- package/skill/SKILL.md +84 -53
- package/skill/Workflows/AutoActivation.md +17 -16
- package/skill/Workflows/Badge.md +6 -0
- package/skill/Workflows/Baseline.md +46 -23
- package/skill/Workflows/Composability.md +12 -5
- package/skill/Workflows/Contribute.md +17 -14
- package/skill/Workflows/Cron.md +56 -79
- package/skill/Workflows/Dashboard.md +45 -34
- package/skill/Workflows/Doctor.md +30 -17
- package/skill/Workflows/Evals.md +64 -40
- package/skill/Workflows/EvolutionMemory.md +2 -0
- package/skill/Workflows/Evolve.md +102 -47
- package/skill/Workflows/EvolveBody.md +6 -6
- package/skill/Workflows/Grade.md +36 -31
- package/skill/Workflows/ImportSkillsBench.md +11 -5
- package/skill/Workflows/Ingest.md +43 -36
- package/skill/Workflows/Initialize.md +44 -30
- package/skill/Workflows/Orchestrate.md +139 -0
- package/skill/Workflows/Replay.md +39 -18
- package/skill/Workflows/Rollback.md +3 -3
- package/skill/Workflows/Schedule.md +61 -0
- package/skill/Workflows/Sync.md +88 -0
- package/skill/Workflows/UnitTest.md +34 -22
- package/skill/Workflows/Watch.md +14 -4
- package/skill/Workflows/Workflows.md +129 -0
- package/skill/assets/activation-rules-default.json +26 -0
- package/skill/assets/multi-skill-settings.json +63 -0
- package/skill/assets/single-skill-settings.json +57 -0
- package/skill/references/invocation-taxonomy.md +2 -2
- package/skill/references/logs.md +164 -2
- package/skill/references/setup-patterns.md +65 -0
- package/skill/references/version-history.md +40 -0
- package/skill/settings_snippet.json +1 -1
- package/templates/multi-skill-settings.json +7 -7
- package/templates/single-skill-settings.json +6 -6
- 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
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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:
|
|
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({
|