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.
Files changed (60) hide show
  1. package/apps/local-dashboard/dist/assets/index-BMIS6uUh.css +2 -0
  2. package/apps/local-dashboard/dist/assets/index-DOu3iLD9.js +16 -0
  3. package/apps/local-dashboard/dist/assets/vendor-ui-DIwlrGlb.js +12 -0
  4. package/apps/local-dashboard/dist/index.html +3 -3
  5. package/cli/selftune/activation-rules.ts +24 -48
  6. package/cli/selftune/analytics.ts +13 -11
  7. package/cli/selftune/badge/badge.ts +13 -9
  8. package/cli/selftune/canonical-export.ts +6 -6
  9. package/cli/selftune/constants.ts +7 -0
  10. package/cli/selftune/contribute/bundle.ts +9 -44
  11. package/cli/selftune/contribute/contribute.ts +2 -1
  12. package/cli/selftune/cron/setup.ts +3 -1
  13. package/cli/selftune/dashboard-contract.ts +22 -0
  14. package/cli/selftune/dashboard.ts +10 -5
  15. package/cli/selftune/eval/baseline.ts +20 -30
  16. package/cli/selftune/eval/hooks-to-evals.ts +27 -34
  17. package/cli/selftune/eval/import-skillsbench.ts +21 -8
  18. package/cli/selftune/eval/unit-test-cli.ts +22 -11
  19. package/cli/selftune/evolution/description-quality.ts +224 -0
  20. package/cli/selftune/evolution/evolve-body.ts +17 -10
  21. package/cli/selftune/evolution/evolve.ts +70 -57
  22. package/cli/selftune/evolution/rollback.ts +7 -6
  23. package/cli/selftune/grading/auto-grade.ts +27 -35
  24. package/cli/selftune/grading/grade-session.ts +24 -30
  25. package/cli/selftune/hooks/auto-activate.ts +12 -3
  26. package/cli/selftune/hooks/evolution-guard.ts +14 -24
  27. package/cli/selftune/hooks/prompt-log.ts +7 -9
  28. package/cli/selftune/hooks/session-stop.ts +0 -8
  29. package/cli/selftune/index.ts +66 -69
  30. package/cli/selftune/ingestors/claude-replay.ts +29 -14
  31. package/cli/selftune/ingestors/codex-rollout.ts +15 -5
  32. package/cli/selftune/ingestors/codex-wrapper.ts +15 -13
  33. package/cli/selftune/ingestors/openclaw-ingest.ts +24 -5
  34. package/cli/selftune/ingestors/opencode-ingest.ts +9 -4
  35. package/cli/selftune/init.ts +14 -9
  36. package/cli/selftune/localdb/queries.ts +57 -0
  37. package/cli/selftune/monitoring/watch.ts +39 -38
  38. package/cli/selftune/normalization.ts +2 -23
  39. package/cli/selftune/orchestrate.ts +224 -24
  40. package/cli/selftune/routes/skill-report.ts +17 -0
  41. package/cli/selftune/schedule.ts +74 -14
  42. package/cli/selftune/sync.ts +7 -3
  43. package/cli/selftune/types.ts +44 -10
  44. package/cli/selftune/utils/cli-error.ts +102 -0
  45. package/cli/selftune/utils/jsonl.ts +2 -0
  46. package/cli/selftune/workflows/workflows.ts +23 -17
  47. package/package.json +3 -1
  48. package/packages/ui/src/components/RecentActivityFeed.tsx +86 -0
  49. package/packages/ui/src/components/index.ts +1 -0
  50. package/packages/ui/src/components/section-cards.tsx +13 -0
  51. package/skill/SKILL.md +1 -1
  52. package/skill/Workflows/Evolve.md +4 -0
  53. package/skill/Workflows/Initialize.md +8 -8
  54. package/skill/Workflows/Orchestrate.md +11 -7
  55. package/skill/Workflows/Schedule.md +11 -0
  56. package/skill/references/logs.md +22 -21
  57. package/skill/settings_snippet.json +29 -6
  58. package/apps/local-dashboard/dist/assets/index-4_dAY17K.js +0 -16
  59. package/apps/local-dashboard/dist/assets/index-BxV5WZHc.css +0 -2
  60. package/apps/local-dashboard/dist/assets/vendor-ui-7xD7fNEU.js +0 -12
@@ -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} ${entry.command}`);
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(entry: ScheduleEntry): { label: string; content: string } {
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 args = toLaunchdArgs(entry.command);
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(entry: ScheduleEntry): {
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 execStart = toSystemdExecStart(entry.command);
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
- console.error(
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
- console.error("Failed to activate installed schedule artifacts.");
527
- process.exit(1);
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
- console.error(
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
- console.error(result.error);
554
- process.exit(1);
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
- cliMain();
616
+ try {
617
+ cliMain();
618
+ } catch (err) {
619
+ handleCLIError(err);
620
+ }
561
621
  }
@@ -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
- console.error(`[ERROR] Invalid --since date: ${values.since}`);
564
- process.exit(1);
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
  }
@@ -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
- session_id?: string;
189
+ tool_use_id?: string;
174
190
  }
175
191
 
176
- export interface PromptSubmitPayload {
177
- user_prompt: string;
178
- session_id?: string;
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
- transcript_path?: string;
200
+ /** Tool execution result, schema depends on the tool. */
201
+ tool_response?: Record<string, unknown>;
183
202
  }
184
203
 
185
- export interface StopPayload {
186
- session_id?: string;
187
- transcript_path?: string;
188
- cwd?: string;
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
- console.error("[ERROR] --min-occurrences must be a non-negative integer.");
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
- console.error("[ERROR] --window must be a non-negative integer.");
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
- console.error("[ERROR] Usage: selftune workflows save <name-or-index>");
108
- process.exit(1);
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
- console.error(`[ERROR] No workflow found matching "${nameArg}".`);
122
- console.error("Run 'selftune workflows' to see discovered workflows and their indices.");
123
- process.exit(1);
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
- console.error(`[ERROR] Multiple SKILL.md paths found for "${firstSkill}":`);
144
- for (const p of uniquePaths) {
145
- console.error(` - ${p}`);
146
- }
147
- console.error("Use --skill-path to specify which one to update.");
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
- console.error(`[ERROR] Could not determine SKILL.md path. Use --skill-path to specify.`);
154
- process.exit(1);
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.13",
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
@@ -12,7 +12,7 @@ description: >
12
12
  even if they don't say "selftune" explicitly.
13
13
  metadata:
14
14
  author: selftune-dev
15
- version: 0.2.13
15
+ version: 0.2.15
16
16
  category: developer-tools
17
17
  ---
18
18
 
@@ -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):