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.
- package/.claude/agents/diagnosis-analyst.md +156 -0
- package/.claude/agents/evolution-reviewer.md +180 -0
- package/.claude/agents/integration-guide.md +212 -0
- package/.claude/agents/pattern-analyst.md +160 -0
- package/CHANGELOG.md +46 -1
- package/README.md +105 -257
- package/apps/local-dashboard/dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/index-C4EOTFZ2.js +15 -0
- package/apps/local-dashboard/dist/assets/index-bl-Webyd.css +1 -0
- package/apps/local-dashboard/dist/assets/vendor-react-U7zYD9Rg.js +60 -0
- package/apps/local-dashboard/dist/assets/vendor-table-B7VF2Ipl.js +26 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-D7_zX_qy.js +346 -0
- package/apps/local-dashboard/dist/favicon.png +0 -0
- package/apps/local-dashboard/dist/index.html +17 -0
- package/apps/local-dashboard/dist/logo.png +0 -0
- package/apps/local-dashboard/dist/logo.svg +9 -0
- package/assets/BeforeAfter.gif +0 -0
- package/assets/FeedbackLoop.gif +0 -0
- package/assets/logo.svg +9 -0
- package/assets/skill-health-badge.svg +20 -0
- package/cli/selftune/activation-rules.ts +171 -0
- package/cli/selftune/badge/badge-data.ts +108 -0
- package/cli/selftune/badge/badge-svg.ts +212 -0
- package/cli/selftune/badge/badge.ts +99 -0
- package/cli/selftune/canonical-export.ts +183 -0
- package/cli/selftune/constants.ts +103 -1
- package/cli/selftune/contribute/bundle.ts +314 -0
- package/cli/selftune/contribute/contribute.ts +214 -0
- package/cli/selftune/contribute/sanitize.ts +162 -0
- package/cli/selftune/cron/setup.ts +266 -0
- package/cli/selftune/dashboard-contract.ts +202 -0
- package/cli/selftune/dashboard-server.ts +1049 -0
- package/cli/selftune/dashboard.ts +43 -156
- package/cli/selftune/eval/baseline.ts +248 -0
- package/cli/selftune/eval/composability-v2.ts +273 -0
- package/cli/selftune/eval/composability.ts +117 -0
- package/cli/selftune/eval/generate-unit-tests.ts +143 -0
- package/cli/selftune/eval/hooks-to-evals.ts +101 -16
- package/cli/selftune/eval/import-skillsbench.ts +221 -0
- package/cli/selftune/eval/synthetic-evals.ts +172 -0
- package/cli/selftune/eval/unit-test-cli.ts +152 -0
- package/cli/selftune/eval/unit-test.ts +196 -0
- package/cli/selftune/evolution/deploy-proposal.ts +142 -1
- package/cli/selftune/evolution/evidence.ts +26 -0
- package/cli/selftune/evolution/evolve-body.ts +586 -0
- package/cli/selftune/evolution/evolve.ts +825 -116
- package/cli/selftune/evolution/extract-patterns.ts +105 -16
- package/cli/selftune/evolution/pareto.ts +314 -0
- package/cli/selftune/evolution/propose-body.ts +171 -0
- package/cli/selftune/evolution/propose-description.ts +100 -2
- package/cli/selftune/evolution/propose-routing.ts +166 -0
- package/cli/selftune/evolution/refine-body.ts +141 -0
- package/cli/selftune/evolution/rollback.ts +21 -4
- package/cli/selftune/evolution/validate-body.ts +254 -0
- package/cli/selftune/evolution/validate-proposal.ts +257 -35
- package/cli/selftune/evolution/validate-routing.ts +177 -0
- package/cli/selftune/grading/auto-grade.ts +200 -0
- package/cli/selftune/grading/grade-session.ts +513 -42
- package/cli/selftune/grading/pre-gates.ts +104 -0
- package/cli/selftune/grading/results.ts +42 -0
- package/cli/selftune/hooks/auto-activate.ts +185 -0
- package/cli/selftune/hooks/evolution-guard.ts +165 -0
- package/cli/selftune/hooks/prompt-log.ts +172 -2
- package/cli/selftune/hooks/session-stop.ts +123 -3
- package/cli/selftune/hooks/skill-change-guard.ts +112 -0
- package/cli/selftune/hooks/skill-eval.ts +119 -3
- package/cli/selftune/index.ts +415 -48
- package/cli/selftune/ingestors/claude-replay.ts +377 -0
- package/cli/selftune/ingestors/codex-rollout.ts +345 -46
- package/cli/selftune/ingestors/codex-wrapper.ts +207 -39
- package/cli/selftune/ingestors/openclaw-ingest.ts +573 -0
- package/cli/selftune/ingestors/opencode-ingest.ts +193 -17
- package/cli/selftune/init.ts +376 -16
- package/cli/selftune/last.ts +14 -5
- package/cli/selftune/localdb/db.ts +63 -0
- package/cli/selftune/localdb/materialize.ts +428 -0
- package/cli/selftune/localdb/queries.ts +376 -0
- package/cli/selftune/localdb/schema.ts +204 -0
- package/cli/selftune/memory/writer.ts +447 -0
- package/cli/selftune/monitoring/watch.ts +90 -16
- package/cli/selftune/normalization.ts +682 -0
- package/cli/selftune/observability.ts +19 -44
- package/cli/selftune/orchestrate.ts +1073 -0
- package/cli/selftune/quickstart.ts +203 -0
- package/cli/selftune/repair/skill-usage.ts +576 -0
- package/cli/selftune/schedule.ts +561 -0
- package/cli/selftune/status.ts +59 -33
- package/cli/selftune/sync.ts +627 -0
- package/cli/selftune/types.ts +525 -5
- package/cli/selftune/utils/canonical-log.ts +45 -0
- package/cli/selftune/utils/frontmatter.ts +217 -0
- package/cli/selftune/utils/hooks.ts +41 -0
- package/cli/selftune/utils/html.ts +27 -0
- package/cli/selftune/utils/llm-call.ts +103 -19
- package/cli/selftune/utils/math.ts +10 -0
- package/cli/selftune/utils/query-filter.ts +139 -0
- package/cli/selftune/utils/skill-discovery.ts +340 -0
- package/cli/selftune/utils/skill-log.ts +68 -0
- package/cli/selftune/utils/skill-usage-confidence.ts +18 -0
- package/cli/selftune/utils/transcript.ts +307 -26
- package/cli/selftune/utils/trigger-check.ts +89 -0
- package/cli/selftune/utils/tui.ts +156 -0
- package/cli/selftune/workflows/discover.ts +254 -0
- package/cli/selftune/workflows/skill-md-writer.ts +288 -0
- package/cli/selftune/workflows/workflows.ts +188 -0
- package/package.json +28 -11
- package/packages/telemetry-contract/README.md +11 -0
- package/packages/telemetry-contract/fixtures/golden.json +87 -0
- package/packages/telemetry-contract/fixtures/golden.test.ts +42 -0
- package/packages/telemetry-contract/index.ts +1 -0
- package/packages/telemetry-contract/package.json +19 -0
- package/packages/telemetry-contract/src/index.ts +2 -0
- package/packages/telemetry-contract/src/types.ts +163 -0
- package/packages/telemetry-contract/src/validators.ts +109 -0
- package/skill/SKILL.md +180 -33
- package/skill/Workflows/AutoActivation.md +145 -0
- package/skill/Workflows/Badge.md +124 -0
- package/skill/Workflows/Baseline.md +144 -0
- package/skill/Workflows/Composability.md +107 -0
- package/skill/Workflows/Contribute.md +94 -0
- package/skill/Workflows/Cron.md +132 -0
- package/skill/Workflows/Dashboard.md +214 -0
- package/skill/Workflows/Doctor.md +63 -14
- package/skill/Workflows/Evals.md +110 -18
- package/skill/Workflows/EvolutionMemory.md +154 -0
- package/skill/Workflows/Evolve.md +181 -21
- package/skill/Workflows/EvolveBody.md +159 -0
- package/skill/Workflows/Grade.md +36 -31
- package/skill/Workflows/ImportSkillsBench.md +117 -0
- package/skill/Workflows/Ingest.md +142 -21
- package/skill/Workflows/Initialize.md +91 -23
- package/skill/Workflows/Orchestrate.md +139 -0
- package/skill/Workflows/Replay.md +91 -0
- package/skill/Workflows/Rollback.md +23 -4
- package/skill/Workflows/Schedule.md +61 -0
- package/skill/Workflows/Sync.md +88 -0
- package/skill/Workflows/UnitTest.md +150 -0
- package/skill/Workflows/Watch.md +33 -1
- package/skill/Workflows/Workflows.md +129 -0
- package/skill/assets/activation-rules-default.json +26 -0
- package/skill/assets/multi-skill-settings.json +63 -0
- package/skill/assets/single-skill-settings.json +57 -0
- package/skill/references/invocation-taxonomy.md +2 -2
- package/skill/references/logs.md +164 -2
- package/skill/references/setup-patterns.md +65 -0
- package/skill/references/version-history.md +40 -0
- package/skill/settings_snippet.json +23 -0
- package/templates/activation-rules-default.json +27 -0
- package/templates/multi-skill-settings.json +64 -0
- package/templates/single-skill-settings.json +58 -0
- package/dashboard/index.html +0 -1119
package/cli/selftune/init.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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.
|
|
272
|
-
|
|
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
|
package/cli/selftune/last.ts
CHANGED
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
* Lightweight, no LLM calls.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { QUERY_LOG,
|
|
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
|
-
|
|
58
|
+
actionableSkillRecords
|
|
52
59
|
.filter((r) => r.session_id === sessionId && r.triggered)
|
|
53
60
|
.map((r) => {
|
|
54
|
-
|
|
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 =
|
|
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 =
|
|
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
|
+
}
|