selftune 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +1 -0
  2. package/apps/local-dashboard/dist/assets/index-Bk9vSHHd.js +15 -0
  3. package/apps/local-dashboard/dist/assets/index-CRtLkBTi.css +1 -0
  4. package/apps/local-dashboard/dist/assets/vendor-react-BQH_6WrG.js +60 -0
  5. package/apps/local-dashboard/dist/assets/{vendor-table-B7VF2Ipl.js → vendor-table-dK1QMLq9.js} +1 -1
  6. package/apps/local-dashboard/dist/assets/{vendor-ui-r2k_Ku_V.js → vendor-ui-CO2mrx6e.js} +60 -65
  7. package/apps/local-dashboard/dist/index.html +5 -5
  8. package/cli/selftune/activation-rules.ts +30 -9
  9. package/cli/selftune/agent-guidance.ts +96 -0
  10. package/cli/selftune/alpha-identity.ts +157 -0
  11. package/cli/selftune/alpha-upload/build-payloads.ts +151 -0
  12. package/cli/selftune/alpha-upload/client.ts +113 -0
  13. package/cli/selftune/alpha-upload/flush.ts +191 -0
  14. package/cli/selftune/alpha-upload/index.ts +194 -0
  15. package/cli/selftune/alpha-upload/queue.ts +252 -0
  16. package/cli/selftune/alpha-upload/stage-canonical.ts +242 -0
  17. package/cli/selftune/alpha-upload-contract.ts +52 -0
  18. package/cli/selftune/auth/device-code.ts +110 -0
  19. package/cli/selftune/auto-update.ts +130 -0
  20. package/cli/selftune/badge/badge.ts +19 -9
  21. package/cli/selftune/canonical-export.ts +16 -3
  22. package/cli/selftune/constants.ts +28 -8
  23. package/cli/selftune/contribute/bundle.ts +32 -5
  24. package/cli/selftune/dashboard-contract.ts +32 -1
  25. package/cli/selftune/dashboard-server.ts +256 -692
  26. package/cli/selftune/dashboard.ts +1 -1
  27. package/cli/selftune/eval/baseline.ts +11 -7
  28. package/cli/selftune/eval/hooks-to-evals.ts +27 -9
  29. package/cli/selftune/eval/synthetic-evals.ts +54 -1
  30. package/cli/selftune/evolution/audit.ts +24 -19
  31. package/cli/selftune/evolution/constitutional.ts +176 -0
  32. package/cli/selftune/evolution/evidence.ts +18 -13
  33. package/cli/selftune/evolution/evolve-body.ts +104 -7
  34. package/cli/selftune/evolution/evolve.ts +195 -22
  35. package/cli/selftune/evolution/propose-body.ts +18 -1
  36. package/cli/selftune/evolution/propose-description.ts +27 -2
  37. package/cli/selftune/evolution/rollback.ts +11 -15
  38. package/cli/selftune/export.ts +84 -0
  39. package/cli/selftune/grading/auto-grade.ts +13 -4
  40. package/cli/selftune/grading/grade-session.ts +16 -6
  41. package/cli/selftune/hooks/evolution-guard.ts +26 -9
  42. package/cli/selftune/hooks/prompt-log.ts +23 -9
  43. package/cli/selftune/hooks/session-stop.ts +78 -15
  44. package/cli/selftune/hooks/skill-eval.ts +189 -10
  45. package/cli/selftune/index.ts +274 -2
  46. package/cli/selftune/ingestors/claude-replay.ts +48 -21
  47. package/cli/selftune/init.ts +249 -47
  48. package/cli/selftune/last.ts +7 -7
  49. package/cli/selftune/localdb/db.ts +90 -10
  50. package/cli/selftune/localdb/direct-write.ts +531 -0
  51. package/cli/selftune/localdb/materialize.ts +296 -42
  52. package/cli/selftune/localdb/queries.ts +325 -32
  53. package/cli/selftune/localdb/schema.ts +109 -0
  54. package/cli/selftune/monitoring/watch.ts +26 -8
  55. package/cli/selftune/normalization.ts +85 -15
  56. package/cli/selftune/observability.ts +248 -2
  57. package/cli/selftune/orchestrate.ts +165 -20
  58. package/cli/selftune/quickstart.ts +34 -10
  59. package/cli/selftune/repair/skill-usage.ts +12 -2
  60. package/cli/selftune/routes/actions.ts +77 -0
  61. package/cli/selftune/routes/badge.ts +66 -0
  62. package/cli/selftune/routes/doctor.ts +12 -0
  63. package/cli/selftune/routes/index.ts +14 -0
  64. package/cli/selftune/routes/orchestrate-runs.ts +13 -0
  65. package/cli/selftune/routes/overview.ts +14 -0
  66. package/cli/selftune/routes/report.ts +293 -0
  67. package/cli/selftune/routes/skill-report.ts +230 -0
  68. package/cli/selftune/status.ts +203 -7
  69. package/cli/selftune/sync.ts +13 -1
  70. package/cli/selftune/types.ts +50 -0
  71. package/cli/selftune/utils/jsonl.ts +58 -1
  72. package/cli/selftune/utils/selftune-meta.ts +38 -0
  73. package/cli/selftune/utils/skill-log.ts +30 -4
  74. package/cli/selftune/utils/transcript.ts +15 -0
  75. package/cli/selftune/workflows/workflows.ts +7 -6
  76. package/package.json +10 -6
  77. package/packages/telemetry-contract/fixtures/complete-push.ts +184 -0
  78. package/packages/telemetry-contract/fixtures/evidence-only-push.ts +58 -0
  79. package/packages/telemetry-contract/fixtures/golden.json +1 -0
  80. package/packages/telemetry-contract/fixtures/index.ts +4 -0
  81. package/packages/telemetry-contract/fixtures/partial-push-no-sessions.ts +40 -0
  82. package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +79 -0
  83. package/packages/telemetry-contract/package.json +6 -1
  84. package/packages/telemetry-contract/src/index.ts +1 -0
  85. package/packages/telemetry-contract/src/schemas.ts +215 -0
  86. package/packages/telemetry-contract/src/types.ts +3 -1
  87. package/packages/telemetry-contract/src/validators.ts +3 -1
  88. package/packages/telemetry-contract/tests/compatibility.test.ts +144 -0
  89. package/packages/ui/package.json +4 -0
  90. package/packages/ui/src/components/ActivityTimeline.tsx +61 -29
  91. package/packages/ui/src/components/section-cards.tsx +31 -14
  92. package/packages/ui/src/types.ts +1 -0
  93. package/skill/SKILL.md +214 -174
  94. package/skill/Workflows/AlphaUpload.md +45 -0
  95. package/skill/Workflows/Baseline.md +18 -12
  96. package/skill/Workflows/Composability.md +3 -3
  97. package/skill/Workflows/Dashboard.md +44 -91
  98. package/skill/Workflows/Doctor.md +93 -66
  99. package/skill/Workflows/Evals.md +49 -40
  100. package/skill/Workflows/Evolve.md +76 -28
  101. package/skill/Workflows/EvolveBody.md +37 -38
  102. package/skill/Workflows/Initialize.md +172 -26
  103. package/skill/Workflows/Orchestrate.md +11 -2
  104. package/skill/Workflows/Sync.md +23 -0
  105. package/skill/Workflows/Watch.md +2 -5
  106. package/skill/agents/diagnosis-analyst.md +163 -0
  107. package/skill/agents/evolution-reviewer.md +149 -0
  108. package/skill/agents/integration-guide.md +154 -0
  109. package/skill/agents/pattern-analyst.md +149 -0
  110. package/skill/assets/multi-skill-settings.json +1 -1
  111. package/skill/assets/single-skill-settings.json +1 -1
  112. package/skill/references/interactive-config.md +39 -0
  113. package/skill/references/invocation-taxonomy.md +34 -0
  114. package/skill/references/logs.md +9 -1
  115. package/skill/references/setup-patterns.md +3 -3
  116. package/skill/settings_snippet.json +1 -1
  117. package/apps/local-dashboard/dist/assets/index-C75H1Q3n.css +0 -1
  118. package/apps/local-dashboard/dist/assets/index-axE4kz3Q.js +0 -15
  119. package/apps/local-dashboard/dist/assets/vendor-react-U7zYD9Rg.js +0 -60
