selftune 0.2.31 → 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 (95) 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/routes/actions.ts +81 -15
  50. package/cli/selftune/routes/overview.ts +1 -1
  51. package/cli/selftune/routes/skill-report.ts +147 -2
  52. package/cli/selftune/run.ts +18 -0
  53. package/cli/selftune/schedule.ts +3 -3
  54. package/cli/selftune/search-run.ts +703 -0
  55. package/cli/selftune/status.ts +35 -11
  56. package/cli/selftune/testing-readiness.ts +431 -40
  57. package/cli/selftune/types.ts +316 -0
  58. package/cli/selftune/utils/eval-readiness.ts +1 -0
  59. package/cli/selftune/utils/json-output.ts +11 -0
  60. package/cli/selftune/utils/lifecycle-surface.ts +48 -0
  61. package/cli/selftune/utils/query-filter.ts +82 -1
  62. package/cli/selftune/utils/tui.ts +85 -2
  63. package/cli/selftune/verify.ts +205 -0
  64. package/cli/selftune/workflows/proposals.ts +1 -1
  65. package/cli/selftune/workflows/skill-scaffold.ts +141 -63
  66. package/cli/selftune/workflows/workflows.ts +4 -4
  67. package/package.json +1 -1
  68. package/skill/SKILL.md +148 -85
  69. package/skill/references/cli-quick-reference.md +16 -1
  70. package/skill/references/creator-playbook.md +31 -10
  71. package/skill/workflows/Baseline.md +8 -9
  72. package/skill/workflows/Contributions.md +4 -4
  73. package/skill/workflows/Create.md +173 -0
  74. package/skill/workflows/CreateTestDeploy.md +34 -30
  75. package/skill/workflows/Cron.md +2 -2
  76. package/skill/workflows/Dashboard.md +3 -3
  77. package/skill/workflows/Evals.md +13 -7
  78. package/skill/workflows/Evolve.md +75 -32
  79. package/skill/workflows/EvolveBody.md +22 -15
  80. package/skill/workflows/Hook.md +1 -1
  81. package/skill/workflows/Improve.md +168 -0
  82. package/skill/workflows/Initialize.md +3 -3
  83. package/skill/workflows/Orchestrate.md +49 -12
  84. package/skill/workflows/Publish.md +100 -0
  85. package/skill/workflows/Run.md +72 -0
  86. package/skill/workflows/Schedule.md +2 -2
  87. package/skill/workflows/SearchRun.md +89 -0
  88. package/skill/workflows/SignalsDashboard.md +2 -2
  89. package/skill/workflows/UnitTest.md +13 -4
  90. package/skill/workflows/Verify.md +136 -0
  91. package/skill/workflows/Watch.md +114 -47
  92. package/skill/workflows/Workflows.md +13 -8
  93. package/apps/local-dashboard/dist/assets/index-B7v_o1WC.js +0 -15
  94. package/apps/local-dashboard/dist/assets/index-CrO77SVi.css +0 -1
  95. package/apps/local-dashboard/dist/assets/vendor-ui-B0H8s1mP.js +0 -1
