selftune 0.2.30 → 0.2.32

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 (102) hide show
  1. package/README.md +83 -56
  2. package/apps/local-dashboard/dist/assets/index-B-ut4w0B.js +15 -0
  3. package/apps/local-dashboard/dist/assets/index-BFGfCVrL.css +1 -0
  4. package/apps/local-dashboard/dist/assets/vendor-ui-DfowE3Hu.js +1 -0
  5. package/apps/local-dashboard/dist/index.html +3 -3
  6. package/cli/selftune/command-surface.ts +613 -2
  7. package/cli/selftune/create/baseline.ts +429 -0
  8. package/cli/selftune/create/check.ts +35 -0
  9. package/cli/selftune/create/init.ts +115 -0
  10. package/cli/selftune/create/package-candidate-state.ts +771 -0
  11. package/cli/selftune/create/package-evaluator.ts +710 -0
  12. package/cli/selftune/create/package-fingerprint.ts +142 -0
  13. package/cli/selftune/create/package-search.ts +377 -0
  14. package/cli/selftune/create/publish.ts +431 -0
  15. package/cli/selftune/create/readiness.ts +495 -0
  16. package/cli/selftune/create/replay.ts +330 -0
  17. package/cli/selftune/create/report.ts +74 -0
  18. package/cli/selftune/create/scaffold.ts +121 -0
  19. package/cli/selftune/create/skills-ref-adapter.ts +177 -0
  20. package/cli/selftune/create/status.ts +33 -0
  21. package/cli/selftune/create/templates.ts +249 -0
  22. package/cli/selftune/cron/setup.ts +1 -1
  23. package/cli/selftune/dashboard-action-events.ts +4 -1
  24. package/cli/selftune/dashboard-action-result.ts +789 -24
  25. package/cli/selftune/dashboard-action-stream.ts +80 -0
  26. package/cli/selftune/dashboard-contract.ts +146 -3
  27. package/cli/selftune/dashboard-server.ts +5 -4
  28. package/cli/selftune/eval/hooks-to-evals.ts +58 -35
  29. package/cli/selftune/eval/synthetic-evals.ts +145 -17
  30. package/cli/selftune/evolution/bounded-mutations.ts +1045 -0
  31. package/cli/selftune/evolution/evolve-body.ts +9 -36
  32. package/cli/selftune/evolution/evolve.ts +8 -72
  33. package/cli/selftune/evolution/stopping-criteria.ts +5 -13
  34. package/cli/selftune/evolution/unblock-suggestions.ts +0 -16
  35. package/cli/selftune/evolution/validate-host-replay.ts +115 -15
  36. package/cli/selftune/improve.ts +206 -0
  37. package/cli/selftune/index.ts +123 -6
  38. package/cli/selftune/init.ts +1 -1
  39. package/cli/selftune/localdb/queries/dashboard.ts +30 -0
  40. package/cli/selftune/localdb/schema.ts +52 -0
  41. package/cli/selftune/monitoring/watch.ts +257 -23
  42. package/cli/selftune/orchestrate/execute.ts +300 -1
  43. package/cli/selftune/orchestrate/finalize.ts +14 -0
  44. package/cli/selftune/orchestrate/plan.ts +22 -5
  45. package/cli/selftune/orchestrate/prepare.ts +59 -4
  46. package/cli/selftune/orchestrate/report.ts +1 -1
  47. package/cli/selftune/orchestrate.ts +34 -1
  48. package/cli/selftune/publish.ts +35 -0
  49. package/cli/selftune/registry/github-install.ts +256 -0
  50. package/cli/selftune/registry/index.ts +1 -1
  51. package/cli/selftune/registry/install.ts +58 -7
  52. package/cli/selftune/routes/actions.ts +81 -15
  53. package/cli/selftune/routes/overview.ts +1 -1
  54. package/cli/selftune/routes/skill-report.ts +147 -2
  55. package/cli/selftune/run.ts +18 -0
  56. package/cli/selftune/schedule.ts +3 -3
  57. package/cli/selftune/search-run.ts +703 -0
  58. package/cli/selftune/status.ts +35 -11
  59. package/cli/selftune/testing-readiness.ts +431 -40
  60. package/cli/selftune/types.ts +316 -0
  61. package/cli/selftune/utils/eval-readiness.ts +1 -0
  62. package/cli/selftune/utils/json-output.ts +11 -0
  63. package/cli/selftune/utils/lifecycle-surface.ts +48 -0
  64. package/cli/selftune/utils/query-filter.ts +82 -1
  65. package/cli/selftune/utils/tui.ts +85 -2
  66. package/cli/selftune/verify.ts +205 -0
  67. package/cli/selftune/workflows/proposals.ts +1 -1
  68. package/cli/selftune/workflows/skill-scaffold.ts +141 -63
  69. package/cli/selftune/workflows/workflows.ts +4 -4
  70. package/package.json +1 -1
  71. package/packages/dashboard-core/src/routes/manifest.ts +2 -2
  72. package/packages/ui/src/components/SkillReportPanels.tsx +7 -7
  73. package/packages/ui/src/primitives/button.tsx +5 -0
  74. package/skill/SKILL.md +148 -85
  75. package/skill/references/cli-quick-reference.md +16 -1
  76. package/skill/references/creator-playbook.md +31 -10
  77. package/skill/workflows/Baseline.md +8 -9
  78. package/skill/workflows/Contributions.md +4 -4
  79. package/skill/workflows/Create.md +173 -0
  80. package/skill/workflows/CreateTestDeploy.md +34 -30
  81. package/skill/workflows/Cron.md +2 -2
  82. package/skill/workflows/Dashboard.md +3 -3
  83. package/skill/workflows/Evals.md +13 -7
  84. package/skill/workflows/Evolve.md +75 -32
  85. package/skill/workflows/EvolveBody.md +22 -15
  86. package/skill/workflows/Hook.md +1 -1
  87. package/skill/workflows/Improve.md +168 -0
  88. package/skill/workflows/Initialize.md +3 -3
  89. package/skill/workflows/Orchestrate.md +49 -12
  90. package/skill/workflows/Publish.md +100 -0
  91. package/skill/workflows/Registry.md +19 -13
  92. package/skill/workflows/Run.md +72 -0
  93. package/skill/workflows/Schedule.md +2 -2
  94. package/skill/workflows/SearchRun.md +89 -0
  95. package/skill/workflows/SignalsDashboard.md +2 -2
  96. package/skill/workflows/UnitTest.md +13 -4
  97. package/skill/workflows/Verify.md +136 -0
  98. package/skill/workflows/Watch.md +114 -47
  99. package/skill/workflows/Workflows.md +13 -8
  100. package/apps/local-dashboard/dist/assets/index-BcXquWFB.css +0 -1
  101. package/apps/local-dashboard/dist/assets/index-Coq42hE4.js +0 -15
  102. package/apps/local-dashboard/dist/assets/vendor-ui-B0H8s1mP.js +0 -1
