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
@@ -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
 
@@ -14,6 +14,7 @@ import {
14
14
  writeFileSync,
15
15
  } from "node:fs";
16
16
  import { dirname } from "node:path";
17
+
17
18
  import { createLogger } from "./logging.js";
18
19
  import type { LogType } from "./schema-validator.js";
19
20
  import { validateRecord } from "./schema-validator.js";
@@ -123,6 +123,9 @@ function sleep(ms: number): Promise<void> {
123
123
  // Call LLM via agent subprocess
124
124
  // ---------------------------------------------------------------------------
125
125
 
126
+ /** Effort level for Claude CLI (controls thinking depth). Opus 4.6 only for 'max'. */
127
+ export type EffortLevel = "low" | "medium" | "high" | "max";
128
+
126
129
  /** Call LLM via agent subprocess (claude/codex/opencode). Returns raw text. */
127
130
  export async function callViaAgent(
128
131
  systemPrompt: string,
@@ -130,6 +133,7 @@ export async function callViaAgent(
130
133
  agent: string,
131
134
  modelFlag?: string,
132
135
  retryOpts?: RetryOptions,
136
+ effort?: EffortLevel,
133
137
  ): Promise<string> {
134
138
  // Write prompt to temp file to avoid shell quoting issues
135
139
  const promptFile = join(tmpdir(), `selftune-llm-${Date.now()}.txt`);
@@ -145,6 +149,9 @@ export async function callViaAgent(
145
149
  const resolved = resolveModelFlag(modelFlag);
146
150
  cmd.push("--model", resolved);
147
151
  }
152
+ if (effort) {
153
+ cmd.push("--effort", effort);
154
+ }
148
155
  } else if (agent === "codex") {
149
156
  cmd = ["codex", "exec", "--skip-git-repo-check", promptContent];
150
157
  } else if (agent === "opencode") {
@@ -173,9 +180,10 @@ export async function callViaAgent(
173
180
  env: { ...process.env, CLAUDECODE: "" },
174
181
  });
175
182
 
176
- // Longer timeout for heavier models (sonnet/opus take longer than haiku)
183
+ // Longer timeout for heavier models and thinking effort levels
177
184
  const isLightModel = modelFlag === "haiku" || modelFlag?.includes("haiku");
178
- const timeoutMs = isLightModel ? 120_000 : 300_000;
185
+ const isThinking = effort === "high" || effort === "max";
186
+ const timeoutMs = isThinking ? 600_000 : isLightModel ? 120_000 : 300_000;
179
187
  const timeout = setTimeout(() => proc.kill(), timeoutMs);
180
188
  const exitCode = await proc.exited;
181
189
  clearTimeout(timeout);
@@ -210,6 +218,125 @@ export async function callViaAgent(
210
218
  }
211
219
  }
212
220
 
221
+ // ---------------------------------------------------------------------------
222
+ // Call LLM via named subagent (multi-turn, agentic)
223
+ // ---------------------------------------------------------------------------
224
+
225
+ /** Options for calling a named Claude Code subagent. */
226
+ export interface SubagentCallOptions {
227
+ /** Name of the subagent (synced into ~/.claude/agents/ by selftune init/update). */
228
+ agentName: string;
229
+ /** The task prompt for the subagent. */
230
+ prompt: string;
231
+ /** Optional system prompt appended to the agent's built-in instructions. */
232
+ appendSystemPrompt?: string;
233
+ /** Maximum agentic turns (default: 8). */
234
+ maxTurns?: number;
235
+ /** Model override (overrides the agent's frontmatter model). */
236
+ modelFlag?: string;
237
+ /** Effort level for thinking depth. */
238
+ effort?: EffortLevel;
239
+ /** Retry options. */
240
+ retryOpts?: RetryOptions;
241
+ /** Tools the agent is allowed to use without prompting. */
242
+ allowedTools?: string[];
243
+ }
244
+
245
+ /**
246
+ * Call a named Claude Code subagent in print mode. The subagent runs its
247
+ * multi-turn workflow (reading files, running commands, etc.) and returns
248
+ * the final text output.
249
+ *
250
+ * Unlike callViaAgent(), this does NOT use --bare (agents need discovery)
251
+ * and passes --agent + --max-turns for agentic multi-turn behavior.
252
+ * Only supports the claude CLI.
253
+ */
254
+ export async function callViaSubagent(options: SubagentCallOptions): Promise<string> {
255
+ const {
256
+ agentName,
257
+ prompt,
258
+ appendSystemPrompt,
259
+ maxTurns = 8,
260
+ modelFlag,
261
+ effort,
262
+ retryOpts,
263
+ allowedTools,
264
+ } = options;
265
+
266
+ const cmd: string[] = [
267
+ "claude",
268
+ "-p",
269
+ prompt,
270
+ "--agent",
271
+ agentName,
272
+ "--max-turns",
273
+ String(maxTurns),
274
+ ];
275
+
276
+ if (appendSystemPrompt) {
277
+ cmd.push("--append-system-prompt", appendSystemPrompt);
278
+ }
279
+ if (modelFlag) {
280
+ const resolved = resolveModelFlag(modelFlag);
281
+ cmd.push("--model", resolved);
282
+ }
283
+ if (effort) {
284
+ cmd.push("--effort", effort);
285
+ }
286
+ if (allowedTools && allowedTools.length > 0) {
287
+ cmd.push("--allowedTools", ...allowedTools);
288
+ }
289
+ // Skip permissions since this runs non-interactively in a pipeline
290
+ cmd.push("--dangerously-skip-permissions");
291
+
292
+ const maxRetries = retryOpts?.maxRetries ?? DEFAULT_MAX_RETRIES;
293
+ const initialBackoffMs = retryOpts?.initialBackoffMs ?? DEFAULT_INITIAL_BACKOFF_MS;
294
+ let lastError: Error | undefined;
295
+
296
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
297
+ if (attempt > 0) {
298
+ const backoffMs = initialBackoffMs * 2 ** (attempt - 1);
299
+ logger.warn(
300
+ `Retry ${attempt}/${maxRetries} for subagent '${agentName}' after ${backoffMs}ms backoff`,
301
+ );
302
+ await sleep(backoffMs);
303
+ }
304
+
305
+ try {
306
+ const proc = Bun.spawn(cmd, {
307
+ stdout: "pipe",
308
+ stderr: "pipe",
309
+ env: { ...process.env, CLAUDECODE: "" },
310
+ });
311
+
312
+ // Subagents get a generous timeout — they do multi-turn work
313
+ const isThinking = effort === "high" || effort === "max";
314
+ const timeoutMs = isThinking ? 600_000 : 300_000;
315
+ const timeout = setTimeout(() => proc.kill(), timeoutMs);
316
+ const exitCode = await proc.exited;
317
+ clearTimeout(timeout);
318
+
319
+ if (exitCode !== 0) {
320
+ const stderr = await new Response(proc.stderr).text();
321
+ throw new Error(
322
+ `Subagent '${agentName}' exited with code ${exitCode}.\nstderr: ${stderr.slice(0, 500)}`,
323
+ );
324
+ }
325
+
326
+ const raw = await new Response(proc.stdout).text();
327
+ return raw;
328
+ } catch (err) {
329
+ lastError = err instanceof Error ? err : new Error(String(err));
330
+ if (!isTransientError(lastError) || attempt === maxRetries) {
331
+ throw lastError;
332
+ }
333
+ logger.warn(`Transient failure on attempt ${attempt + 1}: ${lastError.message}`);
334
+ }
335
+ }
336
+
337
+ throw lastError ?? new Error("callViaSubagent: unexpected retry loop exit");
338
+ }
339
+
213
340
  // ---------------------------------------------------------------------------
