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
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { Database } from "bun:sqlite";
9
+
9
10
  import type {
10
11
  OrchestrateRunReport,
11
12
  OverviewPayload,
@@ -578,6 +579,58 @@ export function queryImprovementSignals(
578
579
  }));
579
580
  }
580
581
 
582
+ // -- Grading results query ----------------------------------------------------
583
+
584
+ /**
585
+ * Read grading results from SQLite for upload staging.
586
+ */
587
+ export function queryGradingResults(db: Database): Array<{
588
+ grading_id: string;
589
+ session_id: string;
590
+ skill_name: string;
591
+ transcript_path: string | null;
592
+ graded_at: string;
593
+ pass_rate: number | null;
594
+ mean_score: number | null;
595
+ score_std_dev: number | null;
596
+ passed_count: number | null;
597
+ failed_count: number | null;
598
+ total_count: number | null;
599
+ expectations_json: string | null;
600
+ claims_json: string | null;
601
+ eval_feedback_json: string | null;
602
+ failure_feedback_json: string | null;
603
+ execution_metrics_json: string | null;
604
+ }> {
605
+ return db
606
+ .query(
607
+ `SELECT grading_id, session_id, skill_name, transcript_path, graded_at,
608
+ pass_rate, mean_score, score_std_dev, passed_count, failed_count, total_count,
609
+ expectations_json, claims_json, eval_feedback_json, failure_feedback_json,
610
+ execution_metrics_json
611
+ FROM grading_results
612
+ ORDER BY graded_at DESC`,
613
+ )
614
+ .all() as Array<{
615
+ grading_id: string;
616
+ session_id: string;
617
+ skill_name: string;
618
+ transcript_path: string | null;
619
+ graded_at: string;
620
+ pass_rate: number | null;
621
+ mean_score: number | null;
622
+ score_std_dev: number | null;
623
+ passed_count: number | null;
624
+ failed_count: number | null;
625
+ total_count: number | null;
626
+ expectations_json: string | null;
627
+ claims_json: string | null;
628
+ eval_feedback_json: string | null;
629
+ failure_feedback_json: string | null;
630
+ execution_metrics_json: string | null;
631
+ }>;
632
+ }
633
+
581
634
  // -- Canonical record staging query -------------------------------------------
582
635
 
