selftune 0.2.8 → 0.2.10

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 (140) hide show
  1. package/README.md +35 -35
  2. package/apps/local-dashboard/dist/assets/index-BZVLv70T.js +16 -0
  3. package/apps/local-dashboard/dist/assets/{index-CRtLkBTi.css → index-Bs3Y4ixf.css} +1 -1
  4. package/apps/local-dashboard/dist/assets/{vendor-react-BQH_6WrG.js → vendor-react-BXP54cYo.js} +4 -4
  5. package/apps/local-dashboard/dist/assets/{vendor-table-dK1QMLq9.js → vendor-table-DTF_SXoy.js} +1 -1
  6. package/apps/local-dashboard/dist/assets/{vendor-ui-CO2mrx6e.js → vendor-ui-CWU0d1wd.js} +66 -66
  7. package/apps/local-dashboard/dist/index.html +15 -15
  8. package/bin/selftune.cjs +1 -1
  9. package/cli/selftune/activation-rules.ts +37 -18
  10. package/cli/selftune/agent-guidance.ts +16 -16
  11. package/cli/selftune/alpha-identity.ts +1 -2
  12. package/cli/selftune/alpha-upload/build-payloads.ts +18 -2
  13. package/cli/selftune/alpha-upload/flush.ts +2 -2
  14. package/cli/selftune/alpha-upload/stage-canonical.ts +106 -3
  15. package/cli/selftune/auth/device-code.ts +32 -0
  16. package/cli/selftune/auto-update.ts +12 -0
  17. package/cli/selftune/badge/badge.ts +1 -0
  18. package/cli/selftune/canonical-export.ts +5 -0
  19. package/cli/selftune/claude-agents.ts +154 -0
  20. package/cli/selftune/contribute/bundle.ts +2 -0
  21. package/cli/selftune/contribute/contribute.ts +1 -0
  22. package/cli/selftune/cron/setup.ts +2 -2
  23. package/cli/selftune/dashboard-contract.ts +1 -1
  24. package/cli/selftune/dashboard-server.ts +11 -52
  25. package/cli/selftune/eval/hooks-to-evals.ts +13 -6
  26. package/cli/selftune/eval/import-skillsbench.ts +1 -0
  27. package/cli/selftune/eval/synthetic-evals.ts +2 -3
  28. package/cli/selftune/eval/unit-test.ts +1 -0
  29. package/cli/selftune/evolution/deploy-proposal.ts +1 -0
  30. package/cli/selftune/evolution/evolve-body.ts +93 -6
  31. package/cli/selftune/evolution/evolve.ts +0 -1
  32. package/cli/selftune/evolution/propose-body.ts +3 -2
  33. package/cli/selftune/evolution/propose-routing.ts +3 -2
  34. package/cli/selftune/evolution/refine-body.ts +3 -2
  35. package/cli/selftune/export.ts +1 -0
  36. package/cli/selftune/grading/auto-grade.ts +1 -0
  37. package/cli/selftune/grading/grade-session.ts +9 -0
  38. package/cli/selftune/hooks/auto-activate.ts +6 -0
  39. package/cli/selftune/hooks/evolution-guard.ts +12 -15
  40. package/cli/selftune/hooks/prompt-log.ts +1 -0
  41. package/cli/selftune/hooks/session-stop.ts +34 -40
  42. package/cli/selftune/hooks/skill-change-guard.ts +1 -0
  43. package/cli/selftune/hooks/skill-eval.ts +1 -1
  44. package/cli/selftune/index.ts +23 -14
  45. package/cli/selftune/ingestors/claude-replay.ts +1 -0
  46. package/cli/selftune/ingestors/codex-rollout.ts +1 -0
  47. package/cli/selftune/ingestors/codex-wrapper.ts +1 -0
  48. package/cli/selftune/ingestors/openclaw-ingest.ts +1 -0
  49. package/cli/selftune/ingestors/opencode-ingest.ts +1 -0
  50. package/cli/selftune/init.ts +197 -96
  51. package/cli/selftune/localdb/db.ts +1 -0
  52. package/cli/selftune/localdb/direct-write.ts +93 -12
  53. package/cli/selftune/localdb/materialize.ts +2 -0
  54. package/cli/selftune/localdb/queries.ts +210 -0
  55. package/cli/selftune/localdb/schema.ts +72 -1
  56. package/cli/selftune/monitoring/watch.ts +1 -0
  57. package/cli/selftune/normalization.ts +4 -0
  58. package/cli/selftune/observability.ts +14 -7
  59. package/cli/selftune/orchestrate.ts +15 -37
  60. package/cli/selftune/repair/skill-usage.ts +7 -3
  61. package/cli/selftune/routes/orchestrate-runs.ts +1 -0
  62. package/cli/selftune/routes/overview.ts +1 -0
  63. package/cli/selftune/routes/skill-report.ts +1 -0
  64. package/cli/selftune/sync.ts +31 -1
  65. package/cli/selftune/types.ts +2 -2
  66. package/cli/selftune/uninstall.ts +412 -0
  67. package/cli/selftune/utils/canonical-log.ts +2 -0
  68. package/cli/selftune/utils/jsonl.ts +1 -0
  69. package/cli/selftune/utils/llm-call.ts +131 -3
  70. package/cli/selftune/utils/skill-log.ts +1 -0
  71. package/cli/selftune/utils/transcript.ts +1 -0
  72. package/cli/selftune/utils/trigger-check.ts +1 -1
  73. package/cli/selftune/workflows/skill-md-writer.ts +5 -5
  74. package/cli/selftune/workflows/workflows.ts +1 -0
  75. package/package.json +38 -33
  76. package/packages/telemetry-contract/fixtures/golden.test.ts +1 -0
  77. package/packages/telemetry-contract/package.json +3 -3
  78. package/packages/telemetry-contract/src/index.ts +0 -1
  79. package/packages/telemetry-contract/src/schemas.ts +6 -24
  80. package/packages/telemetry-contract/tests/compatibility.test.ts +1 -0
  81. package/packages/ui/README.md +35 -34
  82. package/packages/ui/package.json +3 -3
  83. package/packages/ui/src/components/ActivityTimeline.tsx +49 -42
  84. package/packages/ui/src/components/EvidenceViewer.tsx +306 -182
  85. package/packages/ui/src/components/EvolutionTimeline.tsx +83 -72
  86. package/packages/ui/src/components/InfoTip.tsx +4 -3
  87. package/packages/ui/src/components/OrchestrateRunsPanel.tsx +60 -53
  88. package/packages/ui/src/components/section-cards.tsx +19 -24
  89. package/packages/ui/src/components/skill-health-grid.tsx +213 -193
  90. package/packages/ui/src/lib/constants.tsx +1 -0
  91. package/packages/ui/src/primitives/badge.tsx +12 -15
  92. package/packages/ui/src/primitives/button.tsx +7 -7
  93. package/packages/ui/src/primitives/card.tsx +15 -26
  94. package/packages/ui/src/primitives/checkbox.tsx +7 -8
  95. package/packages/ui/src/primitives/collapsible.tsx +5 -5
  96. package/packages/ui/src/primitives/dropdown-menu.tsx +45 -55
  97. package/packages/ui/src/primitives/label.tsx +6 -6
  98. package/packages/ui/src/primitives/select.tsx +28 -37
  99. package/packages/ui/src/primitives/table.tsx +17 -44
  100. package/packages/ui/src/primitives/tabs.tsx +14 -21
  101. package/packages/ui/src/primitives/tooltip.tsx +10 -22
  102. package/skill/SKILL.md +72 -59
  103. package/skill/Workflows/AlphaUpload.md +4 -4
  104. package/skill/Workflows/AutoActivation.md +11 -6
  105. package/skill/Workflows/Badge.md +22 -16
  106. package/skill/Workflows/Baseline.md +34 -36
  107. package/skill/Workflows/Composability.md +16 -11
  108. package/skill/Workflows/Contribute.md +26 -21
  109. package/skill/Workflows/Cron.md +23 -22
  110. package/skill/Workflows/Dashboard.md +40 -40
  111. package/skill/Workflows/Doctor.md +40 -34
  112. package/skill/Workflows/Evals.md +48 -47
  113. package/skill/Workflows/EvolutionMemory.md +31 -21
  114. package/skill/Workflows/Evolve.md +84 -82
  115. package/skill/Workflows/EvolveBody.md +58 -47
  116. package/skill/Workflows/Grade.md +16 -13
  117. package/skill/Workflows/ImportSkillsBench.md +9 -6
  118. package/skill/Workflows/Ingest.md +36 -21
  119. package/skill/Workflows/Initialize.md +138 -97
  120. package/skill/Workflows/Orchestrate.md +22 -16
  121. package/skill/Workflows/Replay.md +12 -7
  122. package/skill/Workflows/Rollback.md +13 -6
  123. package/skill/Workflows/Schedule.md +6 -6
  124. package/skill/Workflows/Sync.md +18 -11
  125. package/skill/Workflows/UnitTest.md +28 -17
  126. package/skill/Workflows/Watch.md +28 -21
  127. package/skill/agents/diagnosis-analyst.md +11 -0
  128. package/skill/agents/evolution-reviewer.md +15 -1
  129. package/skill/agents/integration-guide.md +10 -0
  130. package/skill/agents/pattern-analyst.md +12 -1
  131. package/skill/references/grading-methodology.md +23 -24
  132. package/skill/references/interactive-config.md +7 -7
  133. package/skill/references/invocation-taxonomy.md +22 -20
  134. package/skill/references/logs.md +20 -6
  135. package/skill/references/setup-patterns.md +4 -2
  136. package/.claude/agents/diagnosis-analyst.md +0 -156
  137. package/.claude/agents/evolution-reviewer.md +0 -180
  138. package/.claude/agents/integration-guide.md +0 -212
  139. package/.claude/agents/pattern-analyst.md +0 -160
  140. package/apps/local-dashboard/dist/assets/index-Bk9vSHHd.js +0 -15
