selftune 0.2.23 → 0.2.25

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 (219) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +93 -15
  3. package/apps/local-dashboard/dist/assets/index-DgY2KGP-.css +1 -0
  4. package/apps/local-dashboard/dist/assets/index-Dhgv5BQO.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/alpha-upload/build-payloads.ts +3 -3
  12. package/cli/selftune/alpha-upload/stage-canonical.ts +17 -11
  13. package/cli/selftune/auto-update.ts +200 -8
  14. package/cli/selftune/canonical-export.ts +55 -25
  15. package/cli/selftune/command-surface.ts +397 -0
  16. package/cli/selftune/contribute/contribute.ts +64 -13
  17. package/cli/selftune/contribution-config.ts +57 -3
  18. package/cli/selftune/contribution-preferences.ts +117 -0
  19. package/cli/selftune/contribution-signals.ts +8 -4
  20. package/cli/selftune/contribution-staging.ts +13 -2
  21. package/cli/selftune/contributions.ts +55 -121
  22. package/cli/selftune/creator-contributions.ts +29 -10
  23. package/cli/selftune/cron/setup.ts +7 -3
  24. package/cli/selftune/dashboard-contract.ts +73 -0
  25. package/cli/selftune/dashboard-server.ts +168 -17
  26. package/cli/selftune/dashboard.ts +350 -17
  27. package/cli/selftune/eval/baseline.ts +21 -5
  28. package/cli/selftune/eval/execution-eval.ts +170 -0
  29. package/cli/selftune/eval/family-overlap.ts +2 -2
  30. package/cli/selftune/eval/hooks-to-evals.ts +228 -82
  31. package/cli/selftune/eval/import-skillsbench.ts +2 -2
  32. package/cli/selftune/eval/invocation-classifier.ts +56 -0
  33. package/cli/selftune/eval/synthetic-evals.ts +5 -3
  34. package/cli/selftune/eval/unit-test-cli.ts +7 -4
  35. package/cli/selftune/evolution/apply-proposal.ts +295 -0
  36. package/cli/selftune/evolution/engines/replay-engine.ts +79 -57
  37. package/cli/selftune/evolution/evolve-body.ts +100 -39
  38. package/cli/selftune/evolution/evolve.ts +244 -52
  39. package/cli/selftune/evolution/rollback.ts +0 -1
  40. package/cli/selftune/evolution/validate-body.ts +68 -42
  41. package/cli/selftune/evolution/validate-host-replay.ts +510 -60
  42. package/cli/selftune/evolution/validate-proposal.ts +11 -150
  43. package/cli/selftune/evolution/validate-routing.ts +43 -41
  44. package/cli/selftune/evolution/validation-contract.ts +91 -0
  45. package/cli/selftune/grading/auto-grade.ts +11 -7
  46. package/cli/selftune/grading/grade-session.ts +10 -16
  47. package/cli/selftune/index.ts +35 -10
  48. package/cli/selftune/ingestors/claude-replay.ts +15 -10
  49. package/cli/selftune/ingestors/codex-wrapper.ts +3 -3
  50. package/cli/selftune/ingestors/opencode-ingest.ts +2 -2
  51. package/cli/selftune/ingestors/pi-ingest.ts +3 -2
  52. package/cli/selftune/init.ts +27 -3
  53. package/cli/selftune/localdb/direct-write.ts +35 -1
  54. package/cli/selftune/localdb/queries/cron.ts +34 -0
  55. package/cli/selftune/localdb/queries/dashboard.ts +834 -0
  56. package/cli/selftune/localdb/queries/evolution.ts +158 -0
  57. package/cli/selftune/localdb/queries/execution.ts +133 -0
  58. package/cli/selftune/localdb/queries/json.ts +18 -0
  59. package/cli/selftune/localdb/queries/monitoring.ts +263 -0
  60. package/cli/selftune/localdb/queries/raw.ts +95 -0
  61. package/cli/selftune/localdb/queries/staging.ts +270 -0
  62. package/cli/selftune/localdb/queries/trust.ts +392 -0
  63. package/cli/selftune/localdb/queries.ts +60 -2288
  64. package/cli/selftune/localdb/schema.ts +21 -0
  65. package/cli/selftune/monitoring/watch.ts +96 -29
  66. package/cli/selftune/normalization.ts +3 -0
  67. package/cli/selftune/observability.ts +4 -2
  68. package/cli/selftune/orchestrate/cli.ts +161 -0
  69. package/cli/selftune/orchestrate/execute.ts +295 -0
  70. package/cli/selftune/orchestrate/finalize.ts +157 -0
  71. package/cli/selftune/orchestrate/locks.ts +40 -0
  72. package/cli/selftune/orchestrate/plan.ts +131 -0
  73. package/cli/selftune/orchestrate/post-run.ts +59 -0
  74. package/cli/selftune/orchestrate/prepare.ts +334 -0
  75. package/cli/selftune/orchestrate/report.ts +182 -0
  76. package/cli/selftune/orchestrate/runtime.ts +120 -0
  77. package/cli/selftune/orchestrate/signals.ts +48 -0
  78. package/cli/selftune/orchestrate.ts +150 -1173
  79. package/cli/selftune/repair/skill-usage.ts +5 -2
  80. package/cli/selftune/routes/overview.ts +5 -2
  81. package/cli/selftune/routes/skill-report.ts +15 -2
  82. package/cli/selftune/schedule.ts +5 -5
  83. package/cli/selftune/status.ts +39 -2
  84. package/cli/selftune/testing-readiness.ts +597 -0
  85. package/cli/selftune/types.ts +44 -4
  86. package/cli/selftune/uninstall.ts +2 -1
  87. package/cli/selftune/utils/canonical-log.ts +1 -9
  88. package/cli/selftune/utils/cli-error.ts +9 -0
  89. package/cli/selftune/utils/llm-call.ts +126 -6
  90. package/cli/selftune/utils/skill-discovery.ts +2 -0
  91. package/cli/selftune/workflows/proposals.ts +184 -0
  92. package/cli/selftune/workflows/skill-scaffold.ts +241 -0
  93. package/cli/selftune/workflows/workflows.ts +100 -26
  94. package/node_modules/@selftune/telemetry-contract/fixtures/complete-push.ts +1 -1
  95. package/node_modules/@selftune/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
  96. package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
  97. package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
  98. package/node_modules/@selftune/telemetry-contract/src/schemas.ts +41 -1
  99. package/node_modules/@selftune/telemetry-contract/src/types.ts +103 -2
  100. package/package.json +25 -9
  101. package/packages/dashboard-core/AGENTS.md +18 -0
  102. package/packages/dashboard-core/README.md +30 -0
  103. package/packages/dashboard-core/index.ts +3 -0
  104. package/packages/dashboard-core/package.json +39 -0
  105. package/packages/dashboard-core/src/chrome/DashboardChrome.tsx +74 -0
  106. package/packages/dashboard-core/src/chrome/DashboardHeader.tsx +200 -0
  107. package/packages/dashboard-core/src/chrome/DashboardSidebar.tsx +219 -0
  108. package/packages/dashboard-core/src/chrome/RuntimeBadge.tsx +46 -0
  109. package/packages/dashboard-core/src/chrome/index.ts +14 -0
  110. package/packages/dashboard-core/src/chrome/types.ts +81 -0
  111. package/packages/dashboard-core/src/chrome/utils.ts +23 -0
  112. package/packages/dashboard-core/src/gates/FeatureGate.tsx +11 -0
  113. package/packages/dashboard-core/src/gates/LockedRoute.tsx +29 -0
  114. package/packages/dashboard-core/src/gates/UpgradeCard.tsx +89 -0
  115. package/packages/dashboard-core/src/gates/index.ts +3 -0
  116. package/packages/dashboard-core/src/host/DashboardHostProvider.tsx +62 -0
  117. package/packages/dashboard-core/src/host/adapter.ts +47 -0
  118. package/packages/dashboard-core/src/host/capabilities.ts +55 -0
  119. package/packages/dashboard-core/src/host/index.ts +3 -0
  120. package/packages/dashboard-core/src/models/analytics.ts +39 -0
  121. package/packages/dashboard-core/src/models/index.ts +4 -0
  122. package/packages/dashboard-core/src/models/overview.ts +98 -0
  123. package/packages/dashboard-core/src/models/runtime.ts +7 -0
  124. package/packages/dashboard-core/src/models/skills.ts +34 -0
  125. package/packages/dashboard-core/src/routes/index.ts +2 -0
  126. package/packages/dashboard-core/src/routes/manifest.test.ts +70 -0
  127. package/packages/dashboard-core/src/routes/manifest.ts +451 -0
  128. package/packages/dashboard-core/src/routes/types.ts +39 -0
  129. package/packages/dashboard-core/src/screens/analytics/AnalyticsScreen.tsx +278 -0
  130. package/packages/dashboard-core/src/screens/analytics/index.ts +1 -0
  131. package/packages/dashboard-core/src/screens/index.ts +37 -0
  132. package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.test.ts +101 -0
  133. package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.tsx +393 -0
  134. package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.test.tsx +113 -0
  135. package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.tsx +72 -0
  136. package/packages/dashboard-core/src/screens/overview/OverviewCoreSurface.tsx +71 -0
  137. package/packages/dashboard-core/src/screens/overview/OverviewOnboardingBanner.tsx +90 -0
  138. package/packages/dashboard-core/src/screens/overview/OverviewRunSummary.tsx +40 -0
  139. package/packages/dashboard-core/src/screens/overview/index.ts +16 -0
  140. package/packages/dashboard-core/src/screens/overview/types.ts +13 -0
  141. package/packages/dashboard-core/src/screens/skill-report/SkillReportDailyBreakdownSection.tsx +99 -0
  142. package/packages/dashboard-core/src/screens/skill-report/SkillReportDataQualityTabContent.tsx +35 -0
  143. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceRail.tsx +71 -0
  144. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceSection.tsx +63 -0
  145. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceTabContent.tsx +25 -0
  146. package/packages/dashboard-core/src/screens/skill-report/SkillReportInvocationsSection.tsx +24 -0
  147. package/packages/dashboard-core/src/screens/skill-report/SkillReportMissedQueriesSection.tsx +79 -0
  148. package/packages/dashboard-core/src/screens/skill-report/SkillReportScaffold.tsx +150 -0
  149. package/packages/dashboard-core/src/screens/skill-report/SkillReportSections.test.tsx +224 -0
  150. package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.test.tsx +76 -0
  151. package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.tsx +88 -0
  152. package/packages/dashboard-core/src/screens/skill-report/SkillReportTrendSection.tsx +33 -0
  153. package/packages/dashboard-core/src/screens/skill-report/SkillReportTrustBadge.tsx +67 -0
  154. package/packages/dashboard-core/src/screens/skill-report/index.ts +45 -0
  155. package/packages/dashboard-core/src/screens/skills/SkillsLibraryScreen.tsx +162 -0
  156. package/packages/dashboard-core/src/screens/skills/index.ts +6 -0
  157. package/packages/telemetry-contract/fixtures/complete-push.ts +1 -1
  158. package/packages/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
  159. package/packages/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
  160. package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
  161. package/packages/telemetry-contract/src/schemas.ts +41 -1
  162. package/packages/telemetry-contract/src/types.ts +103 -2
  163. package/packages/ui/src/components/EvidenceViewer.tsx +80 -25
  164. package/packages/ui/src/components/OverviewPanels.tsx +67 -26
  165. package/packages/ui/src/primitives/tabs.tsx +7 -6
  166. package/packages/ui/src/types.ts +10 -0
  167. package/skill/SKILL.md +130 -332
  168. package/skill/agents/diagnosis-analyst.md +3 -3
  169. package/skill/agents/evolution-reviewer.md +3 -3
  170. package/skill/agents/integration-guide.md +3 -3
  171. package/skill/agents/pattern-analyst.md +2 -2
  172. package/skill/references/cli-quick-reference.md +89 -0
  173. package/skill/references/creator-playbook.md +131 -0
  174. package/skill/references/examples.md +48 -0
  175. package/skill/references/troubleshooting.md +47 -0
  176. package/skill/references/version-history.md +1 -1
  177. package/skill/selftune.contribute.json +11 -0
  178. package/skill/{Workflows → workflows}/Baseline.md +20 -1
  179. package/skill/{Workflows → workflows}/Contribute.md +23 -10
  180. package/skill/{Workflows → workflows}/Contributions.md +13 -5
  181. package/skill/workflows/CreateTestDeploy.md +170 -0
  182. package/skill/{Workflows → workflows}/CreatorContributions.md +18 -6
  183. package/skill/{Workflows → workflows}/Cron.md +1 -1
  184. package/skill/{Workflows → workflows}/Dashboard.md +20 -0
  185. package/skill/{Workflows → workflows}/Doctor.md +1 -1
  186. package/skill/{Workflows → workflows}/Evals.md +67 -2
  187. package/skill/{Workflows → workflows}/Evolve.md +119 -30
  188. package/skill/{Workflows → workflows}/EvolveBody.md +41 -1
  189. package/skill/{Workflows → workflows}/Grade.md +1 -1
  190. package/skill/{Workflows → workflows}/Initialize.md +8 -4
  191. package/skill/{Workflows → workflows}/Orchestrate.md +13 -3
  192. package/skill/{Workflows → workflows}/Schedule.md +3 -3
  193. package/skill/workflows/SignalsDashboard.md +87 -0
  194. package/skill/{Workflows → workflows}/UnitTest.md +19 -0
  195. package/skill/{Workflows → workflows}/Watch.md +42 -2
  196. package/skill/{Workflows → workflows}/Workflows.md +39 -2
  197. package/apps/local-dashboard/dist/assets/index-CwOtTrUS.css +0 -1
  198. package/apps/local-dashboard/dist/assets/index-f1HQpbeH.js +0 -59
  199. package/apps/local-dashboard/dist/assets/vendor-react-CKkiCskZ.js +0 -11
  200. package/apps/local-dashboard/dist/assets/vendor-ui-jVSaIZey.js +0 -12
  201. /package/skill/{Workflows → workflows}/AlphaUpload.md +0 -0
  202. /package/skill/{Workflows → workflows}/AutoActivation.md +0 -0
  203. /package/skill/{Workflows → workflows}/Badge.md +0 -0
  204. /package/skill/{Workflows → workflows}/Composability.md +0 -0
  205. /package/skill/{Workflows → workflows}/EvolutionMemory.md +0 -0
  206. /package/skill/{Workflows → workflows}/ExportCanonical.md +0 -0
  207. /package/skill/{Workflows → workflows}/Hook.md +0 -0
  208. /package/skill/{Workflows → workflows}/ImportSkillsBench.md +0 -0
  209. /package/skill/{Workflows → workflows}/Ingest.md +0 -0
  210. /package/skill/{Workflows → workflows}/PlatformHooks.md +0 -0
  211. /package/skill/{Workflows → workflows}/Quickstart.md +0 -0
  212. /package/skill/{Workflows → workflows}/Recover.md +0 -0
  213. /package/skill/{Workflows → workflows}/Registry.md +0 -0
  214. /package/skill/{Workflows → workflows}/RepairSkillUsage.md +0 -0
  215. /package/skill/{Workflows → workflows}/Replay.md +0 -0
  216. /package/skill/{Workflows → workflows}/Rollback.md +0 -0
  217. /package/skill/{Workflows → workflows}/Sync.md +0 -0
  218. /package/skill/{Workflows → workflows}/Telemetry.md +0 -0
  219. /package/skill/{Workflows → workflows}/Uninstall.md +0 -0