214
341
  // Unified dispatcher
215
342
  // ---------------------------------------------------------------------------
@@ -220,9 +347,10 @@ export async function callLlm(
220
347
  userPrompt: string,
221
348
  agent: string,
222
349
  modelFlag?: string,
350
+ effort?: EffortLevel,
223
351
  ): Promise<string> {
224
352
  if (!agent) {
225
353
  throw new Error("Agent must be specified for callLlm");
226
354
  }
227
- return callViaAgent(systemPrompt, userPrompt, agent, modelFlag);
355
+ return callViaAgent(systemPrompt, userPrompt, agent, modelFlag, undefined, effort);
228
356
  }
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
+
3
4
  import { REPAIRED_SKILL_LOG, REPAIRED_SKILL_SESSIONS_MARKER, SKILL_LOG } from "../constants.js";
4
5
  import type { SkillUsageRecord } from "../types.js";
5
6
  import { loadMarker, readJsonl, saveMarker } from "./jsonl.js";
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
6
6
  import { basename, dirname } from "node:path";
7
+
7
8
  import { CLAUDE_CODE_PROJECTS_DIR } from "../constants.js";
8
9
  import type { SessionTelemetryRecord, TranscriptMetrics } from "../types.js";
9
10
  import { isActionableQueryText } from "./query-filter.js";
