selftune 0.2.21 → 0.2.23
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/README.md +15 -8
- package/apps/local-dashboard/dist/assets/index-CwOtTrUS.css +1 -0
- package/apps/local-dashboard/dist/assets/index-f1HQpbeH.js +59 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-jVSaIZey.js +12 -0
- package/apps/local-dashboard/dist/index.html +3 -3
- package/cli/selftune/adapters/cline/hook.ts +167 -0
- package/cli/selftune/adapters/cline/install.ts +197 -0
- package/cli/selftune/adapters/codex/hook.ts +296 -0
- package/cli/selftune/adapters/codex/install.ts +289 -0
- package/cli/selftune/adapters/opencode/hook.ts +222 -0
- package/cli/selftune/adapters/opencode/install.ts +543 -0
- package/cli/selftune/adapters/pi/hook.ts +273 -0
- package/cli/selftune/adapters/pi/install.ts +207 -0
- package/cli/selftune/constants.ts +10 -1
- package/cli/selftune/dashboard-contract.ts +14 -0
- package/cli/selftune/evolution/engines/judge-engine.ts +96 -0
- package/cli/selftune/evolution/engines/replay-engine.ts +158 -0
- package/cli/selftune/evolution/evidence.ts +2 -6
- package/cli/selftune/evolution/evolve-body.ts +73 -20
- package/cli/selftune/evolution/validate-body.ts +78 -42
- package/cli/selftune/evolution/validate-routing.ts +45 -104
- package/cli/selftune/hooks/auto-activate.ts +43 -37
- package/cli/selftune/hooks/skill-eval.ts +2 -1
- package/cli/selftune/hooks-shared/git-metadata.ts +149 -0
- package/cli/selftune/hooks-shared/hook-output.ts +105 -0
- package/cli/selftune/hooks-shared/normalize.ts +196 -0
- package/cli/selftune/hooks-shared/session-state.ts +76 -0
- package/cli/selftune/hooks-shared/skill-paths.ts +50 -0
- package/cli/selftune/hooks-shared/stdin-dispatch.ts +59 -0
- package/cli/selftune/hooks-shared/types.ts +91 -0
- package/cli/selftune/index.ts +76 -6
- package/cli/selftune/ingestors/pi-ingest.ts +726 -0
- package/cli/selftune/init.ts +11 -1
- package/cli/selftune/localdb/direct-write.ts +85 -0
- package/cli/selftune/localdb/materialize.ts +6 -7
- package/cli/selftune/localdb/queries.ts +126 -0
- package/cli/selftune/localdb/schema.ts +38 -0
- package/cli/selftune/observability.ts +8 -1
- package/cli/selftune/orchestrate.ts +43 -0
- package/cli/selftune/registry/client.ts +74 -0
- package/cli/selftune/registry/history.ts +54 -0
- package/cli/selftune/registry/index.ts +90 -0
- package/cli/selftune/registry/install.ts +141 -0
- package/cli/selftune/registry/list.ts +44 -0
- package/cli/selftune/registry/push.ts +171 -0
- package/cli/selftune/registry/rollback.ts +49 -0
- package/cli/selftune/registry/status.ts +62 -0
- package/cli/selftune/registry/sync.ts +125 -0
- package/cli/selftune/repair/skill-usage.ts +4 -1
- package/cli/selftune/status.ts +31 -0
- package/cli/selftune/sync.ts +127 -23
- package/cli/selftune/types.ts +2 -1
- package/cli/selftune/utils/jsonl.ts +1 -30
- package/cli/selftune/utils/llm-call.ts +99 -34
- package/cli/selftune/utils/skill-discovery.ts +22 -0
- package/node_modules/@selftune/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/fixtures/golden.test.ts +0 -1
- package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/package.json +1 -1
- package/node_modules/@selftune/telemetry-contract/src/index.ts +1 -0
- package/node_modules/@selftune/telemetry-contract/src/schemas.ts +22 -4
- package/node_modules/@selftune/telemetry-contract/src/types.ts +1 -12
- package/node_modules/@selftune/telemetry-contract/tests/compatibility.test.ts +0 -1
- package/package.json +1 -1
- package/packages/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
- package/packages/telemetry-contract/fixtures/golden.test.ts +0 -1
- package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
- package/packages/telemetry-contract/package.json +1 -1
- package/packages/telemetry-contract/src/index.ts +1 -0
- package/packages/telemetry-contract/src/schemas.ts +22 -4
- package/packages/telemetry-contract/src/types.ts +1 -12
- package/packages/telemetry-contract/tests/compatibility.test.ts +0 -1
- package/packages/ui/AGENTS.md +16 -0
- package/packages/ui/README.md +1 -1
- package/packages/ui/package.json +1 -1
- package/packages/ui/src/components/ActivityTimeline.tsx +152 -168
- package/packages/ui/src/components/AnalyticsCharts.tsx +344 -0
- package/packages/ui/src/components/EvidenceViewer.tsx +153 -443
- package/packages/ui/src/components/EvolutionTimeline.tsx +34 -87
- package/packages/ui/src/components/InfoTip.tsx +1 -2
- package/packages/ui/src/components/InvocationsPanel.tsx +413 -0
- package/packages/ui/src/components/JobHistoryTimeline.tsx +156 -0
- package/packages/ui/src/components/OrchestrateRunsPanel.tsx +18 -36
- package/packages/ui/src/components/OverviewPanels.tsx +652 -0
- package/packages/ui/src/components/PipelineStatusBar.tsx +65 -0
- package/packages/ui/src/components/SkillReportGuide.tsx +215 -0
- package/packages/ui/src/components/SkillReportPanels.tsx +919 -0
- package/packages/ui/src/components/SkillsLibrary.tsx +437 -0
- package/packages/ui/src/components/index.ts +56 -1
- package/packages/ui/src/components/section-cards.tsx +18 -35
- package/packages/ui/src/components/skill-health-grid.tsx +47 -37
- package/packages/ui/src/lib/constants.tsx +0 -1
- package/packages/ui/src/primitives/card.tsx +1 -1
- package/packages/ui/src/primitives/checkbox.tsx +1 -1
- package/packages/ui/src/primitives/dropdown-menu.tsx +2 -2
- package/packages/ui/src/primitives/select.tsx +2 -2
- package/packages/ui/src/types.ts +172 -4
- package/skill/SKILL.md +26 -2
- package/skill/Workflows/Ingest.md +60 -2
- package/skill/Workflows/Initialize.md +54 -9
- package/skill/Workflows/PlatformHooks.md +109 -0
- package/skill/Workflows/Registry.md +99 -0
- package/skill/Workflows/Sync.md +3 -1
- package/apps/local-dashboard/dist/assets/index-D8O-RG1I.js +0 -60
- package/apps/local-dashboard/dist/assets/index-_EcLywDg.css +0 -1
- package/apps/local-dashboard/dist/assets/vendor-ui-CGEmUayx.js +0 -12
- package/cli/selftune/utils/html.ts +0 -27
- package/packages/ui/src/components/RecentActivityFeed.tsx +0 -117
package/cli/selftune/init.ts
CHANGED
|
@@ -75,13 +75,16 @@ class InitCliError extends Error {
|
|
|
75
75
|
* 1. Claude Code — ~/.claude/ directory exists AND (`which claude` OR env signals)
|
|
76
76
|
* 2. Codex — $CODEX_HOME set OR `which codex`
|
|
77
77
|
* 3. OpenCode — ~/.local/share/opencode/opencode.db exists OR `which opencode`
|
|
78
|
-
* 4.
|
|
78
|
+
* 4. OpenClaw — ~/.openclaw/agents/ exists OR `which openclaw`
|
|
79
|
+
* 5. Pi — ~/.pi/agent/ exists OR `which pi`
|
|
80
|
+
* 6. "unknown" fallback
|
|
79
81
|
*/
|
|
80
82
|
const VALID_AGENT_TYPES: SelftuneConfig["agent_type"][] = [
|
|
81
83
|
"claude_code",
|
|
82
84
|
"codex",
|
|
83
85
|
"opencode",
|
|
84
86
|
"openclaw",
|
|
87
|
+
"pi",
|
|
85
88
|
"unknown",
|
|
86
89
|
];
|
|
87
90
|
|
|
@@ -90,6 +93,7 @@ const AGENT_TYPE_CLI_MAP: Record<string, string> = {
|
|
|
90
93
|
codex: "codex",
|
|
91
94
|
opencode: "opencode",
|
|
92
95
|
openclaw: "openclaw",
|
|
96
|
+
pi: "pi",
|
|
93
97
|
};
|
|
94
98
|
|
|
95
99
|
function agentTypeToCli(agentType: string): string | null {
|
|
@@ -134,6 +138,12 @@ export function detectAgentType(
|
|
|
134
138
|
return "openclaw";
|
|
135
139
|
}
|
|
136
140
|
|
|
141
|
+
// Pi: .pi directory or binary
|
|
142
|
+
const piDir = join(home, ".pi", "agent");
|
|
143
|
+
if (existsSync(piDir) || Bun.which("pi")) {
|
|
144
|
+
return "pi";
|
|
145
|
+
}
|
|
146
|
+
|
|
137
147
|
return "unknown";
|
|
138
148
|
}
|
|
139
149
|
|
|
@@ -500,6 +500,91 @@ export function writeCommitTracking(record: {
|
|
|
500
500
|
});
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
+
// -- Cron run audit writer -----------------------------------------------------
|
|
504
|
+
|
|
505
|
+
export function writeCronRunToDb(
|
|
506
|
+
db: Database,
|
|
507
|
+
entry: {
|
|
508
|
+
jobName: string;
|
|
509
|
+
startedAt: string;
|
|
510
|
+
elapsedMs: number;
|
|
511
|
+
status: "success" | "error";
|
|
512
|
+
metrics?: Record<string, unknown>;
|
|
513
|
+
error?: string;
|
|
514
|
+
},
|
|
515
|
+
): void {
|
|
516
|
+
try {
|
|
517
|
+
getStmt(
|
|
518
|
+
db,
|
|
519
|
+
"cron-run",
|
|
520
|
+
`
|
|
521
|
+
INSERT OR IGNORE INTO cron_runs
|
|
522
|
+
(job_name, started_at, elapsed_ms, status, metrics_json, error)
|
|
523
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
524
|
+
`,
|
|
525
|
+
).run(
|
|
526
|
+
entry.jobName,
|
|
527
|
+
entry.startedAt,
|
|
528
|
+
entry.elapsedMs,
|
|
529
|
+
entry.status,
|
|
530
|
+
entry.metrics ? JSON.stringify(entry.metrics) : null,
|
|
531
|
+
entry.error ?? null,
|
|
532
|
+
);
|
|
533
|
+
} catch {
|
|
534
|
+
/* fail-open: never throw from audit logging */
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// -- Replay entry results writer -----------------------------------------------
|
|
539
|
+
|
|
540
|
+
export interface ReplayEntryResultInput {
|
|
541
|
+
proposal_id: string;
|
|
542
|
+
skill_name: string;
|
|
543
|
+
validation_mode: string;
|
|
544
|
+
phase: string;
|
|
545
|
+
query: string;
|
|
546
|
+
should_trigger: boolean;
|
|
547
|
+
triggered: boolean;
|
|
548
|
+
passed: boolean;
|
|
549
|
+
evidence?: string;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function writeReplayEntryResultsToDb(results: ReplayEntryResultInput[]): boolean {
|
|
553
|
+
if (results.length === 0) return true;
|
|
554
|
+
return safeWrite("replay-entry-results", (db) => {
|
|
555
|
+
db.run("BEGIN TRANSACTION");
|
|
556
|
+
try {
|
|
557
|
+
const stmt = getStmt(
|
|
558
|
+
db,
|
|
559
|
+
"replay-entry-result",
|
|
560
|
+
`
|
|
561
|
+
INSERT INTO replay_entry_results
|
|
562
|
+
(proposal_id, skill_name, validation_mode, phase, query,
|
|
563
|
+
should_trigger, triggered, passed, evidence)
|
|
564
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
565
|
+
`,
|
|
566
|
+
);
|
|
567
|
+
for (const r of results) {
|
|
568
|
+
stmt.run(
|
|
569
|
+
r.proposal_id,
|
|
570
|
+
r.skill_name,
|
|
571
|
+
r.validation_mode,
|
|
572
|
+
r.phase,
|
|
573
|
+
r.query,
|
|
574
|
+
r.should_trigger ? 1 : 0,
|
|
575
|
+
r.triggered ? 1 : 0,
|
|
576
|
+
r.passed ? 1 : 0,
|
|
577
|
+
r.evidence ?? null,
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
db.run("COMMIT");
|
|
581
|
+
} catch (err) {
|
|
582
|
+
db.run("ROLLBACK");
|
|
583
|
+
throw err;
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
503
588
|
// -- Internal insert helpers (used by cached statements) ----------------------
|
|
504
589
|
|
|
505
590
|
function insertSession(db: Database, s: CanonicalSessionRecord): void {
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Materializer: reads JSONL
|
|
2
|
+
* Materializer: reads legacy/exported JSONL files and inserts structured
|
|
3
3
|
* records into the local SQLite database.
|
|
4
4
|
*
|
|
5
|
+
* IMPORTANT: SQLite is the sole write target (Phase 3 complete). JSONL files
|
|
6
|
+
* on disk contain only pre-cutover history. This materializer is ONLY used for:
|
|
7
|
+
* 1. Recovery from selftune export JSONL snapshots
|
|
8
|
+
* 2. Backfill of pre-cutover JSONL data into a fresh SQLite DB
|
|
9
|
+
*
|
|
5
10
|
* Supports two modes:
|
|
6
11
|
* - Full rebuild: drops all data and re-inserts from scratch
|
|
7
12
|
* - Incremental: only inserts records newer than last materialization
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
|
-
// NOTE: With dual-write active (Phase 1+), hooks insert directly into SQLite.
|
|
11
|
-
// The materializer is only needed for:
|
|
12
|
-
// 1. Initial startup (to catch pre-existing JSONL data from before dual-write)
|
|
13
|
-
// 2. Manual recovery after exporting JSONL and recreating the DB file
|
|
14
|
-
// 3. Backfill from batch ingestors that don't yet dual-write
|
|
15
|
-
|
|
16
15
|
import type { Database } from "bun:sqlite";
|
|
17
16
|
|
|
18
17
|
import {
|
|
@@ -2142,6 +2142,132 @@ export function getRecentDecisions(db: Database, limit = 20): AutonomousDecision
|
|
|
2142
2142
|
|
|
2143
2143
|
// -- Helpers ------------------------------------------------------------------
|
|
2144
2144
|
|
|
2145
|
+
// -- Replay entry result queries -----------------------------------------------
|
|
2146
|
+
|
|
2147
|
+
export function queryReplayEntryResults(
|
|
2148
|
+
db: Database,
|
|
2149
|
+
proposalId: string,
|
|
2150
|
+
phase?: string,
|
|
2151
|
+
): Array<{
|
|
2152
|
+
id: number;
|
|
2153
|
+
proposal_id: string;
|
|
2154
|
+
skill_name: string;
|
|
2155
|
+
validation_mode: string;
|
|
2156
|
+
phase: string;
|
|
2157
|
+
query: string;
|
|
2158
|
+
should_trigger: boolean;
|
|
2159
|
+
triggered: boolean;
|
|
2160
|
+
passed: boolean;
|
|
2161
|
+
evidence: string | null;
|
|
2162
|
+
}> {
|
|
2163
|
+
const sql = phase
|
|
2164
|
+
? `SELECT id, proposal_id, skill_name, validation_mode, phase, query,
|
|
2165
|
+
should_trigger, triggered, passed, evidence
|
|
2166
|
+
FROM replay_entry_results
|
|
2167
|
+
WHERE proposal_id = ? AND phase = ?
|
|
2168
|
+
ORDER BY id`
|
|
2169
|
+
: `SELECT id, proposal_id, skill_name, validation_mode, phase, query,
|
|
2170
|
+
should_trigger, triggered, passed, evidence
|
|
2171
|
+
FROM replay_entry_results
|
|
2172
|
+
WHERE proposal_id = ?
|
|
2173
|
+
ORDER BY id`;
|
|
2174
|
+
|
|
2175
|
+
const rows = phase
|
|
2176
|
+
? (db.query(sql).all(proposalId, phase) as Array<Record<string, unknown>>)
|
|
2177
|
+
: (db.query(sql).all(proposalId) as Array<Record<string, unknown>>);
|
|
2178
|
+
|
|
2179
|
+
return rows.map((r) => ({
|
|
2180
|
+
id: r.id as number,
|
|
2181
|
+
proposal_id: r.proposal_id as string,
|
|
2182
|
+
skill_name: r.skill_name as string,
|
|
2183
|
+
validation_mode: r.validation_mode as string,
|
|
2184
|
+
phase: r.phase as string,
|
|
2185
|
+
query: r.query as string,
|
|
2186
|
+
should_trigger: (r.should_trigger as number) === 1,
|
|
2187
|
+
triggered: (r.triggered as number) === 1,
|
|
2188
|
+
passed: (r.passed as number) === 1,
|
|
2189
|
+
evidence: r.evidence as string | null,
|
|
2190
|
+
}));
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
/**
|
|
2194
|
+
* Find regressions: entries that passed in the "before" phase but failed in the "after" phase.
|
|
2195
|
+
*/
|
|
2196
|
+
export function queryReplayRegressions(
|
|
2197
|
+
db: Database,
|
|
2198
|
+
proposalId: string,
|
|
2199
|
+
): Array<{
|
|
2200
|
+
query: string;
|
|
2201
|
+
skill_name: string;
|
|
2202
|
+
before_passed: boolean;
|
|
2203
|
+
after_passed: boolean;
|
|
2204
|
+
}> {
|
|
2205
|
+
const rows = db
|
|
2206
|
+
.query(
|
|
2207
|
+
`SELECT b.query, b.skill_name,
|
|
2208
|
+
b.passed AS before_passed,
|
|
2209
|
+
a.passed AS after_passed
|
|
2210
|
+
FROM replay_entry_results b
|
|
2211
|
+
JOIN replay_entry_results a
|
|
2212
|
+
ON b.proposal_id = a.proposal_id
|
|
2213
|
+
AND b.query = a.query
|
|
2214
|
+
AND b.skill_name = a.skill_name
|
|
2215
|
+
WHERE b.proposal_id = ?
|
|
2216
|
+
AND b.phase = 'before'
|
|
2217
|
+
AND a.phase = 'after'
|
|
2218
|
+
AND b.passed = 1
|
|
2219
|
+
AND a.passed = 0
|
|
2220
|
+
ORDER BY b.query`,
|
|
2221
|
+
)
|
|
2222
|
+
.all(proposalId) as Array<Record<string, unknown>>;
|
|
2223
|
+
|
|
2224
|
+
return rows.map((r) => ({
|
|
2225
|
+
query: r.query as string,
|
|
2226
|
+
skill_name: r.skill_name as string,
|
|
2227
|
+
before_passed: (r.before_passed as number) === 1,
|
|
2228
|
+
after_passed: (r.after_passed as number) === 1,
|
|
2229
|
+
}));
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// -- JSON parse helpers -------------------------------------------------------
|
|
2233
|
+
|
|
2234
|
+
// -- Cron run queries ---------------------------------------------------------
|
|
2235
|
+
|
|
2236
|
+
export interface CronRun {
|
|
2237
|
+
id: number;
|
|
2238
|
+
job_name: string;
|
|
2239
|
+
started_at: string;
|
|
2240
|
+
elapsed_ms: number;
|
|
2241
|
+
status: string;
|
|
2242
|
+
metrics_json: string | null;
|
|
2243
|
+
error: string | null;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
export function getRecentCronRuns(db: Database, limit = 50): CronRun[] {
|
|
2247
|
+
return db
|
|
2248
|
+
.query(
|
|
2249
|
+
`SELECT id, job_name, started_at, elapsed_ms, status, metrics_json, error
|
|
2250
|
+
FROM cron_runs
|
|
2251
|
+
ORDER BY started_at DESC
|
|
2252
|
+
LIMIT ?`,
|
|
2253
|
+
)
|
|
2254
|
+
.all(limit) as CronRun[];
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
export function getCronRunsByJob(db: Database, jobName: string, limit = 50): CronRun[] {
|
|
2258
|
+
return db
|
|
2259
|
+
.query(
|
|
2260
|
+
`SELECT id, job_name, started_at, elapsed_ms, status, metrics_json, error
|
|
2261
|
+
FROM cron_runs
|
|
2262
|
+
WHERE job_name = ?
|
|
2263
|
+
ORDER BY started_at DESC
|
|
2264
|
+
LIMIT ?`,
|
|
2265
|
+
)
|
|
2266
|
+
.all(jobName, limit) as CronRun[];
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
// -- JSON parsing helpers -----------------------------------------------------
|
|
2270
|
+
|
|
2145
2271
|
export function safeParseJsonArray<T = string>(json: string | null): T[] {
|
|
2146
2272
|
if (!json) return [];
|
|
2147
2273
|
try {
|
|
@@ -129,6 +129,22 @@ CREATE TABLE IF NOT EXISTS evolution_audit (
|
|
|
129
129
|
validation_evidence_ref TEXT
|
|
130
130
|
)`;
|
|
131
131
|
|
|
132
|
+
// -- Replay entry results (per-entry validation outcomes) ---------------------
|
|
133
|
+
|
|
134
|
+
export const CREATE_REPLAY_ENTRY_RESULTS = `
|
|
135
|
+
CREATE TABLE IF NOT EXISTS replay_entry_results (
|
|
136
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
137
|
+
proposal_id TEXT NOT NULL,
|
|
138
|
+
skill_name TEXT NOT NULL,
|
|
139
|
+
validation_mode TEXT NOT NULL,
|
|
140
|
+
phase TEXT NOT NULL,
|
|
141
|
+
query TEXT NOT NULL,
|
|
142
|
+
should_trigger INTEGER NOT NULL,
|
|
143
|
+
triggered INTEGER NOT NULL,
|
|
144
|
+
passed INTEGER NOT NULL,
|
|
145
|
+
evidence TEXT
|
|
146
|
+
)`;
|
|
147
|
+
|
|
132
148
|
// -- Local telemetry tables (from JSONL logs) ---------------------------------
|
|
133
149
|
|
|
134
150
|
export const CREATE_SESSION_TELEMETRY = `
|
|
@@ -294,6 +310,20 @@ CREATE TABLE IF NOT EXISTS commit_tracking (
|
|
|
294
310
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
295
311
|
)`;
|
|
296
312
|
|
|
313
|
+
// -- Cron run audit log -------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
export const CREATE_CRON_RUNS = `
|
|
316
|
+
CREATE TABLE IF NOT EXISTS cron_runs (
|
|
317
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
318
|
+
job_name TEXT NOT NULL,
|
|
319
|
+
started_at TEXT NOT NULL,
|
|
320
|
+
elapsed_ms INTEGER NOT NULL,
|
|
321
|
+
status TEXT NOT NULL,
|
|
322
|
+
metrics_json TEXT,
|
|
323
|
+
error TEXT,
|
|
324
|
+
UNIQUE(job_name, started_at)
|
|
325
|
+
)`;
|
|
326
|
+
|
|
297
327
|
// -- Metadata table -----------------------------------------------------------
|
|
298
328
|
|
|
299
329
|
export const CREATE_META = `
|
|
@@ -355,11 +385,17 @@ export const CREATE_INDEXES = [
|
|
|
355
385
|
`CREATE INDEX IF NOT EXISTS idx_staging_kind ON canonical_upload_staging(record_kind)`,
|
|
356
386
|
`CREATE INDEX IF NOT EXISTS idx_staging_session ON canonical_upload_staging(session_id)`,
|
|
357
387
|
`CREATE UNIQUE INDEX IF NOT EXISTS idx_staging_dedup ON canonical_upload_staging(record_kind, record_id)`,
|
|
388
|
+
// -- Replay entry result indexes ---------------------------------------------
|
|
389
|
+
`CREATE INDEX IF NOT EXISTS idx_replay_entry_proposal ON replay_entry_results(proposal_id)`,
|
|
390
|
+
`CREATE INDEX IF NOT EXISTS idx_replay_entry_skill ON replay_entry_results(skill_name)`,
|
|
391
|
+
`CREATE INDEX IF NOT EXISTS idx_replay_entry_passed ON replay_entry_results(passed)`,
|
|
358
392
|
// -- Commit tracking indexes ------------------------------------------------
|
|
359
393
|
`CREATE INDEX IF NOT EXISTS idx_commit_sha ON commit_tracking(commit_sha)`,
|
|
360
394
|
`CREATE INDEX IF NOT EXISTS idx_commit_session ON commit_tracking(session_id)`,
|
|
361
395
|
`CREATE INDEX IF NOT EXISTS idx_commit_ts ON commit_tracking(timestamp)`,
|
|
362
396
|
`CREATE UNIQUE INDEX IF NOT EXISTS idx_commit_dedup ON commit_tracking(session_id, commit_sha)`,
|
|
397
|
+
// -- Cron run indexes -------------------------------------------------------
|
|
398
|
+
`CREATE INDEX IF NOT EXISTS idx_cron_runs_job_ts ON cron_runs(job_name, started_at)`,
|
|
363
399
|
];
|
|
364
400
|
|
|
365
401
|
/**
|
|
@@ -443,6 +479,7 @@ export const ALL_DDL = [
|
|
|
443
479
|
CREATE_EXECUTION_FACTS,
|
|
444
480
|
CREATE_EVOLUTION_EVIDENCE,
|
|
445
481
|
CREATE_EVOLUTION_AUDIT,
|
|
482
|
+
CREATE_REPLAY_ENTRY_RESULTS,
|
|
446
483
|
CREATE_SESSION_TELEMETRY,
|
|
447
484
|
CREATE_SKILL_USAGE,
|
|
448
485
|
CREATE_ORCHESTRATE_RUNS,
|
|
@@ -454,6 +491,7 @@ export const ALL_DDL = [
|
|
|
454
491
|
CREATE_UPLOAD_WATERMARKS,
|
|
455
492
|
CREATE_CANONICAL_UPLOAD_STAGING,
|
|
456
493
|
CREATE_COMMIT_TRACKING,
|
|
494
|
+
CREATE_CRON_RUNS,
|
|
457
495
|
CREATE_META,
|
|
458
496
|
...CREATE_INDEXES,
|
|
459
497
|
];
|
|
@@ -26,7 +26,14 @@ import type {
|
|
|
26
26
|
} from "./types.js";
|
|
27
27
|
import { missingClaudeCodeHookKeys } from "./utils/hooks.js";
|
|
28
28
|
|
|
29
|
-
const VALID_AGENT_TYPES = new Set([
|
|
29
|
+
const VALID_AGENT_TYPES = new Set([
|
|
30
|
+
"claude_code",
|
|
31
|
+
"codex",
|
|
32
|
+
"opencode",
|
|
33
|
+
"openclaw",
|
|
34
|
+
"pi",
|
|
35
|
+
"unknown",
|
|
36
|
+
]);
|
|
30
37
|
const VALID_LLM_MODES = new Set(["agent"]);
|
|
31
38
|
|
|
32
39
|
const LOG_FILES: Record<string, string> = {
|
|
@@ -29,6 +29,7 @@ import { readGradingResultsForSkill } from "./grading/results.js";
|
|
|
29
29
|
import { getDb } from "./localdb/db.js";
|
|
30
30
|
import {
|
|
31
31
|
updateSignalConsumed,
|
|
32
|
+
writeCronRunToDb,
|
|
32
33
|
writeGradingResultToDb,
|
|
33
34
|
writeOrchestrateRunToDb,
|
|
34
35
|
} from "./localdb/direct-write.js";
|
|
@@ -1183,6 +1184,33 @@ export async function orchestrate(
|
|
|
1183
1184
|
/* fail-open */
|
|
1184
1185
|
}
|
|
1185
1186
|
|
|
1187
|
+
// Also log to unified cron_runs timeline
|
|
1188
|
+
const totalLlmCalls = candidates.reduce(
|
|
1189
|
+
(sum, c) => sum + (c.evolveResult?.llmCallCount ?? 0),
|
|
1190
|
+
0,
|
|
1191
|
+
);
|
|
1192
|
+
try {
|
|
1193
|
+
writeCronRunToDb(getDb(), {
|
|
1194
|
+
jobName: "orchestrate",
|
|
1195
|
+
startedAt: runReport.timestamp,
|
|
1196
|
+
elapsedMs: runReport.elapsed_ms,
|
|
1197
|
+
status: "success",
|
|
1198
|
+
metrics: {
|
|
1199
|
+
total_skills: finalTotals.totalSkills,
|
|
1200
|
+
evaluated: finalTotals.evaluated,
|
|
1201
|
+
evolved: finalTotals.evolved,
|
|
1202
|
+
deployed: finalTotals.deployed,
|
|
1203
|
+
watched: finalTotals.watched,
|
|
1204
|
+
skipped: finalTotals.skipped,
|
|
1205
|
+
dry_run: result.summary.dryRun,
|
|
1206
|
+
total_llm_calls: totalLlmCalls,
|
|
1207
|
+
auto_graded: finalTotals.autoGraded,
|
|
1208
|
+
},
|
|
1209
|
+
});
|
|
1210
|
+
} catch {
|
|
1211
|
+
/* fail-open */
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1186
1214
|
// -------------------------------------------------------------------------
|
|
1187
1215
|
// Step 9: Alpha upload (fail-open — never blocks the orchestrate loop)
|
|
1188
1216
|
// -------------------------------------------------------------------------
|
|
@@ -1211,6 +1239,21 @@ export async function orchestrate(
|
|
|
1211
1239
|
}
|
|
1212
1240
|
|
|
1213
1241
|
return result;
|
|
1242
|
+
} catch (err) {
|
|
1243
|
+
// Log failed orchestrate run to unified cron_runs timeline
|
|
1244
|
+
const elapsedMs = Date.now() - startTime;
|
|
1245
|
+
try {
|
|
1246
|
+
writeCronRunToDb(getDb(), {
|
|
1247
|
+
jobName: "orchestrate",
|
|
1248
|
+
startedAt: new Date(startTime).toISOString(),
|
|
1249
|
+
elapsedMs,
|
|
1250
|
+
status: "error",
|
|
1251
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1252
|
+
});
|
|
1253
|
+
} catch {
|
|
1254
|
+
/* fail-open */
|
|
1255
|
+
}
|
|
1256
|
+
throw err;
|
|
1214
1257
|
} finally {
|
|
1215
1258
|
releaseLock();
|
|
1216
1259
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry HTTP client. Never throws — returns typed results.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getSelftuneVersion } from "../utils/selftune-meta.js";
|
|
6
|
+
|
|
7
|
+
export interface RegistryResult<T = unknown> {
|
|
8
|
+
success: boolean;
|
|
9
|
+
data?: T;
|
|
10
|
+
error?: string;
|
|
11
|
+
status?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getConfig(): { apiUrl: string; apiKey: string } | null {
|
|
15
|
+
try {
|
|
16
|
+
const configPath = `${process.env.HOME}/.selftune/config.json`;
|
|
17
|
+
const raw = require("fs").readFileSync(configPath, "utf-8");
|
|
18
|
+
const config = JSON.parse(raw);
|
|
19
|
+
const apiUrl = config?.alpha?.cloud_api_url || "https://api.selftune.dev";
|
|
20
|
+
const apiKey = config?.alpha?.api_key;
|
|
21
|
+
if (!apiKey) return null;
|
|
22
|
+
return { apiUrl, apiKey };
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function registryRequest<T>(
|
|
29
|
+
method: string,
|
|
30
|
+
path: string,
|
|
31
|
+
opts?: { body?: unknown; formData?: FormData },
|
|
32
|
+
): Promise<RegistryResult<T>> {
|
|
33
|
+
const config = getConfig();
|
|
34
|
+
if (!config) {
|
|
35
|
+
return { success: false, error: "Not authenticated. Run 'selftune alpha upload' to set up." };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const headers: Record<string, string> = {
|
|
40
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
41
|
+
"User-Agent": `selftune/${getSelftuneVersion()}`,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let fetchBody: BodyInit | undefined;
|
|
45
|
+
if (opts?.formData) {
|
|
46
|
+
fetchBody = opts.formData;
|
|
47
|
+
// Don't set Content-Type — fetch sets multipart boundary automatically
|
|
48
|
+
} else if (opts?.body) {
|
|
49
|
+
headers["Content-Type"] = "application/json";
|
|
50
|
+
fetchBody = JSON.stringify(opts.body);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const response = await fetch(`${config.apiUrl}/api/v1/registry${path}`, {
|
|
54
|
+
method,
|
|
55
|
+
headers,
|
|
56
|
+
body: fetchBody,
|
|
57
|
+
signal: AbortSignal.timeout(60_000),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const text = await response.text();
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
error: `HTTP ${response.status}: ${text.slice(0, 300)}`,
|
|
65
|
+
status: response.status,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const data = text ? JSON.parse(text) : {};
|
|
70
|
+
return { success: true, data: data as T, status: response.status };
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selftune registry history — Show version timeline for a skill.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { registryRequest } from "./client.js";
|
|
6
|
+
|
|
7
|
+
export async function cliMain() {
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const name = args.find((a) => !a.startsWith("--"));
|
|
10
|
+
|
|
11
|
+
if (!name) {
|
|
12
|
+
console.error(JSON.stringify({ error: "Usage: selftune registry history <name>" }));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const listResult = await registryRequest<{ entries: Array<{ id: string }> }>(
|
|
17
|
+
"GET",
|
|
18
|
+
`?name=${encodeURIComponent(name)}`,
|
|
19
|
+
);
|
|
20
|
+
if (!listResult.success || !listResult.data?.entries?.length) {
|
|
21
|
+
console.error(JSON.stringify({ error: `Skill '${name}' not found in registry` }));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const entryId = listResult.data.entries[0].id;
|
|
26
|
+
const result = await registryRequest<{
|
|
27
|
+
versions: Array<{
|
|
28
|
+
version: string;
|
|
29
|
+
is_current: boolean;
|
|
30
|
+
rolled_back: boolean;
|
|
31
|
+
aggregate_pass_rate: number | null;
|
|
32
|
+
aggregate_sessions: number;
|
|
33
|
+
change_summary: string | null;
|
|
34
|
+
pushed_at: string;
|
|
35
|
+
}>;
|
|
36
|
+
}>("GET", `/${entryId}/versions`);
|
|
37
|
+
|
|
38
|
+
if (!result.success) {
|
|
39
|
+
console.error(JSON.stringify({ error: result.error }));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const versions = result.data?.versions || [];
|
|
44
|
+
const timeline = versions.map((v) => ({
|
|
45
|
+
version: v.version,
|
|
46
|
+
status: v.is_current ? "current" : v.rolled_back ? "rolled_back" : "previous",
|
|
47
|
+
pass_rate: v.aggregate_pass_rate,
|
|
48
|
+
sessions: v.aggregate_sessions,
|
|
49
|
+
summary: v.change_summary,
|
|
50
|
+
pushed_at: v.pushed_at,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
console.log(JSON.stringify({ name, versions: timeline }));
|
|
54
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selftune registry — Team skill distribution.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* push Push current skill folder as a new version
|
|
6
|
+
* install Download and install a skill from the registry
|
|
7
|
+
* sync Check for updates and pull latest versions
|
|
8
|
+
* status Show installed entries and version drift
|
|
9
|
+
* rollback Rollback a skill to a previous version
|
|
10
|
+
* history Show version timeline for a skill
|
|
11
|
+
* list Show all published entries in the org
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { CLIError } from "../utils/cli-error.js";
|
|
15
|
+
|
|
16
|
+
const sub = process.argv[2];
|
|
17
|
+
|
|
18
|
+
export async function cliMain() {
|
|
19
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
20
|
+
console.log(`selftune registry — Team skill distribution
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
selftune registry <subcommand> [options]
|
|
24
|
+
|
|
25
|
+
Subcommands:
|
|
26
|
+
push [name] Push current skill folder as a new version
|
|
27
|
+
install <name> Download and install a skill from the registry
|
|
28
|
+
sync Check for updates and pull latest versions
|
|
29
|
+
status Show installed entries and version drift
|
|
30
|
+
rollback <name> Rollback to a previous version
|
|
31
|
+
history <name> Show version timeline
|
|
32
|
+
list Show all published entries
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--version=<semver> Set version explicitly (push)
|
|
36
|
+
--summary=<text> Change summary (push)
|
|
37
|
+
--global Install to ~/.claude/skills/ (install)
|
|
38
|
+
--to=<version> Target version (rollback)
|
|
39
|
+
--reason=<text> Rollback reason (rollback)
|
|
40
|
+
`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Strip 'registry' from argv so subcommands see the right args
|
|
45
|
+
process.argv = [process.argv[0], process.argv[1], ...process.argv.slice(3)];
|
|
46
|
+
|
|
47
|
+
switch (sub) {
|
|
48
|
+
case "push": {
|
|
49
|
+
const { cliMain } = await import("./push.js");
|
|
50
|
+
await cliMain();
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case "install": {
|
|
54
|
+
const { cliMain } = await import("./install.js");
|
|
55
|
+
await cliMain();
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "sync": {
|
|
59
|
+
const { cliMain } = await import("./sync.js");
|
|
60
|
+
await cliMain();
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case "status": {
|
|
64
|
+
const { cliMain } = await import("./status.js");
|
|
65
|
+
await cliMain();
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case "rollback": {
|
|
69
|
+
const { cliMain } = await import("./rollback.js");
|
|
70
|
+
await cliMain();
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case "history": {
|
|
74
|
+
const { cliMain } = await import("./history.js");
|
|
75
|
+
await cliMain();
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case "list": {
|
|
79
|
+
const { cliMain } = await import("./list.js");
|
|
80
|
+
await cliMain();
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
default:
|
|
84
|
+
throw new CLIError(
|
|
85
|
+
`Unknown registry subcommand: ${sub}`,
|
|
86
|
+
"UNKNOWN_COMMAND",
|
|
87
|
+
"selftune registry --help",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|