selftune 0.2.14 → 0.2.16
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/apps/local-dashboard/dist/assets/index-DOu3iLD9.js +16 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-DIwlrGlb.js +12 -0
- package/apps/local-dashboard/dist/index.html +2 -2
- package/bin/run-hook.cjs +36 -0
- package/cli/selftune/analytics.ts +13 -11
- package/cli/selftune/badge/badge.ts +13 -9
- package/cli/selftune/canonical-export.ts +6 -6
- package/cli/selftune/contribute/contribute.ts +2 -1
- package/cli/selftune/cron/setup.ts +3 -1
- package/cli/selftune/dashboard-contract.ts +10 -0
- package/cli/selftune/dashboard.ts +10 -5
- package/cli/selftune/eval/baseline.ts +20 -30
- package/cli/selftune/eval/hooks-to-evals.ts +22 -12
- package/cli/selftune/eval/import-skillsbench.ts +21 -8
- package/cli/selftune/eval/unit-test-cli.ts +22 -11
- package/cli/selftune/evolution/description-quality.ts +224 -0
- package/cli/selftune/evolution/evolve-body.ts +17 -10
- package/cli/selftune/evolution/evolve.ts +94 -59
- package/cli/selftune/evolution/rollback.ts +7 -6
- package/cli/selftune/evolution/unblock-suggestions.ts +159 -0
- package/cli/selftune/grading/auto-grade.ts +24 -22
- package/cli/selftune/grading/grade-session.ts +21 -17
- package/cli/selftune/hooks/auto-activate.ts +12 -3
- package/cli/selftune/hooks/prompt-log.ts +7 -1
- package/cli/selftune/index.ts +66 -69
- package/cli/selftune/ingestors/claude-replay.ts +29 -14
- package/cli/selftune/ingestors/codex-rollout.ts +6 -1
- package/cli/selftune/init.ts +212 -36
- package/cli/selftune/monitoring/watch.ts +32 -16
- package/cli/selftune/orchestrate.ts +18 -17
- package/cli/selftune/routes/skill-report.ts +17 -0
- package/cli/selftune/schedule.ts +23 -9
- package/cli/selftune/sync.ts +7 -3
- package/cli/selftune/types.ts +45 -10
- package/cli/selftune/utils/cli-error.ts +102 -0
- package/cli/selftune/utils/hooks.ts +12 -2
- package/cli/selftune/workflows/workflows.ts +23 -17
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
- package/skill/Workflows/AutoActivation.md +1 -1
- package/skill/Workflows/Evolve.md +4 -0
- package/skill/Workflows/Initialize.md +8 -8
- package/skill/settings_snippet.json +35 -12
- package/apps/local-dashboard/dist/assets/index-DIrdlu2_.js +0 -16
- package/apps/local-dashboard/dist/assets/vendor-ui-7xD7fNEU.js +0 -12
|
@@ -55,6 +55,7 @@ import type {
|
|
|
55
55
|
SessionTelemetryRecord,
|
|
56
56
|
TranscriptMetrics,
|
|
57
57
|
} from "../types.js";
|
|
58
|
+
import { CLIError, handleCLIError } from "../utils/cli-error.js";
|
|
58
59
|
import { loadMarker, saveMarker } from "../utils/jsonl.js";
|
|
59
60
|
import { isActionableQueryText } from "../utils/query-filter.js";
|
|
60
61
|
import {
|
|
@@ -326,26 +327,36 @@ export function buildCanonicalRecordsFromReplay(session: ParsedSession): Canonic
|
|
|
326
327
|
|
|
327
328
|
// --- CLI main ---
|
|
328
329
|
export function cliMain(): void {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
330
|
+
let values: Record<string, string | boolean | undefined>;
|
|
331
|
+
try {
|
|
332
|
+
({ values } = parseArgs({
|
|
333
|
+
options: {
|
|
334
|
+
"projects-dir": { type: "string", default: CLAUDE_CODE_PROJECTS_DIR },
|
|
335
|
+
since: { type: "string" },
|
|
336
|
+
"dry-run": { type: "boolean", default: false },
|
|
337
|
+
force: { type: "boolean", default: false },
|
|
338
|
+
verbose: { type: "boolean", short: "v", default: false },
|
|
339
|
+
},
|
|
340
|
+
strict: true,
|
|
341
|
+
}));
|
|
342
|
+
} catch (err) {
|
|
343
|
+
throw new CLIError(
|
|
344
|
+
err instanceof Error ? err.message : String(err),
|
|
345
|
+
"INVALID_FLAG",
|
|
346
|
+
"selftune ingest claude --since 2026-01-01",
|
|
347
|
+
);
|
|
348
|
+
}
|
|
339
349
|
|
|
340
350
|
const projectsDir = values["projects-dir"] ?? CLAUDE_CODE_PROJECTS_DIR;
|
|
341
351
|
let since: Date | undefined;
|
|
342
352
|
if (values.since) {
|
|
343
353
|
since = new Date(values.since);
|
|
344
354
|
if (Number.isNaN(since.getTime())) {
|
|
345
|
-
|
|
346
|
-
`
|
|
355
|
+
throw new CLIError(
|
|
356
|
+
`Invalid --since date: "${values.since}"`,
|
|
357
|
+
"INVALID_FLAG",
|
|
358
|
+
"selftune ingest claude --since 2026-01-01",
|
|
347
359
|
);
|
|
348
|
-
process.exit(1);
|
|
349
360
|
}
|
|
350
361
|
}
|
|
351
362
|
|
|
@@ -401,5 +412,9 @@ export function cliMain(): void {
|
|
|
401
412
|
}
|
|
402
413
|
|
|
403
414
|
if (import.meta.main) {
|
|
404
|
-
|
|
415
|
+
try {
|
|
416
|
+
cliMain();
|
|
417
|
+
} catch (err) {
|
|
418
|
+
handleCLIError(err);
|
|
419
|
+
}
|
|
405
420
|
}
|
|
@@ -49,6 +49,7 @@ import type {
|
|
|
49
49
|
SessionTelemetryRecord,
|
|
50
50
|
SkillUsageRecord,
|
|
51
51
|
} from "../types.js";
|
|
52
|
+
import { handleCLIError } from "../utils/cli-error.js";
|
|
52
53
|
import { loadMarker, saveMarker } from "../utils/jsonl.js";
|
|
53
54
|
import { extractActionableQueryText } from "../utils/query-filter.js";
|
|
54
55
|
import {
|
|
@@ -717,5 +718,9 @@ export function cliMain(): void {
|
|
|
717
718
|
}
|
|
718
719
|
|
|
719
720
|
if (import.meta.main) {
|
|
720
|
-
|
|
721
|
+
try {
|
|
722
|
+
cliMain();
|
|
723
|
+
} catch (err) {
|
|
724
|
+
handleCLIError(err);
|
|
725
|
+
}
|
|
721
726
|
}
|
package/cli/selftune/init.ts
CHANGED
|
@@ -44,7 +44,8 @@ import {
|
|
|
44
44
|
import { installAgentFiles } from "./claude-agents.js";
|
|
45
45
|
import { CLAUDE_CODE_HOOK_KEYS, SELFTUNE_CONFIG_DIR, SELFTUNE_CONFIG_PATH } from "./constants.js";
|
|
46
46
|
import type { AgentCommandGuidance, AlphaIdentity, SelftuneConfig } from "./types.js";
|
|
47
|
-
import {
|
|
47
|
+
import { CLIError, handleCLIError } from "./utils/cli-error.js";
|
|
48
|
+
import { hookKeyHasSelftuneEntry, isSelftuneCommand } from "./utils/hooks.js";
|
|
48
49
|
import { detectAgent } from "./utils/llm-call.js";
|
|
49
50
|
|
|
50
51
|
export { installAgentFiles } from "./claude-agents.js";
|
|
@@ -228,10 +229,13 @@ const SETTINGS_SNIPPET_PATH = resolve(
|
|
|
228
229
|
*
|
|
229
230
|
* - Creates settings.json if it does not exist
|
|
230
231
|
* - Creates the hooks section if it does not exist
|
|
231
|
-
* -
|
|
232
|
-
* -
|
|
232
|
+
* - Adds hook entries for keys that don't already have a selftune entry
|
|
233
|
+
* - Updates existing selftune entries with new attributes from the snippet
|
|
234
|
+
* (e.g. `if`, `statusMessage`, `async`, `timeout`) while preserving
|
|
235
|
+
* the resolved `command` path from the existing entry
|
|
236
|
+
* - Never overwrites existing non-selftune hooks
|
|
233
237
|
*
|
|
234
|
-
* Returns the list of hook keys that were added.
|
|
238
|
+
* Returns the list of hook keys that were added or updated.
|
|
235
239
|
*/
|
|
236
240
|
export function installClaudeCodeHooks(options?: {
|
|
237
241
|
settingsPath?: string;
|
|
@@ -278,43 +282,211 @@ export function installClaudeCodeHooks(options?: {
|
|
|
278
282
|
}
|
|
279
283
|
const existingHooks = settings.hooks as Record<string, unknown[]>;
|
|
280
284
|
|
|
281
|
-
// Resolve the
|
|
285
|
+
// Resolve the package root for path substitution
|
|
286
|
+
// cliPath points to cli/selftune/index.ts → package root is two levels up
|
|
282
287
|
const cliPath = options?.cliPath;
|
|
283
|
-
const
|
|
288
|
+
const packageRoot = cliPath ? resolve(dirname(cliPath), "..", "..").replace(/\\/g, "/") : null;
|
|
284
289
|
|
|
285
|
-
const
|
|
290
|
+
const changedKeys: string[] = [];
|
|
286
291
|
|
|
287
292
|
for (const key of Object.keys(snippetHooks)) {
|
|
288
|
-
//
|
|
289
|
-
if (hookKeyHasSelftuneEntry(existingHooks, key)) {
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Get the snippet entries for this key, replacing /PATH/TO/ with actual path
|
|
293
|
+
// Get the snippet entries for this key, replacing /PATH/TO/ with actual package root
|
|
294
294
|
let entries = snippetHooks[key];
|
|
295
|
-
if (
|
|
296
|
-
// Deep clone and substitute
|
|
297
|
-
const raw = JSON.stringify(entries).replace(/\/PATH\/TO
|
|
295
|
+
if (packageRoot) {
|
|
296
|
+
// Deep clone and substitute all /PATH/TO/ references with the resolved package root
|
|
297
|
+
const raw = JSON.stringify(entries).replace(/\/PATH\/TO\//g, `${packageRoot}/`);
|
|
298
298
|
entries = JSON.parse(raw);
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
301
|
+
if (hookKeyHasSelftuneEntry(existingHooks, key)) {
|
|
302
|
+
// Key already has selftune hooks — update them in-place with new attributes
|
|
303
|
+
// while preserving non-selftune entries and the resolved command paths
|
|
304
|
+
if (updateExistingSelftuneHooks(existingHooks, key, entries)) {
|
|
305
|
+
changedKeys.push(key);
|
|
306
|
+
}
|
|
304
307
|
} else {
|
|
305
|
-
|
|
308
|
+
// No selftune entry yet — add the snippet entries
|
|
309
|
+
if (Array.isArray(existingHooks[key])) {
|
|
310
|
+
existingHooks[key] = [...existingHooks[key], ...entries];
|
|
311
|
+
} else {
|
|
312
|
+
existingHooks[key] = entries;
|
|
313
|
+
}
|
|
314
|
+
changedKeys.push(key);
|
|
306
315
|
}
|
|
307
|
-
|
|
308
|
-
addedKeys.push(key);
|
|
309
316
|
}
|
|
310
317
|
|
|
311
|
-
if (
|
|
318
|
+
if (changedKeys.length > 0) {
|
|
312
319
|
// Ensure ~/.claude/ directory exists
|
|
313
320
|
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
314
321
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
315
322
|
}
|
|
316
323
|
|
|
317
|
-
return
|
|
324
|
+
return changedKeys;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Update existing selftune hook entries in-place with new attributes from the snippet.
|
|
329
|
+
*
|
|
330
|
+
* For each matcher group that contains selftune hooks, replaces ALL selftune
|
|
331
|
+
* hook entries with the full set of snippet entries while:
|
|
332
|
+
* - Resolving snippet commands using the package root from existing entries
|
|
333
|
+
* - Preserving non-selftune hooks in the same matcher group
|
|
334
|
+
* - Handling N→M changes (e.g. 2 hooks expanding to 4 with Write/Edit splits)
|
|
335
|
+
*
|
|
336
|
+
* Returns true if any entries were actually modified.
|
|
337
|
+
*/
|
|
338
|
+
export function updateExistingSelftuneHooks(
|
|
339
|
+
existingHooks: Record<string, unknown[]>,
|
|
340
|
+
key: string,
|
|
341
|
+
snippetEntries: unknown[],
|
|
342
|
+
): boolean {
|
|
343
|
+
const existingArray = existingHooks[key];
|
|
344
|
+
if (!Array.isArray(existingArray)) return false;
|
|
345
|
+
|
|
346
|
+
// Collect all snippet hooks (flattened from matcher groups)
|
|
347
|
+
const allSnippetHooks: Array<Record<string, unknown>> = [];
|
|
348
|
+
for (const group of snippetEntries) {
|
|
349
|
+
if (typeof group !== "object" || group === null) continue;
|
|
350
|
+
const g = group as Record<string, unknown>;
|
|
351
|
+
const hooks = g.hooks as Array<Record<string, unknown>> | undefined;
|
|
352
|
+
if (!Array.isArray(hooks)) continue;
|
|
353
|
+
allSnippetHooks.push(...hooks);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (allSnippetHooks.length === 0) return false;
|
|
357
|
+
|
|
358
|
+
let modified = false;
|
|
359
|
+
|
|
360
|
+
for (let i = 0; i < existingArray.length; i++) {
|
|
361
|
+
const group = existingArray[i];
|
|
362
|
+
if (typeof group !== "object" || group === null) continue;
|
|
363
|
+
const g = group as Record<string, unknown>;
|
|
364
|
+
const hooks = g.hooks as Array<Record<string, unknown>> | undefined;
|
|
365
|
+
|
|
366
|
+
// Handle flat entries (direct { command: "..." } without nested hooks array).
|
|
367
|
+
// These are a legacy format from older selftune versions or manual installs.
|
|
368
|
+
if (!Array.isArray(hooks)) {
|
|
369
|
+
if (!isHookSelftune(g)) continue;
|
|
370
|
+
const pkgRoot = derivePackageRootFromCommand(typeof g.command === "string" ? g.command : "");
|
|
371
|
+
|
|
372
|
+
// Replace the flat entry with the full snippet group structure.
|
|
373
|
+
// If we can derive a package root, resolve /PATH/TO/ in the snippet.
|
|
374
|
+
// If not (e.g. "npx selftune hook ..."), use snippet entries as-is
|
|
375
|
+
// (they were already resolved by the caller if a cliPath was provided).
|
|
376
|
+
const resolvedEntries = snippetEntries.map((se) => {
|
|
377
|
+
if (!pkgRoot) return se;
|
|
378
|
+
const raw = JSON.stringify(se).replace(/\/PATH\/TO\//g, `${pkgRoot}/`);
|
|
379
|
+
return JSON.parse(raw);
|
|
380
|
+
});
|
|
381
|
+
existingArray.splice(i, 1, ...resolvedEntries);
|
|
382
|
+
modified = true;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Derive package root from the first selftune hook in this group
|
|
387
|
+
let packageRoot: string | null = null;
|
|
388
|
+
for (const hook of hooks) {
|
|
389
|
+
if (isHookSelftune(hook)) {
|
|
390
|
+
packageRoot = derivePackageRootFromCommand(
|
|
391
|
+
typeof hook.command === "string" ? hook.command : "",
|
|
392
|
+
);
|
|
393
|
+
if (packageRoot) break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Check if this group has any selftune hooks at all
|
|
398
|
+
const hasSelftuneHooks = hooks.some(isHookSelftune);
|
|
399
|
+
if (!hasSelftuneHooks) continue;
|
|
400
|
+
|
|
401
|
+
// Build resolved snippet hooks using the derived package root.
|
|
402
|
+
// If no package root was derivable (e.g. "npx selftune hook ..."),
|
|
403
|
+
// use snippet hooks as-is (already resolved by caller if cliPath was provided).
|
|
404
|
+
const resolvedSnippetHooks = allSnippetHooks.map((snippetHook) => {
|
|
405
|
+
const cmd = typeof snippetHook.command === "string" ? snippetHook.command : "";
|
|
406
|
+
const resolvedCmd = packageRoot ? cmd.replace(/\/PATH\/TO\//g, `${packageRoot}/`) : cmd;
|
|
407
|
+
return { ...snippetHook, command: resolvedCmd };
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Check if anything actually changed (compare sorted JSON for order independence)
|
|
411
|
+
const oldSelftune = hooks.filter(isHookSelftune);
|
|
412
|
+
const oldSorted = JSON.stringify(sortKeys(oldSelftune));
|
|
413
|
+
const newSorted = JSON.stringify(sortKeys(resolvedSnippetHooks));
|
|
414
|
+
if (oldSorted !== newSorted) {
|
|
415
|
+
modified = true;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Rebuild hooks preserving original ordering of non-selftune entries:
|
|
419
|
+
// replace the first selftune hook with all resolved snippet hooks,
|
|
420
|
+
// remove remaining old selftune hooks, keep non-selftune hooks in place
|
|
421
|
+
const updatedHooks: Array<Record<string, unknown>> = [];
|
|
422
|
+
let selftuneInserted = false;
|
|
423
|
+
for (const hook of hooks) {
|
|
424
|
+
if (isHookSelftune(hook)) {
|
|
425
|
+
if (!selftuneInserted) {
|
|
426
|
+
// Insert all resolved snippet hooks at the position of the first selftune hook
|
|
427
|
+
updatedHooks.push(...resolvedSnippetHooks);
|
|
428
|
+
selftuneInserted = true;
|
|
429
|
+
}
|
|
430
|
+
// Skip remaining old selftune hooks (replaced by snippet set above)
|
|
431
|
+
} else {
|
|
432
|
+
updatedHooks.push(hook);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
g.hooks = updatedHooks;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return modified;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Check if a hook entry is a selftune-managed hook. Delegates to shared isSelftuneCommand. */
|
|
442
|
+
function isHookSelftune(hook: Record<string, unknown>): boolean {
|
|
443
|
+
return typeof hook.command === "string" && isSelftuneCommand(hook.command);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Sort object keys recursively for order-independent JSON comparison. */
|
|
447
|
+
function sortKeys(obj: unknown): unknown {
|
|
448
|
+
if (Array.isArray(obj)) return obj.map(sortKeys);
|
|
449
|
+
if (obj !== null && typeof obj === "object") {
|
|
450
|
+
const sorted: Record<string, unknown> = {};
|
|
451
|
+
for (const key of Object.keys(obj as Record<string, unknown>).sort()) {
|
|
452
|
+
sorted[key] = sortKeys((obj as Record<string, unknown>)[key]);
|
|
453
|
+
}
|
|
454
|
+
return sorted;
|
|
455
|
+
}
|
|
456
|
+
return obj;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Derive the selftune package root from an existing hook command.
|
|
461
|
+
* Supports both old format ("bun run .../cli/selftune/hooks/X.ts")
|
|
462
|
+
* and new format ("node .../bin/run-hook.cjs .../cli/selftune/hooks/X.ts").
|
|
463
|
+
*
|
|
464
|
+
* Handles paths with spaces (e.g. "/Users/Alice Smith/...") and
|
|
465
|
+
* optional surrounding quotes in the command string.
|
|
466
|
+
*/
|
|
467
|
+
export function derivePackageRootFromCommand(command: string): string | null {
|
|
468
|
+
// Normalize: strip quotes, collapse backslashes (for Windows-style paths)
|
|
469
|
+
const normalized = command.replace(/["']/g, "").replace(/\\/g, "/");
|
|
470
|
+
// Split on the known directory marker and take the prefix.
|
|
471
|
+
// The command may contain the package root multiple times (e.g.
|
|
472
|
+
// "node /root/bin/run-hook.cjs /root/cli/selftune/hooks/script.ts")
|
|
473
|
+
// so we split on the LAST occurrence of the marker.
|
|
474
|
+
for (const marker of ["/cli/selftune/hooks/", "/bin/run-hook.cjs"]) {
|
|
475
|
+
const idx = normalized.lastIndexOf(marker);
|
|
476
|
+
if (idx === -1) continue;
|
|
477
|
+
// Everything before the marker is "<prefix> <package-root>" or just "<package-root>"
|
|
478
|
+
const beforeMarker = normalized.slice(0, idx);
|
|
479
|
+
// Find the start of the path: scan backwards from end for the path start.
|
|
480
|
+
// Paths start with / (Unix) or a drive letter like C:/ (Windows).
|
|
481
|
+
// The command prefix (e.g. "node " or "bun run ") precedes the path.
|
|
482
|
+
const pathMatch = beforeMarker.match(/.*\s(\/.*|[A-Za-z]:\/.*)/);
|
|
483
|
+
if (pathMatch) return pathMatch[1];
|
|
484
|
+
// No space prefix — the entire string is the path (e.g. no "node " prefix)
|
|
485
|
+
if (beforeMarker.startsWith("/") || /^[A-Za-z]:\//.test(beforeMarker)) {
|
|
486
|
+
return beforeMarker;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return null;
|
|
318
490
|
}
|
|
319
491
|
|
|
320
492
|
// ---------------------------------------------------------------------------
|
|
@@ -614,16 +786,16 @@ export async function runInit(opts: InitOptions): Promise<SelftuneConfig> {
|
|
|
614
786
|
);
|
|
615
787
|
}
|
|
616
788
|
|
|
617
|
-
const
|
|
789
|
+
const changedHookKeys = installClaudeCodeHooks({
|
|
618
790
|
settingsPath,
|
|
619
791
|
cliPath,
|
|
620
792
|
});
|
|
621
|
-
if (
|
|
793
|
+
if (changedHookKeys.length > 0) {
|
|
622
794
|
config.hooks_installed = true;
|
|
623
795
|
// Re-write config with updated hooks_installed flag
|
|
624
796
|
writeSelftuneConfig(configPath, config);
|
|
625
797
|
console.error(
|
|
626
|
-
`[INFO] Installed ${
|
|
798
|
+
`[INFO] Installed/updated ${changedHookKeys.length} selftune hook(s) in ${settingsPath}: ${changedHookKeys.join(", ")}`,
|
|
627
799
|
);
|
|
628
800
|
} else if (!config.hooks_installed) {
|
|
629
801
|
// Re-check in case hooks were already present
|
|
@@ -692,8 +864,11 @@ export async function cliMain(): Promise<void> {
|
|
|
692
864
|
try {
|
|
693
865
|
validateAlphaMetadataFlags(values.alpha, values["alpha-email"], values["alpha-name"]);
|
|
694
866
|
} catch (error) {
|
|
695
|
-
|
|
696
|
-
|
|
867
|
+
throw new CLIError(
|
|
868
|
+
error instanceof Error ? error.message : String(error),
|
|
869
|
+
"INVALID_FLAG",
|
|
870
|
+
"Pass --alpha along with --alpha-email and --alpha-name.",
|
|
871
|
+
);
|
|
697
872
|
}
|
|
698
873
|
|
|
699
874
|
// Check for existing config without force
|
|
@@ -890,10 +1065,11 @@ export async function cliMain(): Promise<void> {
|
|
|
890
1065
|
});
|
|
891
1066
|
|
|
892
1067
|
if (!scheduleResult.activated) {
|
|
893
|
-
|
|
894
|
-
"Failed to activate the autonomous scheduler.
|
|
1068
|
+
throw new CLIError(
|
|
1069
|
+
"Failed to activate the autonomous scheduler.",
|
|
1070
|
+
"OPERATION_FAILED",
|
|
1071
|
+
"Re-run with --schedule-format or use `selftune schedule --install --dry-run` to inspect the generated artifacts first.",
|
|
895
1072
|
);
|
|
896
|
-
process.exit(1);
|
|
897
1073
|
}
|
|
898
1074
|
|
|
899
1075
|
console.log(
|
|
@@ -906,10 +1082,11 @@ export async function cliMain(): Promise<void> {
|
|
|
906
1082
|
}),
|
|
907
1083
|
);
|
|
908
1084
|
} catch (err) {
|
|
909
|
-
|
|
1085
|
+
throw new CLIError(
|
|
910
1086
|
`Failed to enable autonomy: ${err instanceof Error ? err.message : String(err)}`,
|
|
1087
|
+
"OPERATION_FAILED",
|
|
1088
|
+
"Re-run with --schedule-format or use `selftune schedule --install --dry-run` to inspect artifacts.",
|
|
911
1089
|
);
|
|
912
|
-
process.exit(1);
|
|
913
1090
|
}
|
|
914
1091
|
}
|
|
915
1092
|
}
|
|
@@ -947,7 +1124,6 @@ if (isMain) {
|
|
|
947
1124
|
console.error(JSON.stringify(err.payload));
|
|
948
1125
|
process.exit(1);
|
|
949
1126
|
}
|
|
950
|
-
|
|
951
|
-
process.exit(1);
|
|
1127
|
+
handleCLIError(err);
|
|
952
1128
|
});
|
|
953
1129
|
}
|
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
SessionTelemetryRecord,
|
|
27
27
|
SkillUsageRecord,
|
|
28
28
|
} from "../types.js";
|
|
29
|
+
import { CLIError, handleCLIError } from "../utils/cli-error.js";
|
|
29
30
|
import {
|
|
30
31
|
filterActionableQueryRecords,
|
|
31
32
|
filterActionableSkillUsageRecords,
|
|
@@ -354,34 +355,52 @@ Options:
|
|
|
354
355
|
}
|
|
355
356
|
|
|
356
357
|
if (!values.skill || !values["skill-path"]) {
|
|
357
|
-
|
|
358
|
-
|
|
358
|
+
throw new CLIError(
|
|
359
|
+
"--skill and --skill-path are required.",
|
|
360
|
+
"MISSING_FLAG",
|
|
361
|
+
"Usage: selftune watch --skill <name> --skill-path <path>",
|
|
362
|
+
);
|
|
359
363
|
}
|
|
360
364
|
if ((values["sync-force"] ?? false) && !(values["sync-first"] ?? false)) {
|
|
361
|
-
|
|
362
|
-
|
|
365
|
+
throw new CLIError(
|
|
366
|
+
"--sync-force requires --sync-first.",
|
|
367
|
+
"INVALID_FLAG",
|
|
368
|
+
"Add --sync-first when using --sync-force.",
|
|
369
|
+
);
|
|
363
370
|
}
|
|
364
371
|
|
|
365
372
|
const rawWindow = values.window ?? "20";
|
|
366
373
|
if (!/^\d+$/.test(rawWindow)) {
|
|
367
|
-
|
|
368
|
-
|
|
374
|
+
throw new CLIError(
|
|
375
|
+
"--window must be a positive integer >= 1.",
|
|
376
|
+
"INVALID_FLAG",
|
|
377
|
+
"selftune watch --window 20",
|
|
378
|
+
);
|
|
369
379
|
}
|
|
370
380
|
const windowSessions = Number.parseInt(rawWindow, 10);
|
|
371
381
|
if (windowSessions < 1) {
|
|
372
|
-
|
|
373
|
-
|
|
382
|
+
throw new CLIError(
|
|
383
|
+
"--window must be a positive integer >= 1.",
|
|
384
|
+
"INVALID_FLAG",
|
|
385
|
+
"selftune watch --window 20",
|
|
386
|
+
);
|
|
374
387
|
}
|
|
375
388
|
|
|
376
389
|
const rawThreshold = values.threshold ?? "0.1";
|
|
377
390
|
if (!/^\d+(\.\d+)?$/.test(rawThreshold)) {
|
|
378
|
-
|
|
379
|
-
|
|
391
|
+
throw new CLIError(
|
|
392
|
+
"--threshold must be a finite number between 0 and 1.",
|
|
393
|
+
"INVALID_FLAG",
|
|
394
|
+
"selftune watch --threshold 0.1",
|
|
395
|
+
);
|
|
380
396
|
}
|
|
381
397
|
const regressionThreshold = Number.parseFloat(rawThreshold);
|
|
382
398
|
if (regressionThreshold < 0 || regressionThreshold > 1) {
|
|
383
|
-
|
|
384
|
-
|
|
399
|
+
throw new CLIError(
|
|
400
|
+
"--threshold must be a finite number between 0 and 1.",
|
|
401
|
+
"INVALID_FLAG",
|
|
402
|
+
"selftune watch --threshold 0.1",
|
|
403
|
+
);
|
|
385
404
|
}
|
|
386
405
|
|
|
387
406
|
const result = await watch({
|
|
@@ -399,8 +418,5 @@ Options:
|
|
|
399
418
|
}
|
|
400
419
|
|
|
401
420
|
if (import.meta.main) {
|
|
402
|
-
cliMain().catch(
|
|
403
|
-
console.error(`[FATAL] ${err}`);
|
|
404
|
-
process.exit(1);
|
|
405
|
-
});
|
|
421
|
+
cliMain().catch(handleCLIError);
|
|
406
422
|
}
|
|
@@ -53,6 +53,7 @@ import type {
|
|
|
53
53
|
SessionTelemetryRecord,
|
|
54
54
|
SkillUsageRecord,
|
|
55
55
|
} from "./types.js";
|
|
56
|
+
import { CLIError, handleCLIError } from "./utils/cli-error.js";
|
|
56
57
|
import { detectAgent } from "./utils/llm-call.js";
|
|
57
58
|
import { getSelftuneVersion, readConfiguredAgentType } from "./utils/selftune-meta.js";
|
|
58
59
|
import {
|
|
@@ -1249,37 +1250,41 @@ Examples:
|
|
|
1249
1250
|
|
|
1250
1251
|
const maxSkillsRaw = values["max-skills"] ?? "5";
|
|
1251
1252
|
if (!/^\d+$/.test(maxSkillsRaw) || Number(maxSkillsRaw) < 1) {
|
|
1252
|
-
|
|
1253
|
-
"
|
|
1253
|
+
throw new CLIError(
|
|
1254
|
+
"--max-skills must be a positive integer",
|
|
1255
|
+
"INVALID_FLAG",
|
|
1256
|
+
"selftune orchestrate --max-skills 5",
|
|
1254
1257
|
);
|
|
1255
|
-
process.exit(1);
|
|
1256
1258
|
}
|
|
1257
1259
|
const maxSkills = Number(maxSkillsRaw);
|
|
1258
1260
|
|
|
1259
1261
|
const recentWindowRaw = values["recent-window"] ?? "48";
|
|
1260
1262
|
if (!/^\d+$/.test(recentWindowRaw) || Number(recentWindowRaw) < 1) {
|
|
1261
|
-
|
|
1262
|
-
"
|
|
1263
|
+
throw new CLIError(
|
|
1264
|
+
"--recent-window must be a positive integer",
|
|
1265
|
+
"INVALID_FLAG",
|
|
1266
|
+
"selftune orchestrate --recent-window 48",
|
|
1263
1267
|
);
|
|
1264
|
-
process.exit(1);
|
|
1265
1268
|
}
|
|
1266
1269
|
const recentWindow = Number(recentWindowRaw);
|
|
1267
1270
|
|
|
1268
1271
|
const maxAutoGradeRaw = values["max-auto-grade"] ?? "5";
|
|
1269
1272
|
if (!/^\d+$/.test(maxAutoGradeRaw)) {
|
|
1270
|
-
|
|
1271
|
-
"
|
|
1273
|
+
throw new CLIError(
|
|
1274
|
+
"--max-auto-grade must be a non-negative integer",
|
|
1275
|
+
"INVALID_FLAG",
|
|
1276
|
+
"selftune orchestrate --max-auto-grade 5",
|
|
1272
1277
|
);
|
|
1273
|
-
process.exit(1);
|
|
1274
1278
|
}
|
|
1275
1279
|
const maxAutoGrade = Number(maxAutoGradeRaw);
|
|
1276
1280
|
|
|
1277
1281
|
const loopIntervalRaw = values["loop-interval"] ?? "3600";
|
|
1278
1282
|
if (!/^\d+$/.test(loopIntervalRaw) || (values.loop && Number(loopIntervalRaw) < 60)) {
|
|
1279
|
-
|
|
1280
|
-
"
|
|
1283
|
+
throw new CLIError(
|
|
1284
|
+
"--loop-interval must be an integer >= 60 (seconds)",
|
|
1285
|
+
"INVALID_FLAG",
|
|
1286
|
+
"selftune orchestrate --loop --loop-interval 3600",
|
|
1281
1287
|
);
|
|
1282
|
-
process.exit(1);
|
|
1283
1288
|
}
|
|
1284
1289
|
const loopInterval = Number(loopIntervalRaw);
|
|
1285
1290
|
|
|
@@ -1387,9 +1392,5 @@ Examples:
|
|
|
1387
1392
|
}
|
|
1388
1393
|
|
|
1389
1394
|
if (import.meta.main) {
|
|
1390
|
-
cliMain().catch(
|
|
1391
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1392
|
-
console.error(`[FATAL] ${message}`);
|
|
1393
|
-
process.exit(1);
|
|
1394
|
-
});
|
|
1395
|
+
cliMain().catch(handleCLIError);
|
|
1395
1396
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { Database } from "bun:sqlite";
|
|
10
10
|
|
|
11
|
+
import { scoreDescription } from "../evolution/description-quality.js";
|
|
11
12
|
import { getPendingProposals, getSkillReportPayload, safeParseJson } from "../localdb/queries.js";
|
|
12
13
|
|
|
13
14
|
export function handleSkillReport(db: Database, skillName: string): Response {
|
|
@@ -203,6 +204,21 @@ export function handleSkillReport(db: Database, skillName: string): Response {
|
|
|
203
204
|
completion_status: string | null;
|
|
204
205
|
}>;
|
|
205
206
|
|
|
207
|
+
// 8. Description quality score — computed from latest evolution evidence
|
|
208
|
+
const latestEvidence = db
|
|
209
|
+
.query(
|
|
210
|
+
`SELECT proposed_text, original_text FROM evolution_evidence
|
|
211
|
+
WHERE skill_name = ? AND (proposed_text IS NOT NULL OR original_text IS NOT NULL)
|
|
212
|
+
ORDER BY timestamp DESC LIMIT 1`,
|
|
213
|
+
)
|
|
214
|
+
.get(skillName) as { proposed_text: string | null; original_text: string | null } | null;
|
|
215
|
+
|
|
216
|
+
// Use the most recent description: deployed proposed_text, or fallback to original_text
|
|
217
|
+
const currentDescriptionText = latestEvidence?.proposed_text ?? latestEvidence?.original_text;
|
|
218
|
+
const descriptionQuality = currentDescriptionText
|
|
219
|
+
? scoreDescription(currentDescriptionText, skillName)
|
|
220
|
+
: null;
|
|
221
|
+
|
|
206
222
|
return Response.json({
|
|
207
223
|
...report,
|
|
208
224
|
evolution: evolutionWithSnapshot,
|
|
@@ -227,5 +243,6 @@ export function handleSkillReport(db: Database, skillName: string): Response {
|
|
|
227
243
|
is_actionable: p.is_actionable === 1,
|
|
228
244
|
})),
|
|
229
245
|
session_metadata: sessionMeta,
|
|
246
|
+
description_quality: descriptionQuality,
|
|
230
247
|
});
|
|
231
248
|
}
|