selftune 0.2.9 → 0.2.12

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-4_dAY17K.js +16 -0
  3. package/apps/local-dashboard/dist/assets/index-BxV5WZHc.css +2 -0
  4. package/apps/local-dashboard/dist/assets/rolldown-runtime-Dw2cE7zH.js +1 -0
  5. package/apps/local-dashboard/dist/assets/vendor-react-CKkiCskZ.js +11 -0
  6. package/apps/local-dashboard/dist/assets/vendor-table-pHbDxq36.js +8 -0
  7. package/apps/local-dashboard/dist/assets/vendor-ui-7xD7fNEU.js +12 -0
  8. package/apps/local-dashboard/dist/index.html +16 -15
  9. package/bin/selftune.cjs +1 -1
  10. package/cli/selftune/activation-rules.ts +1 -0
  11. package/cli/selftune/alpha-upload/build-payloads.ts +18 -2
  12. package/cli/selftune/alpha-upload/stage-canonical.ts +94 -0
  13. package/cli/selftune/auth/device-code.ts +32 -0
  14. package/cli/selftune/auto-update.ts +12 -0
  15. package/cli/selftune/badge/badge.ts +1 -0
  16. package/cli/selftune/canonical-export.ts +5 -0
  17. package/cli/selftune/claude-agents.ts +154 -0
  18. package/cli/selftune/contribute/bundle.ts +1 -0
  19. package/cli/selftune/contribute/contribute.ts +1 -0
  20. package/cli/selftune/cron/setup.ts +2 -2
  21. package/cli/selftune/dashboard-server.ts +1 -0
  22. package/cli/selftune/eval/hooks-to-evals.ts +1 -0
  23. package/cli/selftune/eval/import-skillsbench.ts +1 -0
  24. package/cli/selftune/eval/synthetic-evals.ts +2 -3
  25. package/cli/selftune/eval/unit-test.ts +1 -0
  26. package/cli/selftune/evolution/deploy-proposal.ts +9 -238
  27. package/cli/selftune/evolution/evolve-body.ts +93 -6
  28. package/cli/selftune/evolution/evolve.ts +3 -7
  29. package/cli/selftune/evolution/propose-body.ts +3 -2
  30. package/cli/selftune/evolution/propose-routing.ts +3 -2
  31. package/cli/selftune/evolution/refine-body.ts +3 -2
  32. package/cli/selftune/evolution/rollback.ts +1 -1
  33. package/cli/selftune/export.ts +1 -0
  34. package/cli/selftune/grading/grade-session.ts +8 -0
  35. package/cli/selftune/hooks/auto-activate.ts +1 -0
  36. package/cli/selftune/hooks/evolution-guard.ts +1 -1
  37. package/cli/selftune/hooks/prompt-log.ts +1 -0
  38. package/cli/selftune/hooks/session-stop.ts +34 -40
  39. package/cli/selftune/hooks/skill-change-guard.ts +1 -0
  40. package/cli/selftune/hooks/skill-eval.ts +1 -1
  41. package/cli/selftune/index.ts +23 -14
  42. package/cli/selftune/ingestors/claude-replay.ts +1 -0
  43. package/cli/selftune/ingestors/codex-rollout.ts +1 -0
  44. package/cli/selftune/ingestors/codex-wrapper.ts +1 -0
  45. package/cli/selftune/ingestors/openclaw-ingest.ts +1 -0
  46. package/cli/selftune/ingestors/opencode-ingest.ts +1 -0
  47. package/cli/selftune/init.ts +121 -29
  48. package/cli/selftune/localdb/db.ts +1 -0
  49. package/cli/selftune/localdb/direct-write.ts +39 -0
  50. package/cli/selftune/localdb/materialize.ts +2 -0
  51. package/cli/selftune/localdb/queries.ts +53 -0
  52. package/cli/selftune/localdb/schema.ts +28 -0
  53. package/cli/selftune/normalization.ts +1 -0
  54. package/cli/selftune/observability.ts +1 -0
  55. package/cli/selftune/repair/skill-usage.ts +1 -0
  56. package/cli/selftune/routes/orchestrate-runs.ts +1 -0
  57. package/cli/selftune/routes/overview.ts +1 -0
  58. package/cli/selftune/routes/report.ts +1 -1
  59. package/cli/selftune/routes/skill-report.ts +2 -1
  60. package/cli/selftune/status.ts +1 -1
  61. package/cli/selftune/sync.ts +30 -1
  62. package/cli/selftune/uninstall.ts +412 -0
  63. package/cli/selftune/utils/canonical-log.ts +2 -0
  64. package/cli/selftune/utils/frontmatter.ts +50 -7
  65. package/cli/selftune/utils/jsonl.ts +1 -0
  66. package/cli/selftune/utils/llm-call.ts +131 -3
  67. package/cli/selftune/utils/skill-log.ts +1 -0
  68. package/cli/selftune/utils/transcript.ts +1 -0
  69. package/cli/selftune/utils/trigger-check.ts +1 -1
  70. package/cli/selftune/workflows/skill-md-writer.ts +5 -5
  71. package/cli/selftune/workflows/workflows.ts +1 -0
  72. package/package.json +37 -33
  73. package/packages/telemetry-contract/fixtures/golden.test.ts +1 -0
  74. package/packages/telemetry-contract/package.json +1 -1
  75. package/packages/telemetry-contract/src/schemas.ts +1 -0
  76. package/packages/telemetry-contract/tests/compatibility.test.ts +1 -0
  77. package/packages/ui/README.md +35 -34
  78. package/packages/ui/package.json +3 -3
  79. package/packages/ui/src/components/ActivityTimeline.tsx +50 -43
  80. package/packages/ui/src/components/EvidenceViewer.tsx +306 -182
  81. package/packages/ui/src/components/EvolutionTimeline.tsx +83 -72
  82. package/packages/ui/src/components/InfoTip.tsx +4 -3
  83. package/packages/ui/src/components/OrchestrateRunsPanel.tsx +60 -53
  84. package/packages/ui/src/components/section-cards.tsx +20 -25
  85. package/packages/ui/src/components/skill-health-grid.tsx +213 -193
  86. package/packages/ui/src/lib/constants.tsx +1 -0
  87. package/packages/ui/src/primitives/badge.tsx +12 -15
  88. package/packages/ui/src/primitives/button.tsx +7 -7
  89. package/packages/ui/src/primitives/card.tsx +15 -26
  90. package/packages/ui/src/primitives/checkbox.tsx +7 -8
  91. package/packages/ui/src/primitives/collapsible.tsx +5 -5
  92. package/packages/ui/src/primitives/dropdown-menu.tsx +45 -55
  93. package/packages/ui/src/primitives/label.tsx +6 -6
  94. package/packages/ui/src/primitives/select.tsx +28 -37
  95. package/packages/ui/src/primitives/table.tsx +17 -44
  96. package/packages/ui/src/primitives/tabs.tsx +14 -21
  97. package/packages/ui/src/primitives/tooltip.tsx +10 -22
  98. package/skill/SKILL.md +70 -57
  99. package/skill/Workflows/AlphaUpload.md +4 -4
  100. package/skill/Workflows/AutoActivation.md +11 -6
  101. package/skill/Workflows/Badge.md +22 -16
  102. package/skill/Workflows/Baseline.md +34 -36
  103. package/skill/Workflows/Composability.md +16 -11
  104. package/skill/Workflows/Contribute.md +26 -21
  105. package/skill/Workflows/Cron.md +23 -22
  106. package/skill/Workflows/Dashboard.md +32 -27
  107. package/skill/Workflows/Doctor.md +33 -27
  108. package/skill/Workflows/Evals.md +48 -47
  109. package/skill/Workflows/EvolutionMemory.md +31 -21
  110. package/skill/Workflows/Evolve.md +84 -82
  111. package/skill/Workflows/EvolveBody.md +58 -47
  112. package/skill/Workflows/Grade.md +16 -13
  113. package/skill/Workflows/ImportSkillsBench.md +9 -6
  114. package/skill/Workflows/Ingest.md +36 -21
  115. package/skill/Workflows/Initialize.md +108 -40
  116. package/skill/Workflows/Orchestrate.md +22 -16
  117. package/skill/Workflows/Replay.md +12 -7
  118. package/skill/Workflows/Rollback.md +13 -6
  119. package/skill/Workflows/Schedule.md +6 -6
  120. package/skill/Workflows/Sync.md +18 -11
  121. package/skill/Workflows/UnitTest.md +28 -17
  122. package/skill/Workflows/Watch.md +28 -21
  123. package/skill/agents/diagnosis-analyst.md +11 -0
  124. package/skill/agents/evolution-reviewer.md +15 -1
  125. package/skill/agents/integration-guide.md +10 -0
  126. package/skill/agents/pattern-analyst.md +12 -1
  127. package/skill/references/grading-methodology.md +23 -24
  128. package/skill/references/interactive-config.md +7 -7
  129. package/skill/references/invocation-taxonomy.md +22 -20
  130. package/skill/references/logs.md +14 -6
  131. package/skill/references/setup-patterns.md +4 -2
  132. package/.claude/agents/diagnosis-analyst.md +0 -156
  133. package/.claude/agents/evolution-reviewer.md +0 -180
  134. package/.claude/agents/integration-guide.md +0 -212
  135. package/.claude/agents/pattern-analyst.md +0 -160
  136. package/apps/local-dashboard/dist/assets/index-Bs3Y4ixf.css +0 -1
  137. package/apps/local-dashboard/dist/assets/index-C4UYGWKr.js +0 -15
  138. package/apps/local-dashboard/dist/assets/vendor-react-BQH_6WrG.js +0 -60
  139. package/apps/local-dashboard/dist/assets/vendor-table-dK1QMLq9.js +0 -26
  140. package/apps/local-dashboard/dist/assets/vendor-ui-CO2mrx6e.js +0 -341
