selftune 0.2.8 → 0.2.10

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 (140) hide show
  1. package/README.md +35 -35
  2. package/apps/local-dashboard/dist/assets/index-BZVLv70T.js +16 -0
  3. package/apps/local-dashboard/dist/assets/{index-CRtLkBTi.css → index-Bs3Y4ixf.css} +1 -1
  4. package/apps/local-dashboard/dist/assets/{vendor-react-BQH_6WrG.js → vendor-react-BXP54cYo.js} +4 -4
  5. package/apps/local-dashboard/dist/assets/{vendor-table-dK1QMLq9.js → vendor-table-DTF_SXoy.js} +1 -1
  6. package/apps/local-dashboard/dist/assets/{vendor-ui-CO2mrx6e.js → vendor-ui-CWU0d1wd.js} +66 -66
  7. package/apps/local-dashboard/dist/index.html +15 -15
  8. package/bin/selftune.cjs +1 -1
  9. package/cli/selftune/activation-rules.ts +37 -18
  10. package/cli/selftune/agent-guidance.ts +16 -16
  11. package/cli/selftune/alpha-identity.ts +1 -2
  12. package/cli/selftune/alpha-upload/build-payloads.ts +18 -2
  13. package/cli/selftune/alpha-upload/flush.ts +2 -2
  14. package/cli/selftune/alpha-upload/stage-canonical.ts +106 -3
  15. package/cli/selftune/auth/device-code.ts +32 -0
  16. package/cli/selftune/auto-update.ts +12 -0
  17. package/cli/selftune/badge/badge.ts +1 -0
  18. package/cli/selftune/canonical-export.ts +5 -0
  19. package/cli/selftune/claude-agents.ts +154 -0
  20. package/cli/selftune/contribute/bundle.ts +2 -0
  21. package/cli/selftune/contribute/contribute.ts +1 -0
  22. package/cli/selftune/cron/setup.ts +2 -2
  23. package/cli/selftune/dashboard-contract.ts +1 -1
  24. package/cli/selftune/dashboard-server.ts +11 -52
  25. package/cli/selftune/eval/hooks-to-evals.ts +13 -6
  26. package/cli/selftune/eval/import-skillsbench.ts +1 -0
  27. package/cli/selftune/eval/synthetic-evals.ts +2 -3
  28. package/cli/selftune/eval/unit-test.ts +1 -0
  29. package/cli/selftune/evolution/deploy-proposal.ts +1 -0
  30. package/cli/selftune/evolution/evolve-body.ts +93 -6
  31. package/cli/selftune/evolution/evolve.ts +0 -1
  32. package/cli/selftune/evolution/propose-body.ts +3 -2
  33. package/cli/selftune/evolution/propose-routing.ts +3 -2
  34. package/cli/selftune/evolution/refine-body.ts +3 -2
  35. package/cli/selftune/export.ts +1 -0
  36. package/cli/selftune/grading/auto-grade.ts +1 -0
  37. package/cli/selftune/grading/grade-session.ts +9 -0
  38. package/cli/selftune/hooks/auto-activate.ts +6 -0
  39. package/cli/selftune/hooks/evolution-guard.ts +12 -15
  40. package/cli/selftune/hooks/prompt-log.ts +1 -0
  41. package/cli/selftune/hooks/session-stop.ts +34 -40
  42. package/cli/selftune/hooks/skill-change-guard.ts +1 -0
  43. package/cli/selftune/hooks/skill-eval.ts +1 -1
  44. package/cli/selftune/index.ts +23 -14
  45. package/cli/selftune/ingestors/claude-replay.ts +1 -0
  46. package/cli/selftune/ingestors/codex-rollout.ts +1 -0
  47. package/cli/selftune/ingestors/codex-wrapper.ts +1 -0
  48. package/cli/selftune/ingestors/openclaw-ingest.ts +1 -0
  49. package/cli/selftune/ingestors/opencode-ingest.ts +1 -0
  50. package/cli/selftune/init.ts +197 -96
  51. package/cli/selftune/localdb/db.ts +1 -0
  52. package/cli/selftune/localdb/direct-write.ts +93 -12
  53. package/cli/selftune/localdb/materialize.ts +2 -0
  54. package/cli/selftune/localdb/queries.ts +210 -0
  55. package/cli/selftune/localdb/schema.ts +72 -1
  56. package/cli/selftune/monitoring/watch.ts +1 -0
  57. package/cli/selftune/normalization.ts +4 -0
  58. package/cli/selftune/observability.ts +14 -7
  59. package/cli/selftune/orchestrate.ts +15 -37
  60. package/cli/selftune/repair/skill-usage.ts +7 -3
  61. package/cli/selftune/routes/orchestrate-runs.ts +1 -0
  62. package/cli/selftune/routes/overview.ts +1 -0
  63. package/cli/selftune/routes/skill-report.ts +1 -0
  64. package/cli/selftune/sync.ts +31 -1
  65. package/cli/selftune/types.ts +2 -2
  66. package/cli/selftune/uninstall.ts +412 -0
  67. package/cli/selftune/utils/canonical-log.ts +2 -0
  68. package/cli/selftune/utils/jsonl.ts +1 -0
  69. package/cli/selftune/utils/llm-call.ts +131 -3
  70. package/cli/selftune/utils/skill-log.ts +1 -0
  71. package/cli/selftune/utils/transcript.ts +1 -0
  72. package/cli/selftune/utils/trigger-check.ts +1 -1
  73. package/cli/selftune/workflows/skill-md-writer.ts +5 -5
  74. package/cli/selftune/workflows/workflows.ts +1 -0
  75. package/package.json +38 -33
  76. package/packages/telemetry-contract/fixtures/golden.test.ts +1 -0
  77. package/packages/telemetry-contract/package.json +3 -3
  78. package/packages/telemetry-contract/src/index.ts +0 -1
  79. package/packages/telemetry-contract/src/schemas.ts +6 -24
  80. package/packages/telemetry-contract/tests/compatibility.test.ts +1 -0
  81. package/packages/ui/README.md +35 -34
  82. package/packages/ui/package.json +3 -3
  83. package/packages/ui/src/components/ActivityTimeline.tsx +49 -42
  84. package/packages/ui/src/components/EvidenceViewer.tsx +306 -182
  85. package/packages/ui/src/components/EvolutionTimeline.tsx +83 -72
  86. package/packages/ui/src/components/InfoTip.tsx +4 -3
  87. package/packages/ui/src/components/OrchestrateRunsPanel.tsx +60 -53
  88. package/packages/ui/src/components/section-cards.tsx +19 -24
  89. package/packages/ui/src/components/skill-health-grid.tsx +213 -193
  90. package/packages/ui/src/lib/constants.tsx +1 -0
  91. package/packages/ui/src/primitives/badge.tsx +12 -15
  92. package/packages/ui/src/primitives/button.tsx +7 -7
  93. package/packages/ui/src/primitives/card.tsx +15 -26
  94. package/packages/ui/src/primitives/checkbox.tsx +7 -8
  95. package/packages/ui/src/primitives/collapsible.tsx +5 -5
  96. package/packages/ui/src/primitives/dropdown-menu.tsx +45 -55
  97. package/packages/ui/src/primitives/label.tsx +6 -6
  98. package/packages/ui/src/primitives/select.tsx +28 -37
  99. package/packages/ui/src/primitives/table.tsx +17 -44
  100. package/packages/ui/src/primitives/tabs.tsx +14 -21
  101. package/packages/ui/src/primitives/tooltip.tsx +10 -22
  102. package/skill/SKILL.md +72 -59
  103. package/skill/Workflows/AlphaUpload.md +4 -4
  104. package/skill/Workflows/AutoActivation.md +11 -6
  105. package/skill/Workflows/Badge.md +22 -16
  106. package/skill/Workflows/Baseline.md +34 -36
  107. package/skill/Workflows/Composability.md +16 -11
  108. package/skill/Workflows/Contribute.md +26 -21
  109. package/skill/Workflows/Cron.md +23 -22
  110. package/skill/Workflows/Dashboard.md +40 -40
  111. package/skill/Workflows/Doctor.md +40 -34
  112. package/skill/Workflows/Evals.md +48 -47
  113. package/skill/Workflows/EvolutionMemory.md +31 -21
  114. package/skill/Workflows/Evolve.md +84 -82
  115. package/skill/Workflows/EvolveBody.md +58 -47
  116. package/skill/Workflows/Grade.md +16 -13
  117. package/skill/Workflows/ImportSkillsBench.md +9 -6
  118. package/skill/Workflows/Ingest.md +36 -21
  119. package/skill/Workflows/Initialize.md +138 -97
  120. package/skill/Workflows/Orchestrate.md +22 -16
  121. package/skill/Workflows/Replay.md +12 -7
  122. package/skill/Workflows/Rollback.md +13 -6
  123. package/skill/Workflows/Schedule.md +6 -6
  124. package/skill/Workflows/Sync.md +18 -11
  125. package/skill/Workflows/UnitTest.md +28 -17
  126. package/skill/Workflows/Watch.md +28 -21
  127. package/skill/agents/diagnosis-analyst.md +11 -0
  128. package/skill/agents/evolution-reviewer.md +15 -1
  129. package/skill/agents/integration-guide.md +10 -0
  130. package/skill/agents/pattern-analyst.md +12 -1
  131. package/skill/references/grading-methodology.md +23 -24
  132. package/skill/references/interactive-config.md +7 -7
  133. package/skill/references/invocation-taxonomy.md +22 -20
  134. package/skill/references/logs.md +20 -6
  135. package/skill/references/setup-patterns.md +4 -2
  136. package/.claude/agents/diagnosis-analyst.md +0 -156
  137. package/.claude/agents/evolution-reviewer.md +0 -180
  138. package/.claude/agents/integration-guide.md +0 -212
  139. package/.claude/agents/pattern-analyst.md +0 -160
  140. package/apps/local-dashboard/dist/assets/index-Bk9vSHHd.js +0 -15
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Usage:
10
10
  * selftune init [--agent <type>] [--cli-path <path>] [--force]