@@ -32,6 +32,11 @@ import {
32
32
  SKILL_LOG,
33
33
  TELEMETRY_LOG,
34
34
  } from "../constants.js";
35
+ import {
36
+ writeQueryToDb,
37
+ writeSessionTelemetryToDb,
38
+ writeSkillCheckToDb,
39
+ } from "../localdb/direct-write.js";
35
40
  import {
36
41
  appendCanonicalRecords,
37
42
  buildCanonicalExecutionFact,
@@ -47,10 +52,9 @@ import type {
47
52
  CanonicalRecord,
48
53
  QueryLogRecord,
49
54
  SessionTelemetryRecord,
50
- SkillUsageRecord,
51
55
  TranscriptMetrics,
52
56
  } from "../types.js";
53
- import { appendJsonl, loadMarker, saveMarker } from "../utils/jsonl.js";
57
+ import { loadMarker, saveMarker } from "../utils/jsonl.js";
54
58
  import { isActionableQueryText } from "../utils/query-filter.js";
55
59
  import {
56
60
  extractActionableUserQueries,
@@ -136,9 +140,9 @@ export function parseSession(transcriptPath: string): ParsedSession | null {
136
140
  export function writeSession(
137
141
  session: ParsedSession,
138
142
  dryRun = false,
139
- queryLogPath: string = QUERY_LOG,
140
- telemetryLogPath: string = TELEMETRY_LOG,
141
- skillLogPath: string = SKILL_LOG,
143
+ _queryLogPath: string = QUERY_LOG,
144
+ _telemetryLogPath: string = TELEMETRY_LOG,
145
+ _skillLogPath: string = SKILL_LOG,
142
146
  canonicalLogPath: string = CANONICAL_LOG,
143
147
  ): void {
144
148
  if (dryRun) {
@@ -150,7 +154,7 @@ export function writeSession(
150
154
  return;
151
155
  }
152
156
 
153
- // Write ONE query record per user query
157
+ // Write ONE query record per user query to SQLite
154
158
  for (const uq of session.user_queries) {
155
159
  const queryRecord: QueryLogRecord = {
156
160
  timestamp: uq.timestamp || session.timestamp,
@@ -158,10 +162,14 @@ export function writeSession(
158
162
  query: uq.query,
159
163
  source: "claude_code_replay",
160
164
  };
161
- appendJsonl(queryLogPath, queryRecord, "all_queries");
165
+ try {
166
+ writeQueryToDb(queryRecord);
167
+ } catch {
168
+ /* fail-open */
169
+ }
162
170
  }
163
171
 
164
- // Write ONE telemetry record per session
172
+ // Write ONE telemetry record per session to SQLite
165
173
  const telemetry: SessionTelemetryRecord = {
166
174
  timestamp: session.timestamp,
167
175
  session_id: session.session_id,
@@ -178,7 +186,11 @@ export function writeSession(
178
186
  last_user_query: session.metrics.last_user_query,
179
187
  source: "claude_code_replay",
180
188
  };
181
- appendJsonl(telemetryLogPath, telemetry, "session_telemetry");
189
+ try {
190
+ writeSessionTelemetryToDb(telemetry);
191
+ } catch {
192
+ /* fail-open */
193
+ }
182
194
 
183
195
  // Write ONE skill record per invoked/triggered skill.
184
196
  // Prefer skills_invoked (actual Skill tool calls) for high-confidence records.
@@ -189,20 +201,33 @@ export function writeSession(
189
201
  session.user_queries[session.user_queries.length - 1]?.query.trim() ??
190
202
  session.metrics.last_user_query.trim();
191
203
 
192
- for (const skillName of skillSource) {
204
+ for (let i = 0; i < skillSource.length; i++) {
205
+ const skillName = skillSource[i];
193
206
  const skillQuery = latestActionableQuery;
194
207
  if (!isActionableQueryText(skillQuery)) continue;
195
208
 
196
- const skillRecord: SkillUsageRecord = {
197
- timestamp: session.timestamp,
198
- session_id: session.session_id,
199
- skill_name: skillName,
200
- skill_path: `(claude_code:${skillName})`,
201
- query: skillQuery,
202
- triggered: true,
203
- source: "claude_code_replay",
204
- };
205
- appendJsonl(skillLogPath, skillRecord, "skill_usage");
209
+ const { invocation_mode, confidence } = deriveInvocationMode({
210
+ has_skill_tool_call: invoked.length > 0,
211
+ has_skill_md_read: invoked.length === 0,
212
+ });
213
+
214
+ try {
215
+ writeSkillCheckToDb({
216
+ skill_invocation_id: deriveSkillInvocationId(session.session_id, skillName, i),
217
+ session_id: session.session_id,
218
+ occurred_at: session.timestamp,
219
+ skill_name: skillName,
220
+ invocation_mode,
221
+ triggered: true,
222
+ confidence,
223
+ platform: "claude_code",
224
+ query: skillQuery,
225
+ skill_path: `(claude_code:${skillName})`,
226
+ source: "claude_code_replay",
227
+ });
228
+ } catch {
229
+ /* fail-open */
230
+ }
206
231
  }
207
232
 
208
233
  // --- Canonical normalization records (additive) ---
@@ -233,7 +258,9 @@ export function buildCanonicalRecordsFromReplay(session: ParsedSession): Canonic
233
258
  records.push(
234
259
  buildCanonicalSession({
235
260
  ...baseInput,
236
- started_at: session.timestamp,
261
+ started_at: session.metrics.started_at ?? session.timestamp,
262
+ ended_at: session.metrics.ended_at,
263
+ model: session.metrics.model,
237
264
  }),
238
265
  );
239
266
 
@@ -12,11 +12,14 @@
12
12
  */
13
13
 
14
14
  import {
15
- copyFileSync,
15
+ closeSync,
16
16
  existsSync,
17
+ fsyncSync,
17
18
  mkdirSync,
19
+ openSync,
18
20
  readdirSync,
19
21
  readFileSync,
22
+ renameSync,
20
23
  writeFileSync,
21
24
  } from "node:fs";
22
25
  import { homedir } from "node:os";
@@ -24,12 +27,34 @@ import { dirname, join, resolve } from "node:path";
24
27
  import { fileURLToPath } from "node:url";
25
28
  import { parseArgs } from "node:util";
26
29
 
30
+ import { getAlphaGuidance } from "./agent-guidance.js";
31
+ import {
32
+ ALPHA_CONSENT_NOTICE,
33
+ generateUserId,
34
+ isValidApiKeyFormat,
35
+ readAlphaIdentity,
36
+ } from "./alpha-identity.js";
27
37
  import { TELEMETRY_NOTICE } from "./analytics.js";
38
+ import { pollDeviceCode, requestDeviceCode } from "./auth/device-code.js";
28
39
  import { CLAUDE_CODE_HOOK_KEYS, SELFTUNE_CONFIG_DIR, SELFTUNE_CONFIG_PATH } from "./constants.js";
29
- import type { SelftuneConfig } from "./types.js";
40
+ import type { AgentCommandGuidance, AlphaIdentity, SelftuneConfig } from "./types.js";
30
41
  import { hookKeyHasSelftuneEntry } from "./utils/hooks.js";
31
42
  import { detectAgent } from "./utils/llm-call.js";
32
43
 
44
+ interface InitCliErrorPayload extends AgentCommandGuidance {
45
+ error: string;
46
+ }
47
+
48
+ class InitCliError extends Error {
49
+ payload: InitCliErrorPayload;
50
+
51
+ constructor(payload: InitCliErrorPayload) {
52
+ super(payload.message);
53
+ this.name = "InitCliError";
54
+ this.payload = payload;
55
+ }
56
+ }
57
+
33
58
  // ---------------------------------------------------------------------------
34
59
  // Agent type detection
35
60
  // ---------------------------------------------------------------------------
@@ -116,6 +141,24 @@ export function determineCliPath(override?: string): string {
116
141
  return resolve(dirname(import.meta.path), "index.ts");
117
142
  }
118
143
 
144
+ function writeSelftuneConfig(configPath: string, config: SelftuneConfig): void {
145
+ const serialized = JSON.stringify(config, null, 2);
146
+ if (!config.alpha?.api_key?.trim()) {
147
+ writeFileSync(configPath, serialized, "utf-8");
148
+ return;
149
+ }
150
+
151
+ const tempPath = `${configPath}.tmp`;
152
+ const fd = openSync(tempPath, "w", 0o600);
153
+ try {
154
+ writeFileSync(fd, serialized, "utf-8");
155
+ fsyncSync(fd);
156
+ } finally {
157
+ closeSync(fd);
158
+ }
159
+ renameSync(tempPath, configPath);
160
+ }
161
+
119
162
  // ---------------------------------------------------------------------------
120
163
  // LLM mode determination
121
164
  // ---------------------------------------------------------------------------
@@ -270,41 +313,13 @@ export function installClaudeCodeHooks(options?: {
270
313
  // Agent file installation
271
314
  // ---------------------------------------------------------------------------
272
315
 
273
- /** Bundled agent files directory (ships with the npm package). */
274
- const BUNDLED_AGENTS_DIR = resolve(dirname(import.meta.path), "..", "..", ".claude", "agents");
275
-
276
316
  /**
277
- * Copy bundled agent markdown files to ~/.claude/agents/.
278
- * Returns a list of file names that were copied (skips files that already exist
279
- * unless `force` is true).
317
+ * @deprecated Agent files are now bundled in skill/agents/ and read directly
318
+ * by the consuming agent via progressive disclosure. No installation needed.
319
+ * Kept as a no-op for backwards compatibility with callers.
280
320
  */
281
- export function installAgentFiles(options?: { homeDir?: string; force?: boolean }): string[] {
282
- const home = options?.homeDir ?? homedir();
283
- const force = options?.force ?? false;
284
- const targetDir = join(home, ".claude", "agents");
285
-
286
- if (!existsSync(BUNDLED_AGENTS_DIR)) return [];
287
-
288
- let sourceFiles: string[];
289
- try {
290
- sourceFiles = readdirSync(BUNDLED_AGENTS_DIR).filter((f) => f.endsWith(".md"));
291
- } catch {
292
- return [];
293
- }
294
-
295
- if (sourceFiles.length === 0) return [];
296
-
297
- mkdirSync(targetDir, { recursive: true });
298
-
299
- const copied: string[] = [];
300
- for (const file of sourceFiles) {
301
- const dest = join(targetDir, file);
302
- if (!force && existsSync(dest)) continue;
303
- copyFileSync(join(BUNDLED_AGENTS_DIR, file), dest);
304
- copied.push(file);
305
- }
306
-
307
- return copied;
321
+ export function installAgentFiles(_options?: { homeDir?: string; force?: boolean }): string[] {
322
+ return [];
308
323
  }
309
324
 
310
325
  // ---------------------------------------------------------------------------
@@ -437,6 +452,11 @@ export interface InitOptions {
437
452
  agentOverride?: string;
438
453
  cliPathOverride?: string;
439
454
  homeDir?: string;
455
+ alpha?: boolean;
456
+ noAlpha?: boolean;
457
+ alphaEmail?: string;
458
+ alphaName?: string;
459
+ alphaKey?: string;
440
460
  }
441
461
 
442
462
  // ---------------------------------------------------------------------------
@@ -447,7 +467,7 @@ export interface InitOptions {
447
467
  * Run the init flow. Returns the written (or existing) config.
448
468
  * Extracted as a pure function for testability.
449
469
  */
450
- export function runInit(opts: InitOptions): SelftuneConfig {
470
+ export async function runInit(opts: InitOptions): Promise<SelftuneConfig> {
451
471
  const { configDir, configPath, force } = opts;
452
472
 
453
473
  // If config exists and no --force, return existing
@@ -462,6 +482,9 @@ export function runInit(opts: InitOptions): SelftuneConfig {
462
482
  }
463
483
  }
464
484
 
485
+ // Capture existing alpha identity before overwriting config (for user_id preservation)
486
+ const existingAlphaBeforeOverwrite = readAlphaIdentity(configPath);
487
+
465
488
  // Detect agent type
466
489
  const agentType = detectAgentType(opts.agentOverride, opts.homeDir);
467
490
 
@@ -485,6 +508,85 @@ export function runInit(opts: InitOptions): SelftuneConfig {
485
508
  const settingsPath = join(home, ".claude", "settings.json");
486
509
  const hooksInstalled = agentType === "claude_code" ? checkClaudeCodeHooks(settingsPath) : false;
487
510
 
511
+ let validatedAlphaIdentity: AlphaIdentity | null = null;
512
+ if (opts.alpha) {
513
+ if (opts.alphaKey) {
514
+ // Direct key entry path — backward compatible, requires email
515
+ if (!opts.alphaEmail) {
516
+ throw new InitCliError({
517
+ error: "alpha_email_required",
518
+ message:
519
+ "The --alpha-email flag is required when using --alpha-key. Run: selftune init --alpha --alpha-email user@example.com --alpha-key st_live_<key>",
520
+ next_command: "selftune init --alpha --alpha-email <email> --alpha-key st_live_<key>",
521
+ suggested_commands: ["selftune init --alpha", "selftune status"],
522
+ blocking: true,
523
+ code: "alpha_email_required",
524
+ });
525
+ }
526
+
527
+ if (!isValidApiKeyFormat(opts.alphaKey)) {
528
+ throw new InitCliError({
529
+ error: "invalid_api_key_format",
530
+ message: "API key must start with 'st_live_' or 'st_test_'. Check the key and retry.",
531
+ next_command: "selftune init --alpha --alpha-email <email> --alpha-key st_live_<key>",
532
+ suggested_commands: ["selftune status", "selftune doctor"],
533
+ blocking: true,
534
+ code: "invalid_api_key_format",
535
+ });
536
+ }
537
+
538
+ validatedAlphaIdentity = {
539
+ enrolled: true,
540
+ user_id: existingAlphaBeforeOverwrite?.user_id ?? generateUserId(),
541
+ email: opts.alphaEmail,
542
+ display_name: opts.alphaName,
543
+ consent_timestamp: new Date().toISOString(),
544
+ api_key: opts.alphaKey,
545
+ };
546
+ } else {
547
+ // Device-code flow — no key provided, authenticate via browser
548
+ process.stderr.write("[alpha] Starting device-code authentication flow...\n");
549
+
550
+ const grant = await requestDeviceCode();
551
+
552
+ // Emit structured JSON for the agent to parse
553
+ console.log(
554
+ JSON.stringify({
555
+ level: "info",
556
+ code: "device_code_issued",
557
+ verification_url: grant.verification_url,
558
+ user_code: grant.user_code,
559
+ expires_in: grant.expires_in,
560
+ message: `Open ${grant.verification_url} and enter code: ${grant.user_code}`,
561
+ }),
562
+ );
563
+
564
+ // Try to open browser
565
+ try {
566
+ const url = `${grant.verification_url}?code=${grant.user_code}`;
567
+ Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore" });
568
+ process.stderr.write(`[alpha] Browser opened. Waiting for approval...\n`);
569
+ } catch {
570
+ process.stderr.write(`[alpha] Could not open browser. Visit the URL above manually.\n`);
571
+ }
572
+
573
+ process.stderr.write("[alpha] Polling");
574
+ const result = await pollDeviceCode(grant.device_code, grant.interval, grant.expires_in);
575
+ process.stderr.write("\n[alpha] Approved!\n");
576
+
577
+ validatedAlphaIdentity = {
578
+ enrolled: true,
579
+ user_id: existingAlphaBeforeOverwrite?.user_id ?? generateUserId(),
580
+ cloud_user_id: result.cloud_user_id,
581
+ cloud_org_id: result.org_id,
582
+ email: opts.alphaEmail,
583
+ display_name: opts.alphaName,
584
+ consent_timestamp: new Date().toISOString(),
585
+ api_key: result.api_key,
586
+ };
587
+ }
588
+ }
589
+
488
590
  const config: SelftuneConfig = {
489
591
  agent_type: agentType,
490
592
  cli_path: cliPath,
@@ -496,13 +598,10 @@ export function runInit(opts: InitOptions): SelftuneConfig {
496
598
 
497
599
  // Write config
498
600
  mkdirSync(configDir, { recursive: true });
499
- writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
601
+ writeSelftuneConfig(configPath, config);
500
602
 
501
- // Install agent files to ~/.claude/agents/
502
- const copiedAgents = installAgentFiles({ homeDir: home, force });
503
- if (copiedAgents.length > 0) {
504
- console.error(`[INFO] Installed agent files: ${copiedAgents.join(", ")}`);
505
- }
603
+ // Agent files are bundled in skill/agents/ and read directly by the
604
+ // consuming agent no installation step needed.
506
605
 
507
606
  // Auto-install hooks into ~/.claude/settings.json (Claude Code only)
508
607
  if (agentType === "claude_code") {
@@ -513,7 +612,7 @@ export function runInit(opts: InitOptions): SelftuneConfig {
513
612
  if (addedHookKeys.length > 0) {
514
613
  config.hooks_installed = true;
515
614
  // Re-write config with updated hooks_installed flag
516
- writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
615
+ writeSelftuneConfig(configPath, config);
517
616
  console.error(
518
617
  `[INFO] Installed ${addedHookKeys.length} selftune hook(s) into ${settingsPath}: ${addedHookKeys.join(", ")}`,
519
618
  );
@@ -521,11 +620,34 @@ export function runInit(opts: InitOptions): SelftuneConfig {
521
620
  // Re-check in case hooks were already present
522
621
  config.hooks_installed = checkClaudeCodeHooks(settingsPath);
523
622
  if (config.hooks_installed) {
524
- writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
623
+ writeSelftuneConfig(configPath, config);
525
624
  }
526
625
  }
527
626
  }
528
627
 
628
+ if (existingAlphaBeforeOverwrite && !opts.alpha && !opts.noAlpha) {
629
+ config.alpha = existingAlphaBeforeOverwrite;
630
+ writeSelftuneConfig(configPath, config);
631
+ }
632
+
633
+ // Handle alpha enrollment
634
+ if (validatedAlphaIdentity) {
635
+ config.alpha = validatedAlphaIdentity;
636
+ writeSelftuneConfig(configPath, config);
637
+
638
+ const readiness = checkAlphaReadiness(configPath);
639
+ console.error(JSON.stringify({ alpha_readiness: readiness }));
640
+ } else if (opts.noAlpha) {
641
+ if (existingAlphaBeforeOverwrite) {
642
+ const identity: AlphaIdentity = {
643
+ ...existingAlphaBeforeOverwrite,
644
+ enrolled: false,
645
+ };
646
+ config.alpha = identity;
647
+ writeSelftuneConfig(configPath, config);
648
+ }
649
+ }
650
+
529
651
  return config;
530
652
  }
531
653
 
@@ -541,6 +663,11 @@ export async function cliMain(): Promise<void> {
541
663
  force: { type: "boolean", default: false },
542
664
  "enable-autonomy": { type: "boolean", default: false },
543
665
  "schedule-format": { type: "string" },
666
+ alpha: { type: "boolean", default: false },
667
+ "no-alpha": { type: "boolean", default: false },
668
+ "alpha-email": { type: "string" },
669
+ "alpha-name": { type: "string" },
670
+ "alpha-key": { type: "string" },
544
671
  },
545
672
  strict: true,
546
673
  });
@@ -551,7 +678,14 @@ export async function cliMain(): Promise<void> {
551
678
  const enableAutonomy = values["enable-autonomy"] ?? false;
552
679
 
553
680
  // Check for existing config without force
554
- if (!force && !enableAutonomy && existsSync(configPath)) {
681
+ const hasAlphaMutation = !!(
682
+ values.alpha ||
683
+ values["no-alpha"] ||
684
+ values["alpha-email"] ||
685
+ values["alpha-name"] ||
686
+ values["alpha-key"]
687
+ );
688
+ if (!force && !enableAutonomy && !hasAlphaMutation && existsSync(configPath)) {
555
689
  try {
556
690
  const raw = readFileSync(configPath, "utf-8");
557
691
  const existing = JSON.parse(raw) as SelftuneConfig;
@@ -565,15 +699,57 @@ export async function cliMain(): Promise<void> {
565
699
  }
566
700
  }
567
701
 
568
- const config = runInit({
702
+ const config = await runInit({
569
703
  configDir,
570
704
  configPath,
571
705
  force,
572
706
  agentOverride: values.agent,
573
707
  cliPathOverride: values["cli-path"],
708
+ alpha: values.alpha ?? false,
709
+ noAlpha: values["no-alpha"] ?? false,
710
+ alphaEmail: values["alpha-email"],
711
+ alphaName: values["alpha-name"],
712
+ alphaKey: values["alpha-key"],
574
713
  });
575
714
 
576
- console.log(JSON.stringify(config, null, 2));
715
+ // Redact api_key before printing to stdout
716
+ const safeConfig = structuredClone(config);
717
+ if (safeConfig.alpha?.api_key) {
718
+ safeConfig.alpha.api_key = "<redacted>";
719
+ }
720
+ console.log(JSON.stringify(safeConfig, null, 2));
721
+
722
+ // Alpha enrollment output
723
+ if (values.alpha) {
724
+ console.log(
725
+ JSON.stringify({
726
+ level: "info",
727
+ code: "alpha_enrolled",
728
+ user_id: config.alpha?.user_id,
729
+ email: config.alpha?.email,
730
+ enrolled: true,
731
+ }),
732
+ );
733
+ console.log(
734
+ JSON.stringify({
735
+ level: "info",
736
+ code: "alpha_upload_ready",
737
+ message:
738
+ "Alpha enrollment complete. Uploads will run automatically during 'selftune orchestrate'. To enable scheduled background sync (includes evolve + watch + upload), run: selftune cron setup",
739
+ next_command: "selftune alpha upload",
740
+ optional_autonomy: "selftune cron setup",
741
+ }),
742
+ );
743
+ console.error(ALPHA_CONSENT_NOTICE);
744
+ } else if (values["no-alpha"]) {
745
+ console.log(
746
+ JSON.stringify({
747
+ level: "info",
748
+ code: "alpha_unenrolled",
749
+ enrolled: false,
750
+ }),
751
+ );
752
+ }
577
753
 
578
754
  // Detect workspace type and report
579
755
  const workspace = detectWorkspaceType(process.cwd());
@@ -637,6 +813,28 @@ export async function cliMain(): Promise<void> {
637
813
  }
638
814
  }
639
815
 
816
+ // ---------------------------------------------------------------------------
817
+ // Alpha readiness check
818
+ // ---------------------------------------------------------------------------
819
+
820
+ export function checkAlphaReadiness(configPath: string): {
821
+ ready: boolean;
822
+ missing: string[];
823
+ guidance: AgentCommandGuidance;
824
+ } {
825
+ const identity = readAlphaIdentity(configPath);
826
+ const missing: string[] = [];
827
+ if (!identity) {
828
+ missing.push("alpha identity not configured");
829
+ return { ready: false, missing, guidance: getAlphaGuidance(identity) };
830
+ }
831
+ if (!identity.enrolled) missing.push("not enrolled");
832
+ if (!identity.api_key) missing.push("api_key not set");
833
+ else if (!isValidApiKeyFormat(identity.api_key))
834
+ missing.push("api_key has invalid format (expected st_live_* or st_test_*)");
835
+ return { ready: missing.length === 0, missing, guidance: getAlphaGuidance(identity) };
836
+ }
837
+
640
838
  // Guard: only run when invoked directly
641
839
  const isMain =
642
840
  (import.meta as Record<string, unknown>).main === true ||
@@ -644,6 +842,10 @@ const isMain =
644
842
 
645
843
  if (isMain) {
646
844
  cliMain().catch((err) => {
845
+ if (err instanceof InitCliError) {
846
+ console.error(JSON.stringify(err.payload));
847
+ process.exit(1);
848
+ }
647
849
  console.error(`[FATAL] ${err}`);
648
850
  process.exit(1);
649
851
  });
@@ -4,14 +4,13 @@
4
4
  * Lightweight, no LLM calls.
5
5
  */
6
6
 
7
- import { QUERY_LOG, TELEMETRY_LOG } from "./constants.js";
7
+ import { getDb } from "./localdb/db.js";
8
+ import { queryQueryLog, querySessionTelemetry, querySkillUsageRecords } from "./localdb/queries.js";
8
9
  import type { QueryLogRecord, SessionTelemetryRecord, SkillUsageRecord } from "./types.js";
9
- import { readJsonl } from "./utils/jsonl.js";
10
10
  import {
11
11
  filterActionableQueryRecords,
12
12
  filterActionableSkillUsageRecords,
13
13
  } from "./utils/query-filter.js";
14
- import { readEffectiveSkillUsageRecords } from "./utils/skill-log.js";
15
14
 
16
15
  // ---------------------------------------------------------------------------
17
16
  // Types
@@ -79,7 +78,7 @@ export function computeLastInsight(
79
78
  let recommendation: string;
80
79
  const unmatched = unmatchedQueries.length;
81
80
  if (unmatched > 0) {
82
- recommendation = `${unmatched} queries had no skill match. Run 'selftune evals --list-skills' to investigate.`;
81
+ recommendation = `${unmatched} queries had no skill match. Run 'selftune eval generate --list-skills' to investigate.`;
83
82
  } else if (errors > 0) {
84
83
  recommendation = `${errors} errors encountered. Check logs for details.`;
85
84
  } else {
@@ -132,9 +131,10 @@ export function formatInsight(insight: LastSessionInsight): string {
132
131
 
133
132
  /** CLI main: reads logs, prints insight. */
134
133
  export function cliMain(): void {
135
- const telemetry = readJsonl<SessionTelemetryRecord>(TELEMETRY_LOG);
136
- const skillRecords = readEffectiveSkillUsageRecords();
137
- const queryRecords = readJsonl<QueryLogRecord>(QUERY_LOG);
134
+ const db = getDb();
135
+ const telemetry = querySessionTelemetry(db) as SessionTelemetryRecord[];
136
+ const skillRecords = querySkillUsageRecords(db) as SkillUsageRecord[];
137
+ const queryRecords = queryQueryLog(db) as QueryLogRecord[];
138
138
 
139
139
  const insight = computeLastInsight(telemetry, skillRecords, queryRecords);
140
140
  if (!insight) {