@@ -64,7 +64,7 @@ export function buildBatchTriggerCheckPrompt(description: string, queries: strin
64
64
  * original query order. Defaults to false for unparseable or missing lines.
65
65
  */
66
66
  export function parseBatchTriggerResponse(response: string, queryCount: number): boolean[] {
67
- const results: boolean[] = new Array(queryCount).fill(false);
67
+ const results: boolean[] = Array.from({ length: queryCount }, () => false);
68
68
  const lines = response.trim().split("\n");
69
69
 
70
70
  for (const line of lines) {
@@ -36,7 +36,7 @@ export function parseWorkflowsSection(content: string): CodifiedWorkflow[] {
36
36
  // Find the end of the section (next ## heading or EOF)
37
37
  let sectionEnd = lines.length;
38
38
  for (let i = sectionStart; i < lines.length; i++) {
39
- if (/^## /.test(lines[i]) && lines[i].trim() !== "## Workflows") {
39
+ if (lines[i].startsWith("## ") && lines[i].trim() !== "## Workflows") {
40
40
  sectionEnd = i;
41
41
  break;
42
42
  }
@@ -155,7 +155,7 @@ export function appendWorkflow(content: string, workflow: CodifiedWorkflow): str
155
155
  // Find the end of the workflows section (next ## heading or EOF)
156
156
  let sectionEnd = lines.length;
157
157
  for (let i = sectionStart + 1; i < lines.length; i++) {
158
- if (/^## /.test(lines[i])) {
158
+ if (lines[i].startsWith("## ")) {
159
159
  sectionEnd = i;
160
160
  break;
161
161
  }
@@ -210,7 +210,7 @@ export function removeWorkflow(content: string, name: string): string {
210
210
  // Find the end of the workflows section
211
211
  let sectionEnd = lines.length;
212
212
  for (let i = sectionStart + 1; i < lines.length; i++) {
213
- if (/^## /.test(lines[i])) {
213
+ if (lines[i].startsWith("## ")) {
214
214
  sectionEnd = i;
215
215
  break;
216
216
  }
@@ -226,7 +226,7 @@ export function removeWorkflow(content: string, name: string): string {
226
226
  // Find the end of this subsection (next ### or ## or sectionEnd)
227
227
  subEnd = sectionEnd;
228
228
  for (let j = i + 1; j < sectionEnd; j++) {
229
- if (/^### /.test(lines[j])) {
229
+ if (lines[j].startsWith("### ")) {
230
230
  subEnd = j;
231
231
  break;
232
232
  }
@@ -255,7 +255,7 @@ export function removeWorkflow(content: string, name: string): string {
255
255
 
256
256
  // Check if the workflows section is now empty
257
257
  const remaining = [...before.slice(sectionStart + 1), ...after.slice(0, sectionEnd - removeTo)];
258
- const hasRemainingWorkflows = remaining.some((l) => /^### /.test(l));
258
+ const hasRemainingWorkflows = remaining.some((l) => l.startsWith("### "));
259
259
 
260
260
  if (!hasRemainingWorkflows) {
261
261
  // Remove the entire ## Workflows section (heading + any blank lines)
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
12
12
  import { parseArgs } from "node:util";
13
+
13
14
  import { getDb } from "../localdb/db.js";
14
15
  import { querySessionTelemetry, querySkillUsageRecords } from "../localdb/queries.js";
15
16
  import type {