@@ -9,9 +9,9 @@
9
9
  */
10
10
 
11
11
  import { execSync } from "node:child_process";
12
- import { closeSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
13
- import { CANONICAL_LOG, ORCHESTRATE_LOCK, TELEMETRY_LOG } from "../constants.js";
12
+ import { readFileSync } from "node:fs";
14
13
 
14
+ import { CANONICAL_LOG, ORCHESTRATE_LOCK, TELEMETRY_LOG } from "../constants.js";
15
15
  import {
16
16
  appendCanonicalRecords,
17
17
  buildCanonicalExecutionFact,
@@ -25,6 +25,22 @@ import { parseTranscript } from "../utils/transcript.js";
25
25
 
26
26
  const LOCK_STALE_MS = 30 * 60 * 1000;
27
27
 
28
+ interface ReactiveSpawnDeps {
29
+ spawnOrchestrate?: () => boolean;
30
+ }
31
+
32
+ function hasFreshOrchestrateLock(lockPath: string): boolean {
33
+ try {
34
+ const lockContent = readFileSync(lockPath, "utf8");
35
+ const lock = JSON.parse(lockContent) as { timestamp?: string };
36
+ if (typeof lock.timestamp !== "string") return false;
37
+ const lockAge = Date.now() - new Date(lock.timestamp).getTime();
38
+ return Number.isFinite(lockAge) && lockAge < LOCK_STALE_MS;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
28
44
  /**
29
45
  * Check for pending improvement signals and spawn a focused orchestrate run
30
46
  * in the background if warranted. Fire-and-forget — the hook exits immediately.
@@ -33,6 +49,7 @@ const LOCK_STALE_MS = 30 * 60 * 1000;
33
49
  */
34
50
  export async function maybeSpawnReactiveOrchestrate(
35
51
  lockPath: string = ORCHESTRATE_LOCK,
52
+ deps: ReactiveSpawnDeps = {},
36
53
  ): Promise<boolean> {
37
54
  try {
38
55
  // Read pending signals from SQLite (dynamic import to reduce hook startup cost)
@@ -42,48 +59,25 @@ export async function maybeSpawnReactiveOrchestrate(
42
59
  const pending = queryImprovementSignals(db, false);
43
60
  if (pending.length === 0) return false;
44
61
 
45
- // Atomically claim the lock openSync with "wx" fails if file exists
46
- let fd: number;
47
- try {
48
- fd = openSync(lockPath, "wx");
49
- writeFileSync(fd, JSON.stringify({ timestamp: new Date().toISOString(), pid: process.pid }));
50
- closeSync(fd);
51
- } catch (lockErr: unknown) {
52
- // Lock exists — check if stale
53
- if ((lockErr as NodeJS.ErrnoException).code === "EEXIST") {
54
- try {
55
- const lockContent = readFileSync(lockPath, "utf8");
56
- const lock = JSON.parse(lockContent);
57
- const lockAge = Date.now() - new Date(lock.timestamp).getTime();
58
- if (lockAge < LOCK_STALE_MS) return false; // Active lock, skip
59
- // Stale lock — override
60
- writeFileSync(
61
- lockPath,
62
- JSON.stringify({ timestamp: new Date().toISOString(), pid: process.pid }),
63
- );
64
- } catch {
65
- return false; // Can't read lock, skip
66
- }
67
- } else {
68
- return false;
69
- }
70
- }
62
+ // Do not pre-claim the orchestrate lock here. The spawned process must
63
+ // acquire its own lock or it will immediately self-block on startup.
64
+ if (hasFreshOrchestrateLock(lockPath)) return false;
71
65
 
72
66
  // Spawn orchestrate in background (fire-and-forget)
73
67
  try {
74
- const proc = Bun.spawn(["selftune", "orchestrate", "--max-skills", "2"], {
75
- stdout: "ignore",
76
- stderr: "ignore",
77
- stdin: "ignore",
78
- });
79
- proc.unref();
68
+ const spawnOrchestrate =
69
+ deps.spawnOrchestrate ??
70
+ (() => {
71
+ const proc = Bun.spawn(["selftune", "orchestrate", "--max-skills", "2"], {
72
+ stdout: "ignore",
73
+ stderr: "ignore",
74
+ stdin: "ignore",
75
+ });
76
+ proc.unref();
77
+ return true;
78
+ });
79
+ if (!spawnOrchestrate()) return false;
80
80
  } catch {
81
- // Spawn failed — release our lock
82
- try {
83
- unlinkSync(lockPath);
84
- } catch {
85
- /* ignore */
86
- }
87
81
  return false;
88
82
  }
89
83
 
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
14
  import { basename, dirname } from "node:path";
15
+
15
16
  import { SESSION_STATE_DIR } from "../constants.js";
16
17
  import type { PreToolUsePayload } from "../types.js";
17
18
 
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { existsSync, readFileSync } from "node:fs";
15
15
  import { basename, dirname } from "node:path";
16
+
16
17
  import { CANONICAL_LOG, SKILL_LOG } from "../constants.js";
17
18
  import {
18
19
  appendCanonicalRecord,
@@ -24,7 +25,6 @@ import {
24
25
  getLatestPromptIdentity,
25
26
  } from "../normalization.js";
26
27
  import type { PostToolUsePayload, SkillUsageRecord } from "../types.js";
27
-
28
28
  import { classifySkillPath } from "../utils/skill-discovery.js";
29
29
  import { getLastUserMessage } from "../utils/transcript.js";
30
30
 
@@ -10,6 +10,7 @@
10
10
  * selftune sync — Sync source-truth telemetry across supported agents
11
11
  * selftune orchestrate — Run autonomous core loop (sync → status → evolve → watch)
12
12
  * selftune init — Initialize agent identity and config
13
+ * selftune uninstall — Clean removal of all selftune data and config
13
14
  * selftune status — Show skill health summary
14
15
  * selftune watch — Monitor post-deploy skill health
15
16
  * selftune doctor — Run health checks
@@ -44,6 +45,7 @@ Commands:
44
45
  sync Sync source-truth telemetry across supported agents
45
46
  orchestrate Run autonomous core loop (sync → status → evolve → watch)
46
47
  init Initialize agent identity and config
48
+ uninstall Clean removal of all selftune data and config
47
49
  status Show skill health summary
48
50
  watch Monitor post-deploy skill health
49
51
  doctor Run health checks
@@ -338,6 +340,11 @@ Run 'selftune eval <action> --help' for action-specific options.`);
338
340
  await cliMain();
339
341
  break;
340
342
  }
343
+ case "uninstall": {
344
+ const { cliMain } = await import("./uninstall.js");
345
+ await cliMain();
346
+ break;
347
+ }
341
348
  case "contribute": {
342
349
  const { cliMain } = await import("./contribute/contribute.js");
343
350
  await cliMain();
@@ -464,7 +471,7 @@ Run 'selftune cron <subcommand> --help' for subcommand-specific options.`);
464
471
  }
465
472
  case "sync": {
466
473
  const { cliMain } = await import("./sync.js");
467
- cliMain();
474
+ await cliMain();
468
475
  break;
469
476
  }
470
477
  case "workflows": {
@@ -606,9 +613,8 @@ Output:
606
613
  const { readAlphaIdentity } = await import("./alpha-identity.js");
607
614
  const { getDb } = await import("./localdb/db.js");
608
615
  const { runUploadCycle } = await import("./alpha-upload/index.js");
609
- const { getSelftuneVersion, readConfiguredAgentType } = await import(
610
- "./utils/selftune-meta.js"
611
- );
616
+ const { getSelftuneVersion, readConfiguredAgentType } =
617
+ await import("./utils/selftune-meta.js");
612
618
 
613
619
  const identity = readAlphaIdentity(SELFTUNE_CONFIG_PATH);
614
620
  if (!identity?.enrolled) {
@@ -670,36 +676,39 @@ Output:
670
676
  }
671
677
  case "relink": {
672
678
  const { SELFTUNE_CONFIG_PATH } = await import("./constants.js");
673
- const { readAlphaIdentity, writeAlphaIdentity, generateUserId } = await import(
674
- "./alpha-identity.js"
675
- );
676
- const { requestDeviceCode, pollDeviceCode } = await import("./auth/device-code.js");
679
+ const { readAlphaIdentity, writeAlphaIdentity, generateUserId } =
680
+ await import("./alpha-identity.js");
681
+ const { buildVerificationUrl, pollDeviceCode, requestDeviceCode, tryOpenUrl } =
682
+ await import("./auth/device-code.js");
677
683
  const { chmodSync } = await import("node:fs");
678
684
 
679
685
  const existingIdentity = readAlphaIdentity(SELFTUNE_CONFIG_PATH);
680
686
  process.stderr.write("[alpha relink] Starting device-code authentication flow...\n");
681
687
 
682
688
  const grant = await requestDeviceCode();
689
+ const verificationUrlWithCode = buildVerificationUrl(
690
+ grant.verification_url,
691
+ grant.user_code,
692
+ );
683
693
 
684
694
  console.log(
685
695
  JSON.stringify({
686
696
  level: "info",
687
697
  code: "device_code_issued",
688
698
  verification_url: grant.verification_url,
699
+ verification_url_with_code: verificationUrlWithCode,
689
700
  user_code: grant.user_code,
690
701
  expires_in: grant.expires_in,
691
- message: `Open ${grant.verification_url} and enter code: ${grant.user_code}`,
702
+ message: `Open ${verificationUrlWithCode} to approve.`,
692
703
  }),
693
704
  );
694
705
 
695
706
  // Try to open browser
696
- try {
697
- const url = `${grant.verification_url}?code=${grant.user_code}`;
698
- Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore" });
707
+ if (tryOpenUrl(verificationUrlWithCode)) {
699
708
  process.stderr.write("[alpha relink] Browser opened. Waiting for approval...\n");
700
- } catch {
709
+ } else {
701
710
  process.stderr.write(
702
- "[alpha relink] Could not open browser. Visit the URL above manually.\n",
711
+ `[alpha relink] Could not open browser. Visit ${verificationUrlWithCode} manually.\n`,
703
712
  );
704
713
  }
705
714
 
@@ -24,6 +24,7 @@
24
24
  import { statSync } from "node:fs";
25
25
  import { basename } from "node:path";
26
26
  import { parseArgs } from "node:util";
27
+
27
28
  import {
28
29
  CANONICAL_LOG,
29
30
  CLAUDE_CODE_MARKER,
@@ -25,6 +25,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
25
25
  import { homedir } from "node:os";
26
26
  import { basename, join } from "node:path";
27
27
  import { parseArgs } from "node:util";
28
+
28
29
  import { CANONICAL_LOG, QUERY_LOG, SKILL_LOG, TELEMETRY_LOG } from "../constants.js";
29
30
  import {
30
31
  appendCanonicalRecords,
@@ -19,6 +19,7 @@
19
19
 
20
20
  import { homedir } from "node:os";
21
21
  import { join } from "node:path";
22
+
22
23
  import { CANONICAL_LOG, QUERY_LOG, SKILL_LOG, TELEMETRY_LOG } from "../constants.js";
23
24
  import {
24
25
  appendCanonicalRecords,
@@ -25,6 +25,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
25
25
  import { homedir } from "node:os";
26
26
  import { basename, join } from "node:path";
27
27
  import { parseArgs } from "node:util";
28
+
28
29
  import {
29
30
  CANONICAL_LOG,
30
31
  OPENCLAW_AGENTS_DIR,
@@ -25,6 +25,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
25
25
  import { homedir } from "node:os";
26
26
  import { basename, join } from "node:path";
27
27
  import { parseArgs } from "node:util";
28
+
28
29
  import { CANONICAL_LOG, QUERY_LOG, SKILL_LOG, TELEMETRY_LOG } from "../constants.js";
29
30
  import {
30
31
  appendCanonicalRecords,
@@ -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
  // ---------------------------------------------------------------------------
@@ -508,7 +503,11 @@ export async function runInit(opts: InitOptions): Promise<SelftuneConfig> {
508
503
  if (!force && !hasAlphaMutation && existsSync(configPath)) {
509
504
  const raw = readFileSync(configPath, "utf-8");
510
505
  try {
511
- 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;
512
511
  } catch (err) {
513
512
  throw new Error(
514
513
  `Config file at ${configPath} contains invalid JSON. Delete it or use --force to reinitialize. Cause: ${err instanceof Error ? err.message : String(err)}`,
@@ -548,6 +547,7 @@ export async function runInit(opts: InitOptions): Promise<SelftuneConfig> {
548
547
  process.stderr.write("[alpha] Starting device-code authentication flow...\n");
549
548
 
550
549
  const grant = await requestDeviceCode();
550
+ const verificationUrlWithCode = buildVerificationUrl(grant.verification_url, grant.user_code);
551
551
 
552
552
  // Emit structured JSON for the agent to parse
553
553
  console.log(
@@ -555,25 +555,24 @@ export async function runInit(opts: InitOptions): Promise<SelftuneConfig> {
555
555
  level: "info",
556
556
  code: "device_code_issued",
557
557
  verification_url: grant.verification_url,
558
+ verification_url_with_code: verificationUrlWithCode,
558
559
  user_code: grant.user_code,
559
560
  expires_in: grant.expires_in,
560
- message: `Open ${grant.verification_url} and enter code: ${grant.user_code}`,
561
+ message: `Open ${verificationUrlWithCode} to approve.`,
561
562
  }),
562
563
  );
563
564
 
564
565
  // Try to open browser (skip in test environments)
565
566
  if (!process.env.BUN_ENV?.includes("test") && !process.env.SELFTUNE_NO_BROWSER) {
566
- try {
567
- const url = `${grant.verification_url}?code=${grant.user_code}`;
568
- Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore" });
567
+ if (tryOpenUrl(verificationUrlWithCode)) {
569
568
  process.stderr.write(`[alpha] Browser opened. Waiting for approval...\n`);
570
- } catch {
571
- 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
+ );
572
573
  }
573
574
  } else {
574
- process.stderr.write(
575
- `[alpha] Visit ${grant.verification_url}?code=${grant.user_code} to approve.\n`,
576
- );
575
+ process.stderr.write(`[alpha] Visit ${verificationUrlWithCode} to approve.\n`);
577
576
  }
578
577
 
579
578
  process.stderr.write("[alpha] Polling");
@@ -606,11 +605,15 @@ export async function runInit(opts: InitOptions): Promise<SelftuneConfig> {
606
605
  mkdirSync(configDir, { recursive: true });
607
606
  writeSelftuneConfig(configPath, config);
608
607
 
609
- // Agent files are bundled in skill/agents/ and read directly by the
610
- // consuming agent — no installation step needed.
611
-
612
608
  // Auto-install hooks into ~/.claude/settings.json (Claude Code only)
613
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
+
614
617
  const addedHookKeys = installClaudeCodeHooks({
615
618
  settingsPath,
616
619
  cliPath,
@@ -668,6 +671,8 @@ export async function cliMain(): Promise<void> {
668
671
  "cli-path": { type: "string" },
669
672
  force: { type: "boolean", default: false },
670
673
  "enable-autonomy": { type: "boolean", default: false },
674
+ "no-sync": { type: "boolean", default: false },
675
+ "no-autonomy": { type: "boolean", default: false },
671
676
  "schedule-format": { type: "string" },
672
677
  alpha: { type: "boolean", default: false },
673
678
  "no-alpha": { type: "boolean", default: false },
@@ -680,7 +685,10 @@ export async function cliMain(): Promise<void> {
680
685
  const configDir = SELFTUNE_CONFIG_DIR;
681
686
  const configPath = SELFTUNE_CONFIG_PATH;
682
687
  const force = values.force ?? false;
683
- 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"];
684
692
  try {
685
693
  validateAlphaMetadataFlags(values.alpha, values["alpha-email"], values["alpha-name"]);
686
694
  } catch (error) {
@@ -695,6 +703,7 @@ export async function cliMain(): Promise<void> {
695
703
  values["alpha-email"] ||
696
704
  values["alpha-name"]
697
705
  );
706
+ let existingConfigDetected = false;
698
707
  if (!force && !enableAutonomy && !hasAlphaMutation && existsSync(configPath)) {
699
708
  try {
700
709
  const raw = readFileSync(configPath, "utf-8");
@@ -708,6 +717,14 @@ export async function cliMain(): Promise<void> {
708
717
  );
709
718
  }
710
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
+ }
711
728
 
712
729
  const config = await runInit({
713
730
  configDir,
@@ -727,6 +744,9 @@ export async function cliMain(): Promise<void> {
727
744
  safeConfig.alpha.api_key = "<redacted>";
728
745
  }
729
746
  console.log(JSON.stringify(safeConfig, null, 2));
747
+ if (existingConfigDetected) {
748
+ console.error("Already initialized. Use --force to reinitialize.");
749
+ }
730
750
 
731
751
  // Alpha enrollment output
732
752
  if (values.alpha) {
@@ -790,6 +810,78 @@ export async function cliMain(): Promise<void> {
790
810
  }),
791
811
  );
792
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
+
793
885
  if (enableAutonomy) {
794
886
  try {
795
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";
@@ -353,6 +357,41 @@ export function writeQueryToDb(record: {
353
357
  });
354
358
  }
355
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
+
356
395
  export function writeImprovementSignalToDb(record: {
357
396
  timestamp: string;
358
397
  session_id: string;
@@ -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,