selftune 0.2.22 → 0.2.24

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 (270) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +95 -15
  3. package/apps/local-dashboard/dist/assets/index-DgY2KGP-.css +1 -0
  4. package/apps/local-dashboard/dist/assets/index-Dmx7LPVX.js +15 -0
  5. package/apps/local-dashboard/dist/assets/vendor-react-C5oyHiV1.js +11 -0
  6. package/apps/local-dashboard/dist/assets/{vendor-table-BIiI3YhS.js → vendor-table-Bc_bbKd8.js} +1 -1
  7. package/apps/local-dashboard/dist/assets/vendor-ui-B3BPIYy7.js +1 -0
  8. package/apps/local-dashboard/dist/index.html +5 -5
  9. package/cli/selftune/adapters/codex/install.ts +310 -78
  10. package/cli/selftune/adapters/opencode/install.ts +3 -4
  11. package/cli/selftune/adapters/pi/hook.ts +273 -0
  12. package/cli/selftune/adapters/pi/install.ts +207 -0
  13. package/cli/selftune/alpha-upload/build-payloads.ts +3 -3
  14. package/cli/selftune/alpha-upload/stage-canonical.ts +17 -11
  15. package/cli/selftune/auto-update.ts +200 -8
  16. package/cli/selftune/canonical-export.ts +55 -25
  17. package/cli/selftune/command-surface.ts +397 -0
  18. package/cli/selftune/constants.ts +10 -1
  19. package/cli/selftune/contribute/contribute.ts +64 -13
  20. package/cli/selftune/contribution-config.ts +57 -3
  21. package/cli/selftune/contribution-preferences.ts +117 -0
  22. package/cli/selftune/contribution-signals.ts +8 -4
  23. package/cli/selftune/contribution-staging.ts +13 -2
  24. package/cli/selftune/contributions.ts +55 -121
  25. package/cli/selftune/creator-contributions.ts +29 -10
  26. package/cli/selftune/cron/setup.ts +7 -3
  27. package/cli/selftune/dashboard-contract.ts +87 -0
  28. package/cli/selftune/dashboard-server.ts +168 -17
  29. package/cli/selftune/dashboard.ts +350 -17
  30. package/cli/selftune/eval/baseline.ts +21 -5
  31. package/cli/selftune/eval/execution-eval.ts +170 -0
  32. package/cli/selftune/eval/family-overlap.ts +2 -2
  33. package/cli/selftune/eval/hooks-to-evals.ts +228 -82
  34. package/cli/selftune/eval/import-skillsbench.ts +2 -2
  35. package/cli/selftune/eval/invocation-classifier.ts +56 -0
  36. package/cli/selftune/eval/synthetic-evals.ts +5 -3
  37. package/cli/selftune/eval/unit-test-cli.ts +7 -4
  38. package/cli/selftune/evolution/apply-proposal.ts +295 -0
  39. package/cli/selftune/evolution/engines/judge-engine.ts +96 -0
  40. package/cli/selftune/evolution/engines/replay-engine.ts +180 -0
  41. package/cli/selftune/evolution/evidence.ts +2 -6
  42. package/cli/selftune/evolution/evolve-body.ts +152 -38
  43. package/cli/selftune/evolution/evolve.ts +244 -52
  44. package/cli/selftune/evolution/rollback.ts +0 -1
  45. package/cli/selftune/evolution/validate-body.ts +111 -49
  46. package/cli/selftune/evolution/validate-host-replay.ts +510 -60
  47. package/cli/selftune/evolution/validate-proposal.ts +11 -150
  48. package/cli/selftune/evolution/validate-routing.ts +51 -108
  49. package/cli/selftune/evolution/validation-contract.ts +91 -0
  50. package/cli/selftune/grading/auto-grade.ts +11 -7
  51. package/cli/selftune/grading/grade-session.ts +10 -16
  52. package/cli/selftune/hooks/skill-eval.ts +2 -1
  53. package/cli/selftune/hooks-shared/types.ts +1 -0
  54. package/cli/selftune/index.ts +58 -15
  55. package/cli/selftune/ingestors/claude-replay.ts +15 -10
  56. package/cli/selftune/ingestors/codex-wrapper.ts +3 -3
  57. package/cli/selftune/ingestors/opencode-ingest.ts +2 -2
  58. package/cli/selftune/ingestors/pi-ingest.ts +727 -0
  59. package/cli/selftune/init.ts +38 -4
  60. package/cli/selftune/localdb/direct-write.ts +120 -1
  61. package/cli/selftune/localdb/materialize.ts +6 -7
  62. package/cli/selftune/localdb/queries/cron.ts +34 -0
  63. package/cli/selftune/localdb/queries/dashboard.ts +834 -0
  64. package/cli/selftune/localdb/queries/evolution.ts +158 -0
  65. package/cli/selftune/localdb/queries/execution.ts +133 -0
  66. package/cli/selftune/localdb/queries/json.ts +18 -0
  67. package/cli/selftune/localdb/queries/monitoring.ts +263 -0
  68. package/cli/selftune/localdb/queries/raw.ts +95 -0
  69. package/cli/selftune/localdb/queries/staging.ts +270 -0
  70. package/cli/selftune/localdb/queries/trust.ts +392 -0
  71. package/cli/selftune/localdb/queries.ts +60 -2162
  72. package/cli/selftune/localdb/schema.ts +59 -0
  73. package/cli/selftune/monitoring/watch.ts +96 -29
  74. package/cli/selftune/normalization.ts +3 -0
  75. package/cli/selftune/observability.ts +12 -3
  76. package/cli/selftune/orchestrate/cli.ts +161 -0
  77. package/cli/selftune/orchestrate/execute.ts +295 -0
  78. package/cli/selftune/orchestrate/finalize.ts +157 -0
  79. package/cli/selftune/orchestrate/locks.ts +40 -0
  80. package/cli/selftune/orchestrate/plan.ts +131 -0
  81. package/cli/selftune/orchestrate/post-run.ts +59 -0
  82. package/cli/selftune/orchestrate/prepare.ts +334 -0
  83. package/cli/selftune/orchestrate/report.ts +182 -0
  84. package/cli/selftune/orchestrate/runtime.ts +120 -0
  85. package/cli/selftune/orchestrate/signals.ts +48 -0
  86. package/cli/selftune/orchestrate.ts +162 -1142
  87. package/cli/selftune/registry/client.ts +74 -0
  88. package/cli/selftune/registry/history.ts +54 -0
  89. package/cli/selftune/registry/index.ts +90 -0
  90. package/cli/selftune/registry/install.ts +141 -0
  91. package/cli/selftune/registry/list.ts +44 -0
  92. package/cli/selftune/registry/push.ts +171 -0
  93. package/cli/selftune/registry/rollback.ts +49 -0
  94. package/cli/selftune/registry/status.ts +62 -0
  95. package/cli/selftune/registry/sync.ts +125 -0
  96. package/cli/selftune/repair/skill-usage.ts +9 -3
  97. package/cli/selftune/routes/overview.ts +5 -2
  98. package/cli/selftune/routes/skill-report.ts +15 -2
  99. package/cli/selftune/schedule.ts +5 -5
  100. package/cli/selftune/status.ts +70 -2
  101. package/cli/selftune/sync.ts +127 -23
  102. package/cli/selftune/testing-readiness.ts +597 -0
  103. package/cli/selftune/types.ts +46 -5
  104. package/cli/selftune/uninstall.ts +2 -1
  105. package/cli/selftune/utils/canonical-log.ts +1 -9
  106. package/cli/selftune/utils/cli-error.ts +9 -0
  107. package/cli/selftune/utils/jsonl.ts +1 -30
  108. package/cli/selftune/utils/llm-call.ts +126 -6
  109. package/cli/selftune/utils/skill-discovery.ts +24 -0
  110. package/cli/selftune/workflows/proposals.ts +184 -0
  111. package/cli/selftune/workflows/skill-scaffold.ts +241 -0
  112. package/cli/selftune/workflows/workflows.ts +100 -26
  113. package/node_modules/@selftune/telemetry-contract/fixtures/complete-push.ts +1 -1
  114. package/node_modules/@selftune/telemetry-contract/fixtures/evidence-only-push.ts +2 -2
  115. package/node_modules/@selftune/telemetry-contract/fixtures/golden.test.ts +0 -1
  116. package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
  117. package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +2 -2
  118. package/node_modules/@selftune/telemetry-contract/package.json +1 -1
  119. package/node_modules/@selftune/telemetry-contract/src/index.ts +1 -0
  120. package/node_modules/@selftune/telemetry-contract/src/schemas.ts +63 -5
  121. package/node_modules/@selftune/telemetry-contract/src/types.ts +97 -7
  122. package/node_modules/@selftune/telemetry-contract/tests/compatibility.test.ts +0 -1
  123. package/package.json +25 -9
  124. package/packages/dashboard-core/AGENTS.md +18 -0
  125. package/packages/dashboard-core/README.md +30 -0
  126. package/packages/dashboard-core/index.ts +3 -0
  127. package/packages/dashboard-core/package.json +39 -0
  128. package/packages/dashboard-core/src/chrome/DashboardChrome.tsx +74 -0
  129. package/packages/dashboard-core/src/chrome/DashboardHeader.tsx +200 -0
  130. package/packages/dashboard-core/src/chrome/DashboardSidebar.tsx +219 -0
  131. package/packages/dashboard-core/src/chrome/RuntimeBadge.tsx +46 -0
  132. package/packages/dashboard-core/src/chrome/index.ts +14 -0
  133. package/packages/dashboard-core/src/chrome/types.ts +81 -0
  134. package/packages/dashboard-core/src/chrome/utils.ts +23 -0
  135. package/packages/dashboard-core/src/gates/FeatureGate.tsx +11 -0
  136. package/packages/dashboard-core/src/gates/LockedRoute.tsx +29 -0
  137. package/packages/dashboard-core/src/gates/UpgradeCard.tsx +89 -0
  138. package/packages/dashboard-core/src/gates/index.ts +3 -0
  139. package/packages/dashboard-core/src/host/DashboardHostProvider.tsx +62 -0
  140. package/packages/dashboard-core/src/host/adapter.ts +47 -0
  141. package/packages/dashboard-core/src/host/capabilities.ts +55 -0
  142. package/packages/dashboard-core/src/host/index.ts +3 -0
  143. package/packages/dashboard-core/src/models/analytics.ts +39 -0
  144. package/packages/dashboard-core/src/models/index.ts +4 -0
  145. package/packages/dashboard-core/src/models/overview.ts +98 -0
  146. package/packages/dashboard-core/src/models/runtime.ts +7 -0
  147. package/packages/dashboard-core/src/models/skills.ts +34 -0
  148. package/packages/dashboard-core/src/routes/index.ts +2 -0
  149. package/packages/dashboard-core/src/routes/manifest.test.ts +70 -0
  150. package/packages/dashboard-core/src/routes/manifest.ts +451 -0
  151. package/packages/dashboard-core/src/routes/types.ts +39 -0
  152. package/packages/dashboard-core/src/screens/analytics/AnalyticsScreen.tsx +278 -0
  153. package/packages/dashboard-core/src/screens/analytics/index.ts +1 -0
  154. package/packages/dashboard-core/src/screens/index.ts +37 -0
  155. package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.test.ts +101 -0
  156. package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.tsx +393 -0
  157. package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.test.tsx +113 -0
  158. package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.tsx +72 -0
  159. package/packages/dashboard-core/src/screens/overview/OverviewCoreSurface.tsx +71 -0
  160. package/packages/dashboard-core/src/screens/overview/OverviewOnboardingBanner.tsx +90 -0
  161. package/packages/dashboard-core/src/screens/overview/OverviewRunSummary.tsx +40 -0
  162. package/packages/dashboard-core/src/screens/overview/index.ts +16 -0
  163. package/packages/dashboard-core/src/screens/overview/types.ts +13 -0
  164. package/packages/dashboard-core/src/screens/skill-report/SkillReportDailyBreakdownSection.tsx +99 -0
  165. package/packages/dashboard-core/src/screens/skill-report/SkillReportDataQualityTabContent.tsx +35 -0
  166. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceRail.tsx +71 -0
  167. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceSection.tsx +63 -0
  168. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceTabContent.tsx +25 -0
  169. package/packages/dashboard-core/src/screens/skill-report/SkillReportInvocationsSection.tsx +24 -0
  170. package/packages/dashboard-core/src/screens/skill-report/SkillReportMissedQueriesSection.tsx +79 -0
  171. package/packages/dashboard-core/src/screens/skill-report/SkillReportScaffold.tsx +150 -0
  172. package/packages/dashboard-core/src/screens/skill-report/SkillReportSections.test.tsx +224 -0
  173. package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.test.tsx +76 -0
  174. package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.tsx +88 -0
  175. package/packages/dashboard-core/src/screens/skill-report/SkillReportTrendSection.tsx +33 -0
  176. package/packages/dashboard-core/src/screens/skill-report/SkillReportTrustBadge.tsx +67 -0
  177. package/packages/dashboard-core/src/screens/skill-report/index.ts +45 -0
  178. package/packages/dashboard-core/src/screens/skills/SkillsLibraryScreen.tsx +162 -0
  179. package/packages/dashboard-core/src/screens/skills/index.ts +6 -0
  180. package/packages/telemetry-contract/fixtures/complete-push.ts +1 -1
  181. package/packages/telemetry-contract/fixtures/evidence-only-push.ts +2 -2
  182. package/packages/telemetry-contract/fixtures/golden.test.ts +0 -1
  183. package/packages/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
  184. package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +2 -2
  185. package/packages/telemetry-contract/package.json +1 -1
  186. package/packages/telemetry-contract/src/index.ts +1 -0
  187. package/packages/telemetry-contract/src/schemas.ts +63 -5
  188. package/packages/telemetry-contract/src/types.ts +97 -7
  189. package/packages/telemetry-contract/tests/compatibility.test.ts +0 -1
  190. package/packages/ui/AGENTS.md +16 -0
  191. package/packages/ui/README.md +1 -1
  192. package/packages/ui/package.json +1 -1
  193. package/packages/ui/src/components/ActivityTimeline.tsx +152 -168
  194. package/packages/ui/src/components/AnalyticsCharts.tsx +344 -0
  195. package/packages/ui/src/components/EvidenceViewer.tsx +229 -464
  196. package/packages/ui/src/components/EvolutionTimeline.tsx +34 -87
  197. package/packages/ui/src/components/InfoTip.tsx +1 -2
  198. package/packages/ui/src/components/InvocationsPanel.tsx +413 -0
  199. package/packages/ui/src/components/JobHistoryTimeline.tsx +156 -0
  200. package/packages/ui/src/components/OrchestrateRunsPanel.tsx +18 -36
  201. package/packages/ui/src/components/OverviewPanels.tsx +693 -0
  202. package/packages/ui/src/components/PipelineStatusBar.tsx +65 -0
  203. package/packages/ui/src/components/SkillReportGuide.tsx +215 -0
  204. package/packages/ui/src/components/SkillReportPanels.tsx +919 -0
  205. package/packages/ui/src/components/SkillsLibrary.tsx +437 -0
  206. package/packages/ui/src/components/index.ts +56 -1
  207. package/packages/ui/src/components/section-cards.tsx +18 -35
  208. package/packages/ui/src/components/skill-health-grid.tsx +47 -37
  209. package/packages/ui/src/lib/constants.tsx +0 -1
  210. package/packages/ui/src/primitives/card.tsx +1 -1
  211. package/packages/ui/src/primitives/checkbox.tsx +1 -1
  212. package/packages/ui/src/primitives/dropdown-menu.tsx +2 -2
  213. package/packages/ui/src/primitives/select.tsx +2 -2
  214. package/packages/ui/src/primitives/tabs.tsx +7 -6
  215. package/packages/ui/src/types.ts +182 -4
  216. package/skill/SKILL.md +130 -318
  217. package/skill/agents/diagnosis-analyst.md +3 -3
  218. package/skill/agents/evolution-reviewer.md +3 -3
  219. package/skill/agents/integration-guide.md +3 -3
  220. package/skill/agents/pattern-analyst.md +2 -2
  221. package/skill/references/cli-quick-reference.md +89 -0
  222. package/skill/references/creator-playbook.md +131 -0
  223. package/skill/references/examples.md +48 -0
  224. package/skill/references/troubleshooting.md +47 -0
  225. package/skill/references/version-history.md +1 -1
  226. package/skill/selftune.contribute.json +11 -0
  227. package/skill/{Workflows → workflows}/Baseline.md +20 -1
  228. package/skill/{Workflows → workflows}/Contribute.md +23 -10
  229. package/skill/{Workflows → workflows}/Contributions.md +13 -5
  230. package/skill/workflows/CreateTestDeploy.md +170 -0
  231. package/skill/{Workflows → workflows}/CreatorContributions.md +18 -6
  232. package/skill/{Workflows → workflows}/Cron.md +1 -1
  233. package/skill/{Workflows → workflows}/Dashboard.md +20 -0
  234. package/skill/{Workflows → workflows}/Doctor.md +1 -1
  235. package/skill/{Workflows → workflows}/Evals.md +67 -2
  236. package/skill/{Workflows → workflows}/Evolve.md +119 -30
  237. package/skill/{Workflows → workflows}/EvolveBody.md +41 -1
  238. package/skill/{Workflows → workflows}/Grade.md +1 -1
  239. package/skill/{Workflows → workflows}/Ingest.md +60 -2
  240. package/skill/{Workflows → workflows}/Initialize.md +16 -9
  241. package/skill/{Workflows → workflows}/Orchestrate.md +13 -3
  242. package/skill/{Workflows → workflows}/PlatformHooks.md +19 -3
  243. package/skill/workflows/Registry.md +99 -0
  244. package/skill/{Workflows → workflows}/Schedule.md +3 -3
  245. package/skill/workflows/SignalsDashboard.md +87 -0
  246. package/skill/{Workflows → workflows}/Sync.md +3 -1
  247. package/skill/{Workflows → workflows}/UnitTest.md +19 -0
  248. package/skill/{Workflows → workflows}/Watch.md +42 -2
  249. package/skill/{Workflows → workflows}/Workflows.md +39 -2
  250. package/apps/local-dashboard/dist/assets/index-D8O-RG1I.js +0 -60
  251. package/apps/local-dashboard/dist/assets/index-_EcLywDg.css +0 -1
  252. package/apps/local-dashboard/dist/assets/vendor-react-CKkiCskZ.js +0 -11
  253. package/apps/local-dashboard/dist/assets/vendor-ui-CGEmUayx.js +0 -12
  254. package/cli/selftune/utils/html.ts +0 -27
  255. package/packages/ui/src/components/RecentActivityFeed.tsx +0 -117
  256. /package/skill/{Workflows → workflows}/AlphaUpload.md +0 -0
  257. /package/skill/{Workflows → workflows}/AutoActivation.md +0 -0
  258. /package/skill/{Workflows → workflows}/Badge.md +0 -0
  259. /package/skill/{Workflows → workflows}/Composability.md +0 -0
  260. /package/skill/{Workflows → workflows}/EvolutionMemory.md +0 -0
  261. /package/skill/{Workflows → workflows}/ExportCanonical.md +0 -0
  262. /package/skill/{Workflows → workflows}/Hook.md +0 -0
  263. /package/skill/{Workflows → workflows}/ImportSkillsBench.md +0 -0
  264. /package/skill/{Workflows → workflows}/Quickstart.md +0 -0
  265. /package/skill/{Workflows → workflows}/Recover.md +0 -0
  266. /package/skill/{Workflows → workflows}/RepairSkillUsage.md +0 -0
  267. /package/skill/{Workflows → workflows}/Replay.md +0 -0
  268. /package/skill/{Workflows → workflows}/Rollback.md +0 -0
  269. /package/skill/{Workflows → workflows}/Telemetry.md +0 -0
  270. /package/skill/{Workflows → workflows}/Uninstall.md +0 -0
