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,214 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* selftune contribute — opt-in export of anonymized skill observability data.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun run cli/selftune/contribute/contribute.ts --skill selftune [--preview] [--output file.json]
|
|
7
|
+
* bun run cli/selftune/contribute/contribute.ts --skill selftune --submit
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { parseArgs } from "node:util";
|
|
13
|
+
import { CONTRIBUTIONS_DIR } from "../constants.js";
|
|
14
|
+
import { assembleBundle } from "./bundle.js";
|
|
15
|
+
import { sanitizeBundle } from "./sanitize.js";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// CLI
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export async function cliMain(): Promise<void> {
|
|
22
|
+
const { values } = parseArgs({
|
|
23
|
+
options: {
|
|
24
|
+
skill: { type: "string", default: "selftune" },
|
|
25
|
+
output: { type: "string" },
|
|
26
|
+
preview: { type: "boolean", default: false },
|
|
27
|
+
sanitize: { type: "string", default: "conservative" },
|
|
28
|
+
since: { type: "string" },
|
|
29
|
+
submit: { type: "boolean", default: false },
|
|
30
|
+
endpoint: { type: "string", default: "https://selftune-api.fly.dev" },
|
|
31
|
+
github: { type: "boolean", default: false },
|
|
32
|
+
},
|
|
33
|
+
strict: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const skillName = values.skill ?? "selftune";
|
|
37
|
+
const sanitizationLevel = values.sanitize === "aggressive" ? "aggressive" : "conservative";
|
|
38
|
+
|
|
39
|
+
let since: Date | undefined;
|
|
40
|
+
if (values.since) {
|
|
41
|
+
since = new Date(values.since);
|
|
42
|
+
if (Number.isNaN(since.getTime())) {
|
|
43
|
+
console.error(
|
|
44
|
+
`Error: Invalid --since date: "${values.since}". Use a valid date format (e.g., 2026-01-01).`,
|
|
45
|
+
);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 1. Assemble raw bundle
|
|
51
|
+
const rawBundle = assembleBundle({
|
|
52
|
+
skillName,
|
|
53
|
+
since,
|
|
54
|
+
sanitizationLevel,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// 2. Sanitize
|
|
58
|
+
const bundle = sanitizeBundle(rawBundle, sanitizationLevel, skillName);
|
|
59
|
+
|
|
60
|
+
// 3. Preview mode
|
|
61
|
+
if (values.preview) {
|
|
62
|
+
console.log(JSON.stringify(bundle, null, 2));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 4. Determine output path
|
|
67
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
68
|
+
const defaultPath = `${CONTRIBUTIONS_DIR}/selftune-contribution-${timestamp}.json`;
|
|
69
|
+
const outputPath = values.output ?? defaultPath;
|
|
70
|
+
|
|
71
|
+
// Ensure parent directory exists
|
|
72
|
+
const dir = outputPath.substring(0, outputPath.lastIndexOf("/"));
|
|
73
|
+
if (dir && !existsSync(dir)) {
|
|
74
|
+
mkdirSync(dir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 5. Write
|
|
78
|
+
const json = JSON.stringify(bundle, null, 2);
|
|
79
|
+
writeFileSync(outputPath, json, "utf-8");
|
|
80
|
+
|
|
81
|
+
// 6. Summary
|
|
82
|
+
console.log(`Contribution bundle written to: ${outputPath}`);
|
|
83
|
+
console.log(` Queries: ${bundle.positive_queries.length}`);
|
|
84
|
+
console.log(` Eval entries: ${bundle.eval_entries.length}`);
|
|
85
|
+
console.log(` Sessions: ${bundle.session_metrics.total_sessions}`);
|
|
86
|
+
console.log(` Sanitization: ${sanitizationLevel}`);
|
|
87
|
+
if (bundle.grading_summary) {
|
|
88
|
+
console.log(
|
|
89
|
+
` Grading: ${bundle.grading_summary.graded_sessions} sessions, ${(bundle.grading_summary.average_pass_rate * 100).toFixed(1)}% avg pass rate`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (bundle.evolution_summary) {
|
|
93
|
+
console.log(
|
|
94
|
+
` Evolution: ${bundle.evolution_summary.total_proposals} proposals, ${bundle.evolution_summary.deployed_proposals} deployed`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 7. Submit
|
|
99
|
+
if (values.submit) {
|
|
100
|
+
if (values.github) {
|
|
101
|
+
const ok = submitToGitHub(json, outputPath);
|
|
102
|
+
if (!ok) process.exit(1);
|
|
103
|
+
} else {
|
|
104
|
+
const endpoint = values.endpoint ?? "https://selftune-api.fly.dev";
|
|
105
|
+
const ok = await submitToService(json, endpoint, skillName);
|
|
106
|
+
if (!ok) {
|
|
107
|
+
console.log("Falling back to GitHub submission...");
|
|
108
|
+
const ghOk = submitToGitHub(json, outputPath);
|
|
109
|
+
if (!ghOk) process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Service submission
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
async function submitToService(
|
|
120
|
+
json: string,
|
|
121
|
+
endpoint: string,
|
|
122
|
+
skillName: string,
|
|
123
|
+
): Promise<boolean> {
|
|
124
|
+
try {
|
|
125
|
+
const url = `${endpoint}/api/submit`;
|
|
126
|
+
const res = await fetch(url, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: { "Content-Type": "application/json" },
|
|
129
|
+
body: json,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!res.ok) {
|
|
133
|
+
const body = await res.text();
|
|
134
|
+
console.error(`[ERROR] Service submission failed (${res.status}): ${body}`);
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(`\nSubmitted to ${endpoint}`);
|
|
139
|
+
console.log(` Badge: ${endpoint}/badge/${encodeURIComponent(skillName)}`);
|
|
140
|
+
console.log(` Report: ${endpoint}/report/${encodeURIComponent(skillName)}`);
|
|
141
|
+
return true;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error(
|
|
144
|
+
`[ERROR] Could not reach ${endpoint}: ${err instanceof Error ? err.message : String(err)}`,
|
|
145
|
+
);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// GitHub submission
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
function submitToGitHub(json: string, outputPath: string): boolean {
|
|
155
|
+
const repo = "selftune-dev/selftune";
|
|
156
|
+
const sizeKB = Buffer.byteLength(json, "utf-8") / 1024;
|
|
157
|
+
|
|
158
|
+
let body: string;
|
|
159
|
+
if (sizeKB < 50) {
|
|
160
|
+
body = `## Selftune Contribution\n\n\`\`\`json\n${json}\n\`\`\``;
|
|
161
|
+
} else {
|
|
162
|
+
// Create gist for large bundles
|
|
163
|
+
try {
|
|
164
|
+
const result = spawnSync("gh", ["gist", "create", outputPath, "--public"], {
|
|
165
|
+
encoding: "utf-8",
|
|
166
|
+
});
|
|
167
|
+
if (result.status !== 0) {
|
|
168
|
+
console.error("[ERROR] Failed to create gist. Is `gh` installed and authenticated?");
|
|
169
|
+
console.error(result.stderr || "gh gist create failed");
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
const gistUrl = result.stdout.trim();
|
|
173
|
+
body = `## Selftune Contribution\n\nBundle too large to inline (${sizeKB.toFixed(1)} KB).\n\nGist: ${gistUrl}`;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.error("[ERROR] Failed to create gist. Is `gh` installed and authenticated?");
|
|
176
|
+
console.error(String(err));
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const result = spawnSync(
|
|
183
|
+
"gh",
|
|
184
|
+
[
|
|
185
|
+
"issue",
|
|
186
|
+
"create",
|
|
187
|
+
"--repo",
|
|
188
|
+
repo,
|
|
189
|
+
"--label",
|
|
190
|
+
"contribution",
|
|
191
|
+
"--title",
|
|
192
|
+
"selftune contribution",
|
|
193
|
+
"--body",
|
|
194
|
+
body,
|
|
195
|
+
],
|
|
196
|
+
{ encoding: "utf-8" },
|
|
197
|
+
);
|
|
198
|
+
if (result.status !== 0) {
|
|
199
|
+
console.error("[ERROR] Failed to create GitHub issue. Is `gh` installed and authenticated?");
|
|
200
|
+
console.error(result.stderr || "gh issue create failed");
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
console.log(`\nSubmitted: ${result.stdout.trim()}`);
|
|
204
|
+
return true;
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.error("[ERROR] Failed to create GitHub issue. Is `gh` installed and authenticated?");
|
|
207
|
+
console.error(String(err));
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (import.meta.main) {
|
|
213
|
+
await cliMain();
|
|
214
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Privacy sanitization for contribution bundles.
|
|
3
|
+
*
|
|
4
|
+
* Two levels:
|
|
5
|
+
* conservative (default) — redacts paths, emails, secrets, IPs, project names, session IDs
|
|
6
|
+
* aggressive — extends conservative with identifiers, quoted strings, modules, truncation
|
|
7
|
+
*
|
|
8
|
+
* All functions are pure (no side effects).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
AGGRESSIVE_MAX_QUERY_LENGTH,
|
|
13
|
+
EMAIL_PATTERN,
|
|
14
|
+
FILE_PATH_PATTERN,
|
|
15
|
+
IDENTIFIER_PATTERN,
|
|
16
|
+
IP_PATTERN,
|
|
17
|
+
MODULE_PATTERN,
|
|
18
|
+
SECRET_PATTERNS,
|
|
19
|
+
} from "../constants.js";
|
|
20
|
+
import type { ContributionBundle } from "../types.js";
|
|
21
|
+
|
|
22
|
+
// UUID v4 pattern for session ID redaction
|
|
23
|
+
const UUID_PATTERN = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
|
|
24
|
+
|
|
25
|
+
// Quoted string patterns for aggressive mode
|
|
26
|
+
const DOUBLE_QUOTED_PATTERN = /"[^"]*"/g;
|
|
27
|
+
const SINGLE_QUOTED_PATTERN = /'[^']*'/g;
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Conservative sanitization
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
export function sanitizeConservative(text: string, projectName?: string): string {
|
|
34
|
+
if (!text) return text;
|
|
35
|
+
|
|
36
|
+
let result = text;
|
|
37
|
+
|
|
38
|
+
// Secrets first (longest/most specific patterns)
|
|
39
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
40
|
+
// Clone regex to reset lastIndex
|
|
41
|
+
result = result.replace(new RegExp(pattern.source, pattern.flags), "[SECRET]");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// File paths
|
|
45
|
+
result = result.replace(new RegExp(FILE_PATH_PATTERN.source, FILE_PATH_PATTERN.flags), "[PATH]");
|
|
46
|
+
|
|
47
|
+
// Emails
|
|
48
|
+
result = result.replace(new RegExp(EMAIL_PATTERN.source, EMAIL_PATTERN.flags), "[EMAIL]");
|
|
49
|
+
|
|
50
|
+
// IPs
|
|
51
|
+
result = result.replace(new RegExp(IP_PATTERN.source, IP_PATTERN.flags), "[IP]");
|
|
52
|
+
|
|
53
|
+
// Project name
|
|
54
|
+
if (projectName) {
|
|
55
|
+
result = result.replace(new RegExp(escapeRegExp(projectName), "g"), "[PROJECT]");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Session IDs (UUIDs)
|
|
59
|
+
result = result.replace(UUID_PATTERN, "[SESSION]");
|
|
60
|
+
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Aggressive sanitization
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
export function sanitizeAggressive(text: string, projectName?: string): string {
|
|
69
|
+
if (!text) return text;
|
|
70
|
+
|
|
71
|
+
// Start with conservative
|
|
72
|
+
let result = sanitizeConservative(text, projectName);
|
|
73
|
+
|
|
74
|
+
// Module paths (import/require/from)
|
|
75
|
+
result = result.replace(new RegExp(MODULE_PATTERN.source, MODULE_PATTERN.flags), (match) => {
|
|
76
|
+
// Preserve the keyword, replace the path
|
|
77
|
+
const keyword = match.match(/^(import|require|from)/)?.[0] ?? "";
|
|
78
|
+
// Determine what follows the keyword
|
|
79
|
+
if (match.includes("(")) {
|
|
80
|
+
return `${keyword}([MODULE])`;
|
|
81
|
+
}
|
|
82
|
+
return `${keyword} [MODULE]`;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Quoted strings
|
|
86
|
+
result = result.replace(DOUBLE_QUOTED_PATTERN, "[STRING]");
|
|
87
|
+
result = result.replace(SINGLE_QUOTED_PATTERN, "[STRING]");
|
|
88
|
+
|
|
89
|
+
// Long identifiers (camelCase/PascalCase > 8 chars)
|
|
90
|
+
result = result.replace(
|
|
91
|
+
new RegExp(IDENTIFIER_PATTERN.source, IDENTIFIER_PATTERN.flags),
|
|
92
|
+
"[IDENTIFIER]",
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Truncate
|
|
96
|
+
if (result.length > AGGRESSIVE_MAX_QUERY_LENGTH) {
|
|
97
|
+
result = result.slice(0, AGGRESSIVE_MAX_QUERY_LENGTH);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Dispatcher
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
export function sanitize(
|
|
108
|
+
text: string,
|
|
109
|
+
level: "conservative" | "aggressive",
|
|
110
|
+
projectName?: string,
|
|
111
|
+
): string {
|
|
112
|
+
return level === "aggressive"
|
|
113
|
+
? sanitizeAggressive(text, projectName)
|
|
114
|
+
: sanitizeConservative(text, projectName);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Bundle sanitization
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
export function sanitizeBundle(
|
|
122
|
+
bundle: ContributionBundle,
|
|
123
|
+
level: "conservative" | "aggressive",
|
|
124
|
+
projectName?: string,
|
|
125
|
+
): ContributionBundle {
|
|
126
|
+
return {
|
|
127
|
+
...bundle,
|
|
128
|
+
sanitization_level: level,
|
|
129
|
+
positive_queries: bundle.positive_queries.map((q) => ({
|
|
130
|
+
...q,
|
|
131
|
+
query: sanitize(q.query, level, projectName),
|
|
132
|
+
})),
|
|
133
|
+
eval_entries: bundle.eval_entries.map((e) => ({
|
|
134
|
+
...e,
|
|
135
|
+
query: sanitize(e.query, level, projectName),
|
|
136
|
+
})),
|
|
137
|
+
...(bundle.unmatched_queries
|
|
138
|
+
? {
|
|
139
|
+
unmatched_queries: bundle.unmatched_queries.map((q) => ({
|
|
140
|
+
...q,
|
|
141
|
+
query: sanitize(q.query, level, projectName),
|
|
142
|
+
})),
|
|
143
|
+
}
|
|
144
|
+
: {}),
|
|
145
|
+
...(bundle.pending_proposals
|
|
146
|
+
? {
|
|
147
|
+
pending_proposals: bundle.pending_proposals.map((p) => ({
|
|
148
|
+
...p,
|
|
149
|
+
details: sanitize(p.details, level, projectName),
|
|
150
|
+
})),
|
|
151
|
+
}
|
|
152
|
+
: {}),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Helpers
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
function escapeRegExp(str: string): string {
|
|
161
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
162
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* selftune cron — OpenClaw cron integration for selftune automation.
|
|
4
|
+
*
|
|
5
|
+
* Subcommands:
|
|
6
|
+
* setup Register default selftune cron jobs with OpenClaw
|
|
7
|
+
* list Show registered selftune cron jobs
|
|
8
|
+
* remove Remove all selftune cron jobs
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* selftune cron setup [--dry-run] [--tz <timezone>]
|
|
12
|
+
* selftune cron list
|
|
13
|
+
* selftune cron remove [--dry-run]
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { parseArgs } from "node:util";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Types & constants
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export interface CronJobConfig {
|
|
26
|
+
name: string;
|
|
27
|
+
cron: string;
|
|
28
|
+
message: string;
|
|
29
|
+
description: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const DEFAULT_CRON_JOBS: CronJobConfig[] = [
|
|
33
|
+
{
|
|
34
|
+
name: "selftune-sync",
|
|
35
|
+
cron: "*/30 * * * *",
|
|
36
|
+
message:
|
|
37
|
+
"Run selftune sync to replay and ingest new Claude Code, Codex, OpenCode, and OpenClaw source data, then rebuild the repaired skill-usage overlay.",
|
|
38
|
+
description: "Sync source-truth telemetry every 30 minutes",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "selftune-status",
|
|
42
|
+
cron: "0 8 * * *",
|
|
43
|
+
message:
|
|
44
|
+
"Run selftune sync first, then run selftune status --json and report any skills with pass rate below 80% or still ungraded due to sparse recent checks.",
|
|
45
|
+
description: "Daily health check after source sync",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "selftune-orchestrate",
|
|
49
|
+
cron: "0 */6 * * *",
|
|
50
|
+
message:
|
|
51
|
+
"Run selftune orchestrate --max-skills 3. This performs source-truth sync, selects candidate skills, evolves validated low-risk descriptions autonomously, and watches recent deployments for regressions.",
|
|
52
|
+
description: "Autonomous improvement loop every 6 hours",
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Helpers (exported for testability)
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/** Build the argument array for `openclaw cron add`. */
|
|
61
|
+
export function buildCronAddArgs(job: CronJobConfig, tz: string): string[] {
|
|
62
|
+
return [
|
|
63
|
+
"cron",
|
|
64
|
+
"add",
|
|
65
|
+
"--name",
|
|
66
|
+
job.name,
|
|
67
|
+
"--cron",
|
|
68
|
+
job.cron,
|
|
69
|
+
"--tz",
|
|
70
|
+
tz,
|
|
71
|
+
"--session",
|
|
72
|
+
"isolated",
|
|
73
|
+
"--message",
|
|
74
|
+
job.message,
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Return the default path to OpenClaw's cron jobs file. */
|
|
79
|
+
export function getOpenClawJobsPath(): string {
|
|
80
|
+
return join(homedir(), ".openclaw", "cron", "jobs.json");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Type guard that validates all required CronJobConfig fields. */
|
|
84
|
+
function isCronJobConfig(value: unknown): value is CronJobConfig {
|
|
85
|
+
if (typeof value !== "object" || value === null) return false;
|
|
86
|
+
const obj = value as Record<string, unknown>;
|
|
87
|
+
return (
|
|
88
|
+
typeof obj.name === "string" &&
|
|
89
|
+
typeof obj.cron === "string" &&
|
|
90
|
+
typeof obj.message === "string" &&
|
|
91
|
+
typeof obj.description === "string"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Load cron jobs from a JSON file, filtering for selftune entries. */
|
|
96
|
+
export function loadCronJobs(jobsPath: string): CronJobConfig[] {
|
|
97
|
+
if (!existsSync(jobsPath)) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const raw = readFileSync(jobsPath, "utf-8");
|
|
102
|
+
const data = JSON.parse(raw);
|
|
103
|
+
if (!Array.isArray(data)) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
return data.filter((j: unknown) => isCronJobConfig(j) && j.name.startsWith("selftune-"));
|
|
107
|
+
} catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Subcommands
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
/** Register default cron jobs with OpenClaw. */
|
|
117
|
+
export async function setupCronJobs(tz: string, dryRun: boolean): Promise<void> {
|
|
118
|
+
const openclawPath = Bun.which("openclaw");
|
|
119
|
+
if (!dryRun && !openclawPath) {
|
|
120
|
+
console.error("Error: openclaw is not installed or not in PATH.");
|
|
121
|
+
console.error("");
|
|
122
|
+
console.error("Install OpenClaw:");
|
|
123
|
+
console.error(" https://openclaw.dev/install");
|
|
124
|
+
console.error("");
|
|
125
|
+
console.error("Or ensure the openclaw binary is in your PATH.");
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log(`Registering ${DEFAULT_CRON_JOBS.length} cron jobs (tz=${tz})...\n`);
|
|
130
|
+
|
|
131
|
+
for (const job of DEFAULT_CRON_JOBS) {
|
|
132
|
+
const args = buildCronAddArgs(job, tz);
|
|
133
|
+
|
|
134
|
+
if (dryRun) {
|
|
135
|
+
console.log(`[DRY RUN] openclaw ${args.join(" ")}`);
|
|
136
|
+
} else {
|
|
137
|
+
const proc = Bun.spawn(["openclaw", ...args], {
|
|
138
|
+
stdout: "inherit",
|
|
139
|
+
stderr: "inherit",
|
|
140
|
+
});
|
|
141
|
+
const exitCode = await proc.exited;
|
|
142
|
+
if (exitCode !== 0) {
|
|
143
|
+
console.error(
|
|
144
|
+
`Error: openclaw cron add failed for "${job.name}" with exit code ${exitCode}`,
|
|
145
|
+
);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
console.log(` Registered: ${job.name} — ${job.description}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log("\nDone.");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Show registered selftune cron jobs. */
|
|
156
|
+
export function listCronJobs(): void {
|
|
157
|
+
const jobsPath = getOpenClawJobsPath();
|
|
158
|
+
const jobs = loadCronJobs(jobsPath);
|
|
159
|
+
|
|
160
|
+
if (jobs.length === 0) {
|
|
161
|
+
if (!existsSync(jobsPath)) {
|
|
162
|
+
console.log("No cron jobs file found at:", jobsPath);
|
|
163
|
+
} else {
|
|
164
|
+
console.log("No selftune cron jobs registered.");
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Print as formatted table
|
|
170
|
+
const nameWidth = Math.max(20, ...jobs.map((j) => j.name.length));
|
|
171
|
+
const cronWidth = Math.max(16, ...jobs.map((j) => j.cron.length));
|
|
172
|
+
|
|
173
|
+
console.log(`${"NAME".padEnd(nameWidth)} ${"SCHEDULE".padEnd(cronWidth)} DESCRIPTION`);
|
|
174
|
+
console.log(`${"─".repeat(nameWidth)} ${"─".repeat(cronWidth)} ${"─".repeat(40)}`);
|
|
175
|
+
|
|
176
|
+
for (const job of jobs) {
|
|
177
|
+
console.log(`${job.name.padEnd(nameWidth)} ${job.cron.padEnd(cronWidth)} ${job.description}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Remove all selftune cron jobs from OpenClaw. */
|
|
182
|
+
export async function removeCronJobs(dryRun: boolean): Promise<void> {
|
|
183
|
+
const jobsPath = getOpenClawJobsPath();
|
|
184
|
+
const jobs = loadCronJobs(jobsPath);
|
|
185
|
+
|
|
186
|
+
if (jobs.length === 0) {
|
|
187
|
+
console.log("No selftune cron jobs to remove.");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.log(`Removing ${jobs.length} selftune cron jobs...\n`);
|
|
192
|
+
|
|
193
|
+
for (const job of jobs) {
|
|
194
|
+
if (dryRun) {
|
|
195
|
+
console.log(`[DRY RUN] openclaw cron remove --name ${job.name}`);
|
|
196
|
+
} else {
|
|
197
|
+
const proc = Bun.spawn(["openclaw", "cron", "remove", "--name", job.name], {
|
|
198
|
+
stdout: "inherit",
|
|
199
|
+
stderr: "inherit",
|
|
200
|
+
});
|
|
201
|
+
const exitCode = await proc.exited;
|
|
202
|
+
if (exitCode !== 0) {
|
|
203
|
+
console.error(
|
|
204
|
+
`Error: openclaw cron remove failed for "${job.name}" with exit code ${exitCode}`,
|
|
205
|
+
);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
console.log(` Removed: ${job.name}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
console.log("\nDone.");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// CLI entry point
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
export async function cliMain(): Promise<void> {
|
|
220
|
+
const subcommand = process.argv[2];
|
|
221
|
+
|
|
222
|
+
const { values } = parseArgs({
|
|
223
|
+
options: {
|
|
224
|
+
"dry-run": { type: "boolean", default: false },
|
|
225
|
+
tz: { type: "string" },
|
|
226
|
+
},
|
|
227
|
+
strict: false,
|
|
228
|
+
allowPositionals: true,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Get timezone: flag > env > system default
|
|
232
|
+
const tz = values.tz ?? process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
233
|
+
|
|
234
|
+
switch (subcommand) {
|
|
235
|
+
case "setup":
|
|
236
|
+
await setupCronJobs(tz, values["dry-run"] ?? false);
|
|
237
|
+
break;
|
|
238
|
+
case "list":
|
|
239
|
+
listCronJobs();
|
|
240
|
+
break;
|
|
241
|
+
case "remove":
|
|
242
|
+
await removeCronJobs(values["dry-run"] ?? false);
|
|
243
|
+
break;
|
|
244
|
+
default:
|
|
245
|
+
console.log(`selftune cron — OpenClaw cron integration
|
|
246
|
+
|
|
247
|
+
Registers selftune automation jobs with OpenClaw's Gateway Scheduler.
|
|
248
|
+
This is an optional convenience for OpenClaw users. For generic scheduling
|
|
249
|
+
with system cron, launchd, or systemd, see: selftune schedule
|
|
250
|
+
|
|
251
|
+
Usage:
|
|
252
|
+
selftune cron setup [--dry-run] [--tz <timezone>]
|
|
253
|
+
selftune cron list
|
|
254
|
+
selftune cron remove [--dry-run]
|
|
255
|
+
|
|
256
|
+
Subcommands:
|
|
257
|
+
setup Register default selftune cron jobs with OpenClaw
|
|
258
|
+
list Show registered selftune cron jobs
|
|
259
|
+
remove Remove all selftune cron jobs`);
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (import.meta.main) {
|
|
265
|
+
await cliMain();
|
|
266
|
+
}
|