@@ -0,0 +1,142 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
+ import { dirname, join, relative, resolve } from "node:path";
4
+
5
+ import { buildCreateSkillManifest, type CreateSkillManifest } from "./templates.js";
6
+
7
+ function resolveDraftSkillPaths(
8
+ skillPathArg: string,
9
+ ): { skillDir: string; skillPath: string } | null {
10
+ const trimmed = skillPathArg.trim();
11
+ if (!trimmed) return null;
12
+
13
+ const absolute = resolve(trimmed);
14
+ if (!existsSync(absolute)) return null;
15
+
16
+ const stat = statSync(absolute);
17
+ if (stat.isDirectory()) {
18
+ const skillPath = join(absolute, "SKILL.md");
19
+ return existsSync(skillPath) ? { skillDir: absolute, skillPath } : null;
20
+ }
21
+
22
+ return { skillDir: dirname(absolute), skillPath: absolute };
23
+ }
24
+
25
+ function loadDraftManifest(skillDir: string): { manifest: CreateSkillManifest; present: boolean } {
26
+ const manifestPath = join(skillDir, "selftune.create.json");
27
+ const fallback = buildCreateSkillManifest();
28
+
29
+ if (!existsSync(manifestPath)) {
30
+ return { manifest: fallback, present: false };
31
+ }
32
+
33
+ try {
34
+ const parsed = JSON.parse(readFileSync(manifestPath, "utf-8")) as Partial<CreateSkillManifest>;
35
+ return {
36
+ manifest: {
37
+ version: 1,
38
+ entry_workflow:
39
+ typeof parsed.entry_workflow === "string" && parsed.entry_workflow.trim().length > 0
40
+ ? parsed.entry_workflow
41
+ : fallback.entry_workflow,
42
+ supports_package_replay:
43
+ typeof parsed.supports_package_replay === "boolean"
44
+ ? parsed.supports_package_replay
45
+ : fallback.supports_package_replay,
46
+ expected_resources: {
47
+ workflows:
48
+ typeof parsed.expected_resources?.workflows === "boolean"
49
+ ? parsed.expected_resources.workflows
50
+ : fallback.expected_resources.workflows,
51
+ references:
52
+ typeof parsed.expected_resources?.references === "boolean"
53
+ ? parsed.expected_resources.references
54
+ : fallback.expected_resources.references,
55
+ scripts:
56
+ typeof parsed.expected_resources?.scripts === "boolean"
57
+ ? parsed.expected_resources.scripts
58
+ : fallback.expected_resources.scripts,
59
+ assets:
60
+ typeof parsed.expected_resources?.assets === "boolean"
61
+ ? parsed.expected_resources.assets
62
+ : fallback.expected_resources.assets,
63
+ },
64
+ },
65
+ present: true,
66
+ };
67
+ } catch {
68
+ return { manifest: fallback, present: false };
69
+ }
70
+ }
71
+
72
+ function collectFiles(root: string, dir: string): string[] {
73
+ if (!existsSync(dir)) return [];
74
+
75
+ const discovered: string[] = [];
76
+ for (const entry of readdirSync(dir)) {
77
+ const absolute = join(dir, entry);
78
+ const stat = statSync(absolute);
79
+ if (stat.isDirectory()) {
80
+ discovered.push(...collectFiles(root, absolute));
81
+ } else if (stat.isFile()) {
82
+ discovered.push(relative(root, absolute));
83
+ }
84
+ }
85
+
86
+ return discovered;
87
+ }
88
+
89
+ export function computeCreatePackageFingerprint(skillPathArg: string): string | null {
90
+ const resolvedPaths = resolveDraftSkillPaths(skillPathArg);
91
+ if (!resolvedPaths) return null;
92
+
93
+ const { skillDir, skillPath } = resolvedPaths;
94
+ const { manifest, present: manifestPresent } = loadDraftManifest(skillDir);
95
+
96
+ const trackedPaths = new Set<string>(["SKILL.md"]);
97
+ if (manifestPresent) {
98
+ trackedPaths.add("selftune.create.json");
99
+ }
100
+
101
+ if (manifest.entry_workflow.trim().length > 0) {
102
+ trackedPaths.add(manifest.entry_workflow);
103
+ }
104
+
105
+ if (manifest.expected_resources.workflows) {
106
+ for (const entry of collectFiles(skillDir, join(skillDir, "workflows"))) {
107
+ trackedPaths.add(entry);
108
+ }
109
+ }
110
+ if (manifest.expected_resources.references) {
111
+ for (const entry of collectFiles(skillDir, join(skillDir, "references"))) {
112
+ trackedPaths.add(entry);
113
+ }
114
+ }
115
+ if (manifest.expected_resources.scripts) {
116
+ for (const entry of collectFiles(skillDir, join(skillDir, "scripts"))) {
117
+ trackedPaths.add(entry);
118
+ }
119
+ }
120
+ if (manifest.expected_resources.assets) {
121
+ for (const entry of collectFiles(skillDir, join(skillDir, "assets"))) {
122
+ trackedPaths.add(entry);
123
+ }
124
+ }
125
+
126
+ const hasher = createHash("sha256");
127
+ hasher.update("selftune:create-package:v1\0");
128
+ hasher.update(`${relative(skillDir, skillPath) || "SKILL.md"}\0`);
129
+
130
+ for (const relativePath of [...trackedPaths].toSorted()) {
131
+ const absolutePath = join(skillDir, relativePath);
132
+ if (!existsSync(absolutePath)) continue;
133
+ const stat = statSync(absolutePath);
134
+ if (!stat.isFile()) continue;
135
+ hasher.update(relativePath);
136
+ hasher.update("\0");
137
+ hasher.update(readFileSync(absolutePath));
138
+ hasher.update("\0");
139
+ }
140
+
141
+ return `pkg_sha256_${hasher.digest("hex").slice(0, 16)}`;
142
+ }
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Bounded package search runner.
3
+ *
4
+ * Orchestrates a minibatch of candidate evaluations against the accepted
5
+ * frontier parent. Candidates are passed in (mutation is external);
6
+ * this module only evaluates, compares, and persists results.
7
+ */
8
+
9
+ import { cpSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { basename, dirname, join } from "node:path";
12
+
13
+ import { randomUUIDv7 } from "bun";
14
+ import type { Database } from "bun:sqlite";
15
+ import type { PackageSearchProvenance, PackageSearchRunResult } from "../types.js";
16
+ import { parseSkillSections, replaceSection } from "../evolution/deploy-proposal.js";
17
+ import {
18
+ listAcceptedPackageFrontierCandidates,
19
+ readPackageCandidateArtifactByFingerprint,
20
+ selectAcceptedPackageFrontierCandidate,
21
+ } from "./package-candidate-state.js";
22
+ import { computeCreatePackageFingerprint } from "./package-fingerprint.js";
23
+ import {
24
+ runCreatePackageEvaluation,
25
+ type CreatePackageEvaluationDeps,
26
+ } from "./package-evaluator.js";
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Search options
30
+ // ---------------------------------------------------------------------------
31
+
32
+ export interface PackageSearchOptions {
33
+ /** Skill name to search packages for. */
34
+ skill_name: string;
35
+ /** Candidate variant paths to evaluate this run. */
36
+ candidate_paths: Array<{
37
+ skill_path: string;
38
+ fingerprint: string;
39
+ mutation_surface?: "routing" | "body" | "merged";
40
+ }>;
41
+ /** Maximum candidates to evaluate per run (minibatch size). Default 5. */
42
+ max_candidates?: number;
43
+ /** Optional measured routing/body budget used to build this search run. */
44
+ surface_plan?: PackageSearchProvenance["surface_plan"];
45
+ /** Database handle. */
46
+ db: Database;
47
+ /** Agent identifier for replay. */
48
+ agent?: string;
49
+ /** Optional eval-set override for package evaluation. */
50
+ evalSetPath?: string;
51
+ /** Optional evaluator dependency overrides. */
52
+ evaluator_deps?: CreatePackageEvaluationDeps;
53
+ }
54
+
55
+ type EvaluatedCandidate = {
56
+ candidateId: string;
57
+ decision: string;
58
+ rationale: string;
59
+ skillPath: string;
60
+ fingerprint: string;
61
+ mutationSurface: "routing" | "body" | "merged" | null;
62
+ evaluation: Awaited<ReturnType<typeof runCreatePackageEvaluation>>;
63
+ };
64
+
65
+ function mergeComplementarySkillCandidates(
66
+ routingSkillPath: string,
67
+ bodySkillPath: string,
68
+ ): string {
69
+ const routingContent = readFileSync(routingSkillPath, "utf-8");
70
+ const bodyContent = readFileSync(bodySkillPath, "utf-8");
71
+ const routingSection = parseSkillSections(routingContent).sections["Workflow Routing"] ?? "";
72
+ if (!routingSection.trim()) {
73
+ throw new Error(
74
+ `Routing variant at ${routingSkillPath} does not contain a Workflow Routing section`,
75
+ );
76
+ }
77
+
78
+ const mergedContent = replaceSection(bodyContent, "Workflow Routing", routingSection.trim());
79
+ const bodyVariantDir = dirname(bodySkillPath);
80
+ const mergedVariantDir = join(
81
+ mkdtempSync(join(tmpdir(), "selftune-package-search-merged-")),
82
+ basename(bodyVariantDir),
83
+ );
84
+ cpSync(bodyVariantDir, mergedVariantDir, { recursive: true });
85
+
86
+ const mergedSkillPath = join(mergedVariantDir, basename(bodySkillPath));
87
+ writeFileSync(mergedSkillPath, mergedContent, "utf-8");
88
+ return mergedSkillPath;
89
+ }
90
+
91
+ function pickBestAcceptedCandidate(
92
+ candidates: EvaluatedCandidate[],
93
+ surface: "routing" | "body",
94
+ ): EvaluatedCandidate | null {
95
+ const matching = candidates.filter(
96
+ (candidate) => candidate.decision === "accepted" && candidate.mutationSurface === surface,
97
+ );
98
+ if (matching.length === 0) return null;
99
+
100
+ return matching.toSorted((left, right) => {
101
+ if (surface === "routing") {
102
+ const leftScore =
103
+ left.evaluation.summary.routing?.pass_rate ?? left.evaluation.summary.replay.pass_rate;
104
+ const rightScore =
105
+ right.evaluation.summary.routing?.pass_rate ?? right.evaluation.summary.replay.pass_rate;
106
+ return rightScore - leftScore;
107
+ }
108
+
109
+ const leftBody = left.evaluation.summary.body;
110
+ const rightBody = right.evaluation.summary.body;
111
+ const leftValid = leftBody?.valid ? 1 : 0;
112
+ const rightValid = rightBody?.valid ? 1 : 0;
113
+ if (rightValid !== leftValid) {
114
+ return rightValid - leftValid;
115
+ }
116
+
117
+ return (rightBody?.quality_score ?? -1) - (leftBody?.quality_score ?? -1);
118
+ })[0]!;
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Search persistence
123
+ // ---------------------------------------------------------------------------
124
+
125
+ /** Persist a search run result to the package_search_runs table. */
126
+ export function insertSearchRun(db: Database, result: PackageSearchRunResult): void {
127
+ db.run(
128
+ `INSERT INTO package_search_runs
129
+ (search_id, skill_name, parent_candidate_id, winner_candidate_id,
130
+ winner_rationale, candidates_evaluated, provenance_json,
131
+ started_at, completed_at)
132
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
133
+ [
134
+ result.search_id,
135
+ result.skill_name,
136
+ result.parent_candidate_id,
137
+ result.winner_candidate_id,
138
+ result.winner_rationale,
139
+ result.candidates_evaluated,
140
+ JSON.stringify(result.provenance),
141
+ result.started_at,
142
+ result.completed_at,
143
+ ],
144
+ );
145
+ }
146
+
147
+ /** Read all search runs for a skill, newest first. */
148
+ export function readSearchRuns(db: Database, skillName: string): PackageSearchRunResult[] {
149
+ const rows = db
150
+ .query(
151
+ `SELECT search_id, skill_name, parent_candidate_id, winner_candidate_id,
152
+ winner_rationale, candidates_evaluated, provenance_json,
153
+ started_at, completed_at
154
+ FROM package_search_runs
155
+ WHERE skill_name = ?
156
+ ORDER BY started_at DESC`,
157
+ )
158
+ .all(skillName) as Array<{
159
+ search_id: string;
160
+ skill_name: string;
161
+ parent_candidate_id: string | null;
162
+ winner_candidate_id: string | null;
163
+ winner_rationale: string | null;
164
+ candidates_evaluated: number;
165
+ provenance_json: string;
166
+ started_at: string;
167
+ completed_at: string;
168
+ }>;
169
+
170
+ return rows.map((r) => ({
171
+ search_id: r.search_id,
172
+ skill_name: r.skill_name,
173
+ parent_candidate_id: r.parent_candidate_id,
174
+ candidates_evaluated: r.candidates_evaluated,
175
+ winner_candidate_id: r.winner_candidate_id,
176
+ winner_rationale: r.winner_rationale,
177
+ started_at: r.started_at,
178
+ completed_at: r.completed_at,
179
+ provenance: JSON.parse(r.provenance_json) as PackageSearchProvenance,
180
+ }));
181
+ }
182
+
183
+ function selectWinningCandidate(
184
+ skillName: string,
185
+ evaluatedCandidateIds: Set<string>,
186
+ db: Database,
187
+ ): {
188
+ winnerCandidateId: string | null;
189
+ winnerRationale: string | null;
190
+ } {
191
+ if (evaluatedCandidateIds.size === 0) {
192
+ return {
193
+ winnerCandidateId: null,
194
+ winnerRationale: null,
195
+ };
196
+ }
197
+
198
+ const winner =
199
+ listAcceptedPackageFrontierCandidates(skillName, db).find((candidate) =>
200
+ evaluatedCandidateIds.has(candidate.candidate_id),
201
+ ) ?? null;
202
+
203
+ return {
204
+ winnerCandidateId: winner?.candidate_id ?? null,
205
+ winnerRationale: winner?.summary.candidate_acceptance?.rationale ?? null,
206
+ };
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // Search runner
211
+ // ---------------------------------------------------------------------------
212
+
213
+ /**
214
+ * Run a bounded package search.
215
+ *
216
+ * 1. Reads the accepted frontier for the skill
217
+ * 2. Selects a parent from the frontier (or null for first-ever run)
218
+ * 3. Evaluates each candidate (up to max_candidates) through the evaluator
219
+ * 4. Compares results, picks the best accepted winner using frontier ranking
220
+ * 5. Persists the search run with full provenance
221
+ */
222
+ export async function runPackageSearch(
223
+ opts: PackageSearchOptions,
224
+ ): Promise<PackageSearchRunResult> {
225
+ const startedAt = new Date().toISOString();
226
+ const searchId = randomUUIDv7();
227
+ const maxCandidates = opts.max_candidates ?? 5;
228
+
229
+ // 1. Read frontier and select parent
230
+ const frontier = listAcceptedPackageFrontierCandidates(opts.skill_name, opts.db);
231
+ const parent = selectAcceptedPackageFrontierCandidate(opts.skill_name, { db: opts.db });
232
+
233
+ // 2. Filter candidates: skip already-evaluated fingerprints, cap at maxCandidates
234
+ const candidatesToEvaluate = opts.candidate_paths
235
+ .filter((c) => {
236
+ const existing = readPackageCandidateArtifactByFingerprint(opts.skill_name, c.fingerprint, {
237
+ db: opts.db,
238
+ });
239
+ return existing === null;
240
+ })
241
+ .slice(0, maxCandidates);
242
+
243
+ // 3. Evaluate each candidate through the shared package evaluator
244
+ const evaluationSummaries: PackageSearchProvenance["evaluation_summaries"] = [];
245
+ const acceptedCandidateIds = new Set<string>();
246
+ const evaluatedCandidates: EvaluatedCandidate[] = [];
247
+
248
+ const deps: CreatePackageEvaluationDeps = {
249
+ ...opts.evaluator_deps,
250
+ getDb: () => opts.db,
251
+ };
252
+
253
+ for (const candidate of candidatesToEvaluate) {
254
+ const evaluation = await runCreatePackageEvaluation(
255
+ {
256
+ skillPath: candidate.skill_path,
257
+ skillName: opts.skill_name,
258
+ mode: "package",
259
+ agent: opts.agent,
260
+ evalSetPath: opts.evalSetPath,
261
+ },
262
+ deps,
263
+ );
264
+
265
+ const acceptance = evaluation.summary.candidate_acceptance;
266
+ const decision = acceptance?.decision ?? "rejected";
267
+ const rationale = acceptance?.rationale ?? "No acceptance summary produced.";
268
+ const candidateId = evaluation.summary.candidate_id ?? candidate.fingerprint;
269
+ const mutationSurface = candidate.mutation_surface ?? null;
270
+
271
+ evaluationSummaries.push({
272
+ candidate_id: candidateId,
273
+ decision,
274
+ rationale,
275
+ });
276
+ evaluatedCandidates.push({
277
+ candidateId,
278
+ decision,
279
+ rationale,
280
+ skillPath: candidate.skill_path,
281
+ fingerprint: candidate.fingerprint,
282
+ mutationSurface,
283
+ evaluation,
284
+ });
285
+ if (decision === "accepted") {
286
+ acceptedCandidateIds.add(candidateId);
287
+ }
288
+ }
289
+
290
+ const acceptedRoutingCandidate = pickBestAcceptedCandidate(evaluatedCandidates, "routing");
291
+ const acceptedBodyCandidate = pickBestAcceptedCandidate(evaluatedCandidates, "body");
292
+
293
+ if (acceptedRoutingCandidate && acceptedBodyCandidate) {
294
+ const mergedVariantPath = mergeComplementarySkillCandidates(
295
+ acceptedRoutingCandidate.skillPath,
296
+ acceptedBodyCandidate.skillPath,
297
+ );
298
+
299
+ const mergedFingerprint = computeCreatePackageFingerprint(mergedVariantPath);
300
+ if (mergedFingerprint) {
301
+ const mergedEvaluation = await runCreatePackageEvaluation(
302
+ {
303
+ skillPath: mergedVariantPath,
304
+ skillName: opts.skill_name,
305
+ mode: "package",
306
+ agent: opts.agent,
307
+ evalSetPath: opts.evalSetPath,
308
+ },
309
+ deps,
310
+ );
311
+
312
+ const mergedAcceptance = mergedEvaluation.summary.candidate_acceptance;
313
+ const mergedDecision = mergedAcceptance?.decision ?? "rejected";
314
+ const mergedRationalePrefix = `Merged accepted routing ${acceptedRoutingCandidate.candidateId} with accepted body ${acceptedBodyCandidate.candidateId}.`;
315
+ const mergedRationale = mergedAcceptance?.rationale
316
+ ? `${mergedRationalePrefix} ${mergedAcceptance.rationale}`
317
+ : mergedRationalePrefix;
318
+ const mergedCandidateId = mergedEvaluation.summary.candidate_id ?? mergedFingerprint;
319
+
320
+ evaluationSummaries.push({
321
+ candidate_id: mergedCandidateId,
322
+ decision: mergedDecision,
323
+ rationale: mergedRationale,
324
+ });
325
+ evaluatedCandidates.push({
326
+ candidateId: mergedCandidateId,
327
+ decision: mergedDecision,
328
+ rationale: mergedRationale,
329
+ skillPath: mergedVariantPath,
330
+ fingerprint: mergedFingerprint,
331
+ mutationSurface: "merged",
332
+ evaluation: mergedEvaluation,
333
+ });
334
+ candidatesToEvaluate.push({
335
+ skill_path: mergedVariantPath,
336
+ fingerprint: mergedFingerprint,
337
+ mutation_surface: "merged",
338
+ });
339
+ if (mergedDecision === "accepted") {
340
+ acceptedCandidateIds.add(mergedCandidateId);
341
+ }
342
+ }
343
+ }
344
+
345
+ const completedAt = new Date().toISOString();
346
+ const { winnerCandidateId, winnerRationale } = selectWinningCandidate(
347
+ opts.skill_name,
348
+ acceptedCandidateIds,
349
+ opts.db,
350
+ );
351
+
352
+ // 4. Build result with provenance
353
+ const provenance: PackageSearchProvenance = {
354
+ frontier_size: frontier.length,
355
+ parent_selection_method: parent ? "highest_ranked_frontier" : "none_first_run",
356
+ candidate_fingerprints: candidatesToEvaluate.map((c) => c.fingerprint),
357
+ ...(opts.surface_plan ? { surface_plan: opts.surface_plan } : {}),
358
+ evaluation_summaries: evaluationSummaries,
359
+ };
360
+
361
+ const result: PackageSearchRunResult = {
362
+ search_id: searchId,
363
+ skill_name: opts.skill_name,
364
+ parent_candidate_id: parent?.candidate_id ?? null,
365
+ candidates_evaluated: candidatesToEvaluate.length,
366
+ winner_candidate_id: winnerCandidateId,
367
+ winner_rationale: winnerRationale,
368
+ started_at: startedAt,
369
+ completed_at: completedAt,
370
+ provenance,
371
+ };
372
+
373
+ // 5. Persist the search run
374
+ insertSearchRun(opts.db, result);
375
+
376
+ return result;
377
+ }