11
- * selftune init --enable-autonomy [--schedule-format cron|launchd|systemd]
11
+ * selftune init [--no-sync] [--no-autonomy] [--schedule-format cron|launchd|systemd]
12
12
  */
13
13
 
14
14
  import {
@@ -35,12 +35,20 @@ import {
35
35
  readAlphaIdentity,
36
36
  } from "./alpha-identity.js";
37
37
  import { TELEMETRY_NOTICE } from "./analytics.js";
38
- import { pollDeviceCode, requestDeviceCode } from "./auth/device-code.js";
38
+ import {
39
+ buildVerificationUrl,
40
+ pollDeviceCode,
41
+ requestDeviceCode,
42
+ tryOpenUrl,
43
+ } from "./auth/device-code.js";
44
+ import { installAgentFiles } from "./claude-agents.js";
39
45
  import { CLAUDE_CODE_HOOK_KEYS, SELFTUNE_CONFIG_DIR, SELFTUNE_CONFIG_PATH } from "./constants.js";
40
46
  import type { AgentCommandGuidance, AlphaIdentity, SelftuneConfig } from "./types.js";
41
47
  import { hookKeyHasSelftuneEntry } from "./utils/hooks.js";
42
48
  import { detectAgent } from "./utils/llm-call.js";
43
49
 
50
+ export { installAgentFiles } from "./claude-agents.js";
51
+
44
52
  interface InitCliErrorPayload extends AgentCommandGuidance {
45
53
  error: string;
46
54
  }
@@ -309,19 +317,6 @@ export function installClaudeCodeHooks(options?: {
309
317
  return addedKeys;
310
318
  }
311
319
 
312
- // ---------------------------------------------------------------------------
313
- // Agent file installation
314
- // ---------------------------------------------------------------------------
315
-
316
- /**
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.
320
- */
321
- export function installAgentFiles(_options?: { homeDir?: string; force?: boolean }): string[] {
322
- return [];
323
- }
324
-
325
320
  // ---------------------------------------------------------------------------
326
321
  // Workspace type detection
327
322
  // ---------------------------------------------------------------------------
@@ -456,7 +451,38 @@ export interface InitOptions {
456
451
  noAlpha?: boolean;
457
452
  alphaEmail?: string;
458
453
  alphaName?: string;
459
- alphaKey?: string;
454
+ }
455
+
456
+ function validateAlphaMetadataFlags(
457
+ alpha: boolean | undefined,
458
+ email?: string,
459
+ name?: string,
460
+ ): void {
461
+ if ((email !== undefined || name !== undefined) && !alpha) {
462
+ throw new Error("--alpha-email and --alpha-name require --alpha");
463
+ }
464
+ }
465
+
466
+ function assertValidApprovedAlphaCredential(result: {
467
+ api_key: string;
468
+ cloud_user_id: string;
469
+ org_id: string;
470
+ }): void {
471
+ if (!isValidApiKeyFormat(result.api_key)) {
472
+ throw new Error(
473
+ "Device-code approval returned an invalid alpha credential. Re-run `selftune init --alpha`.",
474
+ );
475
+ }
476
+ if (!result.cloud_user_id?.trim()) {
477
+ throw new Error(
478
+ "Device-code approval did not include a cloud user id. Re-run `selftune init --alpha`.",
479
+ );
480
+ }
481
+ if (!result.org_id?.trim()) {
482
+ throw new Error(
483
+ "Device-code approval did not include an alpha org id. Re-run `selftune init --alpha`.",
484
+ );
485
+ }
460
486
  }
461
487
 
462
488
  // ---------------------------------------------------------------------------
@@ -469,12 +495,19 @@ export interface InitOptions {
469
495
  */
470
496
  export async function runInit(opts: InitOptions): Promise<SelftuneConfig> {
471
497
  const { configDir, configPath, force } = opts;
498
+ validateAlphaMetadataFlags(opts.alpha, opts.alphaEmail, opts.alphaName);
472
499
 
473
- // If config exists and no --force, return existing
474
- if (!force && existsSync(configPath)) {
500
+ // If config exists and no --force (and no alpha mutation), return existing
501
+ const hasAlphaMutation =
502
+ opts.alpha || opts.noAlpha || opts.alphaEmail !== undefined || opts.alphaName !== undefined;
503
+ if (!force && !hasAlphaMutation && existsSync(configPath)) {
475
504
  const raw = readFileSync(configPath, "utf-8");
476
505
  try {
477
- return JSON.parse(raw) as SelftuneConfig;
506
+ const existingConfig = JSON.parse(raw) as SelftuneConfig;
507
+ if (existingConfig.agent_type === "claude_code") {
508
+ installAgentFiles({ homeDir: opts.homeDir });
509
+ }
510
+ return existingConfig;
478
511
  } catch (err) {
479
512
  throw new Error(
480
513
  `Config file at ${configPath} contains invalid JSON. Delete it or use --force to reinitialize. Cause: ${err instanceof Error ? err.message : String(err)}`,
@@ -510,81 +543,53 @@ export async function runInit(opts: InitOptions): Promise<SelftuneConfig> {
510
543
 
511
544
  let validatedAlphaIdentity: AlphaIdentity | null = null;
512
545
  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");
546
+ // Device-code flow — authenticate via browser approval
547
+ process.stderr.write("[alpha] Starting device-code authentication flow...\n");
549
548
 
550
- const grant = await requestDeviceCode();
549
+ const grant = await requestDeviceCode();
550
+ const verificationUrlWithCode = buildVerificationUrl(grant.verification_url, grant.user_code);
551
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
- );
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
+ verification_url_with_code: verificationUrlWithCode,
559
+ user_code: grant.user_code,
560
+ expires_in: grant.expires_in,
561
+ message: `Open ${verificationUrlWithCode} to approve.`,
562
+ }),
563
+ );
563
564
 
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" });
565
+ // Try to open browser (skip in test environments)
566
+ if (!process.env.BUN_ENV?.includes("test") && !process.env.SELFTUNE_NO_BROWSER) {
567
+ if (tryOpenUrl(verificationUrlWithCode)) {
568
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`);
569
+ } else {
570
+ process.stderr.write(
571
+ `[alpha] Could not open browser. Visit ${verificationUrlWithCode} manually.\n`,
572
+ );
571
573
  }
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
- };
574
+ } else {
575
+ process.stderr.write(`[alpha] Visit ${verificationUrlWithCode} to approve.\n`);
587
576
  }
577
+
578
+ process.stderr.write("[alpha] Polling");
579
+ const result = await pollDeviceCode(grant.device_code, grant.interval, grant.expires_in);
580
+ assertValidApprovedAlphaCredential(result);
581
+ process.stderr.write("\n[alpha] Approved!\n");
582
+
583
+ validatedAlphaIdentity = {
584
+ enrolled: true,
585
+ user_id: existingAlphaBeforeOverwrite?.user_id ?? generateUserId(),
586
+ cloud_user_id: result.cloud_user_id,
587
+ cloud_org_id: result.org_id,
588
+ email: opts.alphaEmail ?? existingAlphaBeforeOverwrite?.email,
589
+ display_name: opts.alphaName ?? existingAlphaBeforeOverwrite?.display_name,
590
+ consent_timestamp: new Date().toISOString(),
591
+ api_key: result.api_key,
592
+ };
588
593
  }
589
594
 
590
595
  const config: SelftuneConfig = {
@@ -600,11 +605,15 @@ export async function runInit(opts: InitOptions): Promise<SelftuneConfig> {
600
605
  mkdirSync(configDir, { recursive: true });
601
606
  writeSelftuneConfig(configPath, config);
602
607
 
603
- // Agent files are bundled in skill/agents/ and read directly by the
604
- // consuming agent — no installation step needed.
605
-
606
608
  // Auto-install hooks into ~/.claude/settings.json (Claude Code only)
607
609
  if (agentType === "claude_code") {
610
+ const syncedAgentFiles = installAgentFiles({ homeDir: home });
611
+ if (syncedAgentFiles.length > 0) {
612
+ console.error(
613
+ `[INFO] Synced ${syncedAgentFiles.length} selftune agent file(s) into ${join(home, ".claude", "agents")}: ${syncedAgentFiles.join(", ")}`,
614
+ );
615
+ }
616
+
608
617
  const addedHookKeys = installClaudeCodeHooks({
609
618
  settingsPath,
610
619
  cliPath,
@@ -662,12 +671,13 @@ export async function cliMain(): Promise<void> {
662
671
  "cli-path": { type: "string" },
663
672
  force: { type: "boolean", default: false },
664
673
  "enable-autonomy": { type: "boolean", default: false },
674
+ "no-sync": { type: "boolean", default: false },
675
+ "no-autonomy": { type: "boolean", default: false },
665
676
  "schedule-format": { type: "string" },
666
677
  alpha: { type: "boolean", default: false },
667
678
  "no-alpha": { type: "boolean", default: false },
668
679
  "alpha-email": { type: "string" },
669
680
  "alpha-name": { type: "string" },
670
- "alpha-key": { type: "string" },
671
681
  },
672
682
  strict: true,
673
683
  });
@@ -675,16 +685,25 @@ export async function cliMain(): Promise<void> {
675
685
  const configDir = SELFTUNE_CONFIG_DIR;
676
686
  const configPath = SELFTUNE_CONFIG_PATH;
677
687
  const force = values.force ?? false;
678
- const enableAutonomy = values["enable-autonomy"] ?? false;
688
+ // Sync and autonomy are on by default; opt out with --no-sync / --no-autonomy
689
+ const enableSync = !(values["no-sync"] ?? false);
690
+ // --enable-autonomy is a backward-compatible alias (now default behavior)
691
+ const enableAutonomy = !values["no-autonomy"];
692
+ try {
693
+ validateAlphaMetadataFlags(values.alpha, values["alpha-email"], values["alpha-name"]);
694
+ } catch (error) {
695
+ console.error(error instanceof Error ? error.message : String(error));
696
+ process.exit(1);
697
+ }
679
698
 
680
699
  // Check for existing config without force
681
700
  const hasAlphaMutation = !!(
682
701
  values.alpha ||
683
702
  values["no-alpha"] ||
684
703
  values["alpha-email"] ||
685
- values["alpha-name"] ||
686
- values["alpha-key"]
704
+ values["alpha-name"]
687
705
  );
706
+ let existingConfigDetected = false;
688
707
  if (!force && !enableAutonomy && !hasAlphaMutation && existsSync(configPath)) {
689
708
  try {
690
709
  const raw = readFileSync(configPath, "utf-8");
@@ -698,6 +717,14 @@ export async function cliMain(): Promise<void> {
698
717
  );
699
718
  }
700
719
  }
720
+ if (!force && !hasAlphaMutation && existsSync(configPath)) {
721
+ try {
722
+ JSON.parse(readFileSync(configPath, "utf-8")) as SelftuneConfig;
723
+ existingConfigDetected = true;
724
+ } catch {
725
+ existingConfigDetected = false;
726
+ }
727
+ }
701
728
 
702
729
  const config = await runInit({
703
730
  configDir,
@@ -709,7 +736,6 @@ export async function cliMain(): Promise<void> {
709
736
  noAlpha: values["no-alpha"] ?? false,
710
737
  alphaEmail: values["alpha-email"],
711
738
  alphaName: values["alpha-name"],
712
- alphaKey: values["alpha-key"],
713
739
  });
714
740
 
715
741
  // Redact api_key before printing to stdout
@@ -718,6 +744,9 @@ export async function cliMain(): Promise<void> {
718
744
  safeConfig.alpha.api_key = "<redacted>";
719
745
  }
720
746
  console.log(JSON.stringify(safeConfig, null, 2));
747
+ if (existingConfigDetected) {
748
+ console.error("Already initialized. Use --force to reinitialize.");
749
+ }
721
750
 
722
751
  // Alpha enrollment output
723
752
  if (values.alpha) {
@@ -781,6 +810,78 @@ export async function cliMain(): Promise<void> {
781
810
  }),
782
811
  );
783
812
 
813
+ // Backfill historical transcripts into SQLite
814
+ if (enableSync) {
815
+ try {
816
+ const { syncSources } = await import("./sync.js");
817
+ const syncResult = syncSources({
818
+ syncClaude: true,
819
+ syncCodex: true,
820
+ syncOpenCode: true,
821
+ syncOpenClaw: true,
822
+ rebuildSkillUsage: true,
823
+ dryRun: false,
824
+ });
825
+
826
+ const totalSynced =
827
+ (syncResult.sources.claude?.synced ?? 0) +
828
+ (syncResult.sources.codex?.synced ?? 0) +
829
+ (syncResult.sources.opencode?.synced ?? 0) +
830
+ (syncResult.sources.openclaw?.synced ?? 0);
831
+
832
+ console.log(
833
+ JSON.stringify({
834
+ level: "info",
835
+ code: "sync_complete",
836
+ sessions_synced: totalSynced,
837
+ repaired_records: syncResult.repair.repaired_records,
838
+ elapsed_ms: syncResult.total_elapsed_ms,
839
+ }),
840
+ );
841
+ } catch (err) {
842
+ // Fail-open: sync failure should not block init completion
843
+ console.log(
844
+ JSON.stringify({
845
+ level: "warn",
846
+ code: "sync_failed",
847
+ error: err instanceof Error ? err.message : String(err),
848
+ }),
849
+ );
850
+ }
851
+ }
852
+
853
+ // Trigger initial alpha upload if enrolled — push synced data immediately
854
+ if (config.alpha?.enrolled && config.alpha?.api_key) {
855
+ try {
856
+ const { runUploadCycle } = await import("./alpha-upload/index.js");
857
+ const { getDb } = await import("./localdb/db.js");
858
+ const db = getDb();
859
+ const uploadSummary = await runUploadCycle(db, {
860
+ enrolled: true,
861
+ userId: config.alpha.user_id,
862
+ apiKey: config.alpha.api_key,
863
+ });
864
+ console.log(
865
+ JSON.stringify({
866
+ level: "info",
867
+ code: "init_upload_complete",
868
+ prepared: uploadSummary.prepared,
869
+ sent: uploadSummary.sent,
870
+ failed: uploadSummary.failed,
871
+ }),
872
+ );
873
+ } catch (err) {
874
+ // Fail-open: upload failure should not block init
875
+ console.log(
876
+ JSON.stringify({
877
+ level: "warn",
878
+ code: "init_upload_failed",
879
+ error: err instanceof Error ? err.message : String(err),
880
+ }),
881
+ );
882
+ }
883
+ }
884
+
784
885
  if (enableAutonomy) {
785
886
  try {
786
887
  const { installSchedule } = await import("./schedule.js");
@@ -11,6 +11,7 @@
11
11
  import { Database } from "bun:sqlite";
12
12
  import { existsSync, mkdirSync } from "node:fs";
13
13
  import { dirname, join } from "node:path";
14
+
14
15
  import { SELFTUNE_CONFIG_DIR } from "../constants.js";
15
16
  import { ALL_DDL, MIGRATIONS, POST_MIGRATION_INDEXES } from "./schema.js";
16
17
 
@@ -10,6 +10,8 @@
10
10
  */
11
11
 
12
12
  import type { Database } from "bun:sqlite";
13
+ import { createHash } from "node:crypto";
14
+
13
15
  import type {
14
16
  CanonicalExecutionFactRecord,
15
17
  CanonicalPromptRecord,
@@ -17,10 +19,12 @@ import type {
17
19
  CanonicalSessionRecord,
18
20
  CanonicalSkillInvocationRecord,
19
21
  } from "@selftune/telemetry-contract";
22
+
20
23
  import type { OrchestrateRunReport } from "../dashboard-contract.js";
21
24
  import type {
22
25
  EvolutionAuditEntry,
23
26
  EvolutionEvidenceEntry,
27
+ GradingResult,
24
28
  SessionTelemetryRecord,
25
29
  SkillUsageRecord,
26
30
  } from "../types.js";
@@ -44,6 +48,9 @@ export interface SkillInvocationWriteInput {
44
48
  platform?: string;
45
49
  schema_version?: string;
46
50
  normalized_at?: string;
51
+ normalizer_version?: string;
52
+ capture_mode?: string;
53
+ raw_source_ref?: Record<string, unknown>;
47
54
  // Extra fields from skill_usage
48
55
  query?: string;
49
56
  skill_path?: string;
@@ -84,6 +91,17 @@ function safeWrite(label: string, fn: (db: Database) => void): boolean {
84
91
  }
85
92
  }
86
93
 
94
+ function safeWriteResult<T>(label: string, fn: (db: Database) => T): T | null {
95
+ try {
96
+ return fn(getDb());
97
+ } catch (err) {
98
+ if (process.env.DEBUG || process.env.NODE_ENV === "development") {
99
+ console.error(`[direct-write] ${label} failed:`, err);
100
+ }
101
+ return null;
102
+ }
103
+ }
104
+
87
105
  // -- Canonical record dispatcher -----------------------------------------------
88
106
 
89
107
  export function writeCanonicalToDb(record: CanonicalRecord): boolean {
@@ -339,6 +357,41 @@ export function writeQueryToDb(record: {
339
357
  });
340
358
  }
341
359
 
360
+ export function writeGradingResultToDb(result: GradingResult): boolean {
361
+ const gradingId = `gr_${createHash("sha256").update(`${result.session_id}:${result.skill_name}:${result.graded_at}`).digest("hex").slice(0, 16)}`;
362
+ return safeWrite("grading-result", (db) => {
363
+ getStmt(
364
+ db,
365
+ "grading-result",
366
+ `
367
+ INSERT OR IGNORE INTO grading_results
368
+ (grading_id, session_id, skill_name, transcript_path, graded_at,
369
+ pass_rate, mean_score, score_std_dev, passed_count, failed_count, total_count,
370
+ expectations_json, claims_json, eval_feedback_json, failure_feedback_json,
371
+ execution_metrics_json)
372
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
373
+ `,
374
+ ).run(
375
+ gradingId,
376
+ result.session_id,
377
+ result.skill_name,
378
+ result.transcript_path,
379
+ result.graded_at,
380
+ result.summary.pass_rate,
381
+ result.summary.mean_score ?? null,
382
+ result.summary.score_std_dev ?? null,
383
+ result.summary.passed,
384
+ result.summary.failed,
385
+ result.summary.total,
386
+ JSON.stringify(result.expectations),
387
+ JSON.stringify(result.claims),
388
+ JSON.stringify(result.eval_feedback),
389
+ result.failure_feedback ? JSON.stringify(result.failure_feedback) : null,
390
+ JSON.stringify(result.execution_metrics),
391
+ );
392
+ });
393
+ }
394
+
342
395
  export function writeImprovementSignalToDb(record: {
343
396
  timestamp: string;
344
397
  session_id: string;
@@ -377,7 +430,7 @@ export function updateSignalConsumed(
377
430
  signalType: string,
378
431
  runId: string,
379
432
  ): boolean {
380
- return safeWrite("signal-consumed", (db) => {
433
+ const result = safeWriteResult("signal-consumed", (db) =>
381
434
  getStmt(
382
435
  db,
383
436
  "signal-consumed",
@@ -386,8 +439,9 @@ export function updateSignalConsumed(
386
439
  SET consumed = 1, consumed_at = ?, consumed_by_run = ?
387
440
  WHERE session_id = ? AND query = ? AND signal_type = ? AND consumed = 0
388
441
  `,
389
- ).run(new Date().toISOString(), runId, sessionId, query, signalType);
390
- });
442
+ ).run(new Date().toISOString(), runId, sessionId, query, signalType),
443
+ );
444
+ return result?.changes > 0;
391
445
  }
392
446
 
393
447
  // -- Internal insert helpers (used by cached statements) ----------------------
@@ -400,8 +454,8 @@ function insertSession(db: Database, s: CanonicalSessionRecord): void {
400
454
  INSERT INTO sessions
401
455
  (session_id, started_at, ended_at, platform, model, completion_status,
402
456
  source_session_kind, agent_cli, workspace_path, repo_remote, branch,
403
- schema_version, normalized_at)
404
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
457
+ schema_version, normalized_at, normalizer_version, capture_mode, raw_source_ref)
458
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
405
459
  ON CONFLICT(session_id) DO UPDATE SET
406
460
  platform = CASE
407
461
  WHEN sessions.platform IS NULL OR sessions.platform = 'unknown'
@@ -416,7 +470,10 @@ function insertSession(db: Database, s: CanonicalSessionRecord): void {
416
470
  agent_cli = COALESCE(sessions.agent_cli, excluded.agent_cli),
417
471
  repo_remote = COALESCE(sessions.repo_remote, excluded.repo_remote),
418
472
  branch = COALESCE(sessions.branch, excluded.branch),
419
- workspace_path = COALESCE(sessions.workspace_path, excluded.workspace_path)
473
+ workspace_path = COALESCE(sessions.workspace_path, excluded.workspace_path),
474
+ normalizer_version = COALESCE(excluded.normalizer_version, sessions.normalizer_version),
475
+ capture_mode = COALESCE(excluded.capture_mode, sessions.capture_mode),
476
+ raw_source_ref = COALESCE(excluded.raw_source_ref, sessions.raw_source_ref)
420
477
  `,
421
478
  ).run(
422
479
  s.session_id,
@@ -432,6 +489,9 @@ function insertSession(db: Database, s: CanonicalSessionRecord): void {
432
489
  s.branch ?? null,
433
490
  s.schema_version,
434
491
  s.normalized_at,
492
+ s.normalizer_version ?? null,
493
+ s.capture_mode ?? null,
494
+ s.raw_source_ref ? JSON.stringify(s.raw_source_ref) : null,
435
495
  );
436
496
  }
437
497
 
@@ -441,8 +501,9 @@ function insertPrompt(db: Database, p: CanonicalPromptRecord): void {
441
501
  "prompt",
442
502
  `
443
503
  INSERT OR IGNORE INTO prompts
444
- (prompt_id, session_id, occurred_at, prompt_kind, is_actionable, prompt_index, prompt_text)
445
- VALUES (?, ?, ?, ?, ?, ?, ?)
504
+ (prompt_id, session_id, occurred_at, prompt_kind, is_actionable, prompt_index, prompt_text,
505
+ schema_version, platform, normalized_at, normalizer_version, capture_mode, raw_source_ref)
506
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
446
507
  `,
447
508
  ).run(
448
509
  p.prompt_id,
@@ -452,6 +513,12 @@ function insertPrompt(db: Database, p: CanonicalPromptRecord): void {
452
513
  p.is_actionable ? 1 : 0,
453
514
  p.prompt_index ?? null,
454
515
  p.prompt_text,
516
+ p.schema_version ?? null,
517
+ p.platform ?? null,
518
+ p.normalized_at ?? null,
519
+ p.normalizer_version ?? null,
520
+ p.capture_mode ?? null,
521
+ p.raw_source_ref ? JSON.stringify(p.raw_source_ref) : null,
455
522
  );
456
523
  }
457
524
 
@@ -483,8 +550,9 @@ function insertSkillInvocation(
483
550
  INSERT OR IGNORE INTO skill_invocations
484
551
  (skill_invocation_id, session_id, occurred_at, skill_name, invocation_mode,
485
552
  triggered, confidence, tool_name, matched_prompt_id, agent_type,
486
- query, skill_path, skill_scope, source)
487
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
553
+ query, skill_path, skill_scope, source,
554
+ schema_version, platform, normalized_at, normalizer_version, capture_mode, raw_source_ref)
555
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
488
556
  `,
489
557
  ).run(
490
558
  si.skill_invocation_id,
@@ -501,6 +569,12 @@ function insertSkillInvocation(
501
569
  ext.skill_path ?? null,
502
570
  ext.skill_scope ?? null,
503
571
  ext.source ?? null,
572
+ si.schema_version ?? null,
573
+ si.platform ?? null,
574
+ si.normalized_at ?? null,
575
+ ext.normalizer_version ?? null,
576
+ ext.capture_mode ?? null,
577
+ ext.raw_source_ref ? JSON.stringify(ext.raw_source_ref) : null,
504
578
  );
505
579
  }
506
580
 
@@ -512,8 +586,9 @@ function insertExecutionFact(db: Database, ef: CanonicalExecutionFactRecord): vo
512
586
  INSERT INTO execution_facts
513
587
  (session_id, occurred_at, prompt_id, tool_calls_json, total_tool_calls,
514
588
  assistant_turns, errors_encountered, input_tokens, output_tokens,
515
- duration_ms, completion_status)
516
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
589
+ duration_ms, completion_status,
590
+ schema_version, platform, normalized_at, normalizer_version, capture_mode, raw_source_ref)
591
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
517
592
  `,
518
593
  ).run(
519
594
  ef.session_id,
@@ -527,5 +602,11 @@ function insertExecutionFact(db: Database, ef: CanonicalExecutionFactRecord): vo
527
602
  ef.output_tokens ?? null,
528
603
  ef.duration_ms ?? null,
529
604
  ef.completion_status ?? null,
605
+ ef.schema_version ?? null,
606
+ ef.platform ?? null,
607
+ ef.normalized_at ?? null,
608
+ ef.normalizer_version ?? null,
609
+ ef.capture_mode ?? null,
610
+ ef.raw_source_ref ? JSON.stringify(ef.raw_source_ref) : null,
530
611
  );
531
612
  }
@@ -14,6 +14,7 @@
14
14
  // 3. Backfill from batch ingestors that don't yet dual-write
15
15
 
16
16
  import type { Database } from "bun:sqlite";
17
+
17
18
  import {
18
19
  type CanonicalExecutionFactRecord,
19
20
  type CanonicalPromptRecord,
@@ -22,6 +23,7 @@ import {
22
23
  type CanonicalSkillInvocationRecord,
23
24
  isCanonicalRecord,
24
25
  } from "@selftune/telemetry-contract";
26
+
25
27
  import {
26
28
  CANONICAL_LOG,
27
29
  EVOLUTION_AUDIT_LOG,