@@ -9,43 +9,43 @@
9
9
  * explicit dry-run and review-required modes for human-in-the-loop operation.
10
10
  */
11
11
 
12
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
13
- import { homedir } from "node:os";
14
- import { dirname, join } from "node:path";
15
- import { parseArgs } from "node:util";
16
-
17
- import { readAlphaIdentity } from "./alpha-identity.js";
18
12
  import type { UploadCycleSummary } from "./alpha-upload/index.js";
19
- import { getOrchestrateLockPath, SELFTUNE_CONFIG_PATH } from "./constants.js";
20
- import type { OrchestrateRunReport, OrchestrateRunSkillAction } from "./dashboard-contract.js";
21
13
  import type { EvolveOptions, EvolveResult } from "./evolution/evolve.js";
22
- import {
23
- buildDefaultGradingOutputPath,
24
- deriveExpectationsFromSkill,
25
- gradeSession,
26
- resolveLatestSessionForSkill,
27
- } from "./grading/grade-session.js";
28
14
  import { readGradingResultsForSkill } from "./grading/results.js";
29
15
  import { getDb } from "./localdb/db.js";
16
+ import { writeCronRunToDb } from "./localdb/direct-write.js";
17
+ import type { WatchResult } from "./monitoring/watch.js";
30
18
  import {
31
- updateSignalConsumed,
32
- writeCronRunToDb,
33
- writeGradingResultToDb,
34
- writeOrchestrateRunToDb,
35
- } from "./localdb/direct-write.js";
19
+ buildOrchestrateJsonOutput,
20
+ parseOrchestrateCliArgs,
21
+ renderOrchestrateHelp,
22
+ } from "./orchestrate/cli.js";
36
23
  import {
37
- queryEvolutionAudit,
38
- queryImprovementSignals,
39
- queryQueryLog,
40
- querySessionTelemetry,
41
- querySkillUsageRecords,
42
- } from "./localdb/queries.js";
43
- import type { WatchResult } from "./monitoring/watch.js";
24
+ autoGradeFreshDeploys,
25
+ buildReplayValidationOptions,
26
+ runEvolutionPhase,
27
+ watchRecentDeploys,
28
+ } from "./orchestrate/execute.js";
29
+ import { finalizeOrchestrateRun } from "./orchestrate/finalize.js";
30
+ import { acquireLock, releaseLock } from "./orchestrate/locks.js";
31
+ import { runPostOrchestrateSideEffects } from "./orchestrate/post-run.js";
32
+ import {
33
+ autoGradeTopUngraded,
34
+ detectCrossSkillOverlap,
35
+ prepareOrchestrateRun,
36
+ } from "./orchestrate/prepare.js";
37
+ import {
38
+ DEFAULT_COOLDOWN_HOURS,
39
+ MIN_CANDIDATE_EVIDENCE,
40
+ selectCandidates,
41
+ } from "./orchestrate/plan.js";
42
+ import { formatOrchestrateReport } from "./orchestrate/report.js";
43
+ import { resolveOrchestrateRuntime } from "./orchestrate/runtime.js";
44
44
  import { doctor } from "./observability.js";
45
- import type { SkillStatus, StatusResult } from "./status.js";
45
+ import type { StatusResult } from "./status.js";
46
46
  import { computeStatus } from "./status.js";
47
47
  import type { SyncResult } from "./sync.js";