@@ -0,0 +1,330 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { parseArgs } from "node:util";
3
+
4
+ import { PUBLIC_COMMAND_SURFACES, renderCommandHelp } from "../command-surface.js";
5
+ import { parseSkillSections } from "../evolution/deploy-proposal.js";
6
+ import {
7
+ buildRoutingReplayFixture,
8
+ resolveRuntimeReplayPlatform,
9
+ runHostRuntimeReplayFixture,
10
+ type RuntimeReplayInvoker,
11
+ } from "../evolution/validate-host-replay.js";
12
+ import { writeReplayEntryResultsToDb } from "../localdb/direct-write.js";
13
+ import { getCanonicalEvalSetPath } from "../testing-readiness.js";
14
+ import type {
15
+ EvalEntry,
16
+ ReplayStagingMode,
17
+ RoutingReplayEntryResult,
18
+ RuntimeReplayAggregateMetrics,
19
+ } from "../types.js";
20
+ import { isLlmBackedAgent, detectLlmAgent } from "../utils/llm-call.js";
21
+ import { CLIError, handleCLIError } from "../utils/cli-error.js";
22
+ import { readCreateSkillContext } from "./readiness.js";
23
+
24
+ export type CreateReplayMode = ReplayStagingMode;
25
+
26
+ export interface CreateReplayResult {
27
+ skill: string;
28
+ skill_path: string;
29
+ mode: CreateReplayMode;
30
+ agent: string;
31
+ proposal_id: string;
32
+ total: number;
33
+ passed: number;
34
+ failed: number;
35
+ pass_rate: number;
36
+ fixture_id: string;
37
+ results: RoutingReplayEntryResult[];
38
+ runtime_metrics: RuntimeReplayAggregateMetrics;
39
+ }
40
+
41
+ export interface RunCreateReplayOptions {
42
+ skillPath: string;
43
+ mode: CreateReplayMode;
44
+ agent?: string | null;
45
+ evalSetPath?: string;
46
+ includeTargetSkill?: boolean;
47
+ runtimeInvoker?: RuntimeReplayInvoker;
48
+ }
49
+
50
+ export function loadCreateEvalSet(skillName: string, explicitPath?: string): EvalEntry[] {
51
+ const path = explicitPath?.trim() || getCanonicalEvalSetPath(skillName);
52
+ if (!existsSync(path)) {
53
+ throw new CLIError(
54
+ `No canonical eval set found for "${skillName}" at ${path}.`,
55
+ "MISSING_DATA",
56
+ `Run selftune eval generate --skill ${skillName} --skill-path /path/to/${skillName}/SKILL.md --auto-synthetic`,
57
+ );
58
+ }
59
+
60
+ try {
61
+ const parsed = JSON.parse(readFileSync(path, "utf-8")) as unknown;
62
+ if (!Array.isArray(parsed)) {
63
+ throw new Error("expected a JSON array");
64
+ }
65
+ return parsed as EvalEntry[];
66
+ } catch (error) {
67
+ const message = error instanceof Error ? error.message : String(error);
68
+ throw new CLIError(
69
+ `Eval set at ${path} is invalid: ${message}`,
70
+ "INVALID_FLAG",
71
+ `Regenerate the eval set with selftune eval generate --skill ${skillName}`,
72
+ );
73
+ }
74
+ }
75
+
76
+ function resolveReplayAgent(requestedAgent?: string | null): string {
77
+ if (requestedAgent) {
78
+ if (!isLlmBackedAgent(requestedAgent)) {
79
+ throw new CLIError(
80
+ `Unsupported --agent value "${requestedAgent}".`,
81
+ "INVALID_FLAG",
82
+ "Use claude, codex, opencode, or pi.",
83
+ );
84
+ }
85
+ if (!Bun.which(requestedAgent)) {
86
+ throw new CLIError(
87
+ `Agent CLI '${requestedAgent}' not found in PATH`,
88
+ "AGENT_NOT_FOUND",
89
+ "Install it or omit --agent to use auto-detection",
90
+ );
91
+ }
92
+ return requestedAgent;
93
+ }
94
+
95
+ const detected = detectLlmAgent();
96
+ if (!detected) {
97
+ throw new CLIError(
98
+ "No supported runtime replay agent was found in PATH.",
99
+ "AGENT_NOT_FOUND",
100
+ "Install Claude Code, Codex, OpenCode, or Pi, or pass --agent explicitly.",
101
+ );
102
+ }
103
+ return detected;
104
+ }
105
+
106
+ function buildReplayContent(
107
+ skillContent: string,
108
+ mode: CreateReplayMode,
109
+ ): {
110
+ content: string;
111
+ contentTarget: "routing" | "body";
112
+ } {
113
+ const parsed = parseSkillSections(skillContent);
114
+ if (mode === "routing") {
115
+ return {
116
+ content: parsed.sections["Workflow Routing"] ?? "",
117
+ contentTarget: "routing",
118
+ };
119
+ }
120
+
121
+ const bodyParts: string[] = [];
122
+ if (parsed.description.trim()) {
123
+ bodyParts.push(parsed.description.trim());
124
+ }
125
+ for (const [sectionName, sectionContent] of Object.entries(parsed.sections)) {
126
+ bodyParts.push(`## ${sectionName}`);
127
+ bodyParts.push("");
128
+ bodyParts.push(sectionContent.trim());
129
+ bodyParts.push("");
130
+ }
131
+
132
+ return {
133
+ content: bodyParts.join("\n").trim(),
134
+ contentTarget: "body",
135
+ };
136
+ }
137
+
138
+ function persistReplayResults(
139
+ proposalId: string,
140
+ skillName: string,
141
+ mode: CreateReplayMode,
142
+ results: RoutingReplayEntryResult[],
143
+ ): void {
144
+ writeReplayEntryResultsToDb(
145
+ results.map((result) => ({
146
+ proposal_id: proposalId,
147
+ skill_name: skillName,
148
+ validation_mode: "host_replay",
149
+ phase: `current_${mode}`,
150
+ query: result.query,
151
+ should_trigger: result.should_trigger,
152
+ triggered: result.triggered,
153
+ passed: result.passed,
154
+ evidence: result.evidence,
155
+ })),
156
+ );
157
+ }
158
+
159
+ function sumKnownMetric(values: Array<number | null | undefined>): {
160
+ total: number | null;
161
+ count: number;
162
+ } {
163
+ let total = 0;
164
+ let count = 0;
165
+ for (const value of values) {
166
+ if (typeof value !== "number" || !Number.isFinite(value)) continue;
167
+ total += value;
168
+ count += 1;
169
+ }
170
+ return {
171
+ total: count > 0 ? total : null,
172
+ count,
173
+ };
174
+ }
175
+
176
+ export function summarizeReplayRuntimeMetrics(
177
+ results: RoutingReplayEntryResult[],
178
+ ): RuntimeReplayAggregateMetrics {
179
+ const evalRuns = results.length;
180
+ const totalDurationMs = results.reduce(
181
+ (sum, result) => sum + (result.runtime_metrics?.duration_ms ?? 0),
182
+ 0,
183
+ );
184
+ const inputTokens = sumKnownMetric(results.map((result) => result.runtime_metrics?.input_tokens));
185
+ const outputTokens = sumKnownMetric(
186
+ results.map((result) => result.runtime_metrics?.output_tokens),
187
+ );
188
+ const cacheCreationTokens = sumKnownMetric(
189
+ results.map((result) => result.runtime_metrics?.cache_creation_input_tokens),
190
+ );
191
+ const cacheReadTokens = sumKnownMetric(
192
+ results.map((result) => result.runtime_metrics?.cache_read_input_tokens),
193
+ );
194
+ const totalCost = sumKnownMetric(results.map((result) => result.runtime_metrics?.total_cost_usd));
195
+ const totalTurns = sumKnownMetric(results.map((result) => result.runtime_metrics?.num_turns));
196
+ const usageObservations = results.filter((result) => {
197
+ const metrics = result.runtime_metrics;
198
+ return Boolean(
199
+ metrics &&
200
+ (metrics.input_tokens != null ||
201
+ metrics.output_tokens != null ||
202
+ metrics.total_cost_usd != null ||
203
+ metrics.num_turns != null),
204
+ );
205
+ }).length;
206
+
207
+ return {
208
+ eval_runs: evalRuns,
209
+ usage_observations: usageObservations,
210
+ total_duration_ms: totalDurationMs,
211
+ avg_duration_ms: evalRuns > 0 ? totalDurationMs / evalRuns : 0,
212
+ total_input_tokens: inputTokens.total,
213
+ total_output_tokens: outputTokens.total,
214
+ total_cache_creation_input_tokens: cacheCreationTokens.total,
215
+ total_cache_read_input_tokens: cacheReadTokens.total,
216
+ total_cost_usd: totalCost.total,
217
+ total_turns: totalTurns.total,
218
+ };
219
+ }
220
+
221
+ export async function runCreateReplay(
222
+ options: RunCreateReplayOptions,
223
+ ): Promise<CreateReplayResult> {
224
+ const context = readCreateSkillContext(options.skillPath);
225
+ const agent = resolveReplayAgent(options.agent);
226
+ const platform = resolveRuntimeReplayPlatform(agent);
227
+ if (!platform) {
228
+ throw new CLIError(
229
+ `Runtime replay is unavailable for agent "${agent}".`,
230
+ "REPLAY_UNAVAILABLE",
231
+ "Use claude, codex, or opencode for create replay.",
232
+ );
233
+ }
234
+
235
+ const evalSet = loadCreateEvalSet(context.skill_name, options.evalSetPath);
236
+ const { content, contentTarget } = buildReplayContent(context.skill_content, options.mode);
237
+ const fixture = buildRoutingReplayFixture({
238
+ skillName: context.skill_name,
239
+ skillPath: context.skill_path,
240
+ platform,
241
+ stagingMode: options.mode,
242
+ });
243
+
244
+ const results = await runHostRuntimeReplayFixture({
245
+ routing: content,
246
+ evalSet,
247
+ fixture,
248
+ contentTarget,
249
+ includeTargetSkill: options.includeTargetSkill,
250
+ runtimeInvoker: options.runtimeInvoker,
251
+ });
252
+
253
+ const passed = results.filter((result) => result.passed).length;
254
+ const total = results.length;
255
+ const proposalId = `create-replay-${context.skill_name}-${options.mode}-${Date.now()}`;
256
+ persistReplayResults(proposalId, context.skill_name, options.mode, results);
257
+ const runtimeMetrics = summarizeReplayRuntimeMetrics(results);
258
+
259
+ return {
260
+ skill: context.skill_name,
261
+ skill_path: context.skill_path,
262
+ mode: options.mode,
263
+ agent,
264
+ proposal_id: proposalId,
265
+ total,
266
+ passed,
267
+ failed: total - passed,
268
+ pass_rate: total > 0 ? passed / total : 0,
269
+ fixture_id: fixture.fixture_id,
270
+ results,
271
+ runtime_metrics: runtimeMetrics,
272
+ };
273
+ }
274
+
275
+ function formatReplayResult(result: CreateReplayResult): string {
276
+ return [
277
+ `Skill: ${result.skill}`,
278
+ `Mode: ${result.mode}`,
279
+ `Agent: ${result.agent}`,
280
+ `Pass rate: ${(result.pass_rate * 100).toFixed(1)}% (${result.passed}/${result.total})`,
281
+ `Replay record: ${result.proposal_id}`,
282
+ ].join("\n");
283
+ }
284
+
285
+ export async function cliMain(): Promise<void> {
286
+ const { values } = parseArgs({
287
+ options: {
288
+ "skill-path": { type: "string" },
289
+ mode: { type: "string", default: "routing" },
290
+ agent: { type: "string" },
291
+ "eval-set": { type: "string" },
292
+ json: { type: "boolean", default: false },
293
+ help: { type: "boolean", short: "h", default: false },
294
+ },
295
+ strict: true,
296
+ });
297
+
298
+ if (values.help) {
299
+ console.log(renderCommandHelp(PUBLIC_COMMAND_SURFACES.createReplay));
300
+ process.exit(0);
301
+ }
302
+
303
+ const mode = values.mode;
304
+ if (mode !== "routing" && mode !== "package") {
305
+ throw new CLIError(
306
+ `Unsupported --mode value "${mode}".`,
307
+ "INVALID_FLAG",
308
+ "Use --mode routing or --mode package.",
309
+ );
310
+ }
311
+
312
+ const result = await runCreateReplay({
313
+ skillPath: values["skill-path"] ?? "",
314
+ mode,
315
+ agent: values.agent,
316
+ evalSetPath: values["eval-set"],
317
+ });
318
+
319
+ if (values.json || !process.stdout.isTTY) {
320
+ console.log(JSON.stringify(result, null, 2));
321
+ } else {
322
+ console.log(formatReplayResult(result));
323
+ }
324
+
325
+ process.exit(result.failed === 0 ? 0 : 1);
326
+ }
327
+
328
+ if (import.meta.main) {
329
+ cliMain().catch(handleCLIError);
330
+ }
@@ -0,0 +1,74 @@
1
+ import { parseArgs } from "node:util";
2
+
3
+ import { PUBLIC_COMMAND_SURFACES, renderCommandHelp } from "../command-surface.js";
4
+ import { CLIError, handleCLIError } from "../utils/cli-error.js";
5
+ import {
6
+ formatCreatePackageBenchmarkReport,
7
+ runCreatePackageEvaluation,
8
+ type CreatePackageEvaluationDeps,
9
+ type CreatePackageEvaluationResult,
10
+ } from "./package-evaluator.js";
11
+
12
+ export interface RunCreateReportOptions {
13
+ skillPath: string;
14
+ agent?: string;
15
+ evalSetPath?: string;
16
+ }
17
+
18
+ export async function runCreateReport(
19
+ options: RunCreateReportOptions,
20
+ deps: CreatePackageEvaluationDeps = {},
21
+ ): Promise<CreatePackageEvaluationResult> {
22
+ if (!options.skillPath.trim()) {
23
+ throw new CLIError(
24
+ "--skill-path <path> is required.",
25
+ "MISSING_FLAG",
26
+ "selftune create report --skill-path <path>",
27
+ );
28
+ }
29
+
30
+ return runCreatePackageEvaluation(
31
+ {
32
+ skillPath: options.skillPath,
33
+ agent: options.agent,
34
+ evalSetPath: options.evalSetPath,
35
+ },
36
+ deps,
37
+ );
38
+ }
39
+
40
+ export async function cliMain(): Promise<void> {
41
+ const { values } = parseArgs({
42
+ options: {
43
+ "skill-path": { type: "string" },
44
+ agent: { type: "string" },
45
+ "eval-set": { type: "string" },
46
+ json: { type: "boolean", default: false },
47
+ help: { type: "boolean", short: "h", default: false },
48
+ },
49
+ strict: true,
50
+ });
51
+
52
+ if (values.help) {
53
+ console.log(renderCommandHelp(PUBLIC_COMMAND_SURFACES.createReport));
54
+ process.exit(0);
55
+ }
56
+
57
+ const result = await runCreateReport({
58
+ skillPath: values["skill-path"] ?? "",
59
+ agent: values.agent,
60
+ evalSetPath: values["eval-set"],
61
+ });
62
+
63
+ if (values.json || !process.stdout.isTTY) {
64
+ console.log(JSON.stringify(result, null, 2));
65
+ } else {
66
+ console.log(formatCreatePackageBenchmarkReport(result));
67
+ }
68
+
69
+ process.exit(result.summary.evaluation_passed ? 0 : 1);
70
+ }
71
+
72
+ if (import.meta.main) {
73
+ cliMain().catch(handleCLIError);
74
+ }
@@ -0,0 +1,121 @@
1
+ import { existsSync } from "node:fs";
2
+ import { parseArgs } from "node:util";
3
+
4
+ import { PUBLIC_COMMAND_SURFACES, renderCommandHelp } from "../command-surface.js";
5
+ import { getDb } from "../localdb/db.js";
6
+ import { querySessionTelemetry, querySkillUsageRecords } from "../localdb/queries.js";
7
+ import type {
8
+ DiscoveredWorkflow,
9
+ SessionTelemetryRecord,
10
+ SkillUsageRecord,
11
+ WorkflowDiscoveryReport,
12
+ } from "../types.js";
13
+ import { CLIError, handleCLIError } from "../utils/cli-error.js";
14
+ import { discoverWorkflows } from "../workflows/discover.js";
15
+ import { buildWorkflowSkillDraft } from "../workflows/skill-scaffold.js";
16
+ import { writeCreateSkillDraft } from "./init.js";
17
+
18
+ function resolveWorkflowSelection(
19
+ report: WorkflowDiscoveryReport,
20
+ selection: string | undefined,
21
+ ): DiscoveredWorkflow {
22
+ if (!selection) {
23
+ throw new CLIError(
24
+ "--from-workflow <id|index> is required",
25
+ "MISSING_FLAG",
26
+ "selftune create scaffold --from-workflow <id|index>",
27
+ );
28
+ }
29
+
30
+ let workflow = report.workflows.find((candidate) => candidate.workflow_id === selection);
31
+ if (!workflow) {
32
+ const index = Number.parseInt(selection, 10);
33
+ if (!Number.isNaN(index) && index >= 1 && index <= report.workflows.length) {
34
+ workflow = report.workflows[index - 1];
35
+ }
36
+ }
37
+
38
+ if (!workflow) {
39
+ throw new CLIError(
40
+ `No workflow found matching "${selection}".`,
41
+ "INVALID_FLAG",
42
+ "Run 'selftune workflows' to inspect discovered workflows first.",
43
+ );
44
+ }
45
+
46
+ return workflow;
47
+ }
48
+
49
+ export async function cliMain(): Promise<void> {
50
+ const { values } = parseArgs({
51
+ options: {
52
+ "from-workflow": { type: "string" },
53
+ "output-dir": { type: "string" },
54
+ "skill-name": { type: "string" },
55
+ description: { type: "string" },
56
+ write: { type: "boolean", default: false },
57
+ force: { type: "boolean", default: false },
58
+ json: { type: "boolean", default: false },
59
+ "min-occurrences": { type: "string" },
60
+ skill: { type: "string" },
61
+ help: { type: "boolean", short: "h", default: false },
62
+ },
63
+ strict: true,
64
+ });
65
+
66
+ if (values.help) {
67
+ console.log(renderCommandHelp(PUBLIC_COMMAND_SURFACES.createScaffold));
68
+ process.exit(0);
69
+ }
70
+
71
+ const minOccurrences = values["min-occurrences"]
72
+ ? Number.parseInt(values["min-occurrences"], 10)
73
+ : undefined;
74
+ if (minOccurrences !== undefined && (Number.isNaN(minOccurrences) || minOccurrences < 0)) {
75
+ throw new CLIError("--min-occurrences must be a non-negative integer.", "INVALID_FLAG");
76
+ }
77
+
78
+ const db = getDb();
79
+ const telemetry = querySessionTelemetry(db) as SessionTelemetryRecord[];
80
+ const usage = querySkillUsageRecords(db) as SkillUsageRecord[];
81
+ const report = discoverWorkflows(telemetry, usage, {
82
+ minOccurrences,
83
+ skill: values.skill,
84
+ });
85
+ const workflow = resolveWorkflowSelection(report, values["from-workflow"]);
86
+ const draft = buildWorkflowSkillDraft(workflow, {
87
+ outputDir: values["output-dir"],
88
+ skillName: values["skill-name"],
89
+ description: values.description,
90
+ generatedBy: "selftune create scaffold",
91
+ });
92
+
93
+ if (values.write) {
94
+ const result = writeCreateSkillDraft(draft, { force: values.force });
95
+ if (values.json || !process.stdout.isTTY) {
96
+ console.log(JSON.stringify(result, null, 2));
97
+ return;
98
+ }
99
+ console.log(
100
+ `Scaffolded skill package "${draft.skill_name}" to ${draft.skill_dir}${result.overwritten ? " (overwritten)" : ""}`,
101
+ );
102
+ return;
103
+ }
104
+
105
+ if (values.json || !process.stdout.isTTY) {
106
+ console.log(JSON.stringify({ ...draft, written: false }, null, 2));
107
+ return;
108
+ }
109
+
110
+ console.log(draft.content);
111
+ if (existsSync(draft.skill_dir)) {
112
+ console.log("");
113
+ console.log(
114
+ `[WARN] ${draft.skill_dir} already exists. Re-run with --write --force to overwrite.`,
115
+ );
116
+ }
117
+ }
118
+
119
+ if (import.meta.main) {
120
+ cliMain().catch(handleCLIError);
121
+ }
@@ -0,0 +1,177 @@
1
+ import type { AgentSkillValidationIssue, AgentSkillValidationResult } from "../types.js";
2
+
3
+ interface ValidatorCommand {
4
+ command: string;
5
+ argv: string[];
6
+ }
7
+
8
+ export interface ValidateAgentSkillDeps {
9
+ which?: (command: string) => string | null;
10
+ spawnSync?: typeof Bun.spawnSync;
11
+ }
12
+
13
+ const VALIDATOR_COMMANDS: readonly ValidatorCommand[] = [
14
+ {
15
+ command: "uvx --from skills-ref agentskills validate",
16
+ argv: ["uvx", "--from", "skills-ref", "agentskills", "validate"],
17
+ },
18
+ {
19
+ command: "uvx skills-ref validate",
20
+ argv: ["uvx", "skills-ref", "validate"],
21
+ },
22
+ {
23
+ command: "npx skills-ref validate",
24
+ argv: ["npx", "skills-ref", "validate"],
25
+ },
26
+ ] as const;
27
+
28
+ function classifyIssueLevel(line: string): "error" | "warning" {
29
+ return /\bwarn(?:ing)?\b/i.test(line) ? "warning" : "error";
30
+ }
31
+
32
+ function normalizeIssues(
33
+ stdout: string,
34
+ stderr: string,
35
+ exitCode: number | null,
36
+ ): AgentSkillValidationIssue[] {
37
+ const merged = `${stderr}\n${stdout}`.trim();
38
+ if (!merged) {
39
+ return exitCode === 0
40
+ ? []
41
+ : [
42
+ {
43
+ level: "error",
44
+ code: "validation_failed",
45
+ message: `skills-ref exited with code ${exitCode ?? "unknown"}.`,
46
+ },
47
+ ];
48
+ }
49
+
50
+ const seen = new Set<string>();
51
+ const lines = merged
52
+ .split(/\r?\n/)
53
+ .map((line) => line.trim())
54
+ .filter(Boolean)
55
+ .filter((line) => {
56
+ if (seen.has(line)) return false;
57
+ seen.add(line);
58
+ return true;
59
+ });
60
+
61
+ return lines.map((line, index) => ({
62
+ level: classifyIssueLevel(line),
63
+ code: `skills_ref_${index + 1}`,
64
+ message: line,
65
+ }));
66
+ }
67
+
68
+ function readSpawnText(output: unknown): string {
69
+ if (typeof output === "string") return output;
70
+ if (output == null) return "";
71
+ return Buffer.from(output as ArrayBufferLike).toString("utf-8");
72
+ }
73
+
74
+ function isValidatorInvocationFailure(
75
+ stdout: string,
76
+ stderr: string,
77
+ exitCode: number | null,
78
+ ): boolean {
79
+ if (exitCode === 0) return false;
80
+ const merged = `${stderr}\n${stdout}`.trim();
81
+ if (!merged) return true;
82
+
83
+ return [
84
+ /No such option/i,
85
+ /unknown command/i,
86
+ /unknown argument/i,
87
+ /unrecognized (argument|option)/i,
88
+ /usage:\s*(uvx|npx|skills-ref|agentskills)\b/i,
89
+ /command not found/i,
90
+ /not found in PATH/i,
91
+ /No such file or directory/i,
92
+ /Unable to locate executable/i,
93
+ /The executable [`'"]?agentskills[`'"]? was not found/i,
94
+ /No package .*skills-ref/i,
95
+ /failed to resolve/i,
96
+ ].some((pattern) => pattern.test(merged));
97
+ }
98
+
99
+ export async function validateAgentSkill(
100
+ skillDir: string,
101
+ deps: ValidateAgentSkillDeps = {},
102
+ ): Promise<AgentSkillValidationResult> {
103
+ const which = deps.which ?? ((command: string) => Bun.which(command));
104
+ const spawnSync = deps.spawnSync ?? Bun.spawnSync;
105
+
106
+ const candidates = VALIDATOR_COMMANDS.filter((option) => which(option.argv[0]) != null);
107
+ if (candidates.length === 0) {
108
+ return {
109
+ ok: false,
110
+ issues: [
111
+ {
112
+ level: "error",
113
+ code: "validator_unavailable",
114
+ message:
115
+ "No Agent Skills validator was found. Install uv/uvx or use npx so selftune can run skills-ref validate.",
116
+ },
117
+ ],
118
+ raw_stdout: "",
119
+ raw_stderr: "",
120
+ exit_code: null,
121
+ validator: "skills-ref",
122
+ command: null,
123
+ };
124
+ }
125
+
126
+ let lastFailure: AgentSkillValidationResult | null = null;
127
+
128
+ for (const candidate of candidates) {
129
+ const result = spawnSync([...candidate.argv, skillDir], {
130
+ cwd: skillDir,
131
+ stdout: "pipe",
132
+ stderr: "pipe",
133
+ env: process.env,
134
+ });
135
+
136
+ const stdout = readSpawnText(result.stdout);
137
+ const stderr = readSpawnText(result.stderr);
138
+ const exitCode = result.exitCode;
139
+ const issues = normalizeIssues(stdout, stderr, exitCode);
140
+ const response: AgentSkillValidationResult = {
141
+ ok: exitCode === 0,
142
+ issues: exitCode === 0 ? issues.filter((issue) => issue.level === "warning") : issues,
143
+ raw_stdout: stdout,
144
+ raw_stderr: stderr,
145
+ exit_code: exitCode,
146
+ validator: "skills-ref",
147
+ command: `${candidate.command} ${skillDir}`,
148
+ };
149
+
150
+ if (exitCode === 0) {
151
+ return response;
152
+ }
153
+
154
+ lastFailure = response;
155
+ if (!isValidatorInvocationFailure(stdout, stderr, exitCode)) {
156
+ return response;
157
+ }
158
+ }
159
+
160
+ return (
161
+ lastFailure ?? {
162
+ ok: false,
163
+ issues: [
164
+ {
165
+ level: "error",
166
+ code: "validation_failed",
167
+ message: "skills-ref validation failed for an unknown reason.",
168
+ },
169
+ ],
170
+ raw_stdout: "",
171
+ raw_stderr: "",
172
+ exit_code: null,
173
+ validator: "skills-ref",
174
+ command: null,
175
+ }
176
+ );
177
+ }