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
@@ -121,7 +121,8 @@ function isSelfttuneHookEntry(entry: unknown): boolean {
121
121
 
122
122
  // Check direct command
123
123
  if (typeof obj.command === "string") {
124
- return SELFTUNE_HOOK_SCRIPTS.some((script) => obj.command?.includes(script));
124
+ const command = obj.command;
125
+ return SELFTUNE_HOOK_SCRIPTS.some((script) => command.includes(script));
125
126
  }
126
127
 
127
128
  // Check hooks array (the nested structure used in settings.json)
@@ -1,4 +1,4 @@
1
- import { existsSync, writeFileSync } from "node:fs";
1
+ import { existsSync } from "node:fs";
2
2
 
3
3
  import {
4
4
  type CanonicalPlatform,
@@ -37,11 +37,3 @@ export function serializeCanonicalRecords(records: CanonicalRecord[], pretty = f
37
37
  records.map((record) => JSON.stringify(record)).join("\n") + (records.length > 0 ? "\n" : "")
38
38
  );
39
39
  }
40
-
41
- export function writeCanonicalExport(
42
- records: CanonicalRecord[],
43
- outPath: string,
44
- pretty = false,
45
- ): void {
46
- writeFileSync(outPath, serializeCanonicalRecords(records, pretty), "utf-8");
47
- }
@@ -21,11 +21,20 @@ export type CLIErrorCode =
21
21
  | "MISSING_FLAG"
22
22
  | "CONFIG_MISSING"
23
23
  | "FILE_NOT_FOUND"
24
+ | "FILE_EXISTS"
24
25
  | "AGENT_NOT_FOUND"
25
26
  | "UNKNOWN_COMMAND"
26
27
  | "GUARD_BLOCKED"
27
28
  | "OPERATION_FAILED"
29
+ | "API_ERROR"
30
+ | "AUTH_MISSING"
31
+ | "BLEND_NO_LOGS"
32
+ | "INVALID_PROPOSAL"
33
+ | "INVALID_STATUS"
28
34
  | "MISSING_DATA"
35
+ | "NOT_FOUND"
36
+ | "REPLAY_UNAVAILABLE"
37
+ | "UNSUPPORTED_TYPE"
29
38
  | "INTERNAL_ERROR";
30
39
 
31
40
  export class CLIError extends Error {
@@ -2,7 +2,7 @@
2
2
  * Shared LLM call utility.
3
3
  *
4
4
  * Provides a unified interface for calling LLMs via agent subprocess
5
- * (claude/codex/opencode). Extracted from grade-session.ts so other
5
+ * (claude/codex/opencode/pi). Extracted from grade-session.ts so other
6
6
  * modules can reuse the same calling logic.
7
7
  */
8
8
 
@@ -14,6 +14,8 @@ import { AGENT_CANDIDATES } from "../constants.js";
14
14
  import { createLogger } from "./logging.js";
15
15
 
16
16
  const logger = createLogger("llm-call");
17
+ export const LLM_BACKED_AGENT_CANDIDATES = ["claude", "codex", "opencode", "pi"] as const;
18
+ export type LlmBackedAgent = (typeof LLM_BACKED_AGENT_CANDIDATES)[number];
17
19
 
18
20
  // ---------------------------------------------------------------------------
19
21
  // Model alias resolution
@@ -48,6 +50,17 @@ function resolveOpenCodeModel(flag: string): string {
48
50
  return OPENCODE_MODEL_MAP[flag] ?? flag;
49
51
  }
50
52
 
53
+ const PI_THINKING_MAP: Record<EffortLevel, string> = {
54
+ low: "low",
55
+ medium: "medium",
56
+ high: "high",
57
+ max: "xhigh",
58
+ };
59
+
60
+ function resolvePiThinking(effort: EffortLevel): string {
61
+ return PI_THINKING_MAP[effort];
62
+ }
63
+
51
64
  // ---------------------------------------------------------------------------
52
65
  // Bundled agent file loading (for codex inline prompt injection)
53
66
  // ---------------------------------------------------------------------------
@@ -79,6 +92,33 @@ export function detectAgent(): string | null {
79
92
  return null;
80
93
  }
81
94
 
95
+ /** Detect first available agent CLI that can execute selftune LLM-backed workflows. */
96
+ export function detectLlmAgent(): LlmBackedAgent | null {
97
+ for (const agent of LLM_BACKED_AGENT_CANDIDATES) {
98
+ if (Bun.which(agent)) return agent;
99
+ }
100
+ return null;
101
+ }
102
+
103
+ export function isLlmBackedAgent(value: string): value is LlmBackedAgent {
104
+ return (LLM_BACKED_AGENT_CANDIDATES as readonly string[]).includes(value);
105
+ }
106
+
107
+ function unsupportedAgentError(agent: string, capability: "llm calls" | "subagent calls"): Error {
108
+ const supported = LLM_BACKED_AGENT_CANDIDATES.join(", ");
109
+ if (agent === "openclaw") {
110
+ return new Error(
111
+ `Detected agent CLI '${agent}', but selftune ${capability} currently support only ${supported}. ` +
112
+ `LLM-backed judge, eval, and optimizer workflows are unavailable on ${agent}; ` +
113
+ `use Claude Code, Codex, OpenCode, or Pi for those workflows, or stay on ingest/sync support for ${agent}.`,
114
+ );
115
+ }
116
+
117
+ return new Error(
118
+ `Unknown agent '${agent}'. selftune ${capability} currently support only ${supported}.`,
119
+ );
120
+ }
121
+
82
122
  // ---------------------------------------------------------------------------
83
123
  // Markdown fence stripping
84
124
  // ---------------------------------------------------------------------------
@@ -160,7 +200,7 @@ function sleep(ms: number): Promise<void> {
160
200
  /** Effort level for Claude CLI (controls thinking depth). Opus 4.6 only for 'max'. */
161
201
  export type EffortLevel = "low" | "medium" | "high" | "max";
162
202
 
163
- /** Call LLM via agent subprocess (claude/codex/opencode). Returns raw text. */
203
+ /** Call LLM via agent subprocess (claude/codex/opencode/pi). Returns raw text. */
164
204
  export async function callViaAgent(
165
205
  systemPrompt: string,
166
206
  userPrompt: string,
@@ -194,8 +234,30 @@ export async function callViaAgent(
194
234
  cmd.push("--model", resolveOpenCodeModel(modelFlag));
195
235
  }
196
236
  cmd.push(promptContent);
237
+ } else if (agent === "pi") {
238
+ cmd = [
239
+ "pi",
240
+ "-p",
241
+ "--mode",
242
+ "text",
243
+ "--no-session",
244
+ "--no-tools",
245
+ "--no-extensions",
246
+ "--no-skills",
247
+ "--no-prompt-templates",
248
+ "--no-themes",
249
+ "--system-prompt",
250
+ systemPrompt,
251
+ ];
252
+ if (modelFlag) {
253
+ cmd.push("--model", modelFlag);
254
+ }
255
+ if (effort) {
256
+ cmd.push("--thinking", resolvePiThinking(effort));
257
+ }
258
+ cmd.push(userPrompt);
197
259
  } else {
198
- throw new Error(`Unknown agent: ${agent}`);
260
+ throw unsupportedAgentError(agent, "llm calls");
199
261
  }
200
262
 
201
263
  // Retry loop with exponential backoff for transient failures
@@ -256,6 +318,23 @@ export async function callViaAgent(
256
318
  }
257
319
  }
258
320
 
321
+ function mapAllowedToolsToPi(tools?: string[]): string[] {
322
+ if (!tools || tools.length === 0) return [];
323
+
324
+ const mapped = new Set<string>();
325
+ for (const tool of tools) {
326
+ if (tool === "Read") mapped.add("read");
327
+ else if (tool === "Write") mapped.add("write");
328
+ else if (tool === "Edit") mapped.add("edit");
329
+ else if (tool === "Bash") mapped.add("bash");
330
+ else if (tool === "Grep") mapped.add("grep");
331
+ else if (tool === "Glob" || tool === "Find") mapped.add("find");
332
+ else if (tool === "LS" || tool === "Ls") mapped.add("ls");
333
+ }
334
+
335
+ return [...mapped];
336
+ }
337
+
259
338
  // ---------------------------------------------------------------------------
260
339
  // Call LLM via named subagent (multi-turn, agentic)
261
340
  // ---------------------------------------------------------------------------
@@ -301,10 +380,10 @@ export async function callViaSubagent(options: SubagentCallOptions): Promise<str
301
380
  allowedTools,
302
381
  } = options;
303
382
 
304
- const agent = detectAgent();
305
- if (!agent || (agent !== "claude" && agent !== "opencode" && agent !== "codex")) {
383
+ const agent = detectLlmAgent();
384
+ if (!agent) {
306
385
  throw new Error(
307
- `Subagent calls require 'claude', 'opencode', or 'codex' CLI in PATH (detected: ${agent ?? "none"})`,
386
+ "Subagent calls require one of these CLIs in PATH: claude, codex, opencode, pi.",
308
387
  );
309
388
  }
310
389
 
@@ -333,6 +412,47 @@ export async function callViaSubagent(options: SubagentCallOptions): Promise<str
333
412
  const agentInstructions = loadAgentInstructions(agentName);
334
413
  const fullPrompt = agentInstructions ? `${agentInstructions}\n\n---\n\n${prompt}` : prompt;
335
414
  cmd = ["codex", "exec", "--skip-git-repo-check", fullPrompt];
415
+ } else if (agent === "pi") {
416
+ if (maxTurns !== 8) {
417
+ logger.warn(`Subagent '${agentName}' on pi: maxTurns is not supported and will be ignored`);
418
+ }
419
+ const agentInstructions = loadAgentInstructions(agentName);
420
+ const systemParts = [agentInstructions, appendSystemPrompt].filter((value): value is string =>
421
+ Boolean(value?.trim()),
422
+ );
423
+
424
+ cmd = [
425
+ "pi",
426
+ "-p",
427
+ "--mode",
428
+ "text",
429
+ "--no-session",
430
+ "--no-extensions",
431
+ "--no-skills",
432
+ "--no-prompt-templates",
433
+ "--no-themes",
434
+ ];
435
+
436
+ if (systemParts.length > 0) {
437
+ cmd.push("--system-prompt", systemParts.join("\n\n"));
438
+ }
439
+ if (modelFlag) {
440
+ cmd.push("--model", modelFlag);
441
+ }
442
+ if (effort) {
443
+ cmd.push("--thinking", resolvePiThinking(effort));
444
+ }
445
+
446
+ const piTools = mapAllowedToolsToPi(allowedTools);
447
+ if (allowedTools && allowedTools.length > 0) {
448
+ if (piTools.length > 0) {
449
+ cmd.push("--tools", piTools.join(","));
450
+ } else {
451
+ cmd.push("--no-tools");
452
+ }
453
+ }
454
+
455
+ cmd.push(prompt);
336
456
  } else {
337
457
  // Claude Code
338
458
  cmd = ["claude", "-p", prompt, "--agent", agentName, "--max-turns", String(maxTurns)];
@@ -341,6 +341,8 @@ export function extractSkillNamesFromPathReferences(
341
341
  const patterns = [
342
342
  /(?:^|[\s"'`])(?:[^"'`\s]*?\.agents\/skills\/)([^/\s"'`]+)(?=\/)/gi,
343
343
  /(?:^|[\s"'`])(?:[^"'`\s]*?\.codex\/skills\/(?:\.system\/)?)([^/\s"'`]+)(?=\/)/gi,
344
+ /(?:^|[\s"'`])(?:[^"'`\s]*?\.opencode\/skills\/)([^/\s"'`]+)(?=\/)/gi,
345
+ /(?:^|[\s"'`])(?:[^"'`\s]*?\.claude\/skills\/)([^/\s"'`]+)(?=\/)/gi,
344
346
  /(?:^|[\s"'`])(\/etc\/codex\/skills\/)([^/\s"'`]+)(?=\/)/gi,
345
347
  ];
346
348
 
@@ -0,0 +1,184 @@
1
+ /**
2
+ * proposals.ts
3
+ *
4
+ * Turns strong multi-skill workflow patterns into review-first new-skill
5
+ * proposals that can be surfaced locally and synced to the cloud.
6
+ */
7
+
8
+ import { createHash } from "node:crypto";
9
+
10
+ import { appendAuditEntry } from "../evolution/audit.js";
11
+ import { appendEvidenceEntry } from "../evolution/evidence.js";
12
+ import type {
13
+ DiscoveredWorkflow,
14
+ EvolutionAuditEntry,
15
+ EvolutionEvidenceEntry,
16
+ SessionTelemetryRecord,
17
+ SkillUsageRecord,
18
+ } from "../types.js";
19
+ import { discoverWorkflows } from "./discover.js";
20
+ import { buildWorkflowSkillDraft, type WorkflowSkillDraft } from "./skill-scaffold.js";
21
+
22
+ export interface WorkflowSkillProposal {
23
+ proposal_id: string;
24
+ source_skill_name: string;
25
+ workflow: DiscoveredWorkflow;
26
+ draft: WorkflowSkillDraft;
27
+ summary: string;
28
+ current_value: string;
29
+ proposed_value: string;
30
+ rationale: string;
31
+ confidence: number;
32
+ }
33
+
34
+ export interface WorkflowSkillProposalOptions {
35
+ cwd?: string;
36
+ skillFilter?: string;
37
+ maxProposals?: number;
38
+ minOccurrences?: number;
39
+ minSynergy?: number;
40
+ minConsistency?: number;
41
+ minCompletionRate?: number;
42
+ resolveSkillPath?: (skillName: string) => string | undefined;
43
+ existingAuditEntries?: EvolutionAuditEntry[];
44
+ }
45
+
46
+ export interface WorkflowSkillProposalPersistOptions {
47
+ now?: Date;
48
+ sourceSkillPath?: string;
49
+ appendAudit?: (entry: EvolutionAuditEntry) => void;
50
+ appendEvidence?: (entry: EvolutionEvidenceEntry) => void;
51
+ }
52
+
53
+ export const DEFAULT_WORKFLOW_PROPOSAL_MIN_OCCURRENCES = 3;
54
+ export const DEFAULT_WORKFLOW_PROPOSAL_MAX = 2;
55
+ export const DEFAULT_WORKFLOW_PROPOSAL_MIN_SYNERGY = 0;
56
+ export const DEFAULT_WORKFLOW_PROPOSAL_MIN_CONSISTENCY = 0.75;
57
+ export const DEFAULT_WORKFLOW_PROPOSAL_MIN_COMPLETION = 0.65;
58
+
59
+ function round2(value: number): number {
60
+ return Math.round(value * 100) / 100;
61
+ }
62
+
63
+ function clamp01(value: number): number {
64
+ return Math.max(0, Math.min(1, value));
65
+ }
66
+
67
+ function buildWorkflowProposalConfidence(workflow: DiscoveredWorkflow): number {
68
+ const normalizedSynergy = clamp01((workflow.synergy_score + 1) / 2);
69
+ const occurrenceBoost = clamp01(workflow.occurrence_count / 6);
70
+ return round2(
71
+ normalizedSynergy * 0.4 +
72
+ workflow.sequence_consistency * 0.3 +
73
+ workflow.completion_rate * 0.2 +
74
+ occurrenceBoost * 0.1,
75
+ );
76
+ }
77
+
78
+ function buildWorkflowProposalId(sourceSkillName: string, draft: WorkflowSkillDraft): string {
79
+ const digest = createHash("sha256")
80
+ .update(`${sourceSkillName}:${draft.skill_name}:${draft.source_workflow.workflow_id}`)
81
+ .digest("hex")
82
+ .slice(0, 16);
83
+ return `wf-${draft.skill_name}-${digest}`;
84
+ }
85
+
86
+ function buildWorkflowProposalSummary(
87
+ workflow: DiscoveredWorkflow,
88
+ draft: WorkflowSkillDraft,
89
+ ): string {
90
+ const chain = workflow.skills.join(" -> ");
91
+ return `Create new_skill "${draft.skill_name}" from workflow ${chain} (${workflow.occurrence_count} sessions, synergy ${workflow.synergy_score.toFixed(2)}, consistency ${Math.round(workflow.sequence_consistency * 100)}%, completion ${Math.round(workflow.completion_rate * 100)}%).`;
92
+ }
93
+
94
+ function hasExistingProposal(proposalId: string, auditEntries: EvolutionAuditEntry[]): boolean {
95
+ return auditEntries.some((entry) => entry.proposal_id === proposalId);
96
+ }
97
+
98
+ export function discoverWorkflowSkillProposals(
99
+ telemetry: SessionTelemetryRecord[],
100
+ usage: SkillUsageRecord[],
101
+ options: WorkflowSkillProposalOptions = {},
102
+ ): WorkflowSkillProposal[] {
103
+ const minOccurrences = options.minOccurrences ?? DEFAULT_WORKFLOW_PROPOSAL_MIN_OCCURRENCES;
104
+ const maxProposals = options.maxProposals ?? DEFAULT_WORKFLOW_PROPOSAL_MAX;
105
+ const minSynergy = options.minSynergy ?? DEFAULT_WORKFLOW_PROPOSAL_MIN_SYNERGY;
106
+ const minConsistency = options.minConsistency ?? DEFAULT_WORKFLOW_PROPOSAL_MIN_CONSISTENCY;
107
+ const minCompletionRate = options.minCompletionRate ?? DEFAULT_WORKFLOW_PROPOSAL_MIN_COMPLETION;
108
+ const report = discoverWorkflows(telemetry, usage, {
109
+ minOccurrences,
110
+ skill: options.skillFilter,
111
+ });
112
+ const existingAuditEntries = options.existingAuditEntries ?? [];
113
+ const proposals: WorkflowSkillProposal[] = [];
114
+
115
+ for (const workflow of report.workflows) {
116
+ if (workflow.occurrence_count < minOccurrences) continue;
117
+ if (workflow.synergy_score < minSynergy) continue;
118
+ if (workflow.sequence_consistency < minConsistency) continue;
119
+ if (workflow.completion_rate < minCompletionRate) continue;
120
+ if (workflow.skills.length < 2) continue;
121
+
122
+ const draft = buildWorkflowSkillDraft(workflow, { cwd: options.cwd });
123
+ if (!draft.skill_name) continue;
124
+ if (options.resolveSkillPath?.(draft.skill_name)) continue;
125
+
126
+ const sourceSkillName = workflow.skills[0];
127
+ const proposalId = buildWorkflowProposalId(sourceSkillName, draft);
128
+ if (hasExistingProposal(proposalId, existingAuditEntries)) continue;
129
+
130
+ const summary = buildWorkflowProposalSummary(workflow, draft);
131
+ const currentValue = `No dedicated workflow skill exists for ${workflow.skills.join(" -> ")}.`;
132
+ const proposedValue = `Create ${draft.skill_name} at ${draft.skill_path}`;
133
+ const queryClause = workflow.representative_query.trim()
134
+ ? ` Common trigger: "${workflow.representative_query.trim()}".`
135
+ : "";
136
+
137
+ proposals.push({
138
+ proposal_id: proposalId,
139
+ source_skill_name: sourceSkillName,
140
+ workflow,
141
+ draft,
142
+ summary,
143
+ current_value: currentValue,
144
+ proposed_value: proposedValue,
145
+ rationale: `${summary}${queryClause}`,
146
+ confidence: buildWorkflowProposalConfidence(workflow),
147
+ });
148
+
149
+ if (proposals.length >= maxProposals) break;
150
+ }
151
+
152
+ return proposals;
153
+ }
154
+
155
+ export function persistWorkflowSkillProposal(
156
+ proposal: WorkflowSkillProposal,
157
+ options: WorkflowSkillProposalPersistOptions = {},
158
+ ): void {
159
+ const timestamp = (options.now ?? new Date()).toISOString();
160
+ const appendAudit = options.appendAudit ?? appendAuditEntry;
161
+ const appendEvidence = options.appendEvidence ?? appendEvidenceEntry;
162
+
163
+ appendAudit({
164
+ timestamp,
165
+ proposal_id: proposal.proposal_id,
166
+ skill_name: proposal.source_skill_name,
167
+ action: "created",
168
+ details: proposal.summary,
169
+ });
170
+
171
+ appendEvidence({
172
+ timestamp,
173
+ proposal_id: proposal.proposal_id,
174
+ skill_name: proposal.source_skill_name,
175
+ skill_path: options.sourceSkillPath ?? "",
176
+ target: "new_skill",
177
+ stage: "proposed",
178
+ rationale: proposal.rationale,
179
+ confidence: proposal.confidence,
180
+ details: proposal.proposed_value,
181
+ original_text: proposal.current_value,
182
+ proposed_text: proposal.draft.content,
183
+ });
184
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * skill-scaffold.ts
3
+ *
4
+ * Builds draft workflow skills from repeated telemetry-discovered workflows.
5
+ * The draft is preview-first by default so agents can review the scaffold before
6
+ * writing it into a local skill registry.
7
+ */
8
+
9
+ import { join } from "node:path";
10
+
11
+ import type { DiscoveredWorkflow } from "../types.js";
12
+ import { findGitRepositoryRoot } from "../utils/skill-discovery.js";
13
+
14
+ export interface WorkflowSkillDraft {
15
+ title: string;
16
+ skill_name: string;
17
+ description: string;
18
+ output_dir: string;
19
+ skill_dir: string;
20
+ skill_path: string;
21
+ content: string;
22
+ source_workflow: {
23
+ workflow_id: string;
24
+ skills: string[];
25
+ occurrence_count: number;
26
+ synergy_score: number;
27
+ representative_query: string;
28
+ };
29
+ }
30
+
31
+ export interface WorkflowSkillDraftOptions {
32
+ outputDir?: string;
33
+ skillName?: string;
34
+ description?: string;
35
+ cwd?: string;
36
+ }
37
+
38
+ const STOPWORDS = new Set([
39
+ "a",
40
+ "an",
41
+ "and",
42
+ "for",
43
+ "from",
44
+ "in",
45
+ "into",
46
+ "of",
47
+ "on",
48
+ "or",
49
+ "the",
50
+ "to",
51
+ "with",
52
+ ]);
53
+
54
+ function splitWords(value: string): string[] {
55
+ return value
56
+ .replace(/[^A-Za-z0-9]+/g, " ")
57
+ .trim()
58
+ .split(/\s+/)
59
+ .filter(Boolean);
60
+ }
61
+
62
+ function titleCase(value: string): string {
63
+ return splitWords(value)
64
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
65
+ .join(" ");
66
+ }
67
+
68
+ export function slugifyWorkflowSkillName(value: string): string {
69
+ return splitWords(value)
70
+ .map((word) => word.toLowerCase())
71
+ .join("-")
72
+ .replace(/-+/g, "-")
73
+ .replace(/^-|-$/g, "");
74
+ }
75
+
76
+ function deriveBaseLabel(workflow: DiscoveredWorkflow): string {
77
+ const filteredQueryWords = splitWords(workflow.representative_query).filter(
78
+ (word) => !STOPWORDS.has(word.toLowerCase()),
79
+ );
80
+
81
+ if (filteredQueryWords.length >= 2) {
82
+ return filteredQueryWords.slice(0, 5).join(" ");
83
+ }
84
+
85
+ return `${workflow.skills.join(" ")} workflow`;
86
+ }
87
+
88
+ function formatList(items: string[]): string {
89
+ if (items.length === 0) return "";
90
+ if (items.length === 1) return items[0];
91
+ if (items.length === 2) return `${items[0]} and ${items[1]}`;
92
+ return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
93
+ }
94
+
95
+ function wrapFoldedScalar(value: string, width = 78): string[] {
96
+ const words = value.split(/\s+/).filter(Boolean);
97
+ const lines: string[] = [];
98
+ let current = "";
99
+
100
+ for (const word of words) {
101
+ const candidate = current.length === 0 ? word : `${current} ${word}`;
102
+ if (candidate.length > width && current.length > 0) {
103
+ lines.push(` ${current}`);
104
+ current = word;
105
+ } else {
106
+ current = candidate;
107
+ }
108
+ }
109
+
110
+ if (current.length > 0) lines.push(` ${current}`);
111
+ return lines.length > 0 ? lines : [" "];
112
+ }
113
+
114
+ export function getDefaultWorkflowSkillOutputDir(cwd: string = process.cwd()): string {
115
+ const repoRoot = findGitRepositoryRoot(cwd);
116
+ return join(repoRoot ?? cwd, ".agents", "skills");
117
+ }
118
+
119
+ export function buildWorkflowSkillDescription(
120
+ workflow: DiscoveredWorkflow,
121
+ override?: string,
122
+ ): string {
123
+ if (override && override.trim().length > 0) return override.trim();
124
+
125
+ const chain = formatList(workflow.skills);
126
+ const query = workflow.representative_query.trim();
127
+ if (query.length > 0) {
128
+ return `Use when the user wants to ${query}. Coordinates ${chain} in sequence.`;
129
+ }
130
+
131
+ return `Use when the task consistently needs ${chain} in sequence.`;
132
+ }
133
+
134
+ export function buildWorkflowSkillContent(
135
+ workflow: DiscoveredWorkflow,
136
+ title: string,
137
+ skillName: string,
138
+ description: string,
139
+ ): string {
140
+ const workflowName = title.endsWith("Workflow") ? title : `${title} Workflow`;
141
+ const chain = workflow.skills.join(" → ");
142
+ const query = workflow.representative_query.trim();
143
+ const foldedDescription = wrapFoldedScalar(description).join("\n");
144
+
145
+ const whenToUseLines =
146
+ query.length > 0
147
+ ? [
148
+ `- The user asks to "${query}"`,
149
+ `- The request repeatedly needs this skill chain: ${chain}`,
150
+ ]
151
+ : [`- The request repeatedly needs this skill chain: ${chain}`];
152
+
153
+ const executionPlanLines = workflow.skills.map(
154
+ (skill, index) =>
155
+ `${index + 1}. Invoke \`${skill}\` in its established role for this workflow.`,
156
+ );
157
+
158
+ return `---
159
+ name: ${skillName}
160
+ description: >
161
+ ${foldedDescription}
162
+ metadata:
163
+ author: selftune-autogen
164
+ version: 0.1.0
165
+ category: developer-tools
166
+ generated_by: selftune workflows scaffold
167
+ source_workflow_id: ${workflow.workflow_id}
168
+ ---
169
+
170
+ # ${title}
171
+
172
+ This draft skill was scaffolded by selftune from repeated workflow telemetry.
173
+ Review the routing language and execution notes before broad distribution.
174
+
175
+ ## When to Use
176
+
177
+ ${whenToUseLines.join("\n")}
178
+
179
+ ## Execution Plan
180
+
181
+ ${executionPlanLines.join("\n")}
182
+
183
+ ## Workflows
184
+
185
+ ### ${workflowName}
186
+ - **Skills:** ${chain}
187
+ ${query.length > 0 ? `- **Trigger:** ${query}\n` : ""}- **Source:** Discovered from ${workflow.occurrence_count} sessions (synergy: ${workflow.synergy_score.toFixed(2)})
188
+
189
+ ## Notes
190
+
191
+ - This is a proposal scaffold, not a silently published marketplace skill.
192
+ - Add tighter scope boundaries and richer examples before publishing.
193
+ `;
194
+ }
195
+
196
+ export function buildWorkflowSkillDraft(
197
+ workflow: DiscoveredWorkflow,
198
+ options: WorkflowSkillDraftOptions = {},
199
+ ): WorkflowSkillDraft {
200
+ const baseLabel = options.skillName?.trim() || deriveBaseLabel(workflow);
201
+ const skillName = slugifyWorkflowSkillName(baseLabel);
202
+ const title = titleCase(baseLabel) || titleCase(`${workflow.skills.join(" ")} workflow`);
203
+ const description = buildWorkflowSkillDescription(workflow, options.description);
204
+ const outputDir = options.outputDir?.trim() || getDefaultWorkflowSkillOutputDir(options.cwd);
205
+ const skillDir = join(outputDir, skillName);
206
+ const skillPath = join(skillDir, "SKILL.md");
207
+
208
+ return {
209
+ title,
210
+ skill_name: skillName,
211
+ description,
212
+ output_dir: outputDir,
213
+ skill_dir: skillDir,
214
+ skill_path: skillPath,
215
+ content: buildWorkflowSkillContent(workflow, title, skillName, description),
216
+ source_workflow: {
217
+ workflow_id: workflow.workflow_id,
218
+ skills: workflow.skills,
219
+ occurrence_count: workflow.occurrence_count,
220
+ synergy_score: workflow.synergy_score,
221
+ representative_query: workflow.representative_query,
222
+ },
223
+ };
224
+ }
225
+
226
+ export function formatWorkflowSkillDraft(draft: WorkflowSkillDraft): string {
227
+ const lines = [
228
+ `Draft workflow skill: ${draft.title}`,
229
+ `Skill name: ${draft.skill_name}`,
230
+ `Output path: ${draft.skill_path}`,
231
+ `Source workflow: ${draft.source_workflow.workflow_id}`,
232
+ `Occurrences: ${draft.source_workflow.occurrence_count} | Synergy: ${draft.source_workflow.synergy_score.toFixed(2)}`,
233
+ ];
234
+
235
+ if (draft.source_workflow.representative_query.trim().length > 0) {
236
+ lines.push(`Representative query: "${draft.source_workflow.representative_query.trim()}"`);
237
+ }
238
+
239
+ lines.push("", draft.content.trimEnd());
240
+ return lines.join("\n");
241
+ }