48
- import { createDefaultSyncOptions, syncSources } from "./sync.js";
48
+ import { syncSources } from "./sync.js";
49
49
  import type {
50
50
  AlphaIdentity,
51
51
  EvolutionAuditEntry,
@@ -54,103 +54,23 @@ import type {
54
54
  SessionTelemetryRecord,
55
55
  SkillUsageRecord,
56
56
  } from "./types.js";
57
- import { CLIError, handleCLIError } from "./utils/cli-error.js";
58
- import { detectAgent } from "./utils/llm-call.js";
59
- import { getSelftuneVersion, readConfiguredAgentType } from "./utils/selftune-meta.js";
57
+ import { handleCLIError } from "./utils/cli-error.js";
58
+ import { detectLlmAgent } from "./utils/llm-call.js";
60
59
  import {
61
- findInstalledSkillPath,
62
- findRepositoryClaudeSkillDirs,
63
- findRepositorySkillDirs,
64
- } from "./utils/skill-discovery.js";
65
- import { readExcerpt } from "./utils/transcript.js";
66
-
67
- // ---------------------------------------------------------------------------
68
- // Lockfile management
69
- // ---------------------------------------------------------------------------
70
-
71
- interface LockInfo {
72
- pid: number;
73
- timestamp: string;
74
- }
75
-
76
- const LOCK_STALE_MS = 30 * 60 * 1000; // 30 minutes
77
-
78
- export function acquireLock(lockPath: string = getOrchestrateLockPath()): boolean {
79
- try {
80
- if (existsSync(lockPath)) {
81
- try {
82
- const raw = readFileSync(lockPath, "utf-8");
83
- const info: LockInfo = JSON.parse(raw);
84
- const lockAge = Date.now() - Date.parse(info.timestamp);
85
- if (lockAge < LOCK_STALE_MS) {
86
- return false; // lock is fresh, cannot acquire
87
- }
88
- // Lock is stale, fall through to overwrite
89
- } catch {
90
- // Corrupted lock file, treat as stale and overwrite
91
- }
92
- }
93
- const lock: LockInfo = { pid: process.pid, timestamp: new Date().toISOString() };
94
- writeFileSync(lockPath, JSON.stringify(lock));
95
- return true;
96
- } catch {
97
- // Fail-open: if we can't check/write, allow the run
98
- return true;
99
- }
100
- }
101
-
102
- export function releaseLock(lockPath: string = getOrchestrateLockPath()): void {
103
- try {
104
- unlinkSync(lockPath);
105
- } catch {
106
- // Silent on errors (file may not exist)
107
- }
108
- }
109
-
110
- // ---------------------------------------------------------------------------
111
- // Signal reading helpers
112
- // ---------------------------------------------------------------------------
113
-
114
- function readPendingSignals(reader?: () => ImprovementSignalRecord[]): ImprovementSignalRecord[] {
115
- const _read =
116
- reader ??
117
- (() => {
118
- const db = getDb();
119
- return queryImprovementSignals(db, false) as ImprovementSignalRecord[];
120
- });
121
- try {
122
- return _read().filter((s) => !s.consumed);
123
- } catch {
124
- return [];
125
- }
126
- }
127
-
128
- export function groupSignalsBySkill(signals: ImprovementSignalRecord[]): Map<string, number> {
129
- const map = new Map<string, number>();
130
- for (const s of signals) {
131
- if (s.mentioned_skill) {
132
- const key = s.mentioned_skill.toLowerCase();
133
- map.set(key, (map.get(key) ?? 0) + 1);
134
- }
135
- }
136
- return map;
137
- }
138
-
139
- export function markSignalsConsumed(signals: ImprovementSignalRecord[], runId: string): void {
140
- try {
141
- if (signals.length === 0) return;
142
- for (const signal of signals) {
143
- const ok = updateSignalConsumed(signal.session_id, signal.query, signal.signal_type, runId);
144
- if (!ok) {
145
- console.error(
146
- `[orchestrate] failed to mark signal consumed: session_id=${signal.session_id}, signal_type=${signal.signal_type}`,
147
- );
148
- }
149
- }
150
- } catch {
151
- // Silent on errors
152
- }
153
- }
60
+ discoverWorkflowSkillProposals,
61
+ persistWorkflowSkillProposal,
62
+ type WorkflowSkillProposal,
63
+ } from "./workflows/proposals.js";
64
+
65
+ export { acquireLock, releaseLock } from "./orchestrate/locks.js";
66
+ export {
67
+ DEFAULT_COOLDOWN_HOURS,
68
+ MIN_CANDIDATE_EVIDENCE,
69
+ selectCandidates,
70
+ } from "./orchestrate/plan.js";
71
+ export { autoGradeTopUngraded, detectCrossSkillOverlap } from "./orchestrate/prepare.js";
72
+ export { formatOrchestrateReport } from "./orchestrate/report.js";
73
+ export { groupSignalsBySkill, markSignalsConsumed } from "./orchestrate/signals.js";
154
74
 
155
75
  // ---------------------------------------------------------------------------
156
76
  // Types
@@ -181,11 +101,24 @@ export interface SkillAction {
181
101
  watchResult?: WatchResult;
182
102
  }
183
103
 