@@ -0,0 +1,334 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+
4
+ import {
5
+ buildDefaultGradingOutputPath,
6
+ deriveExpectationsFromSkill,
7
+ gradeSession,
8
+ resolveLatestSessionForSkill,
9
+ } from "../grading/grade-session.js";
10
+ import { writeGradingResultToDb } from "../localdb/direct-write.js";
11
+ import { createDefaultSyncOptions } from "../sync.js";
12
+ import type {
13
+ ImprovementSignalRecord,
14
+ QueryLogRecord,
15
+ SessionTelemetryRecord,
16
+ SkillUsageRecord,
17
+ } from "../types.js";
18
+ import { readExcerpt } from "../utils/transcript.js";
19
+ import type { OrchestrateOptions, SkillAction } from "../orchestrate.js";
20
+ import { selectCandidates } from "./plan.js";
21
+ import { groupSignalsBySkill, readPendingSignals } from "./signals.js";
22
+ import type { ResolvedOrchestrateRuntime } from "./runtime.js";
23
+
24
+ export interface PreparedOrchestrateRun {
25
+ syncResult: ReturnType<ResolvedOrchestrateRuntime["syncSources"]>;
26
+ statusResult: ReturnType<ResolvedOrchestrateRuntime["computeStatus"]>;
27
+ telemetry: SessionTelemetryRecord[];
28
+ skillRecords: SkillUsageRecord[];
29
+ pendingSignals: ImprovementSignalRecord[];
30
+ candidates: SkillAction[];
31
+ evolveCandidates: SkillAction[];
32
+ agent: string | null;
33
+ autoGradedCount: number;
34
+ }
35
+
36
+ /**
37
+ * Detects significant overlap between the positive eval sets of evolution
38
+ * candidates. When two skills share >30% of their positive queries, it
39
+ * suggests a routing boundary problem. Console-only — no persistence.
40
+ */
41
+ export async function detectCrossSkillOverlap(
42
+ candidates: Array<{ skill: string }>,
43
+ skillRecords: SkillUsageRecord[],
44
+ queryRecords: QueryLogRecord[],
45
+ ): Promise<
46
+ Array<{ skill_a: string; skill_b: string; overlap_pct: number; shared_queries: string[] }>
47
+ > {
48
+ if (candidates.length < 2) return [];
49
+
50
+ const { buildEvalSet } = await import("../eval/hooks-to-evals.js");
51
+
52
+ const evalSets = new Map<string, Set<string>>();
53
+
54
+ for (const candidate of candidates) {
55
+ const evalSet = buildEvalSet(skillRecords, queryRecords, candidate.skill);
56
+ const positives = new Set(
57
+ evalSet
58
+ .filter((entry: { should_trigger: boolean }) => entry.should_trigger)
59
+ .map((entry: { query: string }) => entry.query.toLowerCase()),
60
+ );
61
+ evalSets.set(candidate.skill, positives);
62
+ }
63
+
64
+ const overlaps: Array<{
65
+ skill_a: string;
66
+ skill_b: string;
67
+ overlap_pct: number;
68
+ shared_queries: string[];
69
+ }> = [];
70
+ const skillNames = [...evalSets.keys()];
71
+
72
+ for (let i = 0; i < skillNames.length; i++) {
73
+ for (let j = i + 1; j < skillNames.length; j++) {
74
+ const setA = evalSets.get(skillNames[i]);
75
+ const setB = evalSets.get(skillNames[j]);
76
+ if (!setA || !setB || setA.size === 0 || setB.size === 0) continue;
77
+
78
+ const shared: string[] = [];
79
+ for (const query of setA) {
80
+ if (setB.has(query)) shared.push(query);
81
+ }
82
+
83
+ const overlapPct = shared.length / Math.min(setA.size, setB.size);
84
+ if (overlapPct > 0.3) {
85
+ overlaps.push({
86
+ skill_a: skillNames[i],
87
+ skill_b: skillNames[j],
88
+ overlap_pct: overlapPct,
89
+ shared_queries: shared.slice(0, 10),
90
+ });
91
+ }
92
+ }
93
+ }
94
+
95
+ return overlaps;
96
+ }
97
+
98
+ /**
99
+ * Auto-grade the top ungraded skills that have some session data.
100
+ * Fail-open: individual grading errors are logged but never propagated.
101
+ */
102
+ export async function autoGradeTopUngraded(
103
+ skills: Array<ReturnType<ResolvedOrchestrateRuntime["computeStatus"]>["skills"][number]>,
104
+ maxAutoGrade: number,
105
+ agent: string,
106
+ deps: {
107
+ readTelemetry: () => SessionTelemetryRecord[];
108
+ readSkillRecords: () => SkillUsageRecord[];
109
+ },
110
+ ): Promise<number> {
111
+ const ungradedWithData = skills
112
+ .filter((skill) => skill.status === "UNGRADED" && (skill.snapshot?.skill_checks ?? 0) > 0)
113
+ .sort((a, b) => (b.snapshot?.skill_checks ?? 0) - (a.snapshot?.skill_checks ?? 0))
114
+ .slice(0, maxAutoGrade);
115
+
116
+ if (ungradedWithData.length === 0) return 0;
117
+
118
+ // Cache data reads outside the loop — SQLite tables don't change during grading
119
+ // iterations, so re-reading per-skill is wasteful.
120
+ const cachedTelemetry = deps.readTelemetry();
121
+ const cachedSkillUsage = deps.readSkillRecords();
122
+
123
+ let graded = 0;
124
+
125
+ for (const skill of ungradedWithData) {
126
+ try {
127
+ const resolved = resolveLatestSessionForSkill(cachedTelemetry, cachedSkillUsage, skill.name);
128
+ if (!resolved) {
129
+ console.error(` [auto-grade] ${skill.name}: no session found, skipping`);
130
+ continue;
131
+ }
132
+
133
+ const derived = deriveExpectationsFromSkill(skill.name);
134
+ let transcriptExcerpt = "(no transcript)";
135
+ if (resolved.transcriptPath) {
136
+ try {
137
+ transcriptExcerpt = readExcerpt(resolved.transcriptPath);
138
+ } catch {
139
+ transcriptExcerpt = "(no transcript)";
140
+ }
141
+ }
142
+
143
+ console.error(` [auto-grade] Grading "${skill.name}" (session ${resolved.sessionId})...`);
144
+
145
+ const result = await gradeSession({
146
+ expectations: derived.expectations,
147
+ telemetry: resolved.telemetry,
148
+ sessionId: resolved.sessionId,
149
+ skillName: skill.name,
150
+ transcriptExcerpt,
151
+ transcriptPath: resolved.transcriptPath,
152
+ agent,
153
+ });
154
+
155
+ let persisted = false;
156
+ try {
157
+ persisted = writeGradingResultToDb(result);
158
+ } catch {
159
+ persisted = false;
160
+ }
161
+
162
+ if (!persisted) {
163
+ console.error(` [auto-grade] ${skill.name}: graded but failed to persist result`);
164
+ continue;
165
+ }
166
+
167
+ try {
168
+ const basePath = buildDefaultGradingOutputPath(resolved.sessionId);
169
+ const safeName = skill.name.replace(/[^a-zA-Z0-9_-]/g, "_");
170
+ const outputPath = basePath.replace(/\.json$/, `_${safeName}.json`);
171
+ mkdirSync(dirname(outputPath), { recursive: true });
172
+ writeFileSync(outputPath, JSON.stringify(result, null, 2), "utf-8");
173
+ } catch {
174
+ // DB is authoritative; file output is supplementary only.
175
+ }
176
+
177
+ const passRate = result.summary.pass_rate;
178
+ console.error(
179
+ ` [auto-grade] ${skill.name}: ${result.summary.passed}/${result.summary.total} passed (${Math.round(passRate * 100)}%)`,
180
+ );
181
+ graded++;
182
+ } catch (err) {
183
+ const msg = err instanceof Error ? err.message : String(err);
184
+ console.error(
185
+ ` [auto-grade] ${skill.name}: error — ${msg}. Retry with: selftune grade ${skill.name}`,
186
+ );
187
+ }
188
+ }
189
+
190
+ return graded;
191
+ }
192
+
193
+ export async function prepareOrchestrateRun(
194
+ options: OrchestrateOptions,
195
+ runtime: ResolvedOrchestrateRuntime,
196
+ ): Promise<PreparedOrchestrateRun> {
197
+ console.error("[orchestrate] Syncing source-truth telemetry...");
198
+ const syncResult = runtime.syncSources(createDefaultSyncOptions({ force: options.syncForce }));
199
+ const sourceSynced = Object.values(syncResult.sources).reduce(
200
+ (sum, source) => sum + source.synced,
201
+ 0,
202
+ );
203
+ console.error(
204
+ `[orchestrate] Sync complete: ${sourceSynced} sessions synced, ${syncResult.repair.repaired_records} repaired`,
205
+ );
206
+
207
+ console.error("[orchestrate] Computing skill status...");
208
+ const telemetry = runtime.readTelemetry();
209
+ const skillRecords = runtime.readSkillRecords();
210
+ const queryRecords = runtime.readQueryRecords();
211
+ const auditEntries = runtime.readAuditEntries();
212
+ const doctorResult = await runtime.doctor();
213
+
214
+ let statusResult = runtime.computeStatus(
215
+ telemetry,
216
+ skillRecords,
217
+ queryRecords,
218
+ auditEntries,
219
+ doctorResult,
220
+ );
221
+ console.error(
222
+ `[orchestrate] Status: ${statusResult.skills.length} skills, system=${statusResult.system.healthy ? "healthy" : "unhealthy"}`,
223
+ );
224
+
225
+ let autoGradedCount = 0;
226
+ const scopedSkills = options.skillFilter
227
+ ? statusResult.skills.filter((skill) => skill.name === options.skillFilter)
228
+ : statusResult.skills;
229
+ const ungradedWithData = scopedSkills.filter(
230
+ (skill) => skill.status === "UNGRADED" && (skill.snapshot?.skill_checks ?? 0) > 0,
231
+ );
232
+
233
+ if (!options.dryRun && options.maxAutoGrade > 0 && ungradedWithData.length > 0) {
234
+ const gradeAgent = runtime.detectAgent();
235
+ if (gradeAgent) {
236
+ console.error(
237
+ `[orchestrate] Auto-grading ${Math.min(ungradedWithData.length, options.maxAutoGrade)} ungraded skill(s)...`,
238
+ );
239
+ autoGradedCount = await autoGradeTopUngraded(scopedSkills, options.maxAutoGrade, gradeAgent, {
240
+ readTelemetry: runtime.readTelemetry,
241
+ readSkillRecords: runtime.readSkillRecords,
242
+ });
243
+
244
+ if (autoGradedCount > 0) {
245
+ console.error(
246
+ `[orchestrate] Recomputing status after grading ${autoGradedCount} skill(s)...`,
247
+ );
248
+ try {
249
+ // Re-read telemetry and skill records (grading writes new rows), but
250
+ // reuse queryRecords and auditEntries — auto-grading doesn't touch those tables.
251
+ statusResult = runtime.computeStatus(
252
+ runtime.readTelemetry(),
253
+ runtime.readSkillRecords(),
254
+ queryRecords,
255
+ auditEntries,
256
+ doctorResult,
257
+ );
258
+ } catch (recomputeErr) {
259
+ console.error(
260
+ `[orchestrate] Warning: failed to recompute status after grading — using pre-grade status. ${recomputeErr instanceof Error ? recomputeErr.message : String(recomputeErr)}`,
261
+ );
262
+ }
263
+ }
264
+ } else {
265
+ console.error(
266
+ "[orchestrate] No agent CLI found — skipping auto-grade. To disable, rerun with: selftune orchestrate --max-auto-grade 0",
267
+ );
268
+ }
269
+ }
270
+
271
+ const pendingSignals = readPendingSignals(runtime.readSignals);
272
+ const signaledSkills = groupSignalsBySkill(pendingSignals);
273
+ if (signaledSkills.size > 0) {
274
+ console.error(
275
+ `[orchestrate] Improvement signals: ${pendingSignals.length} pending for ${signaledSkills.size} skill(s)`,
276
+ );
277
+ }
278
+
279
+ const candidates = selectCandidates(statusResult.skills, {
280
+ skillFilter: options.skillFilter,
281
+ maxSkills: options.maxSkills,
282
+ auditEntries,
283
+ signaledSkills,
284
+ });
285
+
286
+ const evolveCandidates = candidates.filter((candidate) => candidate.action === "evolve");
287
+ const skipCount = candidates.filter((candidate) => candidate.action === "skip").length;
288
+ console.error(
289
+ `[orchestrate] Candidates: ${evolveCandidates.length} to evolve, ${skipCount} skipped`,
290
+ );
291
+ for (const candidate of candidates) {
292
+ console.error(
293
+ ` ${candidate.action === "skip" ? "⊘" : "→"} ${candidate.skill}: ${candidate.reason}`,
294
+ );
295
+ }
296
+
297
+ if (evolveCandidates.length >= 2) {
298
+ try {
299
+ const overlap = await detectCrossSkillOverlap(evolveCandidates, skillRecords, queryRecords);
300
+ if (overlap.length > 0) {
301
+ console.error("\n[orchestrate] Cross-skill eval overlap detected:");
302
+ for (const entry of overlap) {
303
+ console.error(
304
+ ` ⚠ ${entry.skill_a} ↔ ${entry.skill_b}: ${(entry.overlap_pct * 100).toFixed(0)}% shared queries (${entry.shared_queries.length} queries)`,
305
+ );
306
+ }
307
+ console.error("");
308
+ }
309
+ } catch {
310
+ // Overlap detection is informative only.
311
+ }
312
+ }
313
+
314
+ const agent = runtime.detectAgent();
315
+ if (!agent && evolveCandidates.length > 0) {
316
+ console.error("[orchestrate] WARNING: No agent CLI found in PATH. Evolve will be skipped.");
317
+ for (const candidate of evolveCandidates) {
318
+ candidate.action = "skip";
319
+ candidate.reason = "no agent CLI available";
320
+ }
321
+ }
322
+
323
+ return {
324
+ syncResult,
325
+ statusResult,
326
+ telemetry,
327
+ skillRecords,
328
+ pendingSignals,
329
+ candidates,
330
+ evolveCandidates,
331
+ agent,
332
+ autoGradedCount,
333
+ };
334
+ }
@@ -0,0 +1,182 @@
1
+ import type { OrchestrateResult, SkillAction } from "../orchestrate.js";
2
+ import type { WorkflowSkillProposal } from "../workflows/proposals.js";
3
+
4
+ function formatSyncPhase(syncResult: OrchestrateResult["syncResult"]): string[] {
5
+ const lines: string[] = ["Phase 1: Sync"];
6
+ const sources: [string, keyof OrchestrateResult["syncResult"]["sources"]][] = [
7
+ ["Claude", "claude"],
8
+ ["Codex", "codex"],
9
+ ["OpenCode", "opencode"],
10
+ ["OpenClaw", "openclaw"],
11
+ ];
12
+
13
+ for (const [label, key] of sources) {
14
+ const source = syncResult.sources[key];
15
+ if (!source.available) {
16
+ lines.push(` ${label.padEnd(12)}not available`);
17
+ } else if (source.synced > 0) {
18
+ lines.push(` ${label.padEnd(12)}scanned ${source.scanned}, synced ${source.synced}`);
19
+ } else {
20
+ lines.push(` ${label.padEnd(12)}scanned ${source.scanned}, up to date`);
21
+ }
22
+ }
23
+
24
+ if (syncResult.repair.ran && syncResult.repair.repaired_records > 0) {
25
+ lines.push(
26
+ ` Repair ${syncResult.repair.repaired_records} records across ${syncResult.repair.repaired_sessions} sessions`,
27
+ );
28
+ }
29
+
30
+ return lines;
31
+ }
32
+
33
+ function formatStatusPhase(statusResult: OrchestrateResult["statusResult"]): string[] {
34
+ const lines: string[] = ["Phase 2: Status"];
35
+ const byStatus: Record<string, number> = {};
36
+ for (const skill of statusResult.skills) {
37
+ byStatus[skill.status] = (byStatus[skill.status] ?? 0) + 1;
38
+ }
39
+ const healthLabel = statusResult.system.healthy ? "healthy" : "UNHEALTHY";
40
+ lines.push(` ${statusResult.skills.length} skills found, system ${healthLabel}`);
41
+
42
+ const parts: string[] = [];
43
+ for (const status of ["CRITICAL", "WARNING", "HEALTHY", "UNGRADED", "UNKNOWN"]) {
44
+ if (byStatus[status]) parts.push(`${byStatus[status]} ${status}`);
45
+ }
46
+ if (parts.length > 0) lines.push(` ${parts.join(", ")}`);
47
+
48
+ return lines;
49
+ }
50
+
51
+ function formatDecisionPhase(candidates: SkillAction[]): string[] {
52
+ const lines: string[] = ["Phase 3: Skill Decisions"];
53
+ if (candidates.length === 0) {
54
+ lines.push(" (no skills to evaluate)");
55
+ return lines;
56
+ }
57
+
58
+ for (const candidate of candidates) {
59
+ const icon = candidate.action === "skip" ? "⊘" : candidate.action === "watch" ? "○" : "→";
60
+ const actionLabel = candidate.action.toUpperCase().padEnd(7);
61
+ lines.push(` ${icon} ${candidate.skill.padEnd(20)} ${actionLabel} ${candidate.reason}`);
62
+ }
63
+
64
+ return lines;
65
+ }
66
+
67
+ function formatEvolutionPhase(candidates: SkillAction[]): string[] {
68
+ const evolved = candidates.filter(
69
+ (candidate) => candidate.action === "evolve" && candidate.evolveResult !== undefined,
70
+ );
71
+ if (evolved.length === 0) return [];
72
+
73
+ const lines: string[] = ["Phase 4: Evolution Results"];
74
+ for (const candidate of evolved) {
75
+ const evolveResult = candidate.evolveResult as NonNullable<typeof candidate.evolveResult>;
76
+ const status = evolveResult.deployed ? "deployed" : "not deployed";
77
+ const detail = evolveResult.reason;
78
+ const validation = evolveResult.validation
79
+ ? ` (${(evolveResult.validation.before_pass_rate * 100).toFixed(0)}% → ${(evolveResult.validation.after_pass_rate * 100).toFixed(0)}%)`
80
+ : "";
81
+ lines.push(` ${candidate.skill.padEnd(20)} ${status}${validation}`);
82
+ lines.push(` ${"".padEnd(20)} ${detail}`);
83
+ }
84
+
85
+ return lines;
86
+ }
87
+
88
+ function formatWatchPhase(candidates: SkillAction[]): string[] {
89
+ const watched = candidates.filter((candidate) => candidate.action === "watch");
90
+ if (watched.length === 0) return [];
91
+
92
+ const lines: string[] = ["Phase 5: Watch"];
93
+ for (const candidate of watched) {
94
+ const snapshot = candidate.watchResult?.snapshot;
95
+ const metrics = snapshot
96
+ ? ` (pass_rate=${snapshot.pass_rate.toFixed(2)}, baseline=${snapshot.baseline_pass_rate.toFixed(2)})`
97
+ : "";
98
+ const alertTag = candidate.watchResult?.alert ? " [ALERT]" : "";
99
+ const rollbackTag = candidate.watchResult?.rolledBack ? " [ROLLED BACK]" : "";
100
+ lines.push(
101
+ ` ${candidate.skill.padEnd(20)} ${candidate.reason}${alertTag}${rollbackTag}${metrics}`,
102
+ );
103
+ }
104
+
105
+ return lines;
106
+ }
107
+
108
+ function formatWorkflowProposalPhase(proposals: WorkflowSkillProposal[]): string[] {
109
+ if (proposals.length === 0) return [];
110
+
111
+ const lines: string[] = ["Phase 6: Workflow Skill Proposals"];
112
+ for (const proposal of proposals) {
113
+ lines.push(
114
+ ` + ${proposal.source_skill_name.padEnd(20)} NEW_SKILL ${proposal.draft.skill_name} (${proposal.workflow.skills.join(" -> ")})`,
115
+ );
116
+ lines.push(` ${"".padEnd(20)} ${proposal.summary}`);
117
+ }
118
+ return lines;
119
+ }
120
+
121
+ export function formatOrchestrateReport(result: OrchestrateResult): string {
122
+ const separator = "═".repeat(48);
123
+ const lines: string[] = [];
124
+
125
+ lines.push(separator);
126
+ lines.push("selftune orchestrate — decision report");
127
+ lines.push(separator);
128
+ lines.push("");
129
+
130
+ if (result.summary.dryRun) {
131
+ lines.push("Mode: DRY RUN (no mutations applied)");
132
+ } else if (result.summary.approvalMode === "review") {
133
+ lines.push("Mode: REVIEW (proposals validated but not deployed)");
134
+ } else {
135
+ lines.push("Mode: AUTONOMOUS (validated changes deployed automatically)");
136
+ }
137
+ lines.push("");
138
+
139
+ lines.push(...formatSyncPhase(result.syncResult));
140
+ lines.push("");
141
+ lines.push(...formatStatusPhase(result.statusResult));
142
+ lines.push("");
143
+ lines.push(...formatDecisionPhase(result.candidates));
144
+ lines.push("");
145
+
146
+ const evolutionLines = formatEvolutionPhase(result.candidates);
147
+ if (evolutionLines.length > 0) {
148
+ lines.push(...evolutionLines);
149
+ lines.push("");
150
+ }
151
+
152
+ const watchLines = formatWatchPhase(result.candidates);
153
+ if (watchLines.length > 0) {
154
+ lines.push(...watchLines);
155
+ lines.push("");
156
+ }
157
+
158
+ const workflowProposalLines = formatWorkflowProposalPhase(result.workflowProposals);
159
+ if (workflowProposalLines.length > 0) {
160
+ lines.push(...workflowProposalLines);
161
+ lines.push("");
162
+ }
163
+
164
+ lines.push("Summary");
165
+ lines.push(` Auto-graded: ${result.summary.autoGraded}`);
166
+ lines.push(` Evaluated: ${result.summary.evaluated} skills`);
167
+ lines.push(` Deployed: ${result.summary.deployed}`);
168
+ lines.push(` Proposed: ${result.workflowProposals.length} workflow skills`);
169
+ lines.push(` Watched: ${result.summary.watched}`);
170
+ lines.push(` Skipped: ${result.summary.skipped}`);
171
+ lines.push(` Elapsed: ${(result.summary.elapsedMs / 1000).toFixed(1)}s`);
172
+
173
+ if (result.summary.dryRun && result.summary.evaluated > 0) {
174
+ lines.push("");
175
+ lines.push(" Rerun without --dry-run to allow validated deployments.");
176
+ } else if (result.summary.approvalMode === "review" && result.summary.evaluated > 0) {
177
+ lines.push("");
178
+ lines.push(" Rerun without --review-required to allow validated deployments.");
179
+ }
180
+
181
+ return lines.join("\n");
182
+ }
@@ -0,0 +1,120 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+
4
+ import { readAlphaIdentity } from "../alpha-identity.js";
5
+ import { SELFTUNE_CONFIG_PATH } from "../constants.js";
6
+ import { readGradingResultsForSkill } from "../grading/results.js";
7
+ import { getDb } from "../localdb/db.js";
8
+ import {
9
+ queryEvolutionAudit,
10
+ queryQueryLog,
11
+ querySessionTelemetry,
12
+ querySkillUsageRecords,
13
+ } from "../localdb/queries.js";
14
+ import { doctor } from "../observability.js";
15
+ import { computeStatus } from "../status.js";
16
+ import { syncSources } from "../sync.js";
17
+ import type {
18
+ AlphaIdentity,
19
+ EvolutionAuditEntry,
20
+ ImprovementSignalRecord,
21
+ QueryLogRecord,
22
+ SessionTelemetryRecord,
23
+ SkillUsageRecord,
24
+ } from "../types.js";
25
+ import { detectLlmAgent, type LlmBackedAgent } from "../utils/llm-call.js";
26
+ import {
27
+ findInstalledSkillPath,
28
+ findRepositoryClaudeSkillDirs,
29
+ findRepositorySkillDirs,
30
+ } from "../utils/skill-discovery.js";
31
+ import {
32
+ discoverWorkflowSkillProposals,
33
+ persistWorkflowSkillProposal,
34
+ } from "../workflows/proposals.js";
35
+ import type { OrchestrateDeps } from "../orchestrate.js";
36
+ import { buildReplayValidationOptions } from "./execute.js";
37
+
38
+ export interface ResolvedOrchestrateRuntime {
39
+ syncSources: typeof syncSources;
40
+ computeStatus: typeof computeStatus;
41
+ evolve: typeof import("../evolution/evolve.js").evolve;
42
+ watch: typeof import("../monitoring/watch.js").watch;
43
+ detectAgent: () => LlmBackedAgent | null;
44
+ doctor: typeof doctor;
45
+ readTelemetry: () => SessionTelemetryRecord[];
46
+ readSkillRecords: () => SkillUsageRecord[];
47
+ readQueryRecords: () => QueryLogRecord[];
48
+ readAuditEntries: () => EvolutionAuditEntry[];
49
+ resolveSkillPath: (skillName: string) => string | undefined;
50
+ readGradingResults: (skillName: string) => ReturnType<typeof readGradingResultsForSkill>;
51
+ readSignals?: () => ImprovementSignalRecord[];
52
+ readAlphaIdentity: () => AlphaIdentity | null;
53
+ discoverWorkflowSkillProposals: typeof discoverWorkflowSkillProposals;
54
+ persistWorkflowSkillProposal: typeof persistWorkflowSkillProposal;
55
+ buildReplayOptions: typeof buildReplayValidationOptions;
56
+ }
57
+
58
+ export function getSkillSearchDirs(): string[] {
59
+ const home = homedir();
60
+ const cwd = process.cwd();
61
+ return [
62
+ join(home, ".claude", "skills"),
63
+ join(home, ".agents", "skills"),
64
+ join(home, ".codex", "skills"),
65
+ ...findRepositorySkillDirs(cwd),
66
+ ...findRepositoryClaudeSkillDirs(cwd),
67
+ ];
68
+ }
69
+
70
+ export function defaultResolveSkillPath(skillName: string): string | undefined {
71
+ return findInstalledSkillPath(skillName, getSkillSearchDirs());
72
+ }
73
+
74
+ export async function resolveOrchestrateRuntime(
75
+ deps: OrchestrateDeps = {},
76
+ ): Promise<ResolvedOrchestrateRuntime> {
77
+ const evolve = deps.evolve ?? (await import("../evolution/evolve.js")).evolve;
78
+ const watch = deps.watch ?? (await import("../monitoring/watch.js")).watch;
79
+
80
+ return {
81
+ syncSources: deps.syncSources ?? syncSources,
82
+ computeStatus: deps.computeStatus ?? computeStatus,
83
+ evolve,
84
+ watch,
85
+ detectAgent: deps.detectAgent ?? detectLlmAgent,
86
+ doctor: deps.doctor ?? doctor,
87
+ readTelemetry:
88
+ deps.readTelemetry ??
89
+ (() => {
90
+ const db = getDb();
91
+ return querySessionTelemetry(db) as SessionTelemetryRecord[];
92
+ }),
93
+ readSkillRecords:
94
+ deps.readSkillRecords ??
95
+ (() => {
96
+ const db = getDb();
97
+ return querySkillUsageRecords(db) as SkillUsageRecord[];
98
+ }),
99
+ readQueryRecords:
100
+ deps.readQueryRecords ??
101
+ (() => {
102
+ const db = getDb();
103
+ return queryQueryLog(db) as QueryLogRecord[];
104
+ }),
105
+ readAuditEntries:
106
+ deps.readAuditEntries ??
107
+ (() => {
108
+ const db = getDb();
109
+ return queryEvolutionAudit(db) as EvolutionAuditEntry[];
110
+ }),
111
+ resolveSkillPath: deps.resolveSkillPath ?? defaultResolveSkillPath,
112
+ readGradingResults: deps.readGradingResults ?? readGradingResultsForSkill,
113
+ readSignals: deps.readSignals,
114
+ readAlphaIdentity: deps.readAlphaIdentity ?? (() => readAlphaIdentity(SELFTUNE_CONFIG_PATH)),
115
+ discoverWorkflowSkillProposals:
116
+ deps.discoverWorkflowSkillProposals ?? discoverWorkflowSkillProposals,
117
+ persistWorkflowSkillProposal: deps.persistWorkflowSkillProposal ?? persistWorkflowSkillProposal,
118
+ buildReplayOptions: deps.buildReplayOptions ?? buildReplayValidationOptions,
119
+ };
120
+ }
@@ -0,0 +1,48 @@
1
+ import { updateSignalConsumed } from "../localdb/direct-write.js";
2
+ import { getDb } from "../localdb/db.js";
3
+ import { queryImprovementSignals } from "../localdb/queries.js";
4
+ import type { ImprovementSignalRecord } from "../types.js";
5
+
6
+ export function readPendingSignals(
7
+ reader?: () => ImprovementSignalRecord[],
8
+ ): ImprovementSignalRecord[] {
9
+ const read =
10
+ reader ??
11
+ (() => {
12
+ const db = getDb();
13
+ return queryImprovementSignals(db, false) as ImprovementSignalRecord[];
14
+ });
15
+
16
+ try {
17
+ return read().filter((signal) => !signal.consumed);
18
+ } catch {
19
+ return [];
20
+ }
21
+ }
22
+
23
+ export function groupSignalsBySkill(signals: ImprovementSignalRecord[]): Map<string, number> {
24
+ const map = new Map<string, number>();
25
+ for (const signal of signals) {
26
+ if (signal.mentioned_skill) {
27
+ const key = signal.mentioned_skill.toLowerCase();
28
+ map.set(key, (map.get(key) ?? 0) + 1);
29
+ }
30
+ }
31
+ return map;
32
+ }
33
+
34
+ export function markSignalsConsumed(signals: ImprovementSignalRecord[], runId: string): void {
35
+ try {
36
+ if (signals.length === 0) return;
37
+ for (const signal of signals) {
38
+ const ok = updateSignalConsumed(signal.session_id, signal.query, signal.signal_type, runId);
39
+ if (!ok) {
40
+ console.error(
41
+ `[orchestrate] failed to mark signal consumed: session_id=${signal.session_id}, signal_type=${signal.signal_type}`,
42
+ );
43
+ }
44
+ }
45
+ } catch {
46
+ // Silent on errors.
47
+ }
48
+ }