583
636
  /**
@@ -188,6 +188,28 @@ CREATE TABLE IF NOT EXISTS queries (
188
188
  source TEXT
189
189
  )`;
190
190
 
191
+ // -- Grading results table (from grade-session output) -----------------------
192
+
193
+ export const CREATE_GRADING_RESULTS = `
194
+ CREATE TABLE IF NOT EXISTS grading_results (
195
+ grading_id TEXT PRIMARY KEY,
196
+ session_id TEXT NOT NULL,
197
+ skill_name TEXT NOT NULL,
198
+ transcript_path TEXT,
199
+ graded_at TEXT NOT NULL,
200
+ pass_rate REAL,
201
+ mean_score REAL,
202
+ score_std_dev REAL,
203
+ passed_count INTEGER,
204
+ failed_count INTEGER,
205
+ total_count INTEGER,
206
+ expectations_json TEXT,
207
+ claims_json TEXT,
208
+ eval_feedback_json TEXT,
209
+ failure_feedback_json TEXT,
210
+ execution_metrics_json TEXT
211
+ )`;
212
+
191
213
  // -- Improvement signal table (from signal_log.jsonl) ------------------------
192
214
 
193
215
  export const CREATE_IMPROVEMENT_SIGNALS = `
@@ -278,6 +300,11 @@ export const CREATE_INDEXES = [
278
300
  `CREATE INDEX IF NOT EXISTS idx_queries_session ON queries(session_id)`,
279
301
  `CREATE INDEX IF NOT EXISTS idx_queries_ts ON queries(timestamp)`,
280
302
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_queries_dedup ON queries(session_id, query, timestamp)`,
303
+ // -- Grading results indexes -------------------------------------------------
304
+ `CREATE INDEX IF NOT EXISTS idx_grading_session ON grading_results(session_id)`,
305
+ `CREATE INDEX IF NOT EXISTS idx_grading_skill ON grading_results(skill_name)`,
306
+ `CREATE INDEX IF NOT EXISTS idx_grading_ts ON grading_results(graded_at)`,
307
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_grading_dedup ON grading_results(session_id, skill_name, graded_at)`,
281
308
  // -- Improvement signal indexes ---------------------------------------------
282
309
  `CREATE INDEX IF NOT EXISTS idx_signals_session ON improvement_signals(session_id)`,
283
310
  `CREATE INDEX IF NOT EXISTS idx_signals_consumed ON improvement_signals(consumed)`,
@@ -347,6 +374,7 @@ export const ALL_DDL = [
347
374
  CREATE_SKILL_USAGE,
348
375
  CREATE_ORCHESTRATE_RUNS,
349
376
  CREATE_QUERIES,
377
+ CREATE_GRADING_RESULTS,
350
378
  CREATE_IMPROVEMENT_SIGNALS,
351
379
  CREATE_UPLOAD_QUEUE,
352
380
  CREATE_UPLOAD_WATERMARKS,
@@ -24,6 +24,7 @@ import {
24
24
  writeFileSync,
25
25
  } from "node:fs";
26
26
  import { basename, dirname } from "node:path";
27
+
27
28
  import { CANONICAL_LOG, canonicalSessionStatePath } from "./constants.js";
28
29
  import { writeCanonicalBatchToDb, writeCanonicalToDb } from "./localdb/direct-write.js";
29
30
  import {
@@ -11,6 +11,7 @@
11
11
  import { existsSync, readFileSync } from "node:fs";
12
12
  import { homedir } from "node:os";
13
13
  import { join } from "node:path";
14
+
14
15
  import { getAlphaGuidance } from "./agent-guidance.js";
15
16
  import { getAlphaLinkState, readAlphaIdentity } from "./alpha-identity.js";
16
17
  import { LOG_DIR, REQUIRED_FIELDS, SELFTUNE_CONFIG_PATH } from "./constants.js";
@@ -3,6 +3,7 @@
3
3
  import { existsSync, readFileSync, statSync } from "node:fs";
4
4
  import { basename, dirname, join } from "node:path";
5
5
  import { parseArgs } from "node:util";
6
+
6
7
  import {
7
8
  CLAUDE_CODE_PROJECTS_DIR,
8
9
  QUERY_LOG,
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { Database } from "bun:sqlite";
8
+
8
9
  import { getOrchestrateRuns } from "../localdb/queries.js";
9
10
 
10
11
  export function handleOrchestrateRuns(db: Database, limit: number): Response {
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { Database } from "bun:sqlite";
8
+
8
9
  import { getOverviewPayload, getSkillsList } from "../localdb/queries.js";
9
10
 
10
11
  export function handleOverview(db: Database, version: string): Response {
@@ -180,7 +180,7 @@ function buildReportHTML(
180
180
  <tr><th>Metric</th><th>Value</th></tr>
181
181
  <tr><td>Total Skills</td><td>${statusResult.skills.length}</td></tr>
182
182
  <tr><td>Unmatched Queries</td><td>${statusResult.unmatchedQueries}</td></tr>
183
- <tr><td>Pending Proposals</td><td>${statusResult.pendingProposals}</td></tr>
183
+ <tr><td>Undeployed Proposals</td><td>${statusResult.pendingProposals}</td></tr>
184
184
  <tr><td>Last Session</td><td>${escapeHtml(statusResult.lastSession ?? "\u2014")}</td></tr>
185
185
  </table>
186
186
  </div>
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Route handler: GET /api/v2/skills/:name
3
3
  *
4
- * Returns SQLite-backed per-skill report with evolution audit, pending proposals,
4
+ * Returns SQLite-backed per-skill report with evolution audit, undeployed proposals,
5
5
  * invocation details, duration stats, selftune resource usage, prompt samples,
6
6
  * and session metadata.
7
7
  */
