selftune 0.1.4 → 0.2.1

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 (153) hide show
  1. package/.claude/agents/diagnosis-analyst.md +156 -0
  2. package/.claude/agents/evolution-reviewer.md +180 -0
  3. package/.claude/agents/integration-guide.md +212 -0
  4. package/.claude/agents/pattern-analyst.md +160 -0
  5. package/CHANGELOG.md +46 -1
  6. package/README.md +105 -257
  7. package/apps/local-dashboard/dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  8. package/apps/local-dashboard/dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  9. package/apps/local-dashboard/dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  10. package/apps/local-dashboard/dist/assets/index-C4EOTFZ2.js +15 -0
  11. package/apps/local-dashboard/dist/assets/index-bl-Webyd.css +1 -0
  12. package/apps/local-dashboard/dist/assets/vendor-react-U7zYD9Rg.js +60 -0
  13. package/apps/local-dashboard/dist/assets/vendor-table-B7VF2Ipl.js +26 -0
  14. package/apps/local-dashboard/dist/assets/vendor-ui-D7_zX_qy.js +346 -0
  15. package/apps/local-dashboard/dist/favicon.png +0 -0
  16. package/apps/local-dashboard/dist/index.html +17 -0
  17. package/apps/local-dashboard/dist/logo.png +0 -0
  18. package/apps/local-dashboard/dist/logo.svg +9 -0
  19. package/assets/BeforeAfter.gif +0 -0
  20. package/assets/FeedbackLoop.gif +0 -0
  21. package/assets/logo.svg +9 -0
  22. package/assets/skill-health-badge.svg +20 -0
  23. package/cli/selftune/activation-rules.ts +171 -0
  24. package/cli/selftune/badge/badge-data.ts +108 -0
  25. package/cli/selftune/badge/badge-svg.ts +212 -0
  26. package/cli/selftune/badge/badge.ts +99 -0
  27. package/cli/selftune/canonical-export.ts +183 -0
  28. package/cli/selftune/constants.ts +103 -1
  29. package/cli/selftune/contribute/bundle.ts +314 -0
  30. package/cli/selftune/contribute/contribute.ts +214 -0
  31. package/cli/selftune/contribute/sanitize.ts +162 -0
  32. package/cli/selftune/cron/setup.ts +266 -0
  33. package/cli/selftune/dashboard-contract.ts +202 -0
  34. package/cli/selftune/dashboard-server.ts +1049 -0
  35. package/cli/selftune/dashboard.ts +43 -156
  36. package/cli/selftune/eval/baseline.ts +248 -0
  37. package/cli/selftune/eval/composability-v2.ts +273 -0
  38. package/cli/selftune/eval/composability.ts +117 -0
  39. package/cli/selftune/eval/generate-unit-tests.ts +143 -0
  40. package/cli/selftune/eval/hooks-to-evals.ts +101 -16
  41. package/cli/selftune/eval/import-skillsbench.ts +221 -0
  42. package/cli/selftune/eval/synthetic-evals.ts +172 -0
  43. package/cli/selftune/eval/unit-test-cli.ts +152 -0
  44. package/cli/selftune/eval/unit-test.ts +196 -0
  45. package/cli/selftune/evolution/deploy-proposal.ts +142 -1
  46. package/cli/selftune/evolution/evidence.ts +26 -0
  47. package/cli/selftune/evolution/evolve-body.ts +586 -0
  48. package/cli/selftune/evolution/evolve.ts +825 -116
  49. package/cli/selftune/evolution/extract-patterns.ts +105 -16
  50. package/cli/selftune/evolution/pareto.ts +314 -0
  51. package/cli/selftune/evolution/propose-body.ts +171 -0
  52. package/cli/selftune/evolution/propose-description.ts +100 -2
  53. package/cli/selftune/evolution/propose-routing.ts +166 -0
  54. package/cli/selftune/evolution/refine-body.ts +141 -0
  55. package/cli/selftune/evolution/rollback.ts +21 -4
  56. package/cli/selftune/evolution/validate-body.ts +254 -0
  57. package/cli/selftune/evolution/validate-proposal.ts +257 -35
  58. package/cli/selftune/evolution/validate-routing.ts +177 -0
  59. package/cli/selftune/grading/auto-grade.ts +200 -0
  60. package/cli/selftune/grading/grade-session.ts +513 -42
  61. package/cli/selftune/grading/pre-gates.ts +104 -0
  62. package/cli/selftune/grading/results.ts +42 -0
  63. package/cli/selftune/hooks/auto-activate.ts +185 -0
  64. package/cli/selftune/hooks/evolution-guard.ts +165 -0
  65. package/cli/selftune/hooks/prompt-log.ts +172 -2
  66. package/cli/selftune/hooks/session-stop.ts +123 -3
  67. package/cli/selftune/hooks/skill-change-guard.ts +112 -0
  68. package/cli/selftune/hooks/skill-eval.ts +119 -3
  69. package/cli/selftune/index.ts +415 -48
  70. package/cli/selftune/ingestors/claude-replay.ts +377 -0
  71. package/cli/selftune/ingestors/codex-rollout.ts +345 -46
  72. package/cli/selftune/ingestors/codex-wrapper.ts +207 -39
  73. package/cli/selftune/ingestors/openclaw-ingest.ts +573 -0
  74. package/cli/selftune/ingestors/opencode-ingest.ts +193 -17
  75. package/cli/selftune/init.ts +376 -16
  76. package/cli/selftune/last.ts +14 -5
  77. package/cli/selftune/localdb/db.ts +63 -0
  78. package/cli/selftune/localdb/materialize.ts +428 -0
  79. package/cli/selftune/localdb/queries.ts +376 -0
  80. package/cli/selftune/localdb/schema.ts +204 -0
  81. package/cli/selftune/memory/writer.ts +447 -0
  82. package/cli/selftune/monitoring/watch.ts +90 -16
  83. package/cli/selftune/normalization.ts +682 -0
  84. package/cli/selftune/observability.ts +19 -44
  85. package/cli/selftune/orchestrate.ts +1073 -0
  86. package/cli/selftune/quickstart.ts +203 -0
  87. package/cli/selftune/repair/skill-usage.ts +576 -0
  88. package/cli/selftune/schedule.ts +561 -0
  89. package/cli/selftune/status.ts +59 -33
  90. package/cli/selftune/sync.ts +627 -0
  91. package/cli/selftune/types.ts +525 -5
  92. package/cli/selftune/utils/canonical-log.ts +45 -0
  93. package/cli/selftune/utils/frontmatter.ts +217 -0
  94. package/cli/selftune/utils/hooks.ts +41 -0
  95. package/cli/selftune/utils/html.ts +27 -0
  96. package/cli/selftune/utils/llm-call.ts +103 -19
  97. package/cli/selftune/utils/math.ts +10 -0
  98. package/cli/selftune/utils/query-filter.ts +139 -0
  99. package/cli/selftune/utils/skill-discovery.ts +340 -0
  100. package/cli/selftune/utils/skill-log.ts +68 -0
  101. package/cli/selftune/utils/skill-usage-confidence.ts +18 -0
  102. package/cli/selftune/utils/transcript.ts +307 -26
  103. package/cli/selftune/utils/trigger-check.ts +89 -0
  104. package/cli/selftune/utils/tui.ts +156 -0
  105. package/cli/selftune/workflows/discover.ts +254 -0
  106. package/cli/selftune/workflows/skill-md-writer.ts +288 -0
  107. package/cli/selftune/workflows/workflows.ts +188 -0
  108. package/package.json +28 -11
  109. package/packages/telemetry-contract/README.md +11 -0
  110. package/packages/telemetry-contract/fixtures/golden.json +87 -0
  111. package/packages/telemetry-contract/fixtures/golden.test.ts +42 -0
  112. package/packages/telemetry-contract/index.ts +1 -0
  113. package/packages/telemetry-contract/package.json +19 -0
  114. package/packages/telemetry-contract/src/index.ts +2 -0
  115. package/packages/telemetry-contract/src/types.ts +163 -0
  116. package/packages/telemetry-contract/src/validators.ts +109 -0
  117. package/skill/SKILL.md +180 -33
  118. package/skill/Workflows/AutoActivation.md +145 -0
  119. package/skill/Workflows/Badge.md +124 -0
  120. package/skill/Workflows/Baseline.md +144 -0
  121. package/skill/Workflows/Composability.md +107 -0
  122. package/skill/Workflows/Contribute.md +94 -0
  123. package/skill/Workflows/Cron.md +132 -0
  124. package/skill/Workflows/Dashboard.md +214 -0
  125. package/skill/Workflows/Doctor.md +63 -14
  126. package/skill/Workflows/Evals.md +110 -18
  127. package/skill/Workflows/EvolutionMemory.md +154 -0
  128. package/skill/Workflows/Evolve.md +181 -21
  129. package/skill/Workflows/EvolveBody.md +159 -0
  130. package/skill/Workflows/Grade.md +36 -31
  131. package/skill/Workflows/ImportSkillsBench.md +117 -0
  132. package/skill/Workflows/Ingest.md +142 -21
  133. package/skill/Workflows/Initialize.md +91 -23
  134. package/skill/Workflows/Orchestrate.md +139 -0
  135. package/skill/Workflows/Replay.md +91 -0
  136. package/skill/Workflows/Rollback.md +23 -4
  137. package/skill/Workflows/Schedule.md +61 -0
  138. package/skill/Workflows/Sync.md +88 -0
  139. package/skill/Workflows/UnitTest.md +150 -0
  140. package/skill/Workflows/Watch.md +33 -1
  141. package/skill/Workflows/Workflows.md +129 -0
  142. package/skill/assets/activation-rules-default.json +26 -0
  143. package/skill/assets/multi-skill-settings.json +63 -0
  144. package/skill/assets/single-skill-settings.json +57 -0
  145. package/skill/references/invocation-taxonomy.md +2 -2
  146. package/skill/references/logs.md +164 -2
  147. package/skill/references/setup-patterns.md +65 -0
  148. package/skill/references/version-history.md +40 -0
  149. package/skill/settings_snippet.json +23 -0
  150. package/templates/activation-rules-default.json +27 -0
  151. package/templates/multi-skill-settings.json +64 -0
  152. package/templates/single-skill-settings.json +58 -0
  153. package/dashboard/index.html +0 -1119