@@ -1,17 +1,17 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>selftune — Dashboard</title>
7
- <link rel="icon" type="image/png" href="/favicon.png" />
8
- <script type="module" crossorigin src="/assets/index-Bk9vSHHd.js"></script>
9
- <link rel="modulepreload" crossorigin href="/assets/vendor-react-BQH_6WrG.js">
10
- <link rel="modulepreload" crossorigin href="/assets/vendor-ui-CO2mrx6e.js">
11
- <link rel="modulepreload" crossorigin href="/assets/vendor-table-dK1QMLq9.js">
12
- <link rel="stylesheet" crossorigin href="/assets/index-CRtLkBTi.css">
13
- </head>
14
- <body>
15
- <div id="root"></div>
16
- </body>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>selftune — Dashboard</title>
7
+ <link rel="icon" type="image/png" href="/favicon.png" />
8
+ <script type="module" crossorigin src="/assets/index-BZVLv70T.js"></script>
9
+ <link rel="modulepreload" crossorigin href="/assets/vendor-react-BXP54cYo.js">
10
+ <link rel="modulepreload" crossorigin href="/assets/vendor-ui-CWU0d1wd.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/vendor-table-DTF_SXoy.js">
12
+ <link rel="stylesheet" crossorigin href="/assets/index-Bs3Y4ixf.css">
13
+ </head>
14
+ <body>
15
+ <div id="root"></div>
16
+ </body>
17
17
  </html>