8
8
 
9
9
  import type { Database } from "bun:sqlite";
10
+
10
11
  import { getPendingProposals, getSkillReportPayload, safeParseJson } from "../localdb/queries.js";
11
12
 
12
13
  export function handleSkillReport(db: Database, skillName: string): Response {
@@ -324,7 +324,7 @@ export function formatStatus(result: StatusResult): string {
324
324
 
325
325
  // Summary stats
326
326
  lines.push(`Unmatched queries: ${result.unmatchedQueries}`);
327
- lines.push(`Pending proposals: ${result.pendingProposals}`);
327
+ lines.push(`Undeployed proposals: ${result.pendingProposals}`);
328
328
 
329
329
  // Last session
330
330
  if (result.lastSession) {
@@ -17,6 +17,7 @@ import { existsSync } from "node:fs";
17
17
  import { homedir } from "node:os";
18
18
  import { join } from "node:path";
19
19
  import { parseArgs } from "node:util";
20
+
20
21
  import {
21
22
  CLAUDE_CODE_MARKER,
22
23
  CLAUDE_CODE_PROJECTS_DIR,
@@ -504,7 +505,7 @@ function formatStepLine(label: string, step: SyncStepResult, timing?: SyncPhaseT
504
505
  return ` ${label}: ${parts.join(", ")}${time}`;
505
506
  }
506
507
 
507
- export function cliMain(): void {
508
+ export async function cliMain(): Promise<void> {
508
509
  const { values } = parseArgs({
509
510
  options: {
510
511
  "projects-dir": { type: "string", default: CLAUDE_CODE_PROJECTS_DIR },
@@ -633,6 +634,34 @@ Options:
633
634
 
634
635
  process.stderr.write(`\nDone in ${formatMs(result.total_elapsed_ms)}\n`);
635
636
  }
637
+
638
+ // Trigger alpha upload if enrolled — pushes freshly synced data to cloud
639
+ if (!result.dry_run) {
640
+ try {
641
+ const { readAlphaIdentity } = await import("./alpha-identity.js");
642
+ const { SELFTUNE_CONFIG_PATH } = await import("./constants.js");
643
+ const identity = readAlphaIdentity(SELFTUNE_CONFIG_PATH);
644
+ if (identity?.enrolled && identity.api_key) {
645
+ const { runUploadCycle } = await import("./alpha-upload/index.js");
646
+ const { getDb } = await import("./localdb/db.js");
647
+ const db = getDb();
648
+ const uploadSummary = await runUploadCycle(db, {
649
+ enrolled: true,
650
+ userId: identity.user_id,
651
+ apiKey: identity.api_key,
652
+ });
653
+ if (!jsonOutput) {
654
+ process.stderr.write(
655
+ `\nAlpha upload: prepared=${uploadSummary.prepared}, sent=${uploadSummary.sent}, failed=${uploadSummary.failed}\n`,
656
+ );
657
+ } else {
658
+ console.log(JSON.stringify({ code: "alpha_upload", ...uploadSummary }));
659
+ }
660
+ }
661
+ } catch {
662
+ // fail-open: upload failure should not break sync
663
+ }
664
+ }
636
665
  }
637
666
 
638
667
  if (import.meta.main) {
@@ -0,0 +1,412 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * selftune uninstall — Clean removal of all selftune data and configuration.
4
+ *
5
+ * Removes:
6
+ * 1. Autonomy scheduling (launchd/cron/systemd + OpenClaw cron)
7
+ * 2. Selftune hooks from ~/.claude/settings.json (surgical — preserves user hooks)
8
+ * 3. Selftune-managed Claude subagents from ~/.claude/agents/
9
+ * 4. JSONL telemetry logs from ~/.claude/
10
+ * 5. Selftune config directory (~/.selftune/)
11
+ * 6. Ingest marker files
12
+ * 7. Optionally: `npm uninstall -g selftune`
13
+ *
14
+ * Usage:
15
+ * selftune uninstall [--dry-run] [--keep-logs] [--npm-uninstall]
16
+ */
17
+
18
+ import { existsSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
19
+ import { homedir } from "node:os";
20
+ import { join } from "node:path";
21
+ import { parseArgs } from "node:util";
22
+
23
+ import { removeInstalledAgentFiles } from "./claude-agents.js";
24
+ import {
25
+ CLAUDE_CODE_MARKER,
26
+ CLAUDE_SETTINGS_PATH,
27
+ CODEX_INGEST_MARKER,
28
+ EVOLUTION_AUDIT_LOG,
29
+ EVOLUTION_EVIDENCE_LOG,
30
+ OPENCODE_INGEST_MARKER,
31
+ OPENCLAW_INGEST_MARKER,
32
+ ORCHESTRATE_LOCK,
33
+ ORCHESTRATE_RUN_LOG,
34
+ QUERY_LOG,
35
+ REPAIRED_SKILL_LOG,
36
+ REPAIRED_SKILL_SESSIONS_MARKER,
37
+ SELFTUNE_CONFIG_DIR,
38
+ SIGNAL_LOG,
39
+ SKILL_LOG,
40
+ TELEMETRY_LOG,
41
+ CANONICAL_LOG,
42
+ } from "./constants.js";
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Types
46
+ // ---------------------------------------------------------------------------
47
+
48
+ interface UninstallResult {
49
+ dryRun: boolean;
50
+ schedule: { removed: boolean; details: string };
51
+ hooks: { removed: number; details: string };
52
+ agents: { removed: number; files: string[] };
53
+ logs: { removed: number; skipped: boolean; files: string[] };
54
+ config: { removed: boolean; path: string };
55
+ markers: { removed: number; files: string[] };
56
+ npm: { uninstalled: boolean; skipped: boolean };
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Step 1: Remove autonomy scheduling
61
+ // ---------------------------------------------------------------------------
62
+
63
+ async function removeScheduling(dryRun: boolean): Promise<{ removed: boolean; details: string }> {
64
+ // Try launchd first (macOS)
65
+ const label = "dev.selftune.orchestrate";
66
+ const plistPath = join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
67
+
68
+ if (existsSync(plistPath)) {
69
+ if (dryRun) {
70
+ return { removed: false, details: `Would remove launchd plist: ${plistPath}` };
71
+ }
72
+ try {
73
+ // Unload before removing
74
+ Bun.spawnSync(["launchctl", "unload", plistPath], { stderr: "pipe" });
75
+ unlinkSync(plistPath);
76
+ return { removed: true, details: `Removed launchd plist: ${plistPath}` };
77
+ } catch (err) {
78
+ return {
79
+ removed: false,
80
+ details: `Failed to remove launchd plist: ${err instanceof Error ? err.message : String(err)}`,
81
+ };
82
+ }
83
+ }
84
+
85
+ // Try OpenClaw cron jobs
86
+ if (dryRun) {
87
+ return { removed: false, details: "Would remove cron jobs via selftune cron remove" };
88
+ }
89
+ try {
90
+ const proc = Bun.spawnSync(["selftune", "cron", "remove"], {
91
+ stdout: "pipe",
92
+ stderr: "pipe",
93
+ });
94
+ if (proc.exitCode === 0) {
95
+ return { removed: true, details: "Removed cron jobs via selftune cron remove" };
96
+ }
97
+ } catch {
98
+ // selftune cron remove not available or failed — not critical
99
+ }
100
+
101
+ return { removed: false, details: "No scheduling artifacts found" };
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Step 2: Remove selftune hooks from settings.json
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /** Selftune hook scripts — used to identify which entries to remove. */
109
+ const SELFTUNE_HOOK_SCRIPTS = [
110
+ "hooks/prompt-log.ts",
111
+ "hooks/auto-activate.ts",
112
+ "hooks/skill-change-guard.ts",
113
+ "hooks/evolution-guard.ts",
114
+ "hooks/skill-eval.ts",
115
+ "hooks/session-stop.ts",
116
+ ];
117
+
118
+ function isSelfttuneHookEntry(entry: unknown): boolean {
119
+ if (typeof entry !== "object" || entry === null) return false;
120
+ const obj = entry as Record<string, unknown>;
121
+
122
+ // Check direct command
123
+ if (typeof obj.command === "string") {
124
+ return SELFTUNE_HOOK_SCRIPTS.some((script) => obj.command?.includes(script));
125
+ }
126
+
127
+ // Check hooks array (the nested structure used in settings.json)
128
+ if (Array.isArray(obj.hooks)) {
129
+ return obj.hooks.some(
130
+ (h: unknown) =>
131
+ typeof h === "object" &&
132
+ h !== null &&
133
+ typeof (h as Record<string, unknown>).command === "string" &&
134
+ SELFTUNE_HOOK_SCRIPTS.some((script) =>
135
+ ((h as Record<string, unknown>).command as string).includes(script),
136
+ ),
137
+ );
138
+ }
139
+
140
+ return false;
141
+ }
142
+
143
+ function removeHooksFromSettings(
144
+ dryRun: boolean,
145
+ settingsPath?: string,
146
+ ): { removed: number; details: string } {
147
+ const path = settingsPath ?? CLAUDE_SETTINGS_PATH;
148
+ if (!existsSync(path)) {
149
+ return { removed: 0, details: "No settings.json found" };
150
+ }
151
+
152
+ let settings: Record<string, unknown>;
153
+ try {
154
+ settings = JSON.parse(readFileSync(path, "utf-8"));
155
+ } catch {
156
+ return { removed: 0, details: "Failed to parse settings.json" };
157
+ }
158
+
159
+ const hooks = settings.hooks as Record<string, unknown[]> | undefined;
160
+ if (!hooks || typeof hooks !== "object") {
161
+ return { removed: 0, details: "No hooks section in settings.json" };
162
+ }
163
+
164
+ let totalRemoved = 0;
165
+
166
+ for (const key of Object.keys(hooks)) {
167
+ if (!Array.isArray(hooks[key])) continue;
168
+
169
+ const before = hooks[key].length;
170
+ hooks[key] = hooks[key].filter((entry) => !isSelfttuneHookEntry(entry));
171
+ const removed = before - hooks[key].length;
172
+ totalRemoved += removed;
173
+
174
+ // Clean up empty arrays
175
+ if (hooks[key].length === 0) {
176
+ delete hooks[key];
177
+ }
178
+ }
179
+
180
+ // Clean up empty hooks object
181
+ if (Object.keys(hooks).length === 0) {
182
+ delete settings.hooks;
183
+ }
184
+
185
+ if (totalRemoved > 0 && !dryRun) {
186
+ writeFileSync(path, JSON.stringify(settings, null, 2), "utf-8");
187
+ }
188
+
189
+ return {
190
+ removed: totalRemoved,
191
+ details: dryRun
192
+ ? `Would remove ${totalRemoved} selftune hook entries from ${path}`
193
+ : `Removed ${totalRemoved} selftune hook entries from ${path}`,
194
+ };
195
+ }
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // Step 3: Remove bundled Claude subagents
199
+ // ---------------------------------------------------------------------------
200
+
201
+ function removeAgents(dryRun: boolean): { removed: number; files: string[] } {
202
+ return removeInstalledAgentFiles({ dryRun });
203
+ }
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // Step 4: Remove JSONL log files
207
+ // ---------------------------------------------------------------------------
208
+
209
+ const LOG_FILES = [
210
+ TELEMETRY_LOG,
211
+ SKILL_LOG,
212
+ REPAIRED_SKILL_LOG,
213
+ CANONICAL_LOG,
214
+ QUERY_LOG,
215
+ EVOLUTION_AUDIT_LOG,
216
+ EVOLUTION_EVIDENCE_LOG,
217
+ ORCHESTRATE_RUN_LOG,
218
+ SIGNAL_LOG,
219
+ ORCHESTRATE_LOCK,
220
+ ];
221
+
222
+ function removeLogs(dryRun: boolean): { removed: number; files: string[] } {
223
+ const removed: string[] = [];
224
+
225
+ for (const logPath of LOG_FILES) {
226
+ if (existsSync(logPath)) {
227
+ if (!dryRun) {
228
+ try {
229
+ unlinkSync(logPath);
230
+ removed.push(logPath);
231
+ } catch {
232
+ // Skip files we can't remove
233
+ }
234
+ } else {
235
+ removed.push(logPath);
236
+ }
237
+ }
238
+ }
239
+
240
+ return { removed: removed.length, files: removed };
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // Step 5: Remove config directory
245
+ // ---------------------------------------------------------------------------
246
+
247
+ function removeConfig(dryRun: boolean): { removed: boolean; path: string } {
248
+ if (!existsSync(SELFTUNE_CONFIG_DIR)) {
249
+ return { removed: false, path: SELFTUNE_CONFIG_DIR };
250
+ }
251
+
252
+ if (!dryRun) {
253
+ try {
254
+ rmSync(SELFTUNE_CONFIG_DIR, { recursive: true, force: true });
255
+ return { removed: true, path: SELFTUNE_CONFIG_DIR };
256
+ } catch {
257
+ return { removed: false, path: SELFTUNE_CONFIG_DIR };
258
+ }
259
+ }
260
+
261
+ return { removed: false, path: SELFTUNE_CONFIG_DIR };
262
+ }
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // Step 6: Remove ingest marker files
266
+ // ---------------------------------------------------------------------------
267
+
268
+ const MARKER_FILES = [
269
+ CLAUDE_CODE_MARKER,
270
+ CODEX_INGEST_MARKER,
271
+ OPENCODE_INGEST_MARKER,
272
+ OPENCLAW_INGEST_MARKER,
273
+ REPAIRED_SKILL_SESSIONS_MARKER,
274
+ ];
275
+
276
+ function removeMarkers(dryRun: boolean): { removed: number; files: string[] } {
277
+ const removed: string[] = [];
278
+
279
+ for (const markerPath of MARKER_FILES) {
280
+ if (existsSync(markerPath)) {
281
+ if (!dryRun) {
282
+ try {
283
+ unlinkSync(markerPath);
284
+ removed.push(markerPath);
285
+ } catch {
286
+ // Skip files we can't remove
287
+ }
288
+ } else {
289
+ removed.push(markerPath);
290
+ }
291
+ }
292
+ }
293
+
294
+ return { removed: removed.length, files: removed };
295
+ }
296
+
297
+ // ---------------------------------------------------------------------------
298
+ // Step 7: npm uninstall
299
+ // ---------------------------------------------------------------------------
300
+
301
+ async function npmUninstall(dryRun: boolean): Promise<{ uninstalled: boolean }> {
302
+ if (dryRun) {
303
+ return { uninstalled: false };
304
+ }
305
+
306
+ try {
307
+ const proc = Bun.spawnSync(["npm", "uninstall", "-g", "selftune"], {
308
+ stdout: "pipe",
309
+ stderr: "pipe",
310
+ });
311
+ return { uninstalled: proc.exitCode === 0 };
312
+ } catch {
313
+ return { uninstalled: false };
314
+ }
315
+ }
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // Main orchestrator
319
+ // ---------------------------------------------------------------------------
320
+
321
+ export interface UninstallOptions {
322
+ dryRun: boolean;
323
+ keepLogs: boolean;
324
+ npmUninstall: boolean;
325
+ settingsPath?: string;
326
+ }
327
+
328
+ export async function uninstall(options: UninstallOptions): Promise<UninstallResult> {
329
+ const { dryRun, keepLogs, settingsPath } = options;
330
+
331
+ // Step 1: Remove scheduling
332
+ const schedule = await removeScheduling(dryRun);
333
+
334
+ // Step 2: Remove hooks
335
+ const hooks = removeHooksFromSettings(dryRun, settingsPath);
336
+
337
+ // Step 3: Remove bundled Claude subagents
338
+ const agents = removeAgents(dryRun);
339
+
340
+ // Step 4: Remove logs
341
+ const logs = keepLogs
342
+ ? { removed: 0, skipped: true, files: [] }
343
+ : { ...removeLogs(dryRun), skipped: false };
344
+
345
+ // Step 5: Remove config directory
346
+ const config = removeConfig(dryRun);
347
+
348
+ // Step 6: Remove ingest markers
349
+ const markers = removeMarkers(dryRun);
350
+
351
+ // Step 7: npm uninstall (optional)
352
+ const npm = options.npmUninstall
353
+ ? { ...(await npmUninstall(dryRun)), skipped: false }
354
+ : { uninstalled: false, skipped: true };
355
+
356
+ return { dryRun, schedule, hooks, agents, logs, config, markers, npm };
357
+ }
358
+
359
+ // ---------------------------------------------------------------------------
360
+ // CLI entry point
361
+ // ---------------------------------------------------------------------------
362
+
363
+ export async function cliMain(): Promise<void> {
364
+ const { values } = parseArgs({
365
+ options: {
366
+ "dry-run": { type: "boolean", default: false },
367
+ "keep-logs": { type: "boolean", default: false },
368
+ "npm-uninstall": { type: "boolean", default: false },
369
+ help: { type: "boolean", default: false },
370
+ },
371
+ strict: true,
372
+ });
373
+
374
+ if (values.help) {
375
+ console.log(`selftune uninstall — Clean removal of all selftune data and configuration
376
+
377
+ Usage:
378
+ selftune uninstall [options]
379
+
380
+ Options:
381
+ --dry-run Preview what would be removed without deleting anything
382
+ --keep-logs Preserve JSONL telemetry logs (remove everything else)
383
+ --npm-uninstall Also run 'npm uninstall -g selftune'
384
+ --help Show this help message
385
+
386
+ Removes:
387
+ 1. Autonomy scheduling (launchd/cron/systemd)
388
+ 2. Selftune hooks from ~/.claude/settings.json (preserves user hooks)
389
+ 3. Selftune-managed Claude subagents from ~/.claude/agents/
390
+ 4. JSONL telemetry logs from ~/.claude/
391
+ 5. Selftune config directory (~/.selftune/)
392
+ 6. Ingest marker files
393
+ 7. npm global package (with --npm-uninstall)`);
394
+ process.exit(0);
395
+ }
396
+
397
+ const result = await uninstall({
398
+ dryRun: values["dry-run"] ?? false,
399
+ keepLogs: values["keep-logs"] ?? false,
400
+ npmUninstall: values["npm-uninstall"] ?? false,
401
+ });
402
+
403
+ console.log(JSON.stringify(result, null, 2));
404
+ process.exit(0);
405
+ }
406
+
407
+ if (import.meta.main) {
408
+ cliMain().catch((err) => {
409
+ console.error(`[FATAL] ${err}`);
410
+ process.exit(1);
411
+ });
412
+ }
@@ -1,10 +1,12 @@
1
1
  import { existsSync, writeFileSync } from "node:fs";
2
+
2
3
  import {
3
4
  type CanonicalPlatform,
4
5
  type CanonicalRecord,
5
6
  type CanonicalRecordKind,
6
7
  isCanonicalRecord,
7
8
  } from "@selftune/telemetry-contract";
9
+
8
10
  import { CANONICAL_LOG } from "../constants.js";
9
11
  import { readJsonl } from "./jsonl.js";
10
12