@@ -8,16 +8,25 @@
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
12
  */
12
13
 
13
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
+ import {
15
+ copyFileSync,
16
+ existsSync,
17
+ mkdirSync,
18
+ readdirSync,
19
+ readFileSync,
20
+ writeFileSync,
21
+ } from "node:fs";
14
22
  import { homedir } from "node:os";
15
23
  import { dirname, join, resolve } from "node:path";
16
24
  import { fileURLToPath } from "node:url";
17
25
  import { parseArgs } from "node:util";
18
26
 
19
- import { SELFTUNE_CONFIG_DIR, SELFTUNE_CONFIG_PATH } from "./constants.js";
27
+ import { CLAUDE_CODE_HOOK_KEYS, SELFTUNE_CONFIG_DIR, SELFTUNE_CONFIG_PATH } from "./constants.js";
20
28
  import type { SelftuneConfig } from "./types.js";
29
+ import { hookKeyHasSelftuneEntry } from "./utils/hooks.js";
21
30
  import { detectAgent } from "./utils/llm-call.js";
22
31
 
23
32
  // ---------------------------------------------------------------------------
@@ -37,6 +46,7 @@ const VALID_AGENT_TYPES: SelftuneConfig["agent_type"][] = [
37
46
  "claude_code",
38
47
  "codex",
39
48
  "opencode",
49
+ "openclaw",
40
50
  "unknown",
41
51
  ];
