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
@@ -5,12 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>selftune — Dashboard</title>
7
7
  <link rel="icon" type="image/png" href="/favicon.png" />
8
- <script type="module" crossorigin src="/assets/index-4_dAY17K.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-DOu3iLD9.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-Dw2cE7zH.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-CKkiCskZ.js">
11
- <link rel="modulepreload" crossorigin href="/assets/vendor-ui-7xD7fNEU.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/vendor-ui-DIwlrGlb.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/vendor-table-pHbDxq36.js">
13
- <link rel="stylesheet" crossorigin href="/assets/index-BxV5WZHc.css">
13
+ <link rel="stylesheet" crossorigin href="/assets/index-BMIS6uUh.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>
@@ -5,64 +5,45 @@
5
5
  * (or null if the rule doesn't fire). Rules must be pure functions —
6
6
  * no network calls, no imports from evolution/monitoring/grading layers.
7
7
  *
8
- * SQLite is the default read path for log data. JSONL fallback is used
9
- * only when context paths differ from the well-known constants
10
- * (test/custom-path override).
8
+ * All log data is read from SQLite.
11
9
  */
12
10
 
13
11
  import { existsSync, readdirSync, readFileSync } from "node:fs";
14
- import { dirname, join } from "node:path";
12
+ import { join } from "node:path";
15
13
 
16
- import { EVOLUTION_AUDIT_LOG, QUERY_LOG } from "./constants.js";
17
14
  import { getDb } from "./localdb/db.js";
18
15
  import { queryEvolutionAudit, queryQueryLog, querySkillUsageRecords } from "./localdb/queries.js";
19
16
  import type { ActivationContext, ActivationRule } from "./types.js";
20
- import { readJsonl } from "./utils/jsonl.js";
21
17
 
22
18
  // ---------------------------------------------------------------------------
23
- // Rule: post-session diagnostic (SQLite-first; JSONL for test/custom paths)
19
+ // Rule: post-session diagnostic
24
20
  // ---------------------------------------------------------------------------
25
21
 
26
22
  const postSessionDiagnostic: ActivationRule = {
27
23
  id: "post-session-diagnostic",
28
24
  description: "Suggest `selftune last` when session has >2 unmatched queries",
29
25
  evaluate(ctx: ActivationContext): string | null {
30
- // Count queries for this session — SQLite is the default path
26
+ // Count queries for this session
31
27
  let queries: Array<{ session_id: string; query: string }>;
32
- if (ctx.query_log_path === QUERY_LOG) {
33
- try {
34
- const db = getDb();
35
- queries = queryQueryLog(db) as Array<{ session_id: string; query: string }>;
36
- } catch {
37
- return null;
38
- }
39
- } else {
40
- // test/custom-path fallback
41
- queries = readJsonl<{ session_id: string; query: string }>(ctx.query_log_path);
28
+ try {
29
+ const db = getDb();
30
+ queries = queryQueryLog(db) as Array<{ session_id: string; query: string }>;
31
+ } catch {
32
+ return null;
42
33
  }
43
34
  const sessionQueries = queries.filter((q) => q.session_id === ctx.session_id);
44
35
 
45
36
  if (sessionQueries.length === 0) return null;
46
37
 
47
- // Count skill usages for this session — SQLite is the default path
38
+ // Count skill usages for this session
48
39
  let skillUsages: Array<{ session_id: string }>;
49
- if (ctx.query_log_path === QUERY_LOG) {
50
- try {
51
- const db = getDb();
52
- skillUsages = (querySkillUsageRecords(db) as Array<{ session_id: string }>).filter(
53
- (s) => s.session_id === ctx.session_id,
54
- );
55
- } catch {
56
- return null;
57
- }
58
- } else {
59
- // test/custom-path fallback
60
- const skillLogPath = join(dirname(ctx.query_log_path), "skill_usage_log.jsonl");
61
- skillUsages = existsSync(skillLogPath)
62
- ? readJsonl<{ session_id: string }>(skillLogPath).filter(
63
- (s) => s.session_id === ctx.session_id,
64
- )
65
- : [];
40
+ try {
41
+ const db = getDb();
42
+ skillUsages = (querySkillUsageRecords(db) as Array<{ session_id: string }>).filter(
43
+ (s) => s.session_id === ctx.session_id,
44
+ );
45
+ } catch {
46
+ return null;
66
47
  }
67
48
 
68
49
  const unmatchedCount = sessionQueries.length - skillUsages.length;
@@ -114,7 +95,7 @@ const gradingThresholdBreach: ActivationRule = {
114
95
  };
115
96
 
116
97
  // ---------------------------------------------------------------------------
117
- // Rule: stale evolution (SQLite-first; JSONL for test/custom paths)
98
+ // Rule: stale evolution
118
99
  // ---------------------------------------------------------------------------
119
100
 
120
101
  const staleEvolution: ActivationRule = {
@@ -124,18 +105,13 @@ const staleEvolution: ActivationRule = {
124
105
  evaluate(ctx: ActivationContext): string | null {
125
106
  const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
126
107
 
127
- // Check last evolution timestamp — SQLite is the default path
108
+ // Check last evolution timestamp
128
109
  let auditEntries: Array<{ timestamp: string; action: string }>;
129
- if (ctx.evolution_audit_log_path === EVOLUTION_AUDIT_LOG) {
130
- try {
131
- const db = getDb();
132
- auditEntries = queryEvolutionAudit(db) as Array<{ timestamp: string; action: string }>;
133
- } catch {
134
- return null;
135
- }
136
- } else {
137
- // test/custom-path fallback
138
- auditEntries = readJsonl<{ timestamp: string; action: string }>(ctx.evolution_audit_log_path);
110
+ try {
111
+ const db = getDb();
112
+ auditEntries = queryEvolutionAudit(db) as Array<{ timestamp: string; action: string }>;
113
+ } catch {
114
+ return null;
139
115
  }
140
116
 
141
117
  if (auditEntries.length === 0) {
@@ -24,6 +24,7 @@ import { join } from "node:path";
24
24
 
25
25
  import { SELFTUNE_CONFIG_DIR, SELFTUNE_CONFIG_PATH } from "./constants.js";
26
26
  import type { SelftuneConfig } from "./types.js";
27
+ import { CLIError } from "./utils/cli-error.js";
27
28
 
28
29
  // ---------------------------------------------------------------------------
29
30
  // Configuration
@@ -280,11 +281,11 @@ https://github.com/selftune-dev/selftune#telemetry`);
280
281
  try {
281
282
  writeConfigField("analytics_disabled", true);
282
283
  } catch {
283
- console.error(
284
- "Failed to disable telemetry: cannot write ~/.selftune/config.json. " +
285
- "Try checking file permissions, or set SELFTUNE_NO_ANALYTICS=1.",
284
+ throw new CLIError(
285
+ "Failed to disable telemetry: cannot write ~/.selftune/config.json",
286
+ "OPERATION_FAILED",
287
+ "Check file permissions, or set SELFTUNE_NO_ANALYTICS=1",
286
288
  );
287
- process.exit(1);
288
289
  }
289
290
  console.log("Telemetry disabled. No anonymous usage data will be sent.");
290
291
  console.log("You can re-enable with: selftune telemetry enable");
@@ -294,11 +295,11 @@ https://github.com/selftune-dev/selftune#telemetry`);
294
295
  try {
295
296
  writeConfigField("analytics_disabled", false);
296
297
  } catch {
297
- console.error(
298
- "Failed to enable telemetry: cannot write ~/.selftune/config.json. " +
299
- "Try checking file permissions.",
298
+ throw new CLIError(
299
+ "Failed to enable telemetry: cannot write ~/.selftune/config.json",
300
+ "OPERATION_FAILED",
301
+ "Check file permissions",
300
302
  );
301
- process.exit(1);
302
303
  }
303
304
  console.log("Telemetry enabled. Anonymous usage data will be sent.");
304
305
  console.log("Disable anytime with: selftune telemetry disable");
@@ -331,10 +332,11 @@ https://github.com/selftune-dev/selftune#telemetry`);
331
332
  break;
332
333
  }
333
334
  default:
334
- console.error(
335
- `Unknown telemetry subcommand: ${sub}\nRun 'selftune telemetry --help' for usage.`,
335
+ throw new CLIError(
336
+ `Unknown telemetry subcommand: ${sub}`,
337
+ "INVALID_FLAG",
338
+ "selftune telemetry --help",
336
339
  );
337
- process.exit(1);
338
340
  }
339
341
  }
340
342
 
@@ -24,6 +24,7 @@ import type {
24
24
  SessionTelemetryRecord,
25
25
  SkillUsageRecord,
26
26
  } from "../types.js";
27
+ import { CLIError, handleCLIError } from "../utils/cli-error.js";
27
28
  import type { BadgeFormat } from "./badge-data.js";
28
29
  import { findSkillBadgeData } from "./badge-data.js";
29
30
  import { formatBadgeOutput } from "./badge-svg.js";
@@ -58,15 +59,15 @@ export async function cliMain(): Promise<void> {
58
59
  }
59
60
 
60
61
  if (!values.skill) {
61
- console.error("Error: --skill is required\n");
62
- console.error(HELP);
63
- process.exit(1);
62
+ throw new CLIError("--skill is required", "MISSING_FLAG", "selftune badge --skill <name>");
64
63
  }
65
64
 
66
65
  if (values.format && !VALID_FORMATS.has(values.format as BadgeFormat)) {
67
- console.error(`Error: invalid format '${values.format}'. Must be one of: svg, markdown, url\n`);
68
- console.error(HELP);
69
- process.exit(1);
66
+ throw new CLIError(
67
+ `Invalid format '${values.format}'. Must be one of: svg, markdown, url`,
68
+ "INVALID_FLAG",
69
+ "selftune badge --skill <name> --format svg",
70
+ );
70
71
  }
71
72
 
72
73
  const format: BadgeFormat =
@@ -90,8 +91,11 @@ export async function cliMain(): Promise<void> {
90
91
  // Find skill badge data
91
92
  const badgeData = findSkillBadgeData(result, values.skill);
92
93
  if (!badgeData) {
93
- console.error(`Skill not found: ${values.skill}`);
94
- process.exit(1);
94
+ throw new CLIError(
95
+ `Skill not found: ${values.skill}`,
96
+ "MISSING_DATA",
97
+ "selftune status --json # list available skill names",
98
+ );
95
99
  }
96
100
 
97
101
  // Generate output
@@ -106,5 +110,5 @@ export async function cliMain(): Promise<void> {
106
110
  }
107
111
 
108
112
  if (import.meta.main) {
109
- cliMain();
113
+ cliMain().catch(handleCLIError);
110
114
  }
@@ -26,13 +26,14 @@ import {
26
26
  readCanonicalRecords,
27
27
  serializeCanonicalRecords,
28
28
  } from "./utils/canonical-log.js";
29
+ import { CLIError, handleCLIError } from "./utils/cli-error.js";
29
30
 
30
31
  function exitWithUsage(message?: string): never {
31
- if (message) console.error(`[ERROR] ${message}`);
32
- console.error(
33
- `Usage: selftune export-canonical [--out FILE] [--platform NAME] [--record-kind KIND] [--pretty] [--log FILE] [--projects-dir PATH] [--push-payload]`,
32
+ throw new CLIError(
33
+ message ?? "Invalid usage.",
34
+ "INVALID_FLAG",
35
+ "Usage: selftune export-canonical [--out FILE] [--platform NAME] [--record-kind KIND] [--pretty] [--log FILE] [--projects-dir PATH] [--push-payload]",
34
36
  );
35
- process.exit(1);
36
37
  }
37
38
 
38
39
  function validatePlatform(value: string | undefined): CanonicalPlatform | undefined {
@@ -195,7 +196,6 @@ if (import.meta.main) {
195
196
  try {
196
197
  cliMain();
197
198
  } catch (error) {
198
- const message = error instanceof Error ? error.message : String(error);
199
- exitWithUsage(message);
199
+ handleCLIError(error);
200
200
  }
201
201
  }
@@ -22,15 +22,22 @@ export const SELFTUNE_CONFIG_PATH = join(SELFTUNE_CONFIG_DIR, "config.json");
22
22
 
23
23
  export const LOG_DIR = (process.env.SELFTUNE_LOG_DIR || undefined) ?? claudeHomeDir;
24
24
 
25
+ /** @deprecated Phase 3: JSONL writes removed. Used only by materializer recovery and export. */
25
26
  export const TELEMETRY_LOG = join(LOG_DIR, "session_telemetry_log.jsonl");
26
27
  export const SKILL_LOG = join(LOG_DIR, "skill_usage_log.jsonl");
27
28
  export const REPAIRED_SKILL_LOG = join(LOG_DIR, "skill_usage_repaired.jsonl");
29
+ /** @deprecated Phase 3: JSONL writes removed. Used only by materializer recovery and export. */
28
30
  export const CANONICAL_LOG = join(LOG_DIR, "canonical_telemetry_log.jsonl");
29
31
  export const REPAIRED_SKILL_SESSIONS_MARKER = join(LOG_DIR, "skill_usage_repaired_sessions.json");
32
+ /** @deprecated Phase 3: JSONL writes removed. Used only by materializer recovery and export. */
30
33
  export const QUERY_LOG = join(LOG_DIR, "all_queries_log.jsonl");
34
+ /** @deprecated Phase 3: JSONL writes removed. Used only by materializer recovery and export. */
31
35
  export const EVOLUTION_AUDIT_LOG = join(LOG_DIR, "evolution_audit_log.jsonl");
36
+ /** @deprecated Phase 3: JSONL writes removed. Used only by materializer recovery and export. */
32
37
  export const EVOLUTION_EVIDENCE_LOG = join(LOG_DIR, "evolution_evidence_log.jsonl");
38
+ /** @deprecated Phase 3: JSONL writes removed. Used only by materializer recovery and export. */
33
39
  export const ORCHESTRATE_RUN_LOG = join(LOG_DIR, "orchestrate_runs.jsonl");
40
+ /** @deprecated Phase 3: JSONL writes removed. Used only by materializer recovery and export. */
34
41
  export const SIGNAL_LOG = join(LOG_DIR, "improvement_signals.jsonl");
35
42
  export const ORCHESTRATE_LOCK = join(LOG_DIR, ".orchestrate.lock");
36
43
 
@@ -9,13 +9,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
9
9
  import { homedir } from "node:os";
10
10
  import { join } from "node:path";
11
11
 
12
- import {
13
- EVOLUTION_AUDIT_LOG,
14
- QUERY_LOG,
15
- SELFTUNE_CONFIG_DIR,
16
- SKILL_LOG,
17
- TELEMETRY_LOG,
18
- } from "../constants.js";
12
+ import { SELFTUNE_CONFIG_DIR } from "../constants.js";
19
13
  import { buildEvalSet, classifyInvocation } from "../eval/hooks-to-evals.js";
20
14
  import { getDb } from "../localdb/db.js";
21
15
  import {
@@ -36,7 +30,6 @@ import type {
36
30
  SessionTelemetryRecord,
37
31
  SkillUsageRecord,
38
32
  } from "../types.js";
39
- import { readJsonl } from "../utils/jsonl.js";
40
33
 
41
34
  // ---------------------------------------------------------------------------
42
35
  // Helpers
@@ -201,42 +194,14 @@ export function assembleBundle(options: {
201
194
  telemetryLogPath?: string;
202
195
  evolutionAuditLogPath?: string;
203
196
  }): ContributionBundle {
204
- const {
205
- skillName,
206
- since,
207
- sanitizationLevel,
208
- queryLogPath = QUERY_LOG,
209
- skillLogPath = SKILL_LOG,
210
- telemetryLogPath = TELEMETRY_LOG,
211
- evolutionAuditLogPath = EVOLUTION_AUDIT_LOG,
212
- } = options;
213
-
214
- // Read from JSONL when custom (non-default) paths are provided (test isolation),
215
- // otherwise read from SQLite (production).
216
- const useJsonl =
217
- queryLogPath !== QUERY_LOG ||
218
- skillLogPath !== SKILL_LOG ||
219
- telemetryLogPath !== TELEMETRY_LOG ||
220
- evolutionAuditLogPath !== EVOLUTION_AUDIT_LOG;
221
-
222
- let allSkillRecords: SkillUsageRecord[];
223
- let allQueryRecords: QueryLogRecord[];
224
- let allTelemetryRecords: SessionTelemetryRecord[];
225
- let allEvolutionRecords: EvolutionAuditEntry[];
226
-
227
- if (useJsonl) {
228
- // JSONL fallback: only used when custom (non-default) log paths are provided (test isolation)
229
- allSkillRecords = readJsonl<SkillUsageRecord>(skillLogPath);
230
- allQueryRecords = readJsonl<QueryLogRecord>(queryLogPath);
231
- allTelemetryRecords = readJsonl<SessionTelemetryRecord>(telemetryLogPath);
232
- allEvolutionRecords = readJsonl<EvolutionAuditEntry>(evolutionAuditLogPath);
233
- } else {
234
- const db = getDb();
235
- allSkillRecords = querySkillUsageRecords(db) as SkillUsageRecord[];
236
- allQueryRecords = queryQueryLog(db) as QueryLogRecord[];
237
- allTelemetryRecords = querySessionTelemetry(db) as SessionTelemetryRecord[];
238
- allEvolutionRecords = queryEvolutionAudit(db) as EvolutionAuditEntry[];
239
- }
197
+ const { skillName, since, sanitizationLevel } = options;
198
+
199
+ const db = getDb();
200
+ const allSkillRecords = querySkillUsageRecords(db) as SkillUsageRecord[];
201
+ const allQueryRecords = queryQueryLog(db) as QueryLogRecord[];
202
+ const allTelemetryRecords = querySessionTelemetry(db) as SessionTelemetryRecord[];
203
+ // queryEvolutionAudit returns DESC order; reverse to ASC for chronological processing
204
+ const allEvolutionRecords = (queryEvolutionAudit(db) as EvolutionAuditEntry[]).toReversed();
240
205
 
241
206
  // Filter by skill and since
242
207
  const skillRecords = filterSince(
@@ -12,6 +12,7 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
12
12
  import { parseArgs } from "node:util";
13
13
 
14
14
  import { CONTRIBUTIONS_DIR } from "../constants.js";
15
+ import { handleCLIError } from "../utils/cli-error.js";
15
16
  import { assembleBundle } from "./bundle.js";
16
17
  import { sanitizeBundle } from "./sanitize.js";
17
18
 
@@ -211,5 +212,5 @@ function submitToGitHub(json: string, outputPath: string): boolean {
211
212
  }
212
213
 
213
214
  if (import.meta.main) {
214
- await cliMain();
215
+ cliMain().catch(handleCLIError);
215
216
  }
@@ -18,6 +18,8 @@ import { homedir } from "node:os";
18
18
  import { join } from "node:path";
19
19
  import { parseArgs } from "node:util";
20
20
 
21
+ import { handleCLIError } from "../utils/cli-error.js";
22
+
21
23
  // ---------------------------------------------------------------------------
22
24
  // Types & constants
23
25
  // ---------------------------------------------------------------------------
@@ -262,5 +264,5 @@ Subcommands:
262
264
  }
263
265
 
264
266
  if (import.meta.main) {
265
- await cliMain();
267
+ cliMain().catch(handleCLIError);
266
268
  }
@@ -48,6 +48,15 @@ export interface PendingProposal {
48
48
  skill_name?: string;
49
49
  }
50
50
 
51
+ export interface RecentActivityItem {
52
+ timestamp: string;
53
+ session_id: string;
54
+ skill_name: string;
55
+ query: string;
56
+ triggered: boolean;
57
+ is_live: boolean;
58
+ }
59
+
51
60
  export interface SkillSummary {
52
61
  skill_name: string;
53
62
  skill_scope: string | null;
@@ -73,6 +82,8 @@ export interface OverviewPayload {
73
82
  };
74
83
  unmatched_queries: UnmatchedQuery[];
75
84
  pending_proposals: PendingProposal[];
85
+ active_sessions: number;
86
+ recent_activity: RecentActivityItem[];
76
87
  }
77
88
 
78
89
  export interface OverviewResponse {
@@ -179,6 +190,7 @@ export interface OrchestrateRunReport {
179
190
  deployed: number;
180
191
  watched: number;
181
192
  skipped: number;
193
+ auto_graded?: number;
182
194
  skill_actions: OrchestrateRunSkillAction[];
183
195
  }
184
196
 
@@ -230,4 +242,14 @@ export interface SkillReportResponse extends SkillReportPayload {
230
242
  };
231
243
  prompt_samples: PromptSample[];
232
244
  session_metadata: SessionMeta[];
245
+ description_quality?: {
246
+ composite: number;
247
+ criteria: {
248
+ length: number;
249
+ trigger_context: number;
250
+ vagueness: number;
251
+ specificity: number;
252
+ not_just_name: number;
253
+ };
254
+ } | null;
233
255
  }
@@ -7,6 +7,8 @@
7
7
  * selftune dashboard --serve — Deprecated alias for the default behavior
8
8
  */
9
9
 
10
+ import { CLIError } from "./utils/cli-error.js";
11
+
10
12
  export async function cliMain(): Promise<void> {
11
13
  const args = process.argv.slice(2);
12
14
 
@@ -22,11 +24,11 @@ Usage:
22
24
  }
23
25
 
24
26
  if (args.includes("--export") || args.includes("--out")) {
25
- console.error("Legacy dashboard export was removed.");
26
- console.error(
27
+ throw new CLIError(
28
+ "Legacy dashboard export was removed.",
29
+ "INVALID_FLAG",
27
30
  "Use `selftune dashboard` to run the SPA locally, then share a route or screenshot instead.",
28
31
  );
29
- process.exit(1);
30
32
  }
31
33
 
32
34
  const portIdx = args.indexOf("--port");
@@ -34,8 +36,11 @@ Usage:
34
36
  if (portIdx !== -1) {
35
37
  const parsed = Number.parseInt(args[portIdx + 1], 10);
36
38
  if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
37
- console.error(`Invalid port "${args[portIdx + 1]}": must be an integer between 1 and 65535.`);
38
- process.exit(1);
39
+ throw new CLIError(
40
+ `Invalid port "${args[portIdx + 1]}": must be an integer between 1 and 65535.`,
41
+ "INVALID_FLAG",
42
+ "Provide a port number between 1 and 65535 (e.g., --port 3141).",
43
+ );
39
44
  }
40
45
  port = parsed;
41
46
  }
@@ -11,6 +11,7 @@
11
11
  import { parseArgs } from "node:util";
12
12
 
13
13
  import type { BaselineResult, EvalEntry } from "../types.js";
14
+ import { CLIError, handleCLIError } from "../utils/cli-error.js";
14
15
  import { callLlm } from "../utils/llm-call.js";
15
16
  import { buildTriggerCheckPrompt, parseTriggerResponse } from "../utils/trigger-check.js";
16
17
 
@@ -166,8 +167,11 @@ Options:
166
167
  }
167
168
 
168
169
  if (!values.skill || !values["skill-path"]) {
169
- console.error("[ERROR] --skill and --skill-path are required");
170
- process.exit(1);
170
+ throw new CLIError(
171
+ "--skill and --skill-path are required",
172
+ "MISSING_FLAG",
173
+ "selftune grade baseline --skill <name> --skill-path <path>",
174
+ );
171
175
  }
172
176
 
173
177
  const { existsSync, readFileSync } = await import("node:fs");
@@ -175,8 +179,11 @@ Options:
175
179
  // Read skill description
176
180
  const skillPath = values["skill-path"];
177
181
  if (!existsSync(skillPath)) {
178
- console.error(`[ERROR] SKILL.md not found at ${skillPath}`);
179
- process.exit(1);
182
+ throw new CLIError(
183
+ `SKILL.md not found at ${skillPath}`,
184
+ "FILE_NOT_FOUND",
185
+ "Provide a valid --skill-path pointing to SKILL.md",
186
+ );
180
187
  }
181
188
  const skillDescription = readFileSync(skillPath, "utf-8");
182
189
 
@@ -204,27 +211,19 @@ Options:
204
211
  const { detectAgent } = await import("../utils/llm-call.js");
205
212
  const requestedAgent = values.agent;
206
213
  if (requestedAgent && !Bun.which(requestedAgent)) {
207
- console.error(
208
- JSON.stringify({
209
- level: "error",
210
- code: "agent_not_in_path",
211
- message: `Agent CLI '${requestedAgent}' not found in PATH.`,
212
- action: "Install it or omit --agent to use auto-detection.",
213
- }),
214
+ throw new CLIError(
215
+ `Agent CLI '${requestedAgent}' not found in PATH`,
216
+ "AGENT_NOT_FOUND",
217
+ "Install it or omit --agent to use auto-detection",
214
218
  );
215
- process.exit(1);
216
219
  }
217
220
  const agent = requestedAgent ?? detectAgent();
218
221
  if (!agent) {
219
- console.error(
220
- JSON.stringify({
221
- level: "error",
222
- code: "agent_not_found",
223
- message: "No agent CLI (claude/codex/opencode) found in PATH.",
224
- action: "Install Claude Code, Codex, or OpenCode.",
225
- }),
222
+ throw new CLIError(
223
+ "No agent CLI (claude/codex/opencode) found in PATH",
224
+ "AGENT_NOT_FOUND",
225
+ "Install Claude Code, Codex, or OpenCode",
226
226
  );
227
- process.exit(1);
228
227
  }
229
228
 
230
229
  const result = await measureBaseline({
@@ -239,14 +238,5 @@ Options:
239
238
  }
240
239
 
241
240
  if (import.meta.main) {
242
- cliMain().catch((err) => {
243
- console.error(
244
- JSON.stringify({
245
- level: "fatal",
246
- message: err instanceof Error ? err.message : String(err),
247
- stack: err instanceof Error ? err.stack : undefined,
248
- }),
249
- );
250
- process.exit(1);
251
- });
241
+ cliMain().catch(handleCLIError);
252
242
  }