package/bin/selftune.cjs CHANGED
@@ -26,6 +26,6 @@ for (const [cmd, args] of runners) {
26
26
  console.error(
27
27
  JSON.stringify({
28
28
  error: "No TypeScript runtime found. Install bun (https://bun.sh) or tsx (npx tsx).",
29
- })
29
+ }),
30
30
  );
31
31
  process.exit(1);
@@ -2,13 +2,17 @@
2
2
  * Default activation rules for the auto-activate hook.
3
3
  *
4
4
  * Each rule evaluates session context and returns a suggestion string
5
- * (or null if the rule doesn't fire). Rules must be pure functions
6
- * that read from the filesystem — no network calls, no imports from
7
- * evolution/monitoring/grading layers.
5
+ * (or null if the rule doesn't fire). Rules must be pure functions
6
+ * no network calls, no imports from evolution/monitoring/grading layers.
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
11
  */
9
12
 
10
13
  import { existsSync, readdirSync, readFileSync } from "node:fs";
11
14
  import { dirname, join } from "node:path";
15
+
12
16
  import { EVOLUTION_AUDIT_LOG, QUERY_LOG } from "./constants.js";
13
17
  import { getDb } from "./localdb/db.js";
14
18
  import { queryEvolutionAudit, queryQueryLog, querySkillUsageRecords } from "./localdb/queries.js";
@@ -16,34 +20,44 @@ import type { ActivationContext, ActivationRule } from "./types.js";
16
20
  import { readJsonl } from "./utils/jsonl.js";
17
21
 
18
22
  // ---------------------------------------------------------------------------
19
- // Rule: post-session diagnostic
23
+ // Rule: post-session diagnostic (SQLite-first; JSONL for test/custom paths)
20
24
  // ---------------------------------------------------------------------------
21
25
 
22
26
  const postSessionDiagnostic: ActivationRule = {
23
27
  id: "post-session-diagnostic",
24
28
  description: "Suggest `selftune last` when session has >2 unmatched queries",
25
29
  evaluate(ctx: ActivationContext): string | null {
26
- // Count queries for this session
30
+ // Count queries for this session — SQLite is the default path
27
31
  let queries: Array<{ session_id: string; query: string }>;
28
32
  if (ctx.query_log_path === QUERY_LOG) {
29
- const db = getDb();
30
- queries = queryQueryLog(db) as Array<{ session_id: string; query: string }>;
33
+ try {
34
+ const db = getDb();
35
+ queries = queryQueryLog(db) as Array<{ session_id: string; query: string }>;
36
+ } catch {
37
+ return null;
38
+ }
31
39
  } else {
40
+ // test/custom-path fallback
32
41
  queries = readJsonl<{ session_id: string; query: string }>(ctx.query_log_path);
33
42
  }
34
43
  const sessionQueries = queries.filter((q) => q.session_id === ctx.session_id);
35
44
 
36
45
  if (sessionQueries.length === 0) return null;
37
46
 
38
- // Count skill usages for this session (skill log is in the same dir as query log)
39
- const skillLogPath = join(dirname(ctx.query_log_path), "skill_usage_log.jsonl");
47
+ // Count skill usages for this session SQLite is the default path
40
48
  let skillUsages: Array<{ session_id: string }>;
41
49
  if (ctx.query_log_path === QUERY_LOG) {
42
- const db = getDb();
43
- skillUsages = (querySkillUsageRecords(db) as Array<{ session_id: string }>).filter(
44
- (s) => s.session_id === ctx.session_id,
45
- );
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
+ }
46
58
  } else {
59
+ // test/custom-path fallback
60
+ const skillLogPath = join(dirname(ctx.query_log_path), "skill_usage_log.jsonl");
47
61
  skillUsages = existsSync(skillLogPath)
48
62
  ? readJsonl<{ session_id: string }>(skillLogPath).filter(
49
63
  (s) => s.session_id === ctx.session_id,
@@ -100,7 +114,7 @@ const gradingThresholdBreach: ActivationRule = {
100
114
  };
101
115
 
102
116
  // ---------------------------------------------------------------------------
103
- // Rule: stale evolution
117
+ // Rule: stale evolution (SQLite-first; JSONL for test/custom paths)
104
118
  // ---------------------------------------------------------------------------
105
119
 
106
120
  const staleEvolution: ActivationRule = {
@@ -110,12 +124,17 @@ const staleEvolution: ActivationRule = {
110
124
  evaluate(ctx: ActivationContext): string | null {
111
125
  const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
112
126
 
113
- // Check last evolution timestamp
127
+ // Check last evolution timestamp — SQLite is the default path
114
128
  let auditEntries: Array<{ timestamp: string; action: string }>;
115
129
  if (ctx.evolution_audit_log_path === EVOLUTION_AUDIT_LOG) {
116
- const db = getDb();
117
- auditEntries = queryEvolutionAudit(db) as Array<{ timestamp: string; action: string }>;
130
+ try {
131
+ const db = getDb();
132
+ auditEntries = queryEvolutionAudit(db) as Array<{ timestamp: string; action: string }>;
133
+ } catch {
134
+ return null;
135
+ }
118
136
  } else {
137
+ // test/custom-path fallback
119
138
  auditEntries = readJsonl<{ timestamp: string; action: string }>(ctx.evolution_audit_log_path);
120
139
  }
121
140
 
@@ -126,7 +145,7 @@ const staleEvolution: ActivationRule = {
126
145
  : null;
127
146
  }
128
147
 
129
- const lastEntry = auditEntries[auditEntries.length - 1];
148
+ const lastEntry = auditEntries[0]; // queryEvolutionAudit returns DESC order
130
149
  const lastTimestamp = new Date(lastEntry.timestamp).getTime();
131
150
  const ageMs = Date.now() - lastTimestamp;
132
151
 
@@ -1,18 +1,18 @@
1
1
  import { getAlphaLinkState } from "./alpha-identity.js";
2
2
  import type { AgentCommandGuidance, AlphaIdentity, AlphaLinkState } from "./types.js";
3
3
 
4
- function emailArg(email?: string): string {
5
- return email?.trim() ? email : "<email>";
4
+ function sanitizeAlphaEmail(email?: string): string | null {
5
+ const trimmed = email?.trim();
6
+ if (!trimmed) return null;
7
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) return null;
8
+ return trimmed;
6
9
  }
7
10
 
8
- function buildAlphaInitCommand(options?: {
9
- email?: string;
10
- includeKey?: boolean;
11
- force?: boolean;
12
- }): string {
13
- const parts = ["selftune", "init", "--alpha", "--alpha-email", emailArg(options?.email)];
14
- if (options?.includeKey) {
15
- parts.push("--alpha-key", "<st_live_key>");
11
+ function buildAlphaInitCommand(options?: { email?: string; force?: boolean }): string {
12
+ const parts = ["selftune", "init", "--alpha"];
13
+ const email = sanitizeAlphaEmail(options?.email);
14
+ if (email) {
15
+ parts.push("--alpha-email", email);
16
16
  }
17
17
  if (options?.force) {
18
18
  parts.push("--force");
@@ -44,24 +44,24 @@ export function getAlphaGuidanceForState(
44
44
  case "not_linked":
45
45
  return buildGuidance(
46
46
  "alpha_cloud_link_required",
47
- "Alpha upload is not linked. Sign in to app.selftune.dev, enroll in alpha, mint an st_live_* credential, then store it locally.",
48
- buildAlphaInitCommand({ email: options?.email, includeKey: true }),
47
+ "Alpha upload is not linked. Run the init command with --alpha to authenticate via browser.",
48
+ buildAlphaInitCommand({ email: options?.email }),
49
49
  true,
50
50
  ["selftune status", "selftune doctor"],
51
51
  );
52
52
  case "linked_not_enrolled":
53
53
  return buildGuidance(
54
54
  "alpha_enrollment_incomplete",
55
- "Cloud account is linked but alpha enrollment is incomplete. Finish enrollment in app.selftune.dev, then refresh the local credential.",
56
- buildAlphaInitCommand({ email: options?.email, includeKey: true, force: true }),
55
+ "Cloud account is linked but alpha enrollment is incomplete. Re-run init with --alpha to complete enrollment via browser.",
56
+ buildAlphaInitCommand({ email: options?.email, force: true }),
57
57
  true,
58
58
  ["selftune status", "selftune doctor"],
59
59
  );
60
60
  case "enrolled_no_credential":
61
61
  return buildGuidance(
62
62
  "alpha_credential_required",
63
- "Alpha enrollment exists, but the local upload credential is missing or invalid.",
64
- buildAlphaInitCommand({ email: options?.email, includeKey: true, force: true }),
63
+ "Alpha enrollment exists, but the local upload credential is missing or invalid. Re-run init with --alpha to re-authenticate via browser.",
64
+ buildAlphaInitCommand({ email: options?.email, force: true }),
65
65
  true,
66
66
  ["selftune status", "selftune doctor"],
67
67
  );
@@ -89,8 +89,7 @@ export function isValidApiKeyFormat(key: string): boolean {
89
89
  * enrolled, valid api_key -> "ready"
90
90
  *
91
91
  * cloud_user_id enriches the identity (confirms cloud link) but is not a gate.
92
- * The direct-key path (--alpha-key) sets api_key without cloud_user_id, and
93
- * that is a valid "ready" state. cloud_user_id can be backfilled later.
92
+ * The device-code flow sets both api_key and cloud_user_id simultaneously.
94
93
  */
95
94
  export function getAlphaLinkState(identity: AlphaIdentity | null): AlphaLinkState {
96
95
  if (!identity) return "not_linked";
@@ -10,7 +10,9 @@
10
10
  */
11
11
 
12
12
  import type { Database } from "bun:sqlite";
13
+
13
14
  import type { CanonicalRecord } from "@selftune/telemetry-contract";
15
+
14
16
  import { buildPushPayloadV2 } from "../canonical-export.js";
15
17
  import type { EvolutionEvidenceEntry } from "../types.js";
16
18
 
@@ -74,6 +76,8 @@ export function buildV2PushPayload(
74
76
  const canonicalRecords: CanonicalRecord[] = [];
75
77
  const evidenceEntries: EvolutionEvidenceEntry[] = [];
76
78
  const orchestrateRuns: Record<string, unknown>[] = [];
79
+ const gradingResults: Record<string, unknown>[] = [];
80
+ const improvementSignals: Record<string, unknown>[] = [];
77
81
  let lastParsedSeq: number | null = null;
78
82
  let hitMalformedRow = false;
79
83
 
@@ -118,6 +122,10 @@ export function buildV2PushPayload(
118
122
  } else if (row.record_kind === "orchestrate_run") {
119
123
  // Orchestrate run records -- pass through as-is
120
124
  orchestrateRuns.push(parsed);
125
+ } else if (row.record_kind === "grading_result") {
126
+ gradingResults.push(parsed);
127
+ } else if (row.record_kind === "improvement_signal") {
128
+ improvementSignals.push(parsed);
121
129
  } else {
122
130
  // Canonical telemetry records -- pass through as-is
123
131
  canonicalRecords.push(parsed as unknown as CanonicalRecord);
@@ -130,12 +138,20 @@ export function buildV2PushPayload(
130
138
  if (
131
139
  canonicalRecords.length === 0 &&
132
140
  evidenceEntries.length === 0 &&
133
- orchestrateRuns.length === 0
141
+ orchestrateRuns.length === 0 &&
142
+ gradingResults.length === 0 &&
143
+ improvementSignals.length === 0
134
144
  ) {
135
145
  return null;
136
146
  }
137
147
 
138
- const payload = buildPushPayloadV2(canonicalRecords, evidenceEntries, orchestrateRuns);
148
+ const payload = buildPushPayloadV2(
149
+ canonicalRecords,
150
+ evidenceEntries,
151
+ orchestrateRuns,
152
+ gradingResults,
153
+ improvementSignals,
154
+ );
139
155
  if (lastParsedSeq === null) {
140
156
  return null;
141
157
  }
@@ -165,8 +165,8 @@ export async function flushQueue(
165
165
  if (isAuthError(status)) {
166
166
  const authMessage =
167
167
  status === 401
168
- ? "Authentication failed: invalid or missing API key. Run 'selftune init --alpha --alpha-key <key>' to set your API key."
169
- : "Authorization denied: your API key does not have permission to upload. Run 'selftune doctor' to verify enrollment and cloud link, then re-run 'selftune init --alpha --alpha-email <email> --alpha-key <key>' if needed.";
168
+ ? "Authentication failed: invalid or missing API key. Run 'selftune init --alpha --alpha-email <email>' to re-authenticate via browser."
169
+ : "Authorization denied: your API key does not have permission to upload. Run 'selftune doctor' to verify enrollment and cloud link, then re-run 'selftune init --alpha --alpha-email <email> --force' to re-authenticate.";
170
170
  markFailedSafely(authMessage);
171
171
  summary.failed++;
172
172
  succeeded = true;
@@ -11,10 +11,18 @@
11
11
 
12
12
  import type { Database } from "bun:sqlite";
13
13
  import { createHash } from "node:crypto";
14
+
14
15
  import type { CanonicalRecord } from "@selftune/telemetry-contract";
15
16
  import { isCanonicalRecord } from "@selftune/telemetry-contract";
17
+
16
18
  import { CANONICAL_LOG } from "../constants.js";
17
- import { getOrchestrateRuns, queryEvolutionEvidence } from "../localdb/queries.js";
19
+ import {
20
+ getOrchestrateRuns,
21
+ queryCanonicalRecordsForStaging,
22
+ queryEvolutionEvidence,
23
+ queryGradingResults,
24
+ queryImprovementSignals,
25
+ } from "../localdb/queries.js";
18
26
  import { readJsonl } from "../utils/jsonl.js";
19
27
 
20
28
  // -- Helpers ------------------------------------------------------------------
@@ -43,6 +51,22 @@ export function generateEvidenceId(record: Record<string, unknown>): string {
43
51
  return `ev_${createHash("sha256").update(key).digest("hex").slice(0, 16)}`;
44
52
  }
45
53
 
54
+ /**
55
+ * Generate a deterministic grading_id from the result's natural key.
56
+ */
57
+ export function generateGradingId(record: Record<string, unknown>): string {
58
+ const key = `${record.session_id}:${record.skill_name}:${record.graded_at}`;
59
+ return `gr_${createHash("sha256").update(key).digest("hex").slice(0, 16)}`;
60
+ }
61
+
62
+ /**
63
+ * Generate a deterministic signal_id from an improvement signal's natural key.
64
+ */
65
+ export function generateSignalId(record: Record<string, unknown>): string {
66
+ const key = `${record.session_id}:${record.query}:${record.signal_type}:${record.timestamp}`;
67
+ return `sig_${createHash("sha256").update(key).digest("hex").slice(0, 16)}`;
68
+ }
69
+
46
70
  /**
47
71
  * Enrich a raw parsed record: if it is an execution_fact missing
48
72
  * execution_fact_id, inject a deterministic one.
@@ -144,8 +168,13 @@ export function stageCanonicalRecords(db: Database, logPath: string = CANONICAL_
144
168
  VALUES (?, ?, ?, ?, ?, ?, ?)
145
169
  `);
146
170
 
147
- // 1. Stage canonical records from JSONL (enriching missing execution_fact_id)
148
- const records = readAndEnrichCanonicalRecords(logPath);
171
+ // 1. Stage canonical records from SQLite (default) or JSONL (custom logPath override)
172
+ const records: CanonicalRecord[] =
173
+ logPath === CANONICAL_LOG
174
+ ? (queryCanonicalRecordsForStaging(db)
175
+ .map(enrichRecord)
176
+ .filter(isCanonicalRecord) as CanonicalRecord[])
177
+ : readAndEnrichCanonicalRecords(logPath);
149
178
  for (const record of records) {
150
179
  const recordId = extractRecordId(record);
151
180
  const result = stmt.run(
@@ -238,5 +267,79 @@ export function stageCanonicalRecords(db: Database, logPath: string = CANONICAL_
238
267
  }
239
268
  }
240
269
 
270
+ // 4. Stage grading results from SQLite
271
+ try {
272
+ const gradingResults = queryGradingResults(db);
273
+ for (const gr of gradingResults) {
274
+ const recordJson = JSON.stringify({
275
+ grading_id: gr.grading_id,
276
+ session_id: gr.session_id,
277
+ skill_name: gr.skill_name,
278
+ transcript_path: gr.transcript_path,
279
+ graded_at: gr.graded_at,
280
+ pass_rate: gr.pass_rate,
281
+ mean_score: gr.mean_score,
282
+ score_std_dev: gr.score_std_dev,
283
+ passed_count: gr.passed_count,
284
+ failed_count: gr.failed_count,
285
+ total_count: gr.total_count,
286
+ expectations_json: gr.expectations_json,
287
+ claims_json: gr.claims_json,
288
+ eval_feedback_json: gr.eval_feedback_json,
289
+ failure_feedback_json: gr.failure_feedback_json,
290
+ execution_metrics_json: gr.execution_metrics_json,
291
+ });
292
+
293
+ const result = stmt.run(
294
+ "grading_result",
295
+ gr.grading_id,
296
+ recordJson,
297
+ gr.session_id,
298
+ null, // no prompt_id
299
+ gr.graded_at,
300
+ now,
301
+ );
302
+ if (result.changes > 0) staged++;
303
+ }
304
+ } catch (err) {
305
+ if (process.env.DEBUG || process.env.NODE_ENV === "development") {
306
+ console.error("[stage-canonical] failed to stage grading results:", err);
307
+ }
308
+ }
309
+
310
+ // 5. Stage improvement signals from SQLite
311
+ try {
312
+ const signals = queryImprovementSignals(db);
313
+ for (const sig of signals) {
314
+ const signalId = generateSignalId(sig);
315
+ const recordJson = JSON.stringify({
316
+ signal_id: signalId,
317
+ timestamp: sig.timestamp,
318
+ session_id: sig.session_id,
319
+ query: sig.query,
320
+ signal_type: sig.signal_type,
321
+ mentioned_skill: sig.mentioned_skill,
322
+ consumed: sig.consumed,
323
+ consumed_at: sig.consumed_at,
324
+ consumed_by_run: sig.consumed_by_run,
325
+ });
326
+
327
+ const result = stmt.run(
328
+ "improvement_signal",
329
+ signalId,
330
+ recordJson,
331
+ sig.session_id,
332
+ null, // no prompt_id
333
+ sig.timestamp,
334
+ now,
335
+ );
336
+ if (result.changes > 0) staged++;
337
+ }
338
+ } catch (err) {
339
+ if (process.env.DEBUG || process.env.NODE_ENV === "development") {
340
+ console.error("[stage-canonical] failed to stage improvement signals:", err);
341
+ }
342
+ }
343
+
241
344
  return staged;
242
345
  }
@@ -22,6 +22,38 @@ export interface DeviceCodeResult {
22
22
  org_id: string;
23
23
  }
24
24
 
25
+ export function tryOpenUrl(url: string): boolean {
26
+ const command =
27
+ process.platform === "darwin"
28
+ ? ["open", url]
29
+ : process.platform === "linux"
30
+ ? ["xdg-open", url]
31
+ : process.platform === "win32"
32
+ ? ["cmd", "/c", "start", "", url]
33
+ : null;
34
+
35
+ if (!command) return false;
36
+ if (process.platform !== "win32" && !Bun.which(command[0])) return false;
37
+
38
+ try {
39
+ Bun.spawn(command, { stdout: "ignore", stderr: "ignore" });
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ export function buildVerificationUrl(verificationUrl: string, userCode: string): string {
47
+ try {
48
+ const url = new URL(verificationUrl);
49
+ url.searchParams.set("code", userCode);
50
+ return url.toString();
51
+ } catch {
52
+ const separator = verificationUrl.includes("?") ? "&" : "?";
53
+ return `${verificationUrl}${separator}code=${encodeURIComponent(userCode)}`;
54
+ }
55
+ }
56
+
25
57
  /**
26
58
  * Derive the cloud API base URL from SELFTUNE_ALPHA_ENDPOINT.
27
59
  * The endpoint is the push URL (e.g., https://api.selftune.dev/api/v1/push).
@@ -8,7 +8,9 @@
8
8
 
9
9
  import { spawnSync } from "node:child_process";
10
10
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { homedir } from "node:os";
11
12
  import { join } from "node:path";
13
+
12
14
  import { SELFTUNE_CONFIG_DIR } from "./constants.js";
13
15
 
14
16
  const UPDATE_CHECK_PATH = join(SELFTUNE_CONFIG_DIR, "update-check.json");
@@ -118,6 +120,16 @@ async function performUpdate(currentVersion: string, latestVersion: string): Pro
118
120
  console.error(`[selftune] Updated to v${latestVersion}.`);
119
121
  // Update cache to reflect new version
120
122
  writeCache({ lastCheck: Date.now(), currentVersion: latestVersion, latestVersion });
123
+
124
+ try {
125
+ const claudeDir = join(homedir(), ".claude");
126
+ if (existsSync(claudeDir)) {
127
+ const { installAgentFiles } = await import("./claude-agents.js");
128
+ installAgentFiles({ force: true });
129
+ }
130
+ } catch {
131
+ // Non-critical — updated CLI is usable even if agent sync fails
132
+ }
121
133
  } else {
122
134
  const stderr = result.stderr?.toString().trim();
123
135
  console.error(
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { writeFileSync } from "node:fs";
10
10
  import { parseArgs } from "node:util";
11
+
11
12
  import { getDb } from "../localdb/db.js";
12
13
  import {
13
14
  queryEvolutionAudit,
@@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto";
4
4
  import { readFileSync, writeFileSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
  import { parseArgs } from "node:util";
7
+
7
8
  import { CANONICAL_LOG, CLAUDE_CODE_PROJECTS_DIR } from "./constants.js";
8
9
  import {
9
10
  buildCanonicalRecordsFromReplay,
@@ -84,6 +85,8 @@ export function buildPushPayloadV2(
84
85
  records: CanonicalRecord[],
85
86
  evidenceEntries: EvolutionEvidenceEntry[] = [],
86
87
  orchestrateRuns: Record<string, unknown>[] = [],
88
+ gradingResults: Record<string, unknown>[] = [],
89
+ improvementSignals: Record<string, unknown>[] = [],
87
90
  ): Record<string, unknown> {
88
91
  const sessions = records.filter((record) => record.record_kind === "session");
89
92
  const prompts = records.filter((record) => record.record_kind === "prompt");
@@ -120,6 +123,8 @@ export function buildPushPayloadV2(
120
123
  validation_json: entry.validation,
121
124
  })),
122
125
  orchestrate_runs: orchestrateRuns,
126
+ grading_results: gradingResults,
127
+ improvement_signals: improvementSignals,
123
128
  },
124
129
  };
125
130
  }