selftune 0.1.4 → 0.2.1
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 +156 -0
- package/.claude/agents/evolution-reviewer.md +180 -0
- package/.claude/agents/integration-guide.md +212 -0
- package/.claude/agents/pattern-analyst.md +160 -0
- package/CHANGELOG.md +46 -1
- package/README.md +105 -257
- 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/assets/BeforeAfter.gif +0 -0
- package/assets/FeedbackLoop.gif +0 -0
- package/assets/logo.svg +9 -0
- package/assets/skill-health-badge.svg +20 -0
- package/cli/selftune/activation-rules.ts +171 -0
- package/cli/selftune/badge/badge-data.ts +108 -0
- package/cli/selftune/badge/badge-svg.ts +212 -0
- package/cli/selftune/badge/badge.ts +99 -0
- package/cli/selftune/canonical-export.ts +183 -0
- package/cli/selftune/constants.ts +103 -1
- package/cli/selftune/contribute/bundle.ts +314 -0
- package/cli/selftune/contribute/contribute.ts +214 -0
- package/cli/selftune/contribute/sanitize.ts +162 -0
- package/cli/selftune/cron/setup.ts +266 -0
- package/cli/selftune/dashboard-contract.ts +202 -0
- package/cli/selftune/dashboard-server.ts +1049 -0
- package/cli/selftune/dashboard.ts +43 -156
- package/cli/selftune/eval/baseline.ts +248 -0
- package/cli/selftune/eval/composability-v2.ts +273 -0
- package/cli/selftune/eval/composability.ts +117 -0
- package/cli/selftune/eval/generate-unit-tests.ts +143 -0
- package/cli/selftune/eval/hooks-to-evals.ts +101 -16
- package/cli/selftune/eval/import-skillsbench.ts +221 -0
- package/cli/selftune/eval/synthetic-evals.ts +172 -0
- package/cli/selftune/eval/unit-test-cli.ts +152 -0
- package/cli/selftune/eval/unit-test.ts +196 -0
- package/cli/selftune/evolution/deploy-proposal.ts +142 -1
- package/cli/selftune/evolution/evidence.ts +26 -0
- package/cli/selftune/evolution/evolve-body.ts +586 -0
- package/cli/selftune/evolution/evolve.ts +825 -116
- package/cli/selftune/evolution/extract-patterns.ts +105 -16
- package/cli/selftune/evolution/pareto.ts +314 -0
- package/cli/selftune/evolution/propose-body.ts +171 -0
- package/cli/selftune/evolution/propose-description.ts +100 -2
- package/cli/selftune/evolution/propose-routing.ts +166 -0
- package/cli/selftune/evolution/refine-body.ts +141 -0
- package/cli/selftune/evolution/rollback.ts +21 -4
- package/cli/selftune/evolution/validate-body.ts +254 -0
- package/cli/selftune/evolution/validate-proposal.ts +257 -35
- package/cli/selftune/evolution/validate-routing.ts +177 -0
- package/cli/selftune/grading/auto-grade.ts +200 -0
- package/cli/selftune/grading/grade-session.ts +513 -42
- package/cli/selftune/grading/pre-gates.ts +104 -0
- package/cli/selftune/grading/results.ts +42 -0
- package/cli/selftune/hooks/auto-activate.ts +185 -0
- package/cli/selftune/hooks/evolution-guard.ts +165 -0
- package/cli/selftune/hooks/prompt-log.ts +172 -2
- package/cli/selftune/hooks/session-stop.ts +123 -3
- package/cli/selftune/hooks/skill-change-guard.ts +112 -0
- package/cli/selftune/hooks/skill-eval.ts +119 -3
- package/cli/selftune/index.ts +415 -48
- package/cli/selftune/ingestors/claude-replay.ts +377 -0
- 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 +573 -0
- package/cli/selftune/ingestors/opencode-ingest.ts +193 -17
- package/cli/selftune/init.ts +376 -16
- 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/memory/writer.ts +447 -0
- package/cli/selftune/monitoring/watch.ts +90 -16
- 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 +59 -33
- package/cli/selftune/sync.ts +627 -0
- package/cli/selftune/types.ts +525 -5
- package/cli/selftune/utils/canonical-log.ts +45 -0
- package/cli/selftune/utils/frontmatter.ts +217 -0
- package/cli/selftune/utils/hooks.ts +41 -0
- package/cli/selftune/utils/html.ts +27 -0
- package/cli/selftune/utils/llm-call.ts +103 -19
- 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 +307 -26
- package/cli/selftune/utils/trigger-check.ts +89 -0
- package/cli/selftune/utils/tui.ts +156 -0
- 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 +28 -11
- 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 +180 -33
- package/skill/Workflows/AutoActivation.md +145 -0
- package/skill/Workflows/Badge.md +124 -0
- package/skill/Workflows/Baseline.md +144 -0
- package/skill/Workflows/Composability.md +107 -0
- package/skill/Workflows/Contribute.md +94 -0
- package/skill/Workflows/Cron.md +132 -0
- package/skill/Workflows/Dashboard.md +214 -0
- package/skill/Workflows/Doctor.md +63 -14
- package/skill/Workflows/Evals.md +110 -18
- package/skill/Workflows/EvolutionMemory.md +154 -0
- package/skill/Workflows/Evolve.md +181 -21
- package/skill/Workflows/EvolveBody.md +159 -0
- package/skill/Workflows/Grade.md +36 -31
- package/skill/Workflows/ImportSkillsBench.md +117 -0
- package/skill/Workflows/Ingest.md +142 -21
- package/skill/Workflows/Initialize.md +91 -23
- package/skill/Workflows/Orchestrate.md +139 -0
- package/skill/Workflows/Replay.md +91 -0
- package/skill/Workflows/Rollback.md +23 -4
- package/skill/Workflows/Schedule.md +61 -0
- package/skill/Workflows/Sync.md +88 -0
- package/skill/Workflows/UnitTest.md +150 -0
- package/skill/Workflows/Watch.md +33 -1
- 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 +23 -0
- package/templates/activation-rules-default.json +27 -0
- package/templates/multi-skill-settings.json +64 -0
- package/templates/single-skill-settings.json +58 -0
- package/dashboard/index.html +0 -1119
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import { CANONICAL_LOG, CLAUDE_CODE_PROJECTS_DIR, EVOLUTION_EVIDENCE_LOG } from "./constants.js";
|
|
8
|
+
import {
|
|
9
|
+
buildCanonicalRecordsFromReplay,
|
|
10
|
+
findTranscriptFiles,
|
|
11
|
+
parseSession,
|
|
12
|
+
} from "./ingestors/claude-replay.js";
|
|
13
|
+
import {
|
|
14
|
+
CANONICAL_PLATFORMS,
|
|
15
|
+
CANONICAL_RECORD_KINDS,
|
|
16
|
+
type CanonicalPlatform,
|
|
17
|
+
type CanonicalRecord,
|
|
18
|
+
type CanonicalRecordKind,
|
|
19
|
+
type EvolutionEvidenceEntry,
|
|
20
|
+
} from "./types.js";
|
|
21
|
+
import {
|
|
22
|
+
filterCanonicalRecords,
|
|
23
|
+
readCanonicalRecords,
|
|
24
|
+
serializeCanonicalRecords,
|
|
25
|
+
} from "./utils/canonical-log.js";
|
|
26
|
+
import { readJsonl } from "./utils/jsonl.js";
|
|
27
|
+
|
|
28
|
+
function exitWithUsage(message?: string): never {
|
|
29
|
+
if (message) console.error(`[ERROR] ${message}`);
|
|
30
|
+
console.error(
|
|
31
|
+
`Usage: selftune export-canonical [--out FILE] [--platform NAME] [--record-kind KIND] [--pretty] [--log FILE] [--projects-dir PATH] [--push-payload]`,
|
|
32
|
+
);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function validatePlatform(value: string | undefined): CanonicalPlatform | undefined {
|
|
37
|
+
if (!value) return undefined;
|
|
38
|
+
if (!CANONICAL_PLATFORMS.includes(value as CanonicalPlatform)) {
|
|
39
|
+
exitWithUsage(`Unknown platform: ${value}`);
|
|
40
|
+
}
|
|
41
|
+
return value as CanonicalPlatform;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validateRecordKind(value: string | undefined): CanonicalRecordKind | undefined {
|
|
45
|
+
if (!value) return undefined;
|
|
46
|
+
if (!CANONICAL_RECORD_KINDS.includes(value as CanonicalRecordKind)) {
|
|
47
|
+
exitWithUsage(`Unknown record kind: ${value}`);
|
|
48
|
+
}
|
|
49
|
+
return value as CanonicalRecordKind;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getClientVersion(): string {
|
|
53
|
+
try {
|
|
54
|
+
const pkg = JSON.parse(readFileSync(join(import.meta.dir, "../../package.json"), "utf-8"));
|
|
55
|
+
return pkg.version ?? "unknown";
|
|
56
|
+
} catch {
|
|
57
|
+
return "unknown";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function loadCanonicalRecordsForExport(
|
|
62
|
+
logPath: string = CANONICAL_LOG,
|
|
63
|
+
projectsDir: string = CLAUDE_CODE_PROJECTS_DIR,
|
|
64
|
+
platform?: CanonicalPlatform,
|
|
65
|
+
): CanonicalRecord[] {
|
|
66
|
+
const canonical = readCanonicalRecords(logPath);
|
|
67
|
+
if (canonical.length > 0) return canonical;
|
|
68
|
+
|
|
69
|
+
// Existing installs may have rich Claude Code transcripts but no canonical log yet.
|
|
70
|
+
// Fall back to synthesizing exportable records directly from transcripts.
|
|
71
|
+
if (platform && platform !== "claude_code") return [];
|
|
72
|
+
|
|
73
|
+
const records: CanonicalRecord[] = [];
|
|
74
|
+
for (const transcriptPath of findTranscriptFiles(projectsDir)) {
|
|
75
|
+
const session = parseSession(transcriptPath);
|
|
76
|
+
if (!session) continue;
|
|
77
|
+
records.push(...buildCanonicalRecordsFromReplay(session));
|
|
78
|
+
}
|
|
79
|
+
return records;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function buildPushPayloadV2(
|
|
83
|
+
records: CanonicalRecord[],
|
|
84
|
+
evidenceEntries: EvolutionEvidenceEntry[] = [],
|
|
85
|
+
): Record<string, unknown> {
|
|
86
|
+
const sessions = records.filter((record) => record.record_kind === "session");
|
|
87
|
+
const prompts = records.filter((record) => record.record_kind === "prompt");
|
|
88
|
+
const skillInvocations = records.filter((record) => record.record_kind === "skill_invocation");
|
|
89
|
+
const executionFacts = records.filter((record) => record.record_kind === "execution_fact");
|
|
90
|
+
const normalizationRuns = records.filter((record) => record.record_kind === "normalization_run");
|
|
91
|
+
const normalizerVersion = records[0]?.normalizer_version ?? "1.0.0";
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
schema_version: "2.0",
|
|
95
|
+
client_version: getClientVersion(),
|
|
96
|
+
push_id: randomUUID(),
|
|
97
|
+
normalizer_version: normalizerVersion,
|
|
98
|
+
canonical: {
|
|
99
|
+
sessions,
|
|
100
|
+
prompts,
|
|
101
|
+
skill_invocations: skillInvocations,
|
|
102
|
+
execution_facts: executionFacts,
|
|
103
|
+
normalization_runs: normalizationRuns,
|
|
104
|
+
evolution_evidence: evidenceEntries.map((entry) => ({
|
|
105
|
+
skill_name: entry.skill_name,
|
|
106
|
+
proposal_id: entry.proposal_id,
|
|
107
|
+
target: entry.target,
|
|
108
|
+
stage: entry.stage,
|
|
109
|
+
rationale: entry.rationale,
|
|
110
|
+
confidence: entry.confidence,
|
|
111
|
+
original_text: entry.original_text,
|
|
112
|
+
proposed_text: entry.proposed_text,
|
|
113
|
+
eval_set_json: entry.eval_set,
|
|
114
|
+
validation_json: entry.validation,
|
|
115
|
+
})),
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function cliMain(): void {
|
|
121
|
+
const { values } = parseArgs({
|
|
122
|
+
args: process.argv.slice(2),
|
|
123
|
+
options: {
|
|
124
|
+
out: { type: "string" },
|
|
125
|
+
platform: { type: "string" },
|
|
126
|
+
"record-kind": { type: "string" },
|
|
127
|
+
pretty: { type: "boolean", default: false },
|
|
128
|
+
log: { type: "string", default: CANONICAL_LOG },
|
|
129
|
+
"projects-dir": { type: "string", default: CLAUDE_CODE_PROJECTS_DIR },
|
|
130
|
+
"push-payload": { type: "boolean", default: false },
|
|
131
|
+
},
|
|
132
|
+
strict: true,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const platform = validatePlatform(values.platform);
|
|
136
|
+
const recordKind = validateRecordKind(values["record-kind"]);
|
|
137
|
+
const records = filterCanonicalRecords(
|
|
138
|
+
loadCanonicalRecordsForExport(values.log, values["projects-dir"], platform),
|
|
139
|
+
{
|
|
140
|
+
platform,
|
|
141
|
+
record_kind: recordKind,
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const output = values["push-payload"]
|
|
146
|
+
? `${JSON.stringify(
|
|
147
|
+
buildPushPayloadV2(records, readJsonl<EvolutionEvidenceEntry>(EVOLUTION_EVIDENCE_LOG)),
|
|
148
|
+
null,
|
|
149
|
+
values.pretty ? 2 : undefined,
|
|
150
|
+
)}\n`
|
|
151
|
+
: serializeCanonicalRecords(records, values.pretty);
|
|
152
|
+
|
|
153
|
+
if (values.out) {
|
|
154
|
+
writeFileSync(values.out, output, "utf-8");
|
|
155
|
+
console.log(
|
|
156
|
+
JSON.stringify(
|
|
157
|
+
{
|
|
158
|
+
ok: true,
|
|
159
|
+
out: values.out,
|
|
160
|
+
count: records.length,
|
|
161
|
+
format: values["push-payload"] ? "push-payload-v2" : "jsonl",
|
|
162
|
+
pretty: values.pretty,
|
|
163
|
+
platform: platform ?? null,
|
|
164
|
+
record_kind: recordKind ?? null,
|
|
165
|
+
},
|
|
166
|
+
null,
|
|
167
|
+
values.pretty ? 2 : undefined,
|
|
168
|
+
),
|
|
169
|
+
);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
process.stdout.write(output);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (import.meta.main) {
|
|
177
|
+
try {
|
|
178
|
+
cliMain();
|
|
179
|
+
} catch (error) {
|
|
180
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
181
|
+
exitWithUsage(message);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -12,8 +12,21 @@ export const LOG_DIR = join(homedir(), ".claude");
|
|
|
12
12
|
|
|
13
13
|
export const TELEMETRY_LOG = join(LOG_DIR, "session_telemetry_log.jsonl");
|
|
14
14
|
export const SKILL_LOG = join(LOG_DIR, "skill_usage_log.jsonl");
|
|
15
|
+
export const REPAIRED_SKILL_LOG = join(LOG_DIR, "skill_usage_repaired.jsonl");
|
|
16
|
+
export const CANONICAL_LOG = join(LOG_DIR, "canonical_telemetry_log.jsonl");
|
|
17
|
+
export const REPAIRED_SKILL_SESSIONS_MARKER = join(LOG_DIR, "skill_usage_repaired_sessions.json");
|
|
15
18
|
export const QUERY_LOG = join(LOG_DIR, "all_queries_log.jsonl");
|
|
16
19
|
export const EVOLUTION_AUDIT_LOG = join(LOG_DIR, "evolution_audit_log.jsonl");
|
|
20
|
+
export const EVOLUTION_EVIDENCE_LOG = join(LOG_DIR, "evolution_evidence_log.jsonl");
|
|
21
|
+
export const ORCHESTRATE_RUN_LOG = join(LOG_DIR, "orchestrate_runs.jsonl");
|
|
22
|
+
export const SIGNAL_LOG = join(LOG_DIR, "improvement_signals.jsonl");
|
|
23
|
+
export const ORCHESTRATE_LOCK = join(LOG_DIR, ".orchestrate.lock");
|
|
24
|
+
|
|
25
|
+
/** Evolution memory directory — human-readable session context that survives resets. */
|
|
26
|
+
export const MEMORY_DIR = join(SELFTUNE_CONFIG_DIR, "memory");
|
|
27
|
+
export const CONTEXT_PATH = join(MEMORY_DIR, "context.md");
|
|
28
|
+
export const PLAN_PATH = join(MEMORY_DIR, "plan.md");
|
|
29
|
+
export const DECISIONS_PATH = join(MEMORY_DIR, "decisions.md");
|
|
17
30
|
|
|
18
31
|
/** Tool names Claude Code uses. */
|
|
19
32
|
export const KNOWN_TOOLS = new Set([
|
|
@@ -59,7 +72,96 @@ export const REQUIRED_FIELDS: Record<string, Set<string>> = {
|
|
|
59
72
|
skill_usage: new Set(["timestamp", "session_id", "skill_name"]),
|
|
60
73
|
all_queries: new Set(["timestamp", "session_id", "query"]),
|
|
61
74
|
evolution_audit: new Set(["timestamp", "proposal_id", "action"]),
|
|
75
|
+
evolution_evidence: new Set(["timestamp", "proposal_id", "skill_name", "stage"]),
|
|
62
76
|
};
|
|
63
77
|
|
|
64
78
|
/** Agent CLI candidates in detection order. */
|
|
65
|
-
export const AGENT_CANDIDATES = ["claude", "codex", "opencode"] as const;
|
|
79
|
+
export const AGENT_CANDIDATES = ["claude", "codex", "opencode", "openclaw"] as const;
|
|
80
|
+
|
|
81
|
+
/** Required Claude Code hook keys in settings.json. */
|
|
82
|
+
export const CLAUDE_CODE_HOOK_KEYS = [
|
|
83
|
+
"UserPromptSubmit",
|
|
84
|
+
"PreToolUse",
|
|
85
|
+
"PostToolUse",
|
|
86
|
+
"Stop",
|
|
87
|
+
] as const;
|
|
88
|
+
|
|
89
|
+
/** Path for user-defined activation rule overrides. */
|
|
90
|
+
export const ACTIVATION_RULES_PATH = join(SELFTUNE_CONFIG_DIR, "activation-rules.json");
|
|
91
|
+
|
|
92
|
+
/** Per-session state file pattern (interpolate session_id). */
|
|
93
|
+
export const SESSION_STATE_DIR = SELFTUNE_CONFIG_DIR;
|
|
94
|
+
|
|
95
|
+
/** Build a session state file path from a session ID. */
|
|
96
|
+
export function sessionStatePath(sessionId: string): string {
|
|
97
|
+
// Sanitize session ID to be filesystem-safe
|
|
98
|
+
const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
99
|
+
return join(SESSION_STATE_DIR, `session-state-${safe}.json`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Build a canonical prompt state file path from a session ID. */
|
|
103
|
+
export function canonicalSessionStatePath(sessionId: string): string {
|
|
104
|
+
const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
105
|
+
return join(SESSION_STATE_DIR, `canonical-session-state-${safe}.json`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Claude Code settings file path. */
|
|
109
|
+
export const CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
110
|
+
|
|
111
|
+
/** Path to Claude Code projects directory containing session transcripts. */
|
|
112
|
+
export const CLAUDE_CODE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
|
|
113
|
+
|
|
114
|
+
/** Marker file tracking which Claude Code sessions have been ingested. */
|
|
115
|
+
export const CLAUDE_CODE_MARKER = join(homedir(), ".claude", "claude_code_ingested_sessions.json");
|
|
116
|
+
|
|
117
|
+
/** Marker file tracking which Codex rollout files have been ingested. */
|
|
118
|
+
export const CODEX_INGEST_MARKER = join(homedir(), ".claude", "codex_ingested_rollouts.json");
|
|
119
|
+
|
|
120
|
+
/** Marker file tracking which OpenCode sessions have been ingested. */
|
|
121
|
+
export const OPENCODE_INGEST_MARKER = join(homedir(), ".claude", "opencode_ingested_sessions.json");
|
|
122
|
+
|
|
123
|
+
/** OpenClaw agents directory containing session data. */
|
|
124
|
+
export const OPENCLAW_AGENTS_DIR = join(homedir(), ".openclaw", "agents");
|
|
125
|
+
|
|
126
|
+
/** Marker file tracking which OpenClaw sessions have been ingested. */
|
|
127
|
+
export const OPENCLAW_INGEST_MARKER = join(SELFTUNE_CONFIG_DIR, "openclaw-ingest-marker.json");
|
|
128
|
+
|
|
129
|
+
/** Default output directory for contribution bundles. */
|
|
130
|
+
export const CONTRIBUTIONS_DIR = join(SELFTUNE_CONFIG_DIR, "contributions");
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Sanitization constants (for contribute command)
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
/** Regex patterns for detecting secrets that must be redacted. */
|
|
137
|
+
export const SECRET_PATTERNS = [
|
|
138
|
+
/sk-[a-zA-Z0-9]{20,}/g, // OpenAI / Anthropic API keys
|
|
139
|
+
/ghp_[a-zA-Z0-9]{36,}/g, // GitHub personal access tokens
|
|
140
|
+
/gho_[a-zA-Z0-9]{36,}/g, // GitHub OAuth tokens
|
|
141
|
+
/github_pat_[a-zA-Z0-9_]{22,}/g, // GitHub fine-grained PATs
|
|
142
|
+
/AKIA[A-Z0-9]{16}/g, // AWS access key IDs
|
|
143
|
+
/xoxb-[a-zA-Z0-9-]+/g, // Slack bot tokens
|
|
144
|
+
/xoxp-[a-zA-Z0-9-]+/g, // Slack user tokens
|
|
145
|
+
/xoxs-[a-zA-Z0-9-]+/g, // Slack session tokens
|
|
146
|
+
/eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g, // JWTs
|
|
147
|
+
/npm_[a-zA-Z0-9]{36}/g, // npm tokens
|
|
148
|
+
/pypi-[a-zA-Z0-9]{36,}/g, // PyPI tokens
|
|
149
|
+
] as const;
|
|
150
|
+
|
|
151
|
+
/** Regex for file paths (Unix and Windows). */
|
|
152
|
+
export const FILE_PATH_PATTERN = /(?:\/[\w.-]+){2,}|[A-Z]:\\[\w\\.-]+/g;
|
|
153
|
+
|
|
154
|
+
/** Regex for email addresses. */
|
|
155
|
+
export const EMAIL_PATTERN = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g;
|
|
156
|
+
|
|
157
|
+
/** Regex for IP addresses (v4). */
|
|
158
|
+
export const IP_PATTERN = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g;
|
|
159
|
+
|
|
160
|
+
/** Regex for camelCase/PascalCase identifiers longer than 8 chars (aggressive mode). */
|
|
161
|
+
export const IDENTIFIER_PATTERN = /\b[a-z][a-zA-Z0-9]{8,}\b|\b[A-Z][a-zA-Z0-9]{8,}\b/g;
|
|
162
|
+
|
|
163
|
+
/** Regex for import/require/from module paths (aggressive mode). */
|
|
164
|
+
export const MODULE_PATTERN = /(?:import|require|from)\s+["']([^"']+)["']/g;
|
|
165
|
+
|
|
166
|
+
/** Max query length for aggressive sanitization. */
|
|
167
|
+
export const AGGRESSIVE_MAX_QUERY_LENGTH = 200;
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle assembly for contribution export.
|
|
3
|
+
*
|
|
4
|
+
* Pure function: reads logs, filters, aggregates, and returns a ContributionBundle.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import {
|
|
12
|
+
EVOLUTION_AUDIT_LOG,
|
|
13
|
+
QUERY_LOG,
|
|
14
|
+
SELFTUNE_CONFIG_DIR,
|
|
15
|
+
SKILL_LOG,
|
|
16
|
+
TELEMETRY_LOG,
|
|
17
|
+
} from "../constants.js";
|
|
18
|
+
import { buildEvalSet, classifyInvocation } from "../eval/hooks-to-evals.js";
|
|
19
|
+
import type {
|
|
20
|
+
ContributionBundle,
|
|
21
|
+
ContributionEvolutionSummary,
|
|
22
|
+
ContributionGradingSummary,
|
|
23
|
+
ContributionQuery,
|
|
24
|
+
ContributionSessionMetrics,
|
|
25
|
+
EvolutionAuditEntry,
|
|
26
|
+
GradingResult,
|
|
27
|
+
QueryLogRecord,
|
|
28
|
+
SessionTelemetryRecord,
|
|
29
|
+
SkillUsageRecord,
|
|
30
|
+
} from "../types.js";
|
|
31
|
+
import { readJsonl } from "../utils/jsonl.js";
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Helpers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
function filterSince<T extends { timestamp: string }>(records: T[], since?: Date): T[] {
|
|
38
|
+
if (!since) return records;
|
|
39
|
+
return records.filter((r) => new Date(r.timestamp) >= since);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getVersion(): string {
|
|
43
|
+
try {
|
|
44
|
+
const pkg = JSON.parse(readFileSync(join(import.meta.dir, "../../../package.json"), "utf-8"));
|
|
45
|
+
return pkg.version ?? "unknown";
|
|
46
|
+
} catch {
|
|
47
|
+
return "unknown";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getAgentType(): string {
|
|
52
|
+
try {
|
|
53
|
+
const configPath = join(SELFTUNE_CONFIG_DIR, "config.json");
|
|
54
|
+
if (!existsSync(configPath)) return "unknown";
|
|
55
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
56
|
+
return config.agent_type ?? "unknown";
|
|
57
|
+
} catch {
|
|
58
|
+
return "unknown";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function avg(nums: number[]): number {
|
|
63
|
+
if (nums.length === 0) return 0;
|
|
64
|
+
return nums.reduce((a, b) => a + b, 0) / nums.length;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Grading summary
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
function buildGradingSummary(skillName: string): ContributionGradingSummary | null {
|
|
72
|
+
const gradingDir = join(homedir(), ".selftune", "grading");
|
|
73
|
+
if (!existsSync(gradingDir)) return null;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const files = readdirSync(gradingDir).filter((f) => f.endsWith(".json"));
|
|
77
|
+
if (files.length === 0) return null;
|
|
78
|
+
|
|
79
|
+
let totalSessions = 0;
|
|
80
|
+
let gradedSessions = 0;
|
|
81
|
+
let passRateSum = 0;
|
|
82
|
+
let expectationCount = 0;
|
|
83
|
+
|
|
84
|
+
for (const file of files) {
|
|
85
|
+
try {
|
|
86
|
+
const data = JSON.parse(readFileSync(join(gradingDir, file), "utf-8")) as GradingResult;
|
|
87
|
+
if (data.skill_name !== skillName) continue;
|
|
88
|
+
totalSessions++;
|
|
89
|
+
if (data.summary) {
|
|
90
|
+
gradedSessions++;
|
|
91
|
+
passRateSum += data.summary.pass_rate ?? 0;
|
|
92
|
+
expectationCount += data.summary.total ?? 0;
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// skip malformed grading files
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (gradedSessions === 0) return null;
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
total_sessions: totalSessions,
|
|
103
|
+
graded_sessions: gradedSessions,
|
|
104
|
+
average_pass_rate: passRateSum / gradedSessions,
|
|
105
|
+
expectation_count: expectationCount,
|
|
106
|
+
};
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Evolution summary
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
function buildEvolutionSummary(
|
|
117
|
+
records: EvolutionAuditEntry[],
|
|
118
|
+
): ContributionEvolutionSummary | null {
|
|
119
|
+
if (records.length === 0) return null;
|
|
120
|
+
|
|
121
|
+
const proposals = new Set<string>();
|
|
122
|
+
let deployed = 0;
|
|
123
|
+
let rolledBack = 0;
|
|
124
|
+
const improvements: number[] = [];
|
|
125
|
+
|
|
126
|
+
for (const r of records) {
|
|
127
|
+
proposals.add(r.proposal_id);
|
|
128
|
+
if (r.action === "deployed") {
|
|
129
|
+
deployed++;
|
|
130
|
+
if (r.eval_snapshot?.pass_rate != null) {
|
|
131
|
+
improvements.push(r.eval_snapshot.pass_rate);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (r.action === "rolled_back") {
|
|
135
|
+
rolledBack++;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
total_proposals: proposals.size,
|
|
141
|
+
deployed_proposals: deployed,
|
|
142
|
+
rolled_back_proposals: rolledBack,
|
|
143
|
+
average_improvement: improvements.length > 0 ? avg(improvements) : 0,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Session metrics
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
function buildSessionMetrics(records: SessionTelemetryRecord[]): ContributionSessionMetrics {
|
|
152
|
+
if (records.length === 0) {
|
|
153
|
+
return {
|
|
154
|
+
total_sessions: 0,
|
|
155
|
+
avg_assistant_turns: 0,
|
|
156
|
+
avg_tool_calls: 0,
|
|
157
|
+
avg_errors: 0,
|
|
158
|
+
top_tools: [],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const toolCounts = new Map<string, number>();
|
|
163
|
+
for (const r of records) {
|
|
164
|
+
for (const [tool, count] of Object.entries(r.tool_calls ?? {})) {
|
|
165
|
+
toolCounts.set(tool, (toolCounts.get(tool) ?? 0) + count);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const topTools = [...toolCounts.entries()]
|
|
170
|
+
.sort((a, b) => b[1] - a[1])
|
|
171
|
+
.slice(0, 10)
|
|
172
|
+
.map(([tool, count]) => ({ tool, count }));
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
total_sessions: records.length,
|
|
176
|
+
avg_assistant_turns: Math.round(avg(records.map((r) => r.assistant_turns ?? 0))),
|
|
177
|
+
avg_tool_calls: Math.round(avg(records.map((r) => r.total_tool_calls ?? 0))),
|
|
178
|
+
avg_errors: Number(avg(records.map((r) => r.errors_encountered ?? 0)).toFixed(2)),
|
|
179
|
+
top_tools: topTools,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Main assembly
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
export function assembleBundle(options: {
|
|
188
|
+
skillName: string;
|
|
189
|
+
since?: Date;
|
|
190
|
+
sanitizationLevel: "conservative" | "aggressive";
|
|
191
|
+
queryLogPath?: string;
|
|
192
|
+
skillLogPath?: string;
|
|
193
|
+
telemetryLogPath?: string;
|
|
194
|
+
evolutionAuditLogPath?: string;
|
|
195
|
+
}): ContributionBundle {
|
|
196
|
+
const {
|
|
197
|
+
skillName,
|
|
198
|
+
since,
|
|
199
|
+
sanitizationLevel,
|
|
200
|
+
queryLogPath = QUERY_LOG,
|
|
201
|
+
skillLogPath = SKILL_LOG,
|
|
202
|
+
telemetryLogPath = TELEMETRY_LOG,
|
|
203
|
+
evolutionAuditLogPath = EVOLUTION_AUDIT_LOG,
|
|
204
|
+
} = options;
|
|
205
|
+
|
|
206
|
+
// Read all logs
|
|
207
|
+
const allSkillRecords = readJsonl<SkillUsageRecord>(skillLogPath);
|
|
208
|
+
const allQueryRecords = readJsonl<QueryLogRecord>(queryLogPath);
|
|
209
|
+
const allTelemetryRecords = readJsonl<SessionTelemetryRecord>(telemetryLogPath);
|
|
210
|
+
const allEvolutionRecords = readJsonl<EvolutionAuditEntry>(evolutionAuditLogPath);
|
|
211
|
+
|
|
212
|
+
// Filter by skill and since
|
|
213
|
+
const skillRecords = filterSince(
|
|
214
|
+
allSkillRecords.filter((r) => r.skill_name === skillName),
|
|
215
|
+
since,
|
|
216
|
+
);
|
|
217
|
+
const queryRecords = filterSince(allQueryRecords, since);
|
|
218
|
+
const telemetryRecords = filterSince(
|
|
219
|
+
allTelemetryRecords.filter((r) => (r.skills_triggered ?? []).includes(skillName)),
|
|
220
|
+
since,
|
|
221
|
+
);
|
|
222
|
+
// TODO: Filter evolution records by skillName once EvolutionAuditEntry gains a skill_name field.
|
|
223
|
+
// Currently includes all skills' proposals. Schema change requires human review (escalation-policy.md).
|
|
224
|
+
const evolutionRecords = filterSince(allEvolutionRecords, since);
|
|
225
|
+
|
|
226
|
+
// Build positive queries
|
|
227
|
+
const seenQueries = new Set<string>();
|
|
228
|
+
const positiveQueries: ContributionQuery[] = [];
|
|
229
|
+
const triggeredQueryTexts = new Set<string>();
|
|
230
|
+
for (const r of skillRecords) {
|
|
231
|
+
const q = (r.query ?? "").trim();
|
|
232
|
+
if (r.triggered) triggeredQueryTexts.add(q);
|
|
233
|
+
if (!q || seenQueries.has(q)) continue;
|
|
234
|
+
seenQueries.add(q);
|
|
235
|
+
positiveQueries.push({
|
|
236
|
+
query: q,
|
|
237
|
+
invocation_type: classifyInvocation(q, skillName),
|
|
238
|
+
source: r.source ?? "skill_log",
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Build unmatched queries: queries with no matching triggered skill record
|
|
243
|
+
const unmatchedQueries = queryRecords
|
|
244
|
+
.filter((r) => !triggeredQueryTexts.has((r.query ?? "").trim()))
|
|
245
|
+
.map((r) => ({ query: (r.query ?? "").trim(), timestamp: r.timestamp }))
|
|
246
|
+
.filter((r) => r.query.length > 0);
|
|
247
|
+
|
|
248
|
+
// Build pending proposals: proposals with created/validated but no terminal action
|
|
249
|
+
const terminalActions = new Set(["deployed", "rejected", "rolled_back"]);
|
|
250
|
+
const proposalActions = new Map<string, EvolutionAuditEntry[]>();
|
|
251
|
+
for (const r of evolutionRecords) {
|
|
252
|
+
const entries = proposalActions.get(r.proposal_id) ?? [];
|
|
253
|
+
entries.push(r);
|
|
254
|
+
proposalActions.set(r.proposal_id, entries);
|
|
255
|
+
}
|
|
256
|
+
const pendingProposals: Array<{
|
|
257
|
+
proposal_id: string;
|
|
258
|
+
skill_name?: string;
|
|
259
|
+
action: string;
|
|
260
|
+
timestamp: string;
|
|
261
|
+
details: string;
|
|
262
|
+
}> = [];
|
|
263
|
+
for (const [proposalId, entries] of proposalActions) {
|
|
264
|
+
const hasTerminal = entries.some((e) => terminalActions.has(e.action));
|
|
265
|
+
if (!hasTerminal) {
|
|
266
|
+
// Use the latest entry for this proposal
|
|
267
|
+
const latest = entries[entries.length - 1];
|
|
268
|
+
pendingProposals.push({
|
|
269
|
+
proposal_id: proposalId,
|
|
270
|
+
skill_name: latest.skill_name,
|
|
271
|
+
action: latest.action,
|
|
272
|
+
timestamp: latest.timestamp,
|
|
273
|
+
details: latest.details,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Build eval entries
|
|
279
|
+
const evalEntries = buildEvalSet(skillRecords, queryRecords, skillName, 50, true, 42, true).map(
|
|
280
|
+
(e) => ({
|
|
281
|
+
query: e.query,
|
|
282
|
+
should_trigger: e.should_trigger,
|
|
283
|
+
invocation_type: e.invocation_type,
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// Build grading summary
|
|
288
|
+
const gradingSummary = buildGradingSummary(skillName);
|
|
289
|
+
|
|
290
|
+
// Build evolution summary
|
|
291
|
+
const evolutionSummary = buildEvolutionSummary(evolutionRecords);
|
|
292
|
+
|
|
293
|
+
// Build session metrics
|
|
294
|
+
const sessionMetrics = buildSessionMetrics(telemetryRecords);
|
|
295
|
+
|
|
296
|
+
const hasNewFields = unmatchedQueries.length > 0 || pendingProposals.length > 0;
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
schema_version: hasNewFields ? "1.2" : "1.1",
|
|
300
|
+
skill_name: skillName,
|
|
301
|
+
contributor_id: randomUUID(),
|
|
302
|
+
created_at: new Date().toISOString(),
|
|
303
|
+
selftune_version: getVersion(),
|
|
304
|
+
agent_type: getAgentType(),
|
|
305
|
+
sanitization_level: sanitizationLevel,
|
|
306
|
+
positive_queries: positiveQueries,
|
|
307
|
+
eval_entries: evalEntries,
|
|
308
|
+
grading_summary: gradingSummary,
|
|
309
|
+
evolution_summary: evolutionSummary,
|
|
310
|
+
session_metrics: sessionMetrics,
|
|
311
|
+
...(unmatchedQueries.length > 0 ? { unmatched_queries: unmatchedQueries } : {}),
|
|
312
|
+
...(pendingProposals.length > 0 ? { pending_proposals: pendingProposals } : {}),
|
|
313
|
+
};
|
|
314
|
+
}
|