42
52
 
@@ -44,6 +54,7 @@ const AGENT_TYPE_CLI_MAP: Record<string, string> = {
44
54
  claude_code: "claude",
45
55
  codex: "codex",
46
56
  opencode: "opencode",
57
+ openclaw: "openclaw",
47
58
  };
48
59
 
49
60
  function agentTypeToCli(agentType: string): string | null {
@@ -82,6 +93,12 @@ export function detectAgentType(
82
93
  return "opencode";
83
94
  }
84
95
 
96
+ // OpenClaw: agents directory or binary
97
+ const openclawDir = join(home, ".openclaw", "agents");
98
+ if (existsSync(openclawDir) || Bun.which("openclaw")) {
99
+ return "openclaw";
100
+ }
101
+
85
102
  return "unknown";
86
103
  }
87
104
 
@@ -116,8 +133,6 @@ export function determineLlmMode(agentCli: string | null): {
116
133
  // Hook detection (Claude Code only)
117
134
  // ---------------------------------------------------------------------------
118
135
 
119
- const REQUIRED_HOOK_KEYS = ["prompt-submit", "post-tool-use", "session-stop"] as const;
120
-
121
136
  /**
122
137
  * Check if the selftune hooks are configured in Claude Code settings.
123
138
  */
@@ -130,15 +145,10 @@ export function checkClaudeCodeHooks(settingsPath: string): boolean {
130
145
  const hooks = settings?.hooks;
131
146
  if (!hooks || typeof hooks !== "object") return false;
132
147
 
133
- for (const key of REQUIRED_HOOK_KEYS) {
134
- const entries = hooks[key];
135
- if (!Array.isArray(entries) || entries.length === 0) return false;
136
- // Check that at least one entry references selftune
137
- const hasSelftune = entries.some(
138
- (e: { command?: string }) =>
139
- typeof e.command === "string" && e.command.includes("selftune"),
140
- );
141
- if (!hasSelftune) return false;
148
+ for (const key of CLAUDE_CODE_HOOK_KEYS) {
149
+ if (!hookKeyHasSelftuneEntry(hooks, key)) {
150
+ return false;
151
+ }
142
152
  }
143
153
 
144
154
  return true;
@@ -147,6 +157,274 @@ export function checkClaudeCodeHooks(settingsPath: string): boolean {
147
157
  }
148
158
  }
149
159
 
160
+ // ---------------------------------------------------------------------------
161
+ // Hook installation (Claude Code only)
162
+ // ---------------------------------------------------------------------------
163
+
164
+ /** Bundled settings snippet (ships with the npm package). */
165
+ const SETTINGS_SNIPPET_PATH = resolve(
166
+ dirname(import.meta.path),
167
+ "..",
168
+ "..",
169
+ "skill",
170
+ "settings_snippet.json",
171
+ );
172
+
173
+ /**
174
+ * Install selftune hooks into ~/.claude/settings.json by merging entries
175
+ * from the bundled settings_snippet.json.
176
+ *
177
+ * - Creates settings.json if it does not exist
178
+ * - Creates the hooks section if it does not exist
179
+ * - Only adds hook entries for keys that don't already have a selftune entry
180
+ * - Never overwrites existing user hooks
181
+ *
182
+ * Returns the list of hook keys that were added.
183
+ */
184
+ export function installClaudeCodeHooks(options?: {
185
+ settingsPath?: string;
186
+ snippetPath?: string;
187
+ cliPath?: string;
188
+ }): string[] {
189
+ const settingsPath = options?.settingsPath ?? join(homedir(), ".claude", "settings.json");
190
+ const snippetPath = options?.snippetPath ?? SETTINGS_SNIPPET_PATH;
191
+
192
+ // Read the snippet
193
+ if (!existsSync(snippetPath)) {
194
+ console.error(`[WARN] Hook snippet not found at ${snippetPath}, skipping hook installation`);
195
+ return [];
196
+ }
197
+
198
+ let snippet: Record<string, unknown>;
199
+ try {
200
+ snippet = JSON.parse(readFileSync(snippetPath, "utf-8"));
201
+ } catch {
202
+ console.error(`[WARN] Failed to parse hook snippet at ${snippetPath}`);
203
+ return [];
204
+ }
205
+
206
+ const snippetHooks = snippet.hooks as Record<string, unknown[]> | undefined;
207
+ if (!snippetHooks || typeof snippetHooks !== "object") {
208
+ console.error("[WARN] Hook snippet has no 'hooks' section");
209
+ return [];
210
+ }
211
+
212
+ // Read existing settings (or start with empty object)
213
+ let settings: Record<string, unknown> = {};
214
+ if (existsSync(settingsPath)) {
215
+ try {
216
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
217
+ } catch {
218
+ console.error(`[WARN] Failed to parse ${settingsPath}, starting with empty settings`);
219
+ settings = {};
220
+ }
221
+ }
222
+
223
+ // Ensure hooks section exists
224
+ if (!settings.hooks || typeof settings.hooks !== "object") {
225
+ settings.hooks = {};
226
+ }
227
+ const existingHooks = settings.hooks as Record<string, unknown[]>;
228
+
229
+ // Resolve the CLI hooks directory for path substitution
230
+ const cliPath = options?.cliPath;
231
+ const hooksDir = cliPath ? `${dirname(cliPath)}/hooks` : null;
232
+
233
+ const addedKeys: string[] = [];
234
+
235
+ for (const key of Object.keys(snippetHooks)) {
236
+ // Skip if this key already has a selftune entry
237
+ if (hookKeyHasSelftuneEntry(existingHooks, key)) {
238
+ continue;
239
+ }
240
+
241
+ // Get the snippet entries for this key, replacing /PATH/TO/ with actual path
242
+ let entries = snippetHooks[key];
243
+ if (hooksDir) {
244
+ // Deep clone and substitute paths
245
+ const raw = JSON.stringify(entries).replace(/\/PATH\/TO\/cli\/selftune\/hooks/g, hooksDir);
246
+ entries = JSON.parse(raw);
247
+ }
248
+
249
+ // Merge: append to existing array or create new one
250
+ if (Array.isArray(existingHooks[key])) {
251
+ existingHooks[key] = [...existingHooks[key], ...entries];
252
+ } else {
253
+ existingHooks[key] = entries;
254
+ }
255
+
256
+ addedKeys.push(key);
257
+ }
258
+
259
+ if (addedKeys.length > 0) {
260
+ // Ensure ~/.claude/ directory exists
261
+ mkdirSync(dirname(settingsPath), { recursive: true });
262
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
263
+ }
264
+
265
+ return addedKeys;
266
+ }
267
+
268
+ // ---------------------------------------------------------------------------
269
+ // Agent file installation
270
+ // ---------------------------------------------------------------------------
271
+
272
+ /** Bundled agent files directory (ships with the npm package). */
273
+ const BUNDLED_AGENTS_DIR = resolve(dirname(import.meta.path), "..", "..", ".claude", "agents");
274
+
275
+ /**
276
+ * Copy bundled agent markdown files to ~/.claude/agents/.
277
+ * Returns a list of file names that were copied (skips files that already exist
278
+ * unless `force` is true).
279
+ */
280
+ export function installAgentFiles(options?: { homeDir?: string; force?: boolean }): string[] {
281
+ const home = options?.homeDir ?? homedir();
282
+ const force = options?.force ?? false;
283
+ const targetDir = join(home, ".claude", "agents");
284
+
285
+ if (!existsSync(BUNDLED_AGENTS_DIR)) return [];
286
+
287
+ let sourceFiles: string[];
288
+ try {
289
+ sourceFiles = readdirSync(BUNDLED_AGENTS_DIR).filter((f) => f.endsWith(".md"));
290
+ } catch {
291
+ return [];
292
+ }
293
+
294
+ if (sourceFiles.length === 0) return [];
295
+
296
+ mkdirSync(targetDir, { recursive: true });
297
+
298
+ const copied: string[] = [];
299
+ for (const file of sourceFiles) {
300
+ const dest = join(targetDir, file);
301
+ if (!force && existsSync(dest)) continue;
302
+ copyFileSync(join(BUNDLED_AGENTS_DIR, file), dest);
303
+ copied.push(file);
304
+ }
305
+
306
+ return copied;
307
+ }
308
+
309
+ // ---------------------------------------------------------------------------
310
+ // Workspace type detection
311
+ // ---------------------------------------------------------------------------
312
+
313
+ const IGNORE_DIRS = new Set(["node_modules", ".git", ".hg", "dist", "build", ".next", ".cache"]);
314
+
315
+ export interface WorkspaceInfo {
316
+ type: "single-skill" | "multi-skill" | "monorepo" | "unknown";
317
+ skillCount: number;
318
+ skillPaths: string[];
319
+ isMonorepo: boolean;
320
+ hasExistingHooks: boolean;
321
+ suggestedTemplate: "single-skill" | "multi-skill" | null;
322
+ }
323
+
324
+ /**
325
+ * Recursively find SKILL.md files under a root directory,
326
+ * skipping ignored directories (node_modules, .git, etc.).
327
+ */
328
+ function findSkillFiles(dir: string, maxDepth = 8, depth = 0): string[] {
329
+ if (depth > maxDepth) return [];
330
+ if (!existsSync(dir)) return [];
331
+
332
+ const results: string[] = [];
333
+
334
+ try {
335
+ const entries = readdirSync(dir, { withFileTypes: true });
336
+ for (const entry of entries) {
337
+ if (entry.isDirectory()) {
338
+ if (IGNORE_DIRS.has(entry.name)) continue;
339
+ results.push(...findSkillFiles(join(dir, entry.name), maxDepth, depth + 1));
340
+ } else if (entry.name === "SKILL.md") {
341
+ results.push(join(dir, entry.name));
342
+ }
343
+ }
344
+ } catch {
345
+ // Permission errors, etc. — skip
346
+ }
347
+
348
+ return results;
349
+ }
350
+
351
+ /**
352
+ * Detect whether the root directory is a monorepo by checking for
353
+ * package.json workspaces or pnpm-workspace.yaml.
354
+ */
355
+ function detectMonorepo(rootDir: string): boolean {
356
+ // Check package.json workspaces field
357
+ const pkgPath = join(rootDir, "package.json");
358
+ if (existsSync(pkgPath)) {
359
+ try {
360
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
361
+ if (pkg.workspaces) return true;
362
+ } catch {
363
+ // invalid JSON — skip
364
+ }
365
+ }
366
+
367
+ // Check pnpm-workspace.yaml
368
+ if (existsSync(join(rootDir, "pnpm-workspace.yaml"))) return true;
369
+
370
+ // Check lerna.json
371
+ if (existsSync(join(rootDir, "lerna.json"))) return true;
372
+
373
+ return false;
374
+ }
375
+
376
+ /**
377
+ * Detect whether the project has existing selftune hooks configured.
378
+ */
379
+ function detectExistingHooks(rootDir: string): boolean {
380
+ const hooksDir = join(rootDir, "cli", "selftune", "hooks");
381
+ if (!existsSync(hooksDir)) return false;
382
+
383
+ try {
384
+ const entries = readdirSync(hooksDir);
385
+ return entries.some((e) => e.endsWith(".ts") || e.endsWith(".js"));
386
+ } catch {
387
+ return false;
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Scan a project root and detect the workspace type, skill layout,
393
+ * and suggest an appropriate template.
394
+ */
395
+ export function detectWorkspaceType(rootDir: string): WorkspaceInfo {
396
+ const skillPaths = findSkillFiles(rootDir);
397
+ const isMonorepo = detectMonorepo(rootDir);
398
+ const hasExistingHooks = detectExistingHooks(rootDir);
399
+ const skillCount = skillPaths.length;
400
+
401
+ let type: WorkspaceInfo["type"];
402
+ let suggestedTemplate: WorkspaceInfo["suggestedTemplate"];
403
+
404
+ if (isMonorepo) {
405
+ type = "monorepo";
406
+ suggestedTemplate = "multi-skill";
407
+ } else if (skillCount === 0) {
408
+ type = "unknown";
409
+ suggestedTemplate = null;
410
+ } else if (skillCount === 1) {
411
+ type = "single-skill";
412
+ suggestedTemplate = "single-skill";
413
+ } else {
414
+ type = "multi-skill";
415
+ suggestedTemplate = "multi-skill";
416
+ }
417
+
418
+ return {
419
+ type,
420
+ skillCount,
421
+ skillPaths,
422
+ isMonorepo,
423
+ hasExistingHooks,
424
+ suggestedTemplate,
425
+ };
426
+ }
427
+
150
428
  // ---------------------------------------------------------------------------
151
429
  // Init options (for testability)
152
430
  // ---------------------------------------------------------------------------
@@ -219,6 +497,34 @@ export function runInit(opts: InitOptions): SelftuneConfig {
219
497
  mkdirSync(configDir, { recursive: true });
220
498
  writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
221
499
 
500
+ // Install agent files to ~/.claude/agents/
501
+ const copiedAgents = installAgentFiles({ homeDir: home, force });
502
+ if (copiedAgents.length > 0) {
503
+ console.error(`[INFO] Installed agent files: ${copiedAgents.join(", ")}`);
504
+ }
505
+
506
+ // Auto-install hooks into ~/.claude/settings.json (Claude Code only)
507
+ if (agentType === "claude_code") {
508
+ const addedHookKeys = installClaudeCodeHooks({
509
+ settingsPath,
510
+ cliPath,
511
+ });
512
+ if (addedHookKeys.length > 0) {
513
+ config.hooks_installed = true;
514
+ // Re-write config with updated hooks_installed flag
515
+ writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
516
+ console.error(
517
+ `[INFO] Installed ${addedHookKeys.length} selftune hook(s) into ${settingsPath}: ${addedHookKeys.join(", ")}`,
518
+ );
519
+ } else if (!config.hooks_installed) {
520
+ // Re-check in case hooks were already present
521
+ config.hooks_installed = checkClaudeCodeHooks(settingsPath);
522
+ if (config.hooks_installed) {
523
+ writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
524
+ }
525
+ }
526
+ }
527
+
222
528
  return config;
223
529
  }
224
530
 
@@ -232,6 +538,8 @@ export async function cliMain(): Promise<void> {
232
538
  agent: { type: "string" },
233
539
  "cli-path": { type: "string" },
234
540
  force: { type: "boolean", default: false },
541
+ "enable-autonomy": { type: "boolean", default: false },
542
+ "schedule-format": { type: "string" },
235
543
  },
236
544
  strict: true,
237
545
  });
@@ -239,9 +547,10 @@ export async function cliMain(): Promise<void> {
239
547
  const configDir = SELFTUNE_CONFIG_DIR;
240
548
  const configPath = SELFTUNE_CONFIG_PATH;
241
549
  const force = values.force ?? false;
550
+ const enableAutonomy = values["enable-autonomy"] ?? false;
242
551
 
243
552
  // Check for existing config without force
244
- if (!force && existsSync(configPath)) {
553
+ if (!force && !enableAutonomy && existsSync(configPath)) {
245
554
  try {
246
555
  const raw = readFileSync(configPath, "utf-8");
247
556
  const existing = JSON.parse(raw) as SelftuneConfig;
@@ -265,12 +574,63 @@ export async function cliMain(): Promise<void> {
265
574
 
266
575
  console.log(JSON.stringify(config, null, 2));
267
576
 
577
+ // Detect workspace type and report
578
+ const workspace = detectWorkspaceType(process.cwd());
579
+ console.log(
580
+ JSON.stringify({
581
+ level: "info",
582
+ code: "workspace_detected",
583
+ type: workspace.type,
584
+ skills: workspace.skillCount,
585
+ monorepo: workspace.isMonorepo,
586
+ suggestedTemplate: workspace.suggestedTemplate
587
+ ? `templates/${workspace.suggestedTemplate}-settings.json`
588
+ : null,
589
+ }),
590
+ );
591
+
268
592
  // Run doctor as post-check
269
593
  const { doctor } = await import("./observability.js");
270
594
  const doctorResult = doctor();
271
- console.error(
272
- `\n[doctor] ${doctorResult.summary.pass}/${doctorResult.summary.total} checks pass`,
595
+ console.log(
596
+ JSON.stringify({
597
+ level: "info",
598
+ code: "doctor_result",
599
+ pass: doctorResult.summary.pass,
600
+ total: doctorResult.summary.total,
601
+ }),
273
602
  );
603
+
604
+ if (enableAutonomy) {
605
+ try {
606
+ const { installSchedule } = await import("./schedule.js");
607
+ const scheduleResult = installSchedule({
608
+ format: values["schedule-format"],
609
+ });
610
+
611
+ if (!scheduleResult.activated) {
612
+ console.error(
613
+ "Failed to activate the autonomous scheduler. Re-run with --schedule-format or use `selftune schedule --install --dry-run` to inspect the generated artifacts first.",
614
+ );
615
+ process.exit(1);
616
+ }
617
+
618
+ console.log(
619
+ JSON.stringify({
620
+ level: "info",
621
+ code: "autonomy_enabled",
622
+ format: scheduleResult.format,
623
+ activated: scheduleResult.activated,
624
+ files: scheduleResult.artifacts.map((artifact) => artifact.path),
625
+ }),
626
+ );
627
+ } catch (err) {
628
+ console.error(
629
+ `Failed to enable autonomy: ${err instanceof Error ? err.message : String(err)}`,
630
+ );
631
+ process.exit(1);
632
+ }
633
+ }
274
634
  }
275
635
 
276
636
  // Guard: only run when invoked directly
@@ -4,9 +4,14 @@
4
4
  * Lightweight, no LLM calls.
5
5
  */
6
6
 
7
- import { QUERY_LOG, SKILL_LOG, TELEMETRY_LOG } from "./constants.js";
7
+ import { QUERY_LOG, TELEMETRY_LOG } from "./constants.js";
8
8
  import type { QueryLogRecord, SessionTelemetryRecord, SkillUsageRecord } from "./types.js";
9
9
  import { readJsonl } from "./utils/jsonl.js";
10
+ import {
11
+ filterActionableQueryRecords,
12
+ filterActionableSkillUsageRecords,
13
+ } from "./utils/query-filter.js";
14
+ import { readEffectiveSkillUsageRecords } from "./utils/skill-log.js";
10
15
 
11
16
  // ---------------------------------------------------------------------------
12
17
  // Types
@@ -36,6 +41,8 @@ export function computeLastInsight(
36
41
  queryRecords: QueryLogRecord[],
37
42
  ): LastSessionInsight | null {
38
43
  if (telemetry.length === 0) return null;
44
+ const actionableSkillRecords = filterActionableSkillUsageRecords(skillRecords);
45
+ const actionableQueryRecords = filterActionableQueryRecords(queryRecords);
39
46
 
40
47
  // Find most recent telemetry record
41
48
  const sorted = [...telemetry].sort(
@@ -48,17 +55,19 @@ export function computeLastInsight(
48
55
  const triggeredSkillQueries = new Set<string>();
49
56
  const skillsTriggered = [
50
57
  ...new Set(
51
- skillRecords
58
+ actionableSkillRecords
52
59
  .filter((r) => r.session_id === sessionId && r.triggered)
53
60
  .map((r) => {
54
- triggeredSkillQueries.add(r.query.toLowerCase().trim());
61
+ if (typeof r.query === "string") {
62
+ triggeredSkillQueries.add(r.query.toLowerCase().trim());
63
+ }
55
64
  return r.skill_name;
56
65
  }),
57
66
  ),
58
67
  ];
59
68
 
60
69
  // Unmatched queries: session queries whose text does NOT appear in any triggered skill record
61
- const sessionQueries = queryRecords.filter((r) => r.session_id === sessionId);
70
+ const sessionQueries = actionableQueryRecords.filter((r) => r.session_id === sessionId);
62
71
  const unmatchedQueries = sessionQueries
63
72
  .filter((q) => !triggeredSkillQueries.has(q.query.toLowerCase().trim()))
64
73
  .map((q) => q.query);
@@ -124,7 +133,7 @@ export function formatInsight(insight: LastSessionInsight): string {
124
133
  /** CLI main: reads logs, prints insight. */
125
134
  export function cliMain(): void {
126
135
  const telemetry = readJsonl<SessionTelemetryRecord>(TELEMETRY_LOG);
127
- const skillRecords = readJsonl<SkillUsageRecord>(SKILL_LOG);
136
+ const skillRecords = readEffectiveSkillUsageRecords();
128
137
  const queryRecords = readJsonl<QueryLogRecord>(QUERY_LOG);
129
138
 
130
139
  const insight = computeLastInsight(telemetry, skillRecords, queryRecords);
@@ -0,0 +1,63 @@
1
+ /**
2
+ * SQLite database lifecycle for selftune local materialized view store.
3
+ *
4
+ * Uses Bun's built-in SQLite driver. The database file lives at
5
+ * ~/.selftune/selftune.db and is treated as a disposable cache —
6
+ * it can always be rebuilt from the authoritative JSONL logs.
7
+ */
8
+
9
+ import { Database } from "bun:sqlite";
10
+ import { existsSync, mkdirSync } from "node:fs";
11
+ import { dirname, join } from "node:path";
12
+ import { SELFTUNE_CONFIG_DIR } from "../constants.js";
13
+ import { ALL_DDL } from "./schema.js";
14
+
15
+ /** Default database file path. */
16
+ export const DB_PATH = join(SELFTUNE_CONFIG_DIR, "selftune.db");
17
+
18
+ /**
19
+ * Open (or create) the selftune SQLite database at the given path.
20
+ * Runs all DDL to ensure the schema exists. Uses WAL mode for
21
+ * concurrent read/write safety.
22
+ *
23
+ * Pass ":memory:" for an in-memory database (useful for tests).
24
+ */
25
+ export function openDb(dbPath: string = DB_PATH): Database {
26
+ // Ensure parent directory exists for file-based databases
27
+ if (dbPath !== ":memory:") {
28
+ const dir = dirname(dbPath);
29
+ if (!existsSync(dir)) {
30
+ mkdirSync(dir, { recursive: true });
31
+ }
32
+ }
33
+
34
+ const db = new Database(dbPath);
35
+
36
+ // Enable WAL mode for better concurrent access
37
+ db.run("PRAGMA journal_mode = WAL");
38
+ db.run("PRAGMA foreign_keys = ON");
39
+
40
+ // Run all DDL statements
41
+ for (const ddl of ALL_DDL) {
42
+ db.run(ddl);
43
+ }
44
+
45
+ return db;
46
+ }
47
+
48
+ /**
49
+ * Get a metadata value from the _meta table.
50
+ */
51
+ export function getMeta(db: Database, key: string): string | null {
52
+ const row = db.query("SELECT value FROM _meta WHERE key = ?").get(key) as {
53
+ value: string;
54
+ } | null;
55
+ return row?.value ?? null;
56
+ }
57
+
58
+ /**
59
+ * Set a metadata value in the _meta table.
60
+ */
61
+ export function setMeta(db: Database, key: string, value: string): void {
62
+ db.run("INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)", [key, value]);
63
+ }