selftune 0.2.13 → 0.2.15
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-BMIS6uUh.css +2 -0
- 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 +3 -3
- package/cli/selftune/activation-rules.ts +24 -48
- 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/constants.ts +7 -0
- package/cli/selftune/contribute/bundle.ts +9 -44
- package/cli/selftune/contribute/contribute.ts +2 -1
- package/cli/selftune/cron/setup.ts +3 -1
- package/cli/selftune/dashboard-contract.ts +22 -0
- package/cli/selftune/dashboard.ts +10 -5
- package/cli/selftune/eval/baseline.ts +20 -30
- package/cli/selftune/eval/hooks-to-evals.ts +27 -34
- 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 +70 -57
- package/cli/selftune/evolution/rollback.ts +7 -6
- package/cli/selftune/grading/auto-grade.ts +27 -35
- package/cli/selftune/grading/grade-session.ts +24 -30
- package/cli/selftune/hooks/auto-activate.ts +12 -3
- package/cli/selftune/hooks/evolution-guard.ts +14 -24
- package/cli/selftune/hooks/prompt-log.ts +7 -9
- package/cli/selftune/hooks/session-stop.ts +0 -8
- package/cli/selftune/index.ts +66 -69
- package/cli/selftune/ingestors/claude-replay.ts +29 -14
- package/cli/selftune/ingestors/codex-rollout.ts +15 -5
- package/cli/selftune/ingestors/codex-wrapper.ts +15 -13
- package/cli/selftune/ingestors/openclaw-ingest.ts +24 -5
- package/cli/selftune/ingestors/opencode-ingest.ts +9 -4
- package/cli/selftune/init.ts +14 -9
- package/cli/selftune/localdb/queries.ts +57 -0
- package/cli/selftune/monitoring/watch.ts +39 -38
- package/cli/selftune/normalization.ts +2 -23
- package/cli/selftune/orchestrate.ts +224 -24
- package/cli/selftune/routes/skill-report.ts +17 -0
- package/cli/selftune/schedule.ts +74 -14
- package/cli/selftune/sync.ts +7 -3
- package/cli/selftune/types.ts +44 -10
- package/cli/selftune/utils/cli-error.ts +102 -0
- package/cli/selftune/utils/jsonl.ts +2 -0
- package/cli/selftune/workflows/workflows.ts +23 -17
- package/package.json +3 -1
- package/packages/ui/src/components/RecentActivityFeed.tsx +86 -0
- package/packages/ui/src/components/index.ts +1 -0
- package/packages/ui/src/components/section-cards.tsx +13 -0
- package/skill/SKILL.md +1 -1
- package/skill/Workflows/Evolve.md +4 -0
- package/skill/Workflows/Initialize.md +8 -8
- package/skill/Workflows/Orchestrate.md +11 -7
- package/skill/Workflows/Schedule.md +11 -0
- package/skill/references/logs.md +22 -21
- package/skill/settings_snippet.json +29 -6
- package/apps/local-dashboard/dist/assets/index-4_dAY17K.js +0 -16
- package/apps/local-dashboard/dist/assets/index-BxV5WZHc.css +0 -2
- package/apps/local-dashboard/dist/assets/vendor-ui-7xD7fNEU.js +0 -12
package/cli/selftune/schedule.ts
CHANGED
|
@@ -18,6 +18,26 @@ import { dirname, join } from "node:path";
|
|
|
18
18
|
import { parseArgs } from "node:util";
|
|
19
19
|
|
|
20
20
|
import { DEFAULT_CRON_JOBS } from "./cron/setup.js";
|
|
21
|
+
import { CLIError, handleCLIError } from "./utils/cli-error.js";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Binary resolution — launchd runs with minimal PATH, so we need full paths
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the absolute path to the `selftune` binary.
|
|
29
|
+
* 1. Bun.which (Bun-native, no spawn)
|
|
30
|
+
* 2. Fallback: ~/.bun/bin/selftune (common bun global install location)
|
|
31
|
+
*/
|
|
32
|
+
export function resolveSelftuneBin(): string {
|
|
33
|
+
try {
|
|
34
|
+
const resolved = Bun.which("selftune");
|
|
35
|
+
if (resolved) return resolved;
|
|
36
|
+
} catch {
|
|
37
|
+
// Bun.which may throw in edge cases — fall through
|
|
38
|
+
}
|
|
39
|
+
return join(homedir(), ".bun", "bin", "selftune");
|
|
40
|
+
}
|
|
21
41
|
|
|
22
42
|
// ---------------------------------------------------------------------------
|
|
23
43
|
// Schedule definitions — derived from the shared DEFAULT_CRON_JOBS
|
|
@@ -137,6 +157,8 @@ function toSystemdExecStart(command: string): string {
|
|
|
137
157
|
// ---------------------------------------------------------------------------
|
|
138
158
|
|
|
139
159
|
export function generateCrontab(): string {
|
|
160
|
+
const resolvedBin = resolveSelftuneBin();
|
|
161
|
+
const home = homedir();
|
|
140
162
|
const lines = [
|
|
141
163
|
"# selftune automation — add to your crontab with: crontab -e",
|
|
142
164
|
"#",
|
|
@@ -144,10 +166,13 @@ export function generateCrontab(): string {
|
|
|
144
166
|
"# status remains a reporting job; orchestrate handles sync, candidate",
|
|
145
167
|
"# selection, low-risk description evolution, and watch/rollback follow-up.",
|
|
146
168
|
"#",
|
|
169
|
+
`PATH=${home}/.bun/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin`,
|
|
170
|
+
"",
|
|
147
171
|
];
|
|
148
172
|
for (const entry of SCHEDULE_ENTRIES) {
|
|
173
|
+
const resolvedCommand = entry.command.replace(/\bselftune\b/g, resolvedBin);
|
|
149
174
|
lines.push(`# ${entry.description}`);
|
|
150
|
-
lines.push(`${entry.schedule} ${
|
|
175
|
+
lines.push(`${entry.schedule} ${resolvedCommand}`);
|
|
151
176
|
lines.push("");
|
|
152
177
|
}
|
|
153
178
|
return lines.join("\n");
|
|
@@ -177,10 +202,17 @@ export function mergeManagedCrontab(existing: string, managedContent: string): s
|
|
|
177
202
|
return `${withoutExistingBlock}\n\n${managedBlock}`;
|
|
178
203
|
}
|
|
179
204
|
|
|
180
|
-
function buildLaunchdDefinition(
|
|
205
|
+
function buildLaunchdDefinition(
|
|
206
|
+
entry: ScheduleEntry,
|
|
207
|
+
binPath?: string,
|
|
208
|
+
): { label: string; content: string } {
|
|
181
209
|
const label = `com.selftune.${entry.name.replace("selftune-", "")}`;
|
|
182
|
-
const
|
|
210
|
+
const resolvedBin = binPath ?? resolveSelftuneBin();
|
|
211
|
+
// Replace bare `selftune` with the resolved absolute path
|
|
212
|
+
const resolvedCommand = entry.command.replace(/\bselftune\b/g, resolvedBin);
|
|
213
|
+
const args = toLaunchdArgs(resolvedCommand);
|
|
183
214
|
const schedule = cronToLaunchdSchedule(entry.schedule);
|
|
215
|
+
const home = homedir();
|
|
184
216
|
|
|
185
217
|
return {
|
|
186
218
|
label,
|
|
@@ -198,6 +230,13 @@ function buildLaunchdDefinition(entry: ScheduleEntry): { label: string; content:
|
|
|
198
230
|
<dict>
|
|
199
231
|
<key>Label</key>
|
|
200
232
|
<string>${label}</string>
|
|
233
|
+
<key>EnvironmentVariables</key>
|
|
234
|
+
<dict>
|
|
235
|
+
<key>PATH</key>
|
|
236
|
+
<string>${home}/.bun/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
237
|
+
<key>HOME</key>
|
|
238
|
+
<string>${home}</string>
|
|
239
|
+
</dict>
|
|
201
240
|
<key>ProgramArguments</key>
|
|
202
241
|
<array>
|
|
203
242
|
${args}
|
|
@@ -222,14 +261,20 @@ export function generateLaunchd(): string {
|
|
|
222
261
|
return plists.join("\n\n");
|
|
223
262
|
}
|
|
224
263
|
|
|
225
|
-
function buildSystemdDefinition(
|
|
264
|
+
function buildSystemdDefinition(
|
|
265
|
+
entry: ScheduleEntry,
|
|
266
|
+
binPath?: string,
|
|
267
|
+
): {
|
|
226
268
|
baseName: string;
|
|
227
269
|
timerContent: string;
|
|
228
270
|
serviceContent: string;
|
|
229
271
|
} {
|
|
230
272
|
const unitName = entry.name;
|
|
231
273
|
const calendar = cronToOnCalendar(entry.schedule);
|
|
232
|
-
const
|
|
274
|
+
const resolvedBin = binPath ?? resolveSelftuneBin();
|
|
275
|
+
const resolvedCommand = entry.command.replace(/\bselftune\b/g, resolvedBin);
|
|
276
|
+
const execStart = toSystemdExecStart(resolvedCommand);
|
|
277
|
+
const home = homedir();
|
|
233
278
|
|
|
234
279
|
return {
|
|
235
280
|
baseName: unitName,
|
|
@@ -247,6 +292,8 @@ Description=${entry.description}
|
|
|
247
292
|
|
|
248
293
|
[Service]
|
|
249
294
|
Type=oneshot
|
|
295
|
+
Environment="PATH=${home}/.bun/bin:/usr/local/bin:/usr/bin:/bin"
|
|
296
|
+
Environment="HOME=${home}"
|
|
250
297
|
ExecStart=${execStart}`,
|
|
251
298
|
};
|
|
252
299
|
}
|
|
@@ -487,10 +534,11 @@ export function cliMain(): void {
|
|
|
487
534
|
applyCronArtifact(values["apply-cron-artifact"]);
|
|
488
535
|
return;
|
|
489
536
|
} catch (err) {
|
|
490
|
-
|
|
537
|
+
throw new CLIError(
|
|
491
538
|
`Failed to apply selftune cron artifact: ${err instanceof Error ? err.message : String(err)}`,
|
|
539
|
+
"OPERATION_FAILED",
|
|
540
|
+
"selftune schedule --install --dry-run",
|
|
492
541
|
);
|
|
493
|
-
process.exit(1);
|
|
494
542
|
}
|
|
495
543
|
}
|
|
496
544
|
|
|
@@ -523,8 +571,11 @@ For OpenClaw-specific scheduling, see: selftune cron`);
|
|
|
523
571
|
dryRun: values["dry-run"] ?? false,
|
|
524
572
|
});
|
|
525
573
|
if (!result.dryRun && !result.activated) {
|
|
526
|
-
|
|
527
|
-
|
|
574
|
+
throw new CLIError(
|
|
575
|
+
"Failed to activate installed schedule artifacts.",
|
|
576
|
+
"OPERATION_FAILED",
|
|
577
|
+
"selftune schedule --install --dry-run",
|
|
578
|
+
);
|
|
528
579
|
}
|
|
529
580
|
console.log(
|
|
530
581
|
JSON.stringify(
|
|
@@ -541,21 +592,30 @@ For OpenClaw-specific scheduling, see: selftune cron`);
|
|
|
541
592
|
);
|
|
542
593
|
return;
|
|
543
594
|
} catch (err) {
|
|
544
|
-
|
|
595
|
+
if (err instanceof CLIError) throw err;
|
|
596
|
+
throw new CLIError(
|
|
545
597
|
`Failed to install schedule artifacts: ${err instanceof Error ? err.message : String(err)}`,
|
|
598
|
+
"OPERATION_FAILED",
|
|
599
|
+
"selftune schedule --install --dry-run",
|
|
546
600
|
);
|
|
547
|
-
process.exit(1);
|
|
548
601
|
}
|
|
549
602
|
}
|
|
550
603
|
|
|
551
604
|
const result = formatOutput(values.format);
|
|
552
605
|
if (!result.ok) {
|
|
553
|
-
|
|
554
|
-
|
|
606
|
+
throw new CLIError(
|
|
607
|
+
result.error ?? "Invalid schedule format",
|
|
608
|
+
"INVALID_FLAG",
|
|
609
|
+
"selftune schedule --format cron",
|
|
610
|
+
);
|
|
555
611
|
}
|
|
556
612
|
console.log(result.data);
|
|
557
613
|
}
|
|
558
614
|
|
|
559
615
|
if (import.meta.main) {
|
|
560
|
-
|
|
616
|
+
try {
|
|
617
|
+
cliMain();
|
|
618
|
+
} catch (err) {
|
|
619
|
+
handleCLIError(err);
|
|
620
|
+
}
|
|
561
621
|
}
|
package/cli/selftune/sync.ts
CHANGED
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
rebuildSkillUsageFromTranscripts,
|
|
63
63
|
} from "./repair/skill-usage.js";
|
|
64
64
|
import type { SkillUsageRecord } from "./types.js";
|
|
65
|
+
import { CLIError, handleCLIError } from "./utils/cli-error.js";
|
|
65
66
|
import { loadMarker, readJsonl, saveMarker } from "./utils/jsonl.js";
|
|
66
67
|
import { writeRepairedSkillUsageRecords } from "./utils/skill-log.js";
|
|
67
68
|
|
|
@@ -560,8 +561,11 @@ Options:
|
|
|
560
561
|
if (values.since) {
|
|
561
562
|
since = new Date(values.since);
|
|
562
563
|
if (Number.isNaN(since.getTime())) {
|
|
563
|
-
|
|
564
|
-
|
|
564
|
+
throw new CLIError(
|
|
565
|
+
`Invalid --since date: ${values.since}`,
|
|
566
|
+
"INVALID_FLAG",
|
|
567
|
+
"selftune sync --since 2026-01-01",
|
|
568
|
+
);
|
|
565
569
|
}
|
|
566
570
|
}
|
|
567
571
|
|
|
@@ -665,5 +669,5 @@ Options:
|
|
|
665
669
|
}
|
|
666
670
|
|
|
667
671
|
if (import.meta.main) {
|
|
668
|
-
cliMain();
|
|
672
|
+
cliMain().catch(handleCLIError);
|
|
669
673
|
}
|
package/cli/selftune/types.ts
CHANGED
|
@@ -166,26 +166,46 @@ export interface TranscriptMetrics {
|
|
|
166
166
|
// Hook payloads (received via stdin from Claude Code)
|
|
167
167
|
// ---------------------------------------------------------------------------
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Common fields present on ALL hook event payloads per Claude Code docs.
|
|
171
|
+
* Individual payloads extend this with event-specific fields.
|
|
172
|
+
*/
|
|
173
|
+
export interface CommonHookPayload {
|
|
174
|
+
session_id?: string;
|
|
175
|
+
transcript_path?: string;
|
|
176
|
+
cwd?: string;
|
|
177
|
+
permission_mode?: string;
|
|
178
|
+
hook_event_name?: string;
|
|
179
|
+
/** Present when hook fires inside a subagent. */
|
|
180
|
+
agent_id?: string;
|
|
181
|
+
/** Agent name (e.g. "Explore", "Plan", or custom agent name). */
|
|
182
|
+
agent_type?: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
169
185
|
// Shared base for pre/post tool-use hook payloads
|
|
170
|
-
export interface BaseToolUsePayload {
|
|
186
|
+
export interface BaseToolUsePayload extends CommonHookPayload {
|
|
171
187
|
tool_name: string;
|
|
172
188
|
tool_input: Record<string, unknown>;
|
|
173
|
-
|
|
189
|
+
tool_use_id?: string;
|
|
174
190
|
}
|
|
175
191
|
|
|
176
|
-
export interface PromptSubmitPayload {
|
|
177
|
-
|
|
178
|
-
|
|
192
|
+
export interface PromptSubmitPayload extends CommonHookPayload {
|
|
193
|
+
/** Current field name per Claude Code docs (2025+). */
|
|
194
|
+
prompt?: string;
|
|
195
|
+
/** Legacy field name — kept for backwards compatibility. */
|
|
196
|
+
user_prompt?: string;
|
|
179
197
|
}
|
|
180
198
|
|
|
181
199
|
export interface PostToolUsePayload extends BaseToolUsePayload {
|
|
182
|
-
|
|
200
|
+
/** Tool execution result, schema depends on the tool. */
|
|
201
|
+
tool_response?: Record<string, unknown>;
|
|
183
202
|
}
|
|
184
203
|
|
|
185
|
-
export interface StopPayload {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
204
|
+
export interface StopPayload extends CommonHookPayload {
|
|
205
|
+
/** True when Claude Code is continuing as a result of a stop hook. */
|
|
206
|
+
stop_hook_active?: boolean;
|
|
207
|
+
/** Text content of Claude's final response. */
|
|
208
|
+
last_assistant_message?: string;
|
|
189
209
|
}
|
|
190
210
|
|
|
191
211
|
// ---------------------------------------------------------------------------
|
|
@@ -394,6 +414,18 @@ export interface EvolutionConfig {
|
|
|
394
414
|
// Validation result base (self-contained for Pareto types)
|
|
395
415
|
// ---------------------------------------------------------------------------
|
|
396
416
|
|
|
417
|
+
/** Heuristic quality score for a skill description (no LLM, pure function). */
|
|
418
|
+
export interface DescriptionQualityScore {
|
|
419
|
+
composite: number; // 0.0-1.0 weighted aggregate
|
|
420
|
+
criteria: {
|
|
421
|
+
length: number; // description length in optimal range
|
|
422
|
+
trigger_context: number; // includes when/if/before/after context
|
|
423
|
+
vagueness: number; // absence of vague words
|
|
424
|
+
specificity: number; // concrete action verbs present
|
|
425
|
+
not_just_name: number; // not just restating the skill name
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
397
429
|
/** Compact summary of an evolve run, used for CLI JSON output. */
|
|
398
430
|
export interface EvolveResultSummary {
|
|
399
431
|
skill: string;
|
|
@@ -412,6 +444,8 @@ export interface EvolveResultSummary {
|
|
|
412
444
|
rationale: string;
|
|
413
445
|
version?: string;
|
|
414
446
|
dashboard_url: string;
|
|
447
|
+
description_quality_before?: number;
|
|
448
|
+
description_quality_after?: number;
|
|
415
449
|
}
|
|
416
450
|
|
|
417
451
|
export interface ValidationResultBase {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed CLI error with machine-readable code, agent-actionable suggestion, and exit code.
|
|
3
|
+
*
|
|
4
|
+
* Replaces ad-hoc `console.error() + process.exit(1)` patterns across the CLI.
|
|
5
|
+
* When `--json` mode is active, errors serialize to structured JSON on stderr.
|
|
6
|
+
* When text mode is active, errors print human-readable messages with suggestions.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* throw new CLIError(
|
|
11
|
+
* "No selftune config found",
|
|
12
|
+
* "CONFIG_MISSING",
|
|
13
|
+
* "Run: selftune init",
|
|
14
|
+
* 4, // exit code for config-missing per agent-cli-contract
|
|
15
|
+
* );
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type CLIErrorCode =
|
|
20
|
+
| "INVALID_FLAG"
|
|
21
|
+
| "MISSING_FLAG"
|
|
22
|
+
| "CONFIG_MISSING"
|
|
23
|
+
| "FILE_NOT_FOUND"
|
|
24
|
+
| "AGENT_NOT_FOUND"
|
|
25
|
+
| "UNKNOWN_COMMAND"
|
|
26
|
+
| "GUARD_BLOCKED"
|
|
27
|
+
| "OPERATION_FAILED"
|
|
28
|
+
| "MISSING_DATA"
|
|
29
|
+
| "INTERNAL_ERROR";
|
|
30
|
+
|
|
31
|
+
export class CLIError extends Error {
|
|
32
|
+
constructor(
|
|
33
|
+
message: string,
|
|
34
|
+
/** Machine-readable error code (SCREAMING_SNAKE_CASE). */
|
|
35
|
+
public readonly code: CLIErrorCode,
|
|
36
|
+
/** Agent-actionable next command or remediation step. */
|
|
37
|
+
public readonly suggestion?: string,
|
|
38
|
+
/** Process exit code. Default 1 (general error). */
|
|
39
|
+
public readonly exitCode: number = 1,
|
|
40
|
+
/** Whether the agent should retry the same command. */
|
|
41
|
+
public readonly retryable: boolean = false,
|
|
42
|
+
) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "CLIError";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Structured JSON representation for `--json` mode. */
|
|
48
|
+
toJSON(): {
|
|
49
|
+
error: {
|
|
50
|
+
code: CLIErrorCode;
|
|
51
|
+
message: string;
|
|
52
|
+
suggestion?: string;
|
|
53
|
+
retryable: boolean;
|
|
54
|
+
};
|
|
55
|
+
} {
|
|
56
|
+
return {
|
|
57
|
+
error: {
|
|
58
|
+
code: this.code,
|
|
59
|
+
message: this.message,
|
|
60
|
+
...(this.suggestion ? { suggestion: this.suggestion } : {}),
|
|
61
|
+
retryable: this.retryable,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Top-level error handler for CLI entry points.
|
|
69
|
+
*
|
|
70
|
+
* Install at the bottom of any CLI entry point:
|
|
71
|
+
* ```ts
|
|
72
|
+
* cliMain().catch(handleCLIError);
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
/** Detect JSON output mode: explicit --json flag or non-TTY stdout (automation). */
|
|
76
|
+
export function isJsonOutputMode(): boolean {
|
|
77
|
+
return process.argv.includes("--json") || process.stdout?.isTTY === false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function handleCLIError(error: unknown): never {
|
|
81
|
+
const jsonMode = isJsonOutputMode();
|
|
82
|
+
|
|
83
|
+
if (error instanceof CLIError) {
|
|
84
|
+
if (jsonMode) {
|
|
85
|
+
console.error(JSON.stringify(error.toJSON()));
|
|
86
|
+
process.exit(error.exitCode);
|
|
87
|
+
}
|
|
88
|
+
console.error(`[ERROR] ${error.message}`);
|
|
89
|
+
if (error.suggestion) {
|
|
90
|
+
console.error(` → ${error.suggestion}`);
|
|
91
|
+
}
|
|
92
|
+
process.exit(error.exitCode);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
96
|
+
if (jsonMode) {
|
|
97
|
+
console.error(JSON.stringify({ error: { code: "INTERNAL_ERROR", message, retryable: false } }));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
console.error(`[FATAL] ${message}`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
@@ -90,6 +90,8 @@ export function readJsonlFrom<T = Record<string, unknown>>(
|
|
|
90
90
|
* Append a single record to a JSONL file. Creates parent directories if needed.
|
|
91
91
|
* When logType is provided, validates the record and logs warnings on failure
|
|
92
92
|
* but still writes the record (fail-open: hooks must never block).
|
|
93
|
+
*
|
|
94
|
+
* @deprecated Phase 3: JSONL writes removed. Retained for materializer/test utilities only.
|
|
93
95
|
*/
|
|
94
96
|
export function appendJsonl(path: string, record: unknown, logType?: LogType): void {
|
|
95
97
|
if (logType) {
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
SkillUsageRecord,
|
|
20
20
|
WorkflowDiscoveryReport,
|
|
21
21
|
} from "../types.js";
|
|
22
|
+
import { CLIError } from "../utils/cli-error.js";
|
|
22
23
|
import { discoverWorkflows } from "./discover.js";
|
|
23
24
|
import { appendWorkflow } from "./skill-md-writer.js";
|
|
24
25
|
|
|
@@ -79,13 +80,11 @@ export async function cliMain(): Promise<void> {
|
|
|
79
80
|
? Number.parseInt(values["min-occurrences"], 10)
|
|
80
81
|
: undefined;
|
|
81
82
|
if (minOccurrences !== undefined && (Number.isNaN(minOccurrences) || minOccurrences < 0)) {
|
|
82
|
-
|
|
83
|
-
process.exit(1);
|
|
83
|
+
throw new CLIError("--min-occurrences must be a non-negative integer.", "INVALID_FLAG");
|
|
84
84
|
}
|
|
85
85
|
const window = values.window ? Number.parseInt(values.window, 10) : undefined;
|
|
86
86
|
if (window !== undefined && (Number.isNaN(window) || window < 0)) {
|
|
87
|
-
|
|
88
|
-
process.exit(1);
|
|
87
|
+
throw new CLIError("--window must be a non-negative integer.", "INVALID_FLAG");
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
// Read telemetry and skill usage logs from SQLite
|
|
@@ -104,8 +103,11 @@ export async function cliMain(): Promise<void> {
|
|
|
104
103
|
// Save subcommand: find workflow, append to SKILL.md
|
|
105
104
|
const nameArg = positionals[1];
|
|
106
105
|
if (!nameArg) {
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
throw new CLIError(
|
|
107
|
+
"Usage: selftune workflows save <name-or-index>",
|
|
108
|
+
"MISSING_FLAG",
|
|
109
|
+
"Provide a workflow name or index (e.g., selftune workflows save 1).",
|
|
110
|
+
);
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
// Match by numeric index (1-based) or workflow_id
|
|
@@ -118,9 +120,11 @@ export async function cliMain(): Promise<void> {
|
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
if (!workflow) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
throw new CLIError(
|
|
124
|
+
`No workflow found matching "${nameArg}".`,
|
|
125
|
+
"INVALID_FLAG",
|
|
126
|
+
"Run 'selftune workflows' to see discovered workflows and their indices.",
|
|
127
|
+
);
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
// Determine SKILL.md path
|
|
@@ -140,18 +144,20 @@ export async function cliMain(): Promise<void> {
|
|
|
140
144
|
skillPath = uniquePaths[0];
|
|
141
145
|
} else if (uniquePaths.length > 1) {
|
|
142
146
|
// Ambiguous: multiple SKILL.md paths found across contributing sessions
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
process.exit(1);
|
|
147
|
+
throw new CLIError(
|
|
148
|
+
`Multiple SKILL.md paths found for "${firstSkill}": ${uniquePaths.join(", ")}`,
|
|
149
|
+
"INVALID_FLAG",
|
|
150
|
+
"Use --skill-path to specify which one to update.",
|
|
151
|
+
);
|
|
149
152
|
}
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
if (!skillPath || !existsSync(skillPath)) {
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
throw new CLIError(
|
|
157
|
+
"Could not determine SKILL.md path.",
|
|
158
|
+
"FILE_NOT_FOUND",
|
|
159
|
+
"Use --skill-path to specify the SKILL.md file to update.",
|
|
160
|
+
);
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
// Build CodifiedWorkflow
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "selftune",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"description": "Self-improving skills CLI for AI agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -73,12 +73,14 @@
|
|
|
73
73
|
"prepublishOnly": "bun run sync-version && bun run build:dashboard",
|
|
74
74
|
"typecheck:dashboard": "cd apps/local-dashboard && bunx tsc --noEmit",
|
|
75
75
|
"check": "bun run lint && bun run format:check && bun run lint:arch && bun run typecheck:dashboard && bun run test",
|
|
76
|
+
"prepare": "bunx lefthook install || true",
|
|
76
77
|
"start": "bun run cli/selftune/index.ts --help"
|
|
77
78
|
},
|
|
78
79
|
"dependencies": {
|
|
79
80
|
"@selftune/telemetry-contract": "file:packages/telemetry-contract"
|
|
80
81
|
},
|
|
81
82
|
"devDependencies": {
|
|
83
|
+
"@evilmartians/lefthook": "^1.13.6",
|
|
82
84
|
"@types/bun": "^1.1.0",
|
|
83
85
|
"oxfmt": "^0.41.0",
|
|
84
86
|
"oxlint": "^1.56.0"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ZapIcon, CircleDotIcon } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { timeAgo } from "../lib/format";
|
|
4
|
+
import { Badge } from "../primitives/badge";
|
|
5
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../primitives/card";
|
|
6
|
+
|
|
7
|
+
export interface RecentActivityItem {
|
|
8
|
+
timestamp: string;
|
|
9
|
+
session_id: string;
|
|
10
|
+
skill_name: string;
|
|
11
|
+
query: string;
|
|
12
|
+
triggered: boolean;
|
|
13
|
+
is_live: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function RecentActivityFeed({ items }: { items: RecentActivityItem[] }) {
|
|
17
|
+
if (items.length === 0) {
|
|
18
|
+
return (
|
|
19
|
+
<Card>
|
|
20
|
+
<CardHeader>
|
|
21
|
+
<CardTitle className="flex items-center gap-2 text-sm">
|
|
22
|
+
<ZapIcon className="size-4" />
|
|
23
|
+
Recent Activity
|
|
24
|
+
</CardTitle>
|
|
25
|
+
</CardHeader>
|
|
26
|
+
<CardContent>
|
|
27
|
+
<p className="text-sm text-muted-foreground text-center py-8">
|
|
28
|
+
No recent skill invocations
|
|
29
|
+
</p>
|
|
30
|
+
</CardContent>
|
|
31
|
+
</Card>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Card>
|
|
37
|
+
<CardHeader>
|
|
38
|
+
<CardTitle className="flex items-center gap-2 text-sm">
|
|
39
|
+
<ZapIcon className="size-4" />
|
|
40
|
+
Recent Activity
|
|
41
|
+
</CardTitle>
|
|
42
|
+
<CardDescription>Latest skill invocations across sessions</CardDescription>
|
|
43
|
+
</CardHeader>
|
|
44
|
+
<CardContent className="space-y-2.5">
|
|
45
|
+
{items.slice(0, 20).map((item, i) => (
|
|
46
|
+
<div
|
|
47
|
+
key={`${item.session_id}-${item.skill_name}-${i}`}
|
|
48
|
+
className="flex gap-3 rounded-md p-1.5"
|
|
49
|
+
>
|
|
50
|
+
<div
|
|
51
|
+
className={`mt-1 size-2 shrink-0 rounded-full ${
|
|
52
|
+
item.triggered ? "bg-emerald-500" : "bg-muted-foreground/40"
|
|
53
|
+
}`}
|
|
54
|
+
/>
|
|
55
|
+
<div className="flex-1 min-w-0 space-y-0.5">
|
|
56
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
57
|
+
<span className="text-xs font-medium truncate">{item.skill_name}</span>
|
|
58
|
+
{item.is_live && (
|
|
59
|
+
<Badge variant="outline" className="h-4 px-1 text-[10px] gap-1">
|
|
60
|
+
<CircleDotIcon className="size-2.5 text-emerald-500" />
|
|
61
|
+
live
|
|
62
|
+
</Badge>
|
|
63
|
+
)}
|
|
64
|
+
{item.triggered ? (
|
|
65
|
+
<Badge variant="default" className="h-4 px-1 text-[10px]">
|
|
66
|
+
triggered
|
|
67
|
+
</Badge>
|
|
68
|
+
) : (
|
|
69
|
+
<Badge variant="secondary" className="h-4 px-1 text-[10px]">
|
|
70
|
+
checked
|
|
71
|
+
</Badge>
|
|
72
|
+
)}
|
|
73
|
+
<span className="text-[10px] text-muted-foreground font-mono ml-auto shrink-0">
|
|
74
|
+
{timeAgo(item.timestamp)}
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
{item.query && (
|
|
78
|
+
<p className="text-xs text-muted-foreground line-clamp-1 font-mono">{item.query}</p>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
))}
|
|
83
|
+
</CardContent>
|
|
84
|
+
</Card>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -3,5 +3,6 @@ export { EvidenceViewer } from "./EvidenceViewer";
|
|
|
3
3
|
export { EvolutionTimeline } from "./EvolutionTimeline";
|
|
4
4
|
export { InfoTip } from "./InfoTip";
|
|
5
5
|
export { OrchestrateRunsPanel } from "./OrchestrateRunsPanel";
|
|
6
|
+
export { RecentActivityFeed } from "./RecentActivityFeed";
|
|
6
7
|
export { SectionCards } from "./section-cards";
|
|
7
8
|
export { SkillHealthGrid } from "./skill-health-grid";
|
|
@@ -21,6 +21,7 @@ interface SectionCardsProps {
|
|
|
21
21
|
pendingCount: number;
|
|
22
22
|
evidenceCount: number;
|
|
23
23
|
hasEvolution?: boolean;
|
|
24
|
+
activeSessionsCount?: number;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export function SectionCards({
|
|
@@ -31,6 +32,7 @@ export function SectionCards({
|
|
|
31
32
|
pendingCount,
|
|
32
33
|
evidenceCount,
|
|
33
34
|
hasEvolution = true,
|
|
35
|
+
activeSessionsCount = 0,
|
|
34
36
|
}: SectionCardsProps) {
|
|
35
37
|
const passRateStr = avgPassRate !== null ? `${Math.round(avgPassRate * 100)}%` : "--";
|
|
36
38
|
const passRateGood = avgPassRate !== null && avgPassRate >= 0.7;
|
|
@@ -118,6 +120,17 @@ export function SectionCards({
|
|
|
118
120
|
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
|
119
121
|
{sessionsCount}
|
|
120
122
|
</CardTitle>
|
|
123
|
+
{activeSessionsCount > 0 && (
|
|
124
|
+
<CardAction>
|
|
125
|
+
<Badge variant="outline" className="gap-1.5">
|
|
126
|
+
<span className="relative flex size-2">
|
|
127
|
+
<span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-75" />
|
|
128
|
+
<span className="relative inline-flex size-2 rounded-full bg-emerald-500" />
|
|
129
|
+
</span>
|
|
130
|
+
{activeSessionsCount} in progress
|
|
131
|
+
</Badge>
|
|
132
|
+
</CardAction>
|
|
133
|
+
)}
|
|
121
134
|
</CardHeader>
|
|
122
135
|
</Card>
|
|
123
136
|
|
package/skill/SKILL.md
CHANGED
|
@@ -278,6 +278,10 @@ After evolution completes (deploy or dry-run), the memory writer updates:
|
|
|
278
278
|
This ensures the next evolve, watch, or rollback workflow has full context
|
|
279
279
|
even after a context window reset.
|
|
280
280
|
|
|
281
|
+
### Description Quality Scoring
|
|
282
|
+
|
|
283
|
+
Proposals are scored on heuristic quality criteria (no LLM required). The composite score (0.0–1.0) uses five weighted criteria: trigger context (0.30), vagueness absence (0.20), specificity (0.20), length (0.15), and not-just-name (0.15). Proposals that regress in quality score are rejected. See `docs/design-docs/evolution-pipeline.md` for full criteria details.
|
|
284
|
+
|
|
281
285
|
### Stopping Criteria
|
|
282
286
|
|
|
283
287
|
The evolution loop stops when any of these conditions is met (priority order):
|