104
+ /** Context for candidate selection beyond simple status checks. */
105
+ export interface CandidateContext {
106
+ skillFilter?: string;
107
+ maxSkills: number;
108
+ auditEntries?: EvolutionAuditEntry[];
109
+ /** Hours since last deploy before a skill can be re-evolved. */
110
+ cooldownHours?: number;
111
+ /** Skill name (lowercase) to improvement signal count. */
112
+ signaledSkills?: Map<string, number>;
113
+ }
114
+
184
115
  export interface OrchestrateResult {
185
116
  syncResult: SyncResult;
186
117
  statusResult: StatusResult;
187
118
  candidates: SkillAction[];
119
+ workflowProposals: WorkflowSkillProposal[];
188
120
  uploadSummary?: UploadCycleSummary;
121
+ contributionRelaySummary?: { attempted: number; sent: number; failed: number };
189
122
  summary: {
190
123
  totalSkills: number;
191
124
  evaluated: number;
@@ -194,190 +127,13 @@ export interface OrchestrateResult {
194
127
  watched: number;
195
128
  skipped: number;
196
129
  autoGraded: number;
130
+ freshlyWatchedSkills: string[];
197
131
  dryRun: boolean;
198
132
  approvalMode: "auto" | "review";
199
133
  elapsedMs: number;
200
134
  };
201
135
  }
202
136
 
203
- // ---------------------------------------------------------------------------
204
- // Human-readable decision report
205
- // ---------------------------------------------------------------------------
206
-
207
- function formatSyncPhase(syncResult: SyncResult): string[] {
208
- const lines: string[] = ["Phase 1: Sync"];
209
- const sources: [string, keyof SyncResult["sources"]][] = [
210
- ["Claude", "claude"],
211
- ["Codex", "codex"],
212
- ["OpenCode", "opencode"],
213
- ["OpenClaw", "openclaw"],
214
- ];
215
-
216
- for (const [label, key] of sources) {
217
- const s = syncResult.sources[key];
218
- if (!s.available) {
219
- lines.push(` ${label.padEnd(12)}not available`);
220
- } else if (s.synced > 0) {
221
- lines.push(` ${label.padEnd(12)}scanned ${s.scanned}, synced ${s.synced}`);
222
- } else {
223
- lines.push(` ${label.padEnd(12)}scanned ${s.scanned}, up to date`);
224
- }
225
- }
226
-
227
- if (syncResult.repair.ran && syncResult.repair.repaired_records > 0) {
228
- lines.push(
229
- ` Repair ${syncResult.repair.repaired_records} records across ${syncResult.repair.repaired_sessions} sessions`,
230
- );
231
- }
232
-
233
- return lines;
234
- }
235
-
236
- function formatStatusPhase(statusResult: StatusResult): string[] {
237
- const lines: string[] = ["Phase 2: Status"];
238
- const byStatus: Record<string, number> = {};
239
- for (const skill of statusResult.skills) {
240
- byStatus[skill.status] = (byStatus[skill.status] ?? 0) + 1;
241
- }
242
- const healthLabel = statusResult.system.healthy ? "healthy" : "UNHEALTHY";
243
- lines.push(` ${statusResult.skills.length} skills found, system ${healthLabel}`);
244
-
245
- const parts: string[] = [];
246
- for (const s of ["CRITICAL", "WARNING", "HEALTHY", "UNGRADED", "UNKNOWN"]) {
247
- if (byStatus[s]) parts.push(`${byStatus[s]} ${s}`);
248
- }
249
- if (parts.length > 0) lines.push(` ${parts.join(", ")}`);
250
-
251
- return lines;
252
- }
253
-
254
- function formatDecisionPhase(candidates: SkillAction[]): string[] {
255
- const lines: string[] = ["Phase 3: Skill Decisions"];
256
- if (candidates.length === 0) {
257
- lines.push(" (no skills to evaluate)");
258
- return lines;
259
- }
260
-
261
- for (const c of candidates) {
262
- const icon = c.action === "skip" ? "⊘" : c.action === "watch" ? "○" : "→";
263
- const actionLabel = c.action.toUpperCase().padEnd(7);
264
- lines.push(` ${icon} ${c.skill.padEnd(20)} ${actionLabel} ${c.reason}`);
265
- }
266
-
267
- return lines;
268
- }
269
-
270
- function formatEvolutionPhase(candidates: SkillAction[]): string[] {
271
- const evolved = candidates.filter((c) => c.action === "evolve" && c.evolveResult !== undefined);
272
- if (evolved.length === 0) return [];
273
-
274
- const lines: string[] = ["Phase 4: Evolution Results"];
275
- for (const c of evolved) {
276
- const r = c.evolveResult as NonNullable<typeof c.evolveResult>;
277
- const status = r.deployed ? "deployed" : "not deployed";
278
- const detail = r.reason;
279
- const validation = r.validation
280
- ? ` (${(r.validation.before_pass_rate * 100).toFixed(0)}% → ${(r.validation.after_pass_rate * 100).toFixed(0)}%)`
281
- : "";
282
- lines.push(` ${c.skill.padEnd(20)} ${status}${validation}`);
283
- lines.push(` ${"".padEnd(20)} ${detail}`);
284
- }
285
-
286
- return lines;
287
- }
288
-
289
- function formatWatchPhase(candidates: SkillAction[]): string[] {
290
- const watched = candidates.filter((c) => c.action === "watch");
291
- if (watched.length === 0) return [];
292
-
293
- const lines: string[] = ["Phase 5: Watch"];
294
- for (const c of watched) {
295
- const snap = c.watchResult?.snapshot;
296
- const metrics = snap
297
- ? ` (pass_rate=${snap.pass_rate.toFixed(2)}, baseline=${snap.baseline_pass_rate.toFixed(2)})`
298
- : "";
299
- const alertTag = c.watchResult?.alert ? " [ALERT]" : "";
300
- const rollbackTag = c.watchResult?.rolledBack ? " [ROLLED BACK]" : "";
301
- lines.push(` ${c.skill.padEnd(20)} ${c.reason}${alertTag}${rollbackTag}${metrics}`);
302
- }
303
-
304
- return lines;
305
- }
306
-
307
- export function formatOrchestrateReport(result: OrchestrateResult): string {
308
- const sep = "═".repeat(48);
309
- const lines: string[] = [];
310
-
311
- lines.push(sep);
312
- lines.push("selftune orchestrate — decision report");
313
- lines.push(sep);
314
- lines.push("");
315
-
316
- // Mode banner
317
- if (result.summary.dryRun) {
318
- lines.push("Mode: DRY RUN (no mutations applied)");
319
- } else if (result.summary.approvalMode === "review") {
320
- lines.push("Mode: REVIEW (proposals validated but not deployed)");
321
- } else {
322
- lines.push("Mode: AUTONOMOUS (validated changes deployed automatically)");
323
- }
324
- lines.push("");
325
-
326
- // Phase 1: Sync
327
- lines.push(...formatSyncPhase(result.syncResult));
328
- lines.push("");
329
-
330
- // Phase 2: Status
331
- lines.push(...formatStatusPhase(result.statusResult));
332
- lines.push("");
333
-
334
- // Phase 3: Skill decisions
335
- lines.push(...formatDecisionPhase(result.candidates));
336
- lines.push("");
337
-
338
- // Phase 4: Evolution results (only if any evolve ran)
339
- const evoLines = formatEvolutionPhase(result.candidates);
340
- if (evoLines.length > 0) {
341
- lines.push(...evoLines);
342
- lines.push("");
343
- }
344
-
345
- // Phase 5: Watch (only if any watched)
346
- const watchLines = formatWatchPhase(result.candidates);
347
- if (watchLines.length > 0) {
348
- lines.push(...watchLines);
349
- lines.push("");
350
- }
351
-
352
- // Final summary
353
- lines.push("Summary");
354
- lines.push(` Auto-graded: ${result.summary.autoGraded}`);
355
- lines.push(` Evaluated: ${result.summary.evaluated} skills`);
356
- lines.push(` Deployed: ${result.summary.deployed}`);
357
- lines.push(` Watched: ${result.summary.watched}`);
358
- lines.push(` Skipped: ${result.summary.skipped}`);
359
- lines.push(` Elapsed: ${(result.summary.elapsedMs / 1000).toFixed(1)}s`);
360
-
361
- if (result.summary.dryRun && result.summary.evaluated > 0) {
362
- lines.push("");
363
- lines.push(" Rerun without --dry-run to allow validated deployments.");
364
- } else if (result.summary.approvalMode === "review" && result.summary.evaluated > 0) {
365
- lines.push("");
366
- lines.push(" Rerun without --review-required to allow validated deployments.");
367
- }
368
-
369
- return lines.join("\n");
370
- }
371
-
372
- /** Candidate selection criteria. */
373
- const CANDIDATE_STATUSES = new Set(["CRITICAL", "WARNING", "UNGRADED"]);
374
-
375
- /** Minimum skill_checks before autonomous evolution is allowed. */
376
- export const MIN_CANDIDATE_EVIDENCE = 3;
377
-
378
- /** Default cooldown hours after a deploy before re-evolving the same skill. */
379
- export const DEFAULT_COOLDOWN_HOURS = 24;
380
-
381
137
  type AutonomousEvolveDefaults = Pick<
382
138
  EvolveOptions,
383
139
  | "paretoEnabled"
@@ -405,15 +161,6 @@ const AUTONOMOUS_EVOLVE_DEFAULTS: AutonomousEvolveDefaults = {
405
161
  proposalModel: "haiku",
406
162
  };
407
163
 
408
- function candidatePriority(skill: SkillStatus, signalCount = 0): number {
409
- const statusWeight = skill.status === "CRITICAL" ? 300 : skill.status === "WARNING" ? 200 : 100;
410
- const missedWeight = Math.min(skill.missedQueries, 50);
411
- const passPenalty = skill.passRate === null ? 0 : Math.round((1 - skill.passRate) * 100);
412
- const trendBoost = skill.trend === "down" ? 30 : 0;
413
- const signalBoost = Math.min(signalCount * 150, 450);
414
- return statusWeight + missedWeight + passPenalty + trendBoost + signalBoost;
415
- }
416
-
417
164
  /**
418
165
  * Injectable dependencies for orchestrate(). Pass overrides in tests.
419
166
  */
@@ -422,7 +169,7 @@ export interface OrchestrateDeps {
422
169
  computeStatus?: typeof computeStatus;
423
170
  evolve?: typeof import("./evolution/evolve.js").evolve;
424
171
  watch?: typeof import("./monitoring/watch.js").watch;
425
- detectAgent?: typeof detectAgent;
172
+ detectAgent?: typeof detectLlmAgent;
426
173
  doctor?: typeof doctor;
427
174
  readTelemetry?: () => SessionTelemetryRecord[];
428
175
  readSkillRecords?: () => SkillUsageRecord[];
@@ -432,341 +179,9 @@ export interface OrchestrateDeps {
432
179
  readGradingResults?: (skillName: string) => ReturnType<typeof readGradingResultsForSkill>;
433
180
  readSignals?: () => ImprovementSignalRecord[];
434
181
  readAlphaIdentity?: () => AlphaIdentity | null;
435
- }
436
-
437
- // ---------------------------------------------------------------------------
438
- // Skill path resolution
439
- // ---------------------------------------------------------------------------
440
-
441
- function getSkillSearchDirs(): string[] {
442
- const home = homedir();
443
- const cwd = process.cwd();
444
- return [
445
- join(home, ".claude", "skills"),
446
- join(home, ".agents", "skills"),
447
- join(home, ".codex", "skills"),
448
- ...findRepositorySkillDirs(cwd),
449
- ...findRepositoryClaudeSkillDirs(cwd),
450
- ];
451
- }
452
-
453
- function defaultResolveSkillPath(skillName: string): string | undefined {
454
- return findInstalledSkillPath(skillName, getSkillSearchDirs());
455
- }
456
-
457
- // ---------------------------------------------------------------------------
458
- // Cross-skill eval set overlap detection (internal — exported for testing only)
459
- // ---------------------------------------------------------------------------
460
-
461
- /**
462
- * Detects significant overlap between the positive eval sets of evolution
463
- * candidates. When two skills share >30% of their positive queries, it
464
- * suggests a routing boundary problem. Console-only — no persistence.
465
- *
466
- * @internal Exported solely for unit testing.
467
- */
468
- export async function detectCrossSkillOverlap(
469
- candidates: Array<{ skill: string }>,
470
- skillRecords: SkillUsageRecord[],
471
- queryRecords: QueryLogRecord[],
472
- ): Promise<
473
- Array<{ skill_a: string; skill_b: string; overlap_pct: number; shared_queries: string[] }>
474
- > {
475
- if (candidates.length < 2) return [];
476
-
477
- const { buildEvalSet } = await import("./eval/hooks-to-evals.js");
478
-
479
- const evalSets = new Map<string, Set<string>>();
480
-
481
- for (const c of candidates) {
482
- const evalSet = buildEvalSet(skillRecords, queryRecords, c.skill);
483
- const positives = new Set(
484
- evalSet
485
- .filter((e: { should_trigger: boolean }) => e.should_trigger)
486
- .map((e: { query: string }) => e.query.toLowerCase()),
487
- );
488
- evalSets.set(c.skill, positives);
489
- }
490
-
491
- const overlaps: Array<{
492
- skill_a: string;
493
- skill_b: string;
494
- overlap_pct: number;
495
- shared_queries: string[];
496
- }> = [];
497
- const skillNames = [...evalSets.keys()];
498
-
499
- for (let i = 0; i < skillNames.length; i++) {
500
- for (let j = i + 1; j < skillNames.length; j++) {
501
- const setA = evalSets.get(skillNames[i]);
502
- const setB = evalSets.get(skillNames[j]);
503
- if (!setA || !setB) continue;
504
-
505
- if (setA.size === 0 || setB.size === 0) continue;
506
-
507
- const shared: string[] = [];
508
- for (const q of setA) {
509
- if (setB.has(q)) shared.push(q);
510
- }
511
-
512
- const overlapPct = shared.length / Math.min(setA.size, setB.size);
513
-
514
- if (overlapPct > 0.3) {
515
- overlaps.push({
516
- skill_a: skillNames[i],
517
- skill_b: skillNames[j],
518
- overlap_pct: overlapPct,
519
- shared_queries: shared.slice(0, 10),
520
- });
521
- }
522
- }
523
- }
524
-
525
- return overlaps;
526
- }
527
-
528
- // ---------------------------------------------------------------------------
529
- // Candidate selection
530
- // ---------------------------------------------------------------------------
531
-
532
- /** Context for candidate selection beyond simple status checks. */
533
- export interface CandidateContext {
534
- skillFilter?: string;
535
- maxSkills: number;
536
- auditEntries?: EvolutionAuditEntry[];
537
- /** Hours since last deploy before a skill can be re-evolved. */
538
- cooldownHours?: number;
539
- /** Skill name (lowercase) to improvement signal count. */
540
- signaledSkills?: Map<string, number>;
541
- }
542
-
543
- export function selectCandidates(skills: SkillStatus[], options: CandidateContext): SkillAction[] {
544
- const actions: SkillAction[] = [];
545
- const orderedSkills = [...skills].sort((a, b) => {
546
- const aSignals = options.signaledSkills?.get(a.name.toLowerCase()) ?? 0;
547
- const bSignals = options.signaledSkills?.get(b.name.toLowerCase()) ?? 0;
548
- return candidatePriority(b, bSignals) - candidatePriority(a, aSignals);
549
- });
550
-
551
- const cooldownHours = options.cooldownHours ?? DEFAULT_COOLDOWN_HOURS;
552
- const recentlyDeployed = findRecentlyDeployedSkills(options.auditEntries ?? [], cooldownHours);
553
-
554
- for (const skill of orderedSkills) {
555
- const signalCount = options.signaledSkills?.get(skill.name.toLowerCase()) ?? 0;
556
-
557
- // Apply skill filter
558
- if (options.skillFilter && skill.name !== options.skillFilter) {
559
- actions.push({
560
- skill: skill.name,
561
- action: "skip",
562
- reason: `filtered out (--skill ${options.skillFilter})`,
563
- });
564
- continue;
565
- }
566
-
567
- // Check if skill is a candidate
568
- if (!CANDIDATE_STATUSES.has(skill.status)) {
569
- actions.push({
570
- skill: skill.name,
571
- action: "skip",
572
- reason: `status=${skill.status} — no action needed`,
573
- });
574
- continue;
575
- }
576
-
577
- // Gate: cooldown — skip if this skill was deployed recently
578
- if (recentlyDeployed.has(skill.name)) {
579
- actions.push({
580
- skill: skill.name,
581
- action: "skip",
582
- reason: `recently evolved (cooldown ${cooldownHours}h) — let it bake`,
583
- });
584
- continue;
585
- }
586
-
587
- // Gate: insufficient evidence — need enough data points for autonomous action
588
- // Bypass if there are improvement signals for this skill
589
- const skillChecks = skill.snapshot?.skill_checks ?? 0;
590
- if (skillChecks < MIN_CANDIDATE_EVIDENCE && skill.status !== "UNGRADED" && signalCount === 0) {
591
- actions.push({
592
- skill: skill.name,
593
- action: "skip",
594
- reason: `insufficient evidence (${skillChecks}/${MIN_CANDIDATE_EVIDENCE} checks) — need more data`,
595
- });
596
- continue;
597
- }
598
-
599
- // UNGRADED: only evolve if there are missed queries (some signal)
600
- // Bypass if there are improvement signals for this skill
601
- if (skill.status === "UNGRADED" && skill.missedQueries === 0 && signalCount === 0) {
602
- actions.push({
603
- skill: skill.name,
604
- action: "skip",
605
- reason: "UNGRADED with 0 missed queries — insufficient signal",
606
- });
607
- continue;
608
- }
609
-
610
- // Gate: weak WARNING signal — skip if no missed queries and trend isn't declining
611
- if (skill.status === "WARNING" && skill.missedQueries === 0 && skill.trend !== "down") {
612
- actions.push({
613
- skill: skill.name,
614
- action: "skip",
615
- reason: `WARNING but no missed queries and trend=${skill.trend} — weak signal`,
616
- });
617
- continue;
618
- }
619
-
620
- actions.push({
621
- skill: skill.name,
622
- action: "evolve",
623
- reason: `status=${skill.status}, passRate=${skill.passRate !== null ? `${(skill.passRate * 100).toFixed(0)}%` : "—"}, missed=${skill.missedQueries}, trend=${skill.trend}`,
624
- });
625
- }
626
-
627
- // Apply max-skills cap to evolve candidates only
628
- let evolveCount = 0;
629
- for (const action of actions) {
630
- if (action.action === "evolve") {
631
- evolveCount++;
632
- if (evolveCount > options.maxSkills) {
633
- action.action = "skip";
634
- action.reason = `capped by --max-skills ${options.maxSkills}`;
635
- }
636
- }
637
- }
638
-
639
- return actions;
640
- }
641
-
642
- /**
643
- * Find skills deployed within the given window.
644
- * Used for both cooldown gating (don't re-evolve) and watch targeting
645
- * (monitor recently deployed skills for regressions).
646
- */
647
- function findRecentlyDeployedSkills(
648
- auditEntries: EvolutionAuditEntry[],
649
- windowHours: number,
650
- ): Set<string> {
651
- const cutoffMs = Date.now() - windowHours * 60 * 60 * 1000;
652
- const names = new Set<string>();
653
- for (const entry of auditEntries) {
654
- const deployedAtMs = Date.parse(entry.timestamp);
655
- if (
656
- entry.action === "deployed" &&
657
- entry.skill_name &&
658
- Number.isFinite(deployedAtMs) &&
659
- deployedAtMs >= cutoffMs
660
- ) {
661
- names.add(entry.skill_name);
662
- }
663
- }
664
- return names;
665
- }
666
-
667
- // ---------------------------------------------------------------------------
668
- // Auto-grade ungraded skills
669
- // ---------------------------------------------------------------------------
670
-
671
- /**
672
- * Auto-grade the top ungraded skills that have some session data.
673
- * Fail-open: individual grading errors are logged but never propagated.
674
- *
675
- * @returns Number of skills successfully graded.
676
- */
677
- export async function autoGradeTopUngraded(
678
- skills: SkillStatus[],
679
- maxAutoGrade: number,
680
- agent: string,
681
- deps: {
682
- readTelemetry: () => SessionTelemetryRecord[];
683
- readSkillRecords: () => SkillUsageRecord[];
684
- },
685
- ): Promise<number> {
686
- // Filter: UNGRADED skills with some data (skill_checks > 0)
687
- const ungradedWithData = skills
688
- .filter((s) => s.status === "UNGRADED" && (s.snapshot?.skill_checks ?? 0) > 0)
689
- .sort((a, b) => (b.snapshot?.skill_checks ?? 0) - (a.snapshot?.skill_checks ?? 0))
690
- .slice(0, maxAutoGrade);
691
-
692
- if (ungradedWithData.length === 0) return 0;
693
-
694
- let graded = 0;
695
-
696
- for (const skill of ungradedWithData) {
697
- try {
698
- const telemetry = deps.readTelemetry();
699
- const skillUsage = deps.readSkillRecords();
700
-
701
- // Resolve the latest session for this skill
702
- const resolved = resolveLatestSessionForSkill(telemetry, skillUsage, skill.name);
703
- if (!resolved) {
704
- console.error(` [auto-grade] ${skill.name}: no session found, skipping`);
705
- continue;
706
- }
707
-
708
- // Derive expectations from SKILL.md
709
- const derived = deriveExpectationsFromSkill(skill.name);
710
- let transcriptExcerpt = "(no transcript)";
711
- if (resolved.transcriptPath) {
712
- try {
713
- transcriptExcerpt = readExcerpt(resolved.transcriptPath);
714
- } catch {
715
- transcriptExcerpt = "(no transcript)";
716
- }
717
- }
718
-
719
- console.error(` [auto-grade] Grading "${skill.name}" (session ${resolved.sessionId})...`);
720
-
721
- const result = await gradeSession({
722
- expectations: derived.expectations,
723
- telemetry: resolved.telemetry,
724
- sessionId: resolved.sessionId,
725
- skillName: skill.name,
726
- transcriptExcerpt,
727
- transcriptPath: resolved.transcriptPath,
728
- agent,
729
- });
730
-
731
- // Persist to SQLite — only count as graded if DB write succeeds
732
- let persisted = false;
733
- try {
734
- persisted = writeGradingResultToDb(result);
735
- } catch {
736
- persisted = false;
737
- }
738
- if (!persisted) {
739
- console.error(` [auto-grade] ${skill.name}: graded but failed to persist result`);
740
- continue;
741
- }
742
-
743
- // Persist to file (fail-open, supplementary)
744
- try {
745
- const basePath = buildDefaultGradingOutputPath(resolved.sessionId);
746
- const safeName = skill.name.replace(/[^a-zA-Z0-9_-]/g, "_");
747
- const outputPath = basePath.replace(/\.json$/, `_${safeName}.json`);
748
- const outputDir = dirname(outputPath);
749
- mkdirSync(outputDir, { recursive: true });
750
- writeFileSync(outputPath, JSON.stringify(result, null, 2), "utf-8");
751
- } catch {
752
- // fail-open: DB is authoritative, file is supplementary
753
- }
754
-
755
- const passRate = result.summary.pass_rate;
756
- console.error(
757
- ` [auto-grade] ${skill.name}: ${result.summary.passed}/${result.summary.total} passed (${Math.round(passRate * 100)}%)`,
758
- );
759
- graded++;
760
- } catch (err) {
761
- const msg = err instanceof Error ? err.message : String(err);
762
- console.error(
763
- ` [auto-grade] ${skill.name}: error — ${msg}. Retry with: selftune grade ${skill.name}`,
764
- );
765
- // fail-open: continue to next skill
766
- }
767
- }
768
-
769
- return graded;
182
+ discoverWorkflowSkillProposals?: typeof discoverWorkflowSkillProposals;
183
+ persistWorkflowSkillProposal?: typeof persistWorkflowSkillProposal;
184
+ buildReplayOptions?: typeof buildReplayValidationOptions;
770
185
  }
771
186
 
772
187
  // ---------------------------------------------------------------------------
@@ -777,6 +192,8 @@ export async function orchestrate(
777
192
  options: OrchestrateOptions,
778
193
  deps: OrchestrateDeps = {},
779
194
  ): Promise<OrchestrateResult> {
195
+ const startTime = Date.now();
196
+
780
197
  if (!acquireLock()) {
781
198
  // Another orchestrate run is in progress
782
199
  console.error("[orchestrate] Another run is in progress (lock held). Exiting.");
@@ -789,6 +206,7 @@ export async function orchestrate(
789
206
  codex: { available: false, scanned: 0, synced: 0, skipped: 0 },
790
207
  opencode: { available: false, scanned: 0, synced: 0, skipped: 0 },
791
208
  openclaw: { available: false, scanned: 0, synced: 0, skipped: 0 },
209
+ pi: { available: false, scanned: 0, synced: 0, skipped: 0 },
792
210
  },
793
211
  repair: {
794
212
  ran: false,
@@ -796,6 +214,12 @@ export async function orchestrate(
796
214
  repaired_records: 0,
797
215
  codex_repaired_records: 0,
798
216
  },
217
+ creator_contributions: {
218
+ ran: false,
219
+ eligible_skills: 0,
220
+ built_signals: 0,
221
+ staged_signals: 0,
222
+ },
799
223
  timings: [],
800
224
  total_elapsed_ms: 0,
801
225
  },
@@ -807,6 +231,7 @@ export async function orchestrate(
807
231
  system: { healthy: true, pass: 0, fail: 0, warn: 0 },
808
232
  },
809
233
  candidates: [],
234
+ workflowProposals: [],
810
235
  summary: {
811
236
  totalSkills: 0,
812
237
  evaluated: 0,
@@ -815,6 +240,7 @@ export async function orchestrate(
815
240
  watched: 0,
816
241
  skipped: 0,
817
242
  autoGraded: 0,
243
+ freshlyWatchedSkills: [],
818
244
  dryRun: options.dryRun,
819
245
  approvalMode: options.approvalMode,
820
246
  elapsedMs: 0,
@@ -823,420 +249,103 @@ export async function orchestrate(
823
249
  }
824
250
 
825
251
  try {
826
- const startTime = Date.now();
827
-
828
- const _syncSources = deps.syncSources ?? syncSources;
829
- const _computeStatus = deps.computeStatus ?? computeStatus;
830
- const _detectAgent = deps.detectAgent ?? detectAgent;
831
- const _doctor = deps.doctor ?? doctor;
832
- const _readTelemetry =
833
- deps.readTelemetry ??
834
- (() => {
835
- const db = getDb();
836
- return querySessionTelemetry(db) as SessionTelemetryRecord[];
837
- });
838
- const _readSkillRecords =
839
- deps.readSkillRecords ??
840
- (() => {
841
- const db = getDb();
842
- return querySkillUsageRecords(db) as SkillUsageRecord[];
843
- });
844
- const _readQueryRecords =
845
- deps.readQueryRecords ??
846
- (() => {
847
- const db = getDb();
848
- return queryQueryLog(db) as QueryLogRecord[];
849
- });
850
- const _readAuditEntries =
851
- deps.readAuditEntries ??
852
- (() => {
853
- const db = getDb();
854
- return queryEvolutionAudit(db) as EvolutionAuditEntry[];
855
- });
856
- const _resolveSkillPath = deps.resolveSkillPath ?? defaultResolveSkillPath;
857
- const _readGradingResults = deps.readGradingResults ?? readGradingResultsForSkill;
858
- const _readAlphaIdentity =
859
- deps.readAlphaIdentity ?? (() => readAlphaIdentity(SELFTUNE_CONFIG_PATH));
860
-
861
- // Lazy-load evolve and watch to avoid circular imports
862
- const _evolve = deps.evolve ?? (await import("./evolution/evolve.js")).evolve;
863
- const _watch = deps.watch ?? (await import("./monitoring/watch.js")).watch;
864
-
865
- // -------------------------------------------------------------------------
866
- // Step 1: Sync source-truth telemetry (mandatory)
867
- // -------------------------------------------------------------------------
868
- console.error("[orchestrate] Syncing source-truth telemetry...");
869
- const syncResult = _syncSources(createDefaultSyncOptions({ force: options.syncForce }));
870
- const sourceSynced = Object.values(syncResult.sources).reduce((sum, s) => sum + s.synced, 0);
871
- console.error(
872
- `[orchestrate] Sync complete: ${sourceSynced} sessions synced, ${syncResult.repair.repaired_records} repaired`,
873
- );
874
-
875
- // -------------------------------------------------------------------------
876
- // Step 2: Compute status
877
- // -------------------------------------------------------------------------
878
- console.error("[orchestrate] Computing skill status...");
879
- const telemetry = _readTelemetry();
880
- const skillRecords = _readSkillRecords();
881
- const queryRecords = _readQueryRecords();
882
- const auditEntries = _readAuditEntries();
883
- const doctorResult = await _doctor();
884
-
885
- let statusResult = _computeStatus(
252
+ const runtime = await resolveOrchestrateRuntime(deps);
253
+ const {
254
+ syncResult,
255
+ statusResult,
886
256
  telemetry,
887
257
  skillRecords,
888
- queryRecords,
889
- auditEntries,
890
- doctorResult,
891
- );
892
- console.error(
893
- `[orchestrate] Status: ${statusResult.skills.length} skills, system=${statusResult.system.healthy ? "healthy" : "unhealthy"}`,
894
- );
258
+ pendingSignals,
259
+ candidates,
260
+ evolveCandidates,
261
+ agent,
262
+ autoGradedCount,
263
+ } = await prepareOrchestrateRun(options, runtime);
895
264
 
896
265
  // -------------------------------------------------------------------------
897
- // Step 2a: Auto-grade ungraded skills with sufficient data
266
+ // Step 5: Evolve candidates
898
267
  // -------------------------------------------------------------------------
899
- let autoGradedCount = 0;
900
- const scopedSkills = options.skillFilter
901
- ? statusResult.skills.filter((s) => s.name === options.skillFilter)
902
- : statusResult.skills;
903
- const ungradedWithData = scopedSkills.filter(
904
- (s) => s.status === "UNGRADED" && (s.snapshot?.skill_checks ?? 0) > 0,
905
- );
906
-
907
- if (!options.dryRun && options.maxAutoGrade > 0 && ungradedWithData.length > 0) {
908
- const gradeAgent = _detectAgent();
909
- if (gradeAgent) {
910
- console.error(
911
- `[orchestrate] Auto-grading ${Math.min(ungradedWithData.length, options.maxAutoGrade)} ungraded skill(s)...`,
912
- );
913
- autoGradedCount = await autoGradeTopUngraded(
914
- scopedSkills,
915
- options.maxAutoGrade,
916
- gradeAgent,
917
- { readTelemetry: _readTelemetry, readSkillRecords: _readSkillRecords },
918
- );
919
-
920
- if (autoGradedCount > 0) {
921
- // Recompute status so candidate selection sees updated grades
922
- console.error(
923
- `[orchestrate] Recomputing status after grading ${autoGradedCount} skill(s)...`,
924
- );
925
- try {
926
- const freshTelemetry = _readTelemetry();
927
- const freshSkillRecords = _readSkillRecords();
928
- const freshQueryRecords = _readQueryRecords();
929
- const freshAudit = _readAuditEntries();
930
- const freshDoctor = doctorResult; // reuse — environment unchanged during grading
931
- statusResult = _computeStatus(
932
- freshTelemetry,
933
- freshSkillRecords,
934
- freshQueryRecords,
935
- freshAudit,
936
- freshDoctor,
937
- );
938
- } catch (recomputeErr) {
939
- console.error(
940
- `[orchestrate] Warning: failed to recompute status after grading — using pre-grade status. ${recomputeErr instanceof Error ? recomputeErr.message : String(recomputeErr)}`,
941
- );
942
- }
943
- }
944
- } else {
945
- console.error(
946
- "[orchestrate] No agent CLI found — skipping auto-grade. To disable, rerun with: selftune orchestrate --max-auto-grade 0",
947
- );
948
- }
949
- }
268
+ const freshlyDeployedInThisRun = await runEvolutionPhase({
269
+ evolveCandidates,
270
+ agent,
271
+ options,
272
+ resolveSkillPath: runtime.resolveSkillPath,
273
+ readGradingResults: runtime.readGradingResults,
274
+ evolve: runtime.evolve,
275
+ buildReplayOptions: runtime.buildReplayOptions,
276
+ evolveDefaults: AUTONOMOUS_EVOLVE_DEFAULTS,
277
+ });
950
278
 
951
279
  // -------------------------------------------------------------------------
952
- // Step 2b: Read pending improvement signals
280
+ // Step 5b: Auto-grade & write baselines for freshly deployed skills
953
281
  // -------------------------------------------------------------------------
954
- const pendingSignals = readPendingSignals(deps.readSignals);
955
- const signaledSkills = groupSignalsBySkill(pendingSignals);
956
- if (signaledSkills.size > 0) {
957
- console.error(
958
- `[orchestrate] Improvement signals: ${pendingSignals.length} pending for ${signaledSkills.size} skill(s)`,
959
- );
960
- }
282
+ await autoGradeFreshDeploys({
283
+ freshlyDeployedCandidates: freshlyDeployedInThisRun,
284
+ dryRun: options.dryRun,
285
+ agent,
286
+ detectAgent: runtime.detectAgent,
287
+ readTelemetry: runtime.readTelemetry,
288
+ readSkillRecords: runtime.readSkillRecords,
289
+ });
961
290
 
962
291
  // -------------------------------------------------------------------------
963
- // Step 3: Select candidates
292
+ // Step 6: Watch recently evolved skills (including freshly deployed in this run)
964
293
  // -------------------------------------------------------------------------
965
- const candidates = selectCandidates(statusResult.skills, {
294
+ const { freshAuditEntries, freshlyWatchedSkills } = await watchRecentDeploys({
295
+ candidates,
296
+ freshlyDeployedCandidates: freshlyDeployedInThisRun,
966
297
  skillFilter: options.skillFilter,
967
- maxSkills: options.maxSkills,
968
- auditEntries,
969
- signaledSkills,
298
+ recentWindowHours: options.recentWindowHours,
299
+ readAuditEntries: runtime.readAuditEntries,
300
+ resolveSkillPath: runtime.resolveSkillPath,
301
+ watch: runtime.watch,
970
302
  });
971
303
 
972
- const evolveCandidates = candidates.filter((c) => c.action === "evolve");
973
- const skipCount = candidates.filter((c) => c.action === "skip").length;
974
- console.error(
975
- `[orchestrate] Candidates: ${evolveCandidates.length} to evolve, ${skipCount} skipped`,
976
- );
977
-
978
- // Log each decision
979
- for (const c of candidates) {
980
- console.error(` ${c.action === "skip" ? "⊘" : "→"} ${c.skill}: ${c.reason}`);
981
- }
982
-
983
- // Cross-skill overlap detection (console-only, non-critical)
984
- if (evolveCandidates.length >= 2) {
985
- try {
986
- const overlap = await detectCrossSkillOverlap(evolveCandidates, skillRecords, queryRecords);
987
- if (overlap.length > 0) {
988
- console.error("\n[orchestrate] Cross-skill eval overlap detected:");
989
- for (const o of overlap) {
990
- console.error(
991
- ` ⚠ ${o.skill_a} ↔ ${o.skill_b}: ${(o.overlap_pct * 100).toFixed(0)}% shared queries (${o.shared_queries.length} queries)`,
992
- );
993
- }
994
- console.error("");
995
- }
996
- } catch {
997
- // fail-open: overlap detection is non-critical
998
- }
999
- }
1000
-
1001
304
  // -------------------------------------------------------------------------
1002
- // Step 4: Detect agent
305
+ // Step 6b: Generate workflow-skill proposals from strong telemetry patterns
1003
306
  // -------------------------------------------------------------------------
1004
- const agent = _detectAgent();
1005
- if (!agent && evolveCandidates.length > 0) {
1006
- console.error("[orchestrate] WARNING: No agent CLI found in PATH. Evolve will be skipped.");
1007
- for (const c of evolveCandidates) {
1008
- c.action = "skip";
1009
- c.reason = "no agent CLI available";
1010
- }
1011
- }
1012
-
1013
- // -------------------------------------------------------------------------
1014
- // Step 5: Evolve candidates
1015
- // -------------------------------------------------------------------------
1016
- for (const candidate of evolveCandidates) {
1017
- // Skip if agent detection marked this candidate as skip
1018
- if (candidate.action === "skip") continue;
1019
-
1020
- const skillPath = _resolveSkillPath(candidate.skill);
1021
- if (!skillPath) {
1022
- candidate.action = "skip";
1023
- candidate.reason = `SKILL.md not found for "${candidate.skill}"`;
1024
- console.error(` ⊘ ${candidate.skill}: ${candidate.reason}`);
1025
- continue;
1026
- }
307
+ const workflowProposals = runtime.discoverWorkflowSkillProposals(telemetry, skillRecords, {
308
+ cwd: process.cwd(),
309
+ skillFilter: options.skillFilter,
310
+ resolveSkillPath: runtime.resolveSkillPath,
311
+ existingAuditEntries: freshAuditEntries,
312
+ });
1027
313
 
1028
- const effectiveDryRun = options.dryRun || options.approvalMode === "review";
314
+ if (workflowProposals.length > 0) {
1029
315
  console.error(
1030
- `[orchestrate] Evolving "${candidate.skill}"${effectiveDryRun ? " (dry-run)" : ""}...`,
316
+ `[orchestrate] Workflow skill proposals: ${workflowProposals.length}${options.dryRun ? " (dry-run)" : ""}`,
1031
317
  );
1032
-
1033
- try {
1034
- const evolveResult = await _evolve({
1035
- skillName: candidate.skill,
1036
- skillPath,
1037
- agent: agent as string,
1038
- dryRun: effectiveDryRun,
1039
- confidenceThreshold: 0.6,
1040
- maxIterations: 3,
1041
- gradingResults: _readGradingResults(candidate.skill),
1042
- syncFirst: false, // We already synced
1043
- ...AUTONOMOUS_EVOLVE_DEFAULTS,
1044
- });
1045
-
1046
- candidate.evolveResult = evolveResult;
1047
-
1048
- if (evolveResult.deployed) {
1049
- console.error(` ✓ ${candidate.skill}: deployed (${evolveResult.reason})`);
1050
- } else {
1051
- console.error(` ✗ ${candidate.skill}: not deployed (${evolveResult.reason})`);
318
+ for (const proposal of workflowProposals) {
319
+ console.error(` + ${proposal.draft.skill_name}: ${proposal.summary}`);
320
+ if (!options.dryRun) {
321
+ runtime.persistWorkflowSkillProposal(proposal, {
322
+ sourceSkillPath: runtime.resolveSkillPath(proposal.source_skill_name),
323
+ });
1052
324
  }
1053
- } catch (err) {
1054
- const msg = err instanceof Error ? err.message : String(err);
1055
- candidate.action = "skip";
1056
- candidate.reason = `evolve error: ${msg}`;
1057
- console.error(` ✗ ${candidate.skill}: error — ${msg}`);
1058
- }
1059
- }
1060
-
1061
- // -------------------------------------------------------------------------
1062
- // Step 6: Watch recently evolved skills
1063
- // -------------------------------------------------------------------------
1064
- // Re-read audit entries to capture any newly-deployed entries from the evolve loop above.
1065
- // evolve() writes audit entries synchronously, so a fresh read is needed.
1066
- const freshAuditEntries = _readAuditEntries();
1067
- const recentlyEvolved = findRecentlyDeployedSkills(
1068
- freshAuditEntries,
1069
- options.recentWindowHours,
1070
- );
1071
-
1072
- // O(1) lookup for skills already processed as evolve candidates
1073
- const evolvedSkillNames = new Set(
1074
- candidates.filter((c) => c.action === "evolve").map((c) => c.skill),
1075
- );
1076
-
1077
- for (const skillName of recentlyEvolved) {
1078
- // Skip if already processed in this run as evolve candidate
1079
- if (evolvedSkillNames.has(skillName)) {
1080
- continue;
1081
- }
1082
-
1083
- // Apply skill filter
1084
- if (options.skillFilter && skillName !== options.skillFilter) continue;
1085
-
1086
- const skillPath = _resolveSkillPath(skillName);
1087
- if (!skillPath) continue;
1088
-
1089
- console.error(`[orchestrate] Watching "${skillName}" (recently evolved)...`);
1090
-
1091
- try {
1092
- const watchResult = await _watch({
1093
- skillName,
1094
- skillPath,
1095
- windowSessions: 20,
1096
- regressionThreshold: 0.1,
1097
- autoRollback: true,
1098
- syncFirst: false,
1099
- });
1100
-
1101
- candidates.push({
1102
- skill: skillName,
1103
- action: "watch",
1104
- reason: watchResult.alert ?? "stable",
1105
- watchResult,
1106
- });
1107
-
1108
- console.error(
1109
- ` ${watchResult.alert ? "⚠" : "✓"} ${skillName}: ${watchResult.recommendation}`,
1110
- );
1111
- } catch (err) {
1112
- const msg = err instanceof Error ? err.message : String(err);
1113
- console.error(` ✗ ${skillName}: watch error — ${msg}`);
1114
325
  }
1115
326
  }
1116
327
 
1117
328
  // -------------------------------------------------------------------------
1118
329
  // Step 7: Build summary (single source of truth for both CLI and dashboard)
1119
330
  // -------------------------------------------------------------------------
1120
- const finalTotals = {
1121
- totalSkills: statusResult.skills.length,
1122
- evaluated: candidates.filter((c) => c.action === "evolve").length,
1123
- evolved: candidates.filter((c) => c.action === "evolve" && c.evolveResult !== undefined)
1124
- .length,
1125
- deployed: candidates.filter((c) => c.evolveResult?.deployed).length,
1126
- watched: candidates.filter((c) => c.action === "watch").length,
1127
- skipped: candidates.filter((c) => c.action === "skip").length,
1128
- autoGraded: autoGradedCount,
1129
- };
1130
-
1131
- const result: OrchestrateResult = {
331
+ const result = finalizeOrchestrateRun({
1132
332
  syncResult,
1133
333
  statusResult,
1134
334
  candidates,
1135
- summary: {
1136
- ...finalTotals,
1137
- dryRun: options.dryRun,
1138
- approvalMode: options.approvalMode,
1139
- elapsedMs: Date.now() - startTime,
1140
- },
1141
- };
1142
-
1143
- // -------------------------------------------------------------------------
1144
- // Step 7b: Mark consumed signals
1145
- // -------------------------------------------------------------------------
1146
- const runId = `run_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1147
- if (pendingSignals.length > 0) {
1148
- markSignalsConsumed(pendingSignals, runId);
1149
- }
1150
-
1151
- // -------------------------------------------------------------------------
1152
- // Step 8: Persist run report
1153
- // -------------------------------------------------------------------------
1154
- const runReport: OrchestrateRunReport = {
1155
- run_id: runId,
1156
- timestamp: new Date().toISOString(),
1157
- elapsed_ms: result.summary.elapsedMs,
1158
- dry_run: result.summary.dryRun,
1159
- approval_mode: result.summary.approvalMode,
1160
- total_skills: finalTotals.totalSkills,
1161
- evaluated: finalTotals.evaluated,
1162
- evolved: finalTotals.evolved,
1163
- deployed: finalTotals.deployed,
1164
- watched: finalTotals.watched,
1165
- skipped: finalTotals.skipped,
1166
- auto_graded: finalTotals.autoGraded,
1167
- skill_actions: candidates.map(
1168
- (c): OrchestrateRunSkillAction => ({
1169
- skill: c.skill,
1170
- action: c.action,
1171
- reason: c.reason,
1172
- deployed: c.evolveResult?.deployed,
1173
- rolledBack: c.watchResult?.rolledBack,
1174
- alert: c.watchResult?.alert,
1175
- elapsed_ms: c.evolveResult?.elapsedMs,
1176
- llm_calls: c.evolveResult?.llmCallCount,
1177
- }),
1178
- ),
1179
- };
1180
-
1181
- try {
1182
- writeOrchestrateRunToDb(runReport);
1183
- } catch {
1184
- /* fail-open */
1185
- }
1186
-
1187
- // Also log to unified cron_runs timeline
1188
- const totalLlmCalls = candidates.reduce(
1189
- (sum, c) => sum + (c.evolveResult?.llmCallCount ?? 0),
1190
- 0,
1191
- );
1192
- try {
1193
- writeCronRunToDb(getDb(), {
1194
- jobName: "orchestrate",
1195
- startedAt: runReport.timestamp,
1196
- elapsedMs: runReport.elapsed_ms,
1197
- status: "success",
1198
- metrics: {
1199
- total_skills: finalTotals.totalSkills,
1200
- evaluated: finalTotals.evaluated,
1201
- evolved: finalTotals.evolved,
1202
- deployed: finalTotals.deployed,
1203
- watched: finalTotals.watched,
1204
- skipped: finalTotals.skipped,
1205
- dry_run: result.summary.dryRun,
1206
- total_llm_calls: totalLlmCalls,
1207
- auto_graded: finalTotals.autoGraded,
1208
- },
1209
- });
1210
- } catch {
1211
- /* fail-open */
1212
- }
335
+ workflowProposals,
336
+ dryRun: options.dryRun,
337
+ approvalMode: options.approvalMode,
338
+ autoGradedCount,
339
+ freshlyWatchedSkills,
340
+ pendingSignals,
341
+ elapsedMs: Date.now() - startTime,
342
+ });
1213
343
 
1214
- // -------------------------------------------------------------------------
1215
- // Step 9: Alpha upload (fail-open — never blocks the orchestrate loop)
1216
- // -------------------------------------------------------------------------
1217
- const alphaIdentity = _readAlphaIdentity();
1218
- if (alphaIdentity?.enrolled) {
1219
- try {
1220
- console.error("[orchestrate] Running alpha upload cycle...");
1221
- const { runUploadCycle } = await import("./alpha-upload/index.js");
1222
- const db = getDb();
1223
- const uploadSummary = await runUploadCycle(db, {
1224
- enrolled: true,
1225
- userId: alphaIdentity.user_id,
1226
- agentType: readConfiguredAgentType(SELFTUNE_CONFIG_PATH, "unknown"),
1227
- selftuneVersion: getSelftuneVersion(),
1228
- dryRun: options.dryRun,
1229
- apiKey: alphaIdentity.api_key,
1230
- });
1231
- result.uploadSummary = uploadSummary;
1232
- console.error(
1233
- `[orchestrate] Alpha upload: prepared=${uploadSummary.prepared}, sent=${uploadSummary.sent}, failed=${uploadSummary.failed}, skipped=${uploadSummary.skipped}`,
1234
- );
1235
- } catch (err) {
1236
- const msg = err instanceof Error ? err.message : String(err);
1237
- console.error(`[orchestrate] Alpha upload failed (non-blocking): ${msg}`);
1238
- }
1239
- }
344
+ await runPostOrchestrateSideEffects({
345
+ result,
346
+ dryRun: options.dryRun,
347
+ readAlphaIdentity: runtime.readAlphaIdentity,
348
+ });
1240
349
 
1241
350
  return result;
1242
351
  } catch (err) {
@@ -1264,113 +373,18 @@ export async function orchestrate(
1264
373
  // ---------------------------------------------------------------------------
1265
374
 
1266
375
  export async function cliMain(): Promise<void> {
1267
- const { values } = parseArgs({
1268
- options: {
1269
- "dry-run": { type: "boolean", default: false },
1270
- "review-required": { type: "boolean", default: false },
1271
- "auto-approve": { type: "boolean", default: false },
1272
- skill: { type: "string" },
1273
- "max-skills": { type: "string", default: "5" },
1274
- "recent-window": { type: "string", default: "48" },
1275
- "sync-force": { type: "boolean", default: false },
1276
- "max-auto-grade": { type: "string", default: "5" },
1277
- loop: { type: "boolean", default: false },
1278
- "loop-interval": { type: "string", default: "3600" },
1279
- help: { type: "boolean", short: "h", default: false },
1280
- },
1281
- strict: true,
1282
- });
1283
-
1284
- if (values.help) {
1285
- console.log(`selftune orchestrate — Autonomous core loop
1286
-
1287
- Runs the full improvement cycle: sync → status → auto-grade → evolve → watch.
1288
-
1289
- Usage:
1290
- selftune orchestrate [options]
1291
-
1292
- Options:
1293
- --dry-run Preview actions without mutations
1294
- --review-required Validate candidates but require human review before deploy
1295
- --auto-approve Deprecated alias; autonomous mode is now the default
1296
- --skill <name> Scope to a single skill
1297
- --max-skills <n> Cap skills processed per run (default: 5)
1298
- --recent-window <hrs> Hours to look back for watch targets (default: 48)
1299
- --sync-force Force full rescan during sync
1300
- --max-auto-grade <n> Max ungraded skills to auto-grade per run (default: 5, 0 to disable)
1301
- --loop Run in continuous loop mode (never stops)
1302
- --loop-interval <s> Seconds between iterations (default: 3600, min: 60)
1303
- -h, --help Show this help message
1304
-
1305
- Safety:
1306
- By default, low-risk description evolution runs autonomously after
1307
- validation. Use --review-required to keep a human in the loop, or
1308
- --dry-run to preview the whole loop without mutations. Every deploy
1309
- still passes validation gates first.
376
+ const cli = parseOrchestrateCliArgs();
1310
377
 
1311
- Examples:
1312
- selftune orchestrate # autonomous description evolution
1313
- selftune orchestrate --review-required # validate but do not deploy
1314
- selftune orchestrate --dry-run # preview only
1315
- selftune orchestrate --skill Research # single skill
1316
- selftune orchestrate --max-skills 3 # limit scope
1317
- selftune orchestrate --loop # continuous loop (hourly)
1318
- selftune orchestrate --loop --loop-interval 600 # every 10 minutes`);
378
+ if (cli.showHelp) {
379
+ console.log(renderOrchestrateHelp());
1319
380
  process.exit(0);
1320
381
  }
1321
382
 
1322
- const maxSkillsRaw = values["max-skills"] ?? "5";
1323
- if (!/^\d+$/.test(maxSkillsRaw) || Number(maxSkillsRaw) < 1) {
1324
- throw new CLIError(
1325
- "--max-skills must be a positive integer",
1326
- "INVALID_FLAG",
1327
- "selftune orchestrate --max-skills 5",
1328
- );
383
+ for (const warning of cli.warnings) {
384
+ console.error(warning);
1329
385
  }
1330
- const maxSkills = Number(maxSkillsRaw);
1331
386
 
1332
- const recentWindowRaw = values["recent-window"] ?? "48";
1333
- if (!/^\d+$/.test(recentWindowRaw) || Number(recentWindowRaw) < 1) {
1334
- throw new CLIError(
1335
- "--recent-window must be a positive integer",
1336
- "INVALID_FLAG",
1337
- "selftune orchestrate --recent-window 48",
1338
- );
1339
- }
1340
- const recentWindow = Number(recentWindowRaw);
1341
-
1342
- const maxAutoGradeRaw = values["max-auto-grade"] ?? "5";
1343
- if (!/^\d+$/.test(maxAutoGradeRaw)) {
1344
- throw new CLIError(
1345
- "--max-auto-grade must be a non-negative integer",
1346
- "INVALID_FLAG",
1347
- "selftune orchestrate --max-auto-grade 5",
1348
- );
1349
- }
1350
- const maxAutoGrade = Number(maxAutoGradeRaw);
1351
-
1352
- const loopIntervalRaw = values["loop-interval"] ?? "3600";
1353
- if (!/^\d+$/.test(loopIntervalRaw) || (values.loop && Number(loopIntervalRaw) < 60)) {
1354
- throw new CLIError(
1355
- "--loop-interval must be an integer >= 60 (seconds)",
1356
- "INVALID_FLAG",
1357
- "selftune orchestrate --loop --loop-interval 3600",
1358
- );
1359
- }
1360
- const loopInterval = Number(loopIntervalRaw);
1361
-
1362
- const autoApprove = values["auto-approve"] ?? false;
1363
- if (autoApprove) {
1364
- console.error(
1365
- "[orchestrate] --auto-approve is deprecated; autonomous mode is now the default.",
1366
- );
1367
- }
1368
-
1369
- const reviewRequired = values["review-required"] ?? false;
1370
- const dryRun = values["dry-run"] ?? false;
1371
- const approvalMode: "auto" | "review" = reviewRequired ? "review" : "auto";
1372
-
1373
- const isLoop = values.loop ?? false;
387
+ const isLoop = cli.loop;
1374
388
  let stopRequested = false;
1375
389
  let sleepTimer: ReturnType<typeof setTimeout> | null = null;
1376
390
  let sleepResolve: (() => void) | null = null;
@@ -1400,54 +414,17 @@ Examples:
1400
414
  }
1401
415
 
1402
416
  const result = await orchestrate({
1403
- dryRun,
1404
- approvalMode,
1405
- skillFilter: values.skill,
1406
- maxSkills,
1407
- recentWindowHours: recentWindow,
1408
- syncForce: values["sync-force"] ?? false,
1409
- maxAutoGrade,
417
+ ...cli.runOptions,
1410
418
  });
1411
419
 
1412
- // JSON output: include per-skill decisions for machine consumption
1413
- const jsonOutput = {
1414
- ...result.summary,
1415
- ...(result.uploadSummary ? { upload: result.uploadSummary } : {}),
1416
- decisions: result.candidates.map((c) => ({
1417
- skill: c.skill,
1418
- action: c.action,
1419
- reason: c.reason,
1420
- ...(c.evolveResult
1421
- ? {
1422
- deployed: c.evolveResult.deployed,
1423
- evolveReason: c.evolveResult.reason,
1424
- validation: c.evolveResult.validation
1425
- ? {
1426
- before: c.evolveResult.validation.before_pass_rate,
1427
- after: c.evolveResult.validation.after_pass_rate,
1428
- improved: c.evolveResult.validation.improved,
1429
- }
1430
- : null,
1431
- }
1432
- : {}),
1433
- ...(c.watchResult
1434
- ? {
1435
- alert: c.watchResult.alert,
1436
- rolledBack: c.watchResult.rolledBack,
1437
- passRate: c.watchResult.snapshot?.pass_rate ?? null,
1438
- recommendation: c.watchResult.recommendation,
1439
- }
1440
- : {}),
1441
- })),
1442
- };
1443
- console.log(JSON.stringify(jsonOutput, null, 2));
420
+ console.log(JSON.stringify(buildOrchestrateJsonOutput(result), null, 2));
1444
421
 
1445
422
  // Print human-readable decision report to stderr
1446
423
  console.error(`\n${formatOrchestrateReport(result)}`);
1447
424
 
1448
425
  if (!isLoop || stopRequested) break;
1449
426
 
1450
- const nextMinutes = Math.round(loopInterval / 60);
427
+ const nextMinutes = Math.round(cli.loopIntervalSeconds / 60);
1451
428
  console.error(`\n[orchestrate] Next cycle in ${nextMinutes} minute(s)... (Ctrl+C to stop)`);
1452
429
  await new Promise<void>((resolve) => {
1453
430
  sleepResolve = resolve;
@@ -1455,7 +432,7 @@ Examples:
1455
432
  sleepTimer = null;
1456
433
  sleepResolve = null;
1457
434
  resolve();
1458
- }, loopInterval * 1000);
435
+ }, cli.loopIntervalSeconds * 1000);
1459
436
  });
1460
437
  } while (isLoop && !stopRequested);
1461
438