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.
- package/README.md +83 -56
- package/apps/local-dashboard/dist/assets/index-B-ut4w0B.js +15 -0
- package/apps/local-dashboard/dist/assets/index-BFGfCVrL.css +1 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-DfowE3Hu.js +1 -0
- package/apps/local-dashboard/dist/index.html +3 -3
- package/cli/selftune/command-surface.ts +613 -2
- package/cli/selftune/create/baseline.ts +429 -0
- package/cli/selftune/create/check.ts +35 -0
- package/cli/selftune/create/init.ts +115 -0
- package/cli/selftune/create/package-candidate-state.ts +771 -0
- package/cli/selftune/create/package-evaluator.ts +710 -0
- package/cli/selftune/create/package-fingerprint.ts +142 -0
- package/cli/selftune/create/package-search.ts +377 -0
- package/cli/selftune/create/publish.ts +431 -0
- package/cli/selftune/create/readiness.ts +495 -0
- package/cli/selftune/create/replay.ts +330 -0
- package/cli/selftune/create/report.ts +74 -0
- package/cli/selftune/create/scaffold.ts +121 -0
- package/cli/selftune/create/skills-ref-adapter.ts +177 -0
- package/cli/selftune/create/status.ts +33 -0
- package/cli/selftune/create/templates.ts +249 -0
- package/cli/selftune/cron/setup.ts +1 -1
- package/cli/selftune/dashboard-action-events.ts +4 -1
- package/cli/selftune/dashboard-action-result.ts +789 -24
- package/cli/selftune/dashboard-action-stream.ts +80 -0
- package/cli/selftune/dashboard-contract.ts +146 -3
- package/cli/selftune/dashboard-server.ts +5 -4
- package/cli/selftune/eval/hooks-to-evals.ts +58 -35
- package/cli/selftune/eval/synthetic-evals.ts +145 -17
- package/cli/selftune/evolution/bounded-mutations.ts +1045 -0
- package/cli/selftune/evolution/evolve-body.ts +9 -36
- package/cli/selftune/evolution/evolve.ts +8 -72
- package/cli/selftune/evolution/stopping-criteria.ts +5 -13
- package/cli/selftune/evolution/unblock-suggestions.ts +0 -16
- package/cli/selftune/evolution/validate-host-replay.ts +115 -15
- package/cli/selftune/improve.ts +206 -0
- package/cli/selftune/index.ts +123 -6
- package/cli/selftune/init.ts +1 -1
- package/cli/selftune/localdb/queries/dashboard.ts +30 -0
- package/cli/selftune/localdb/schema.ts +52 -0
- package/cli/selftune/monitoring/watch.ts +257 -23
- package/cli/selftune/orchestrate/execute.ts +300 -1
- package/cli/selftune/orchestrate/finalize.ts +14 -0
- package/cli/selftune/orchestrate/plan.ts +22 -5
- package/cli/selftune/orchestrate/prepare.ts +59 -4
- package/cli/selftune/orchestrate/report.ts +1 -1
- package/cli/selftune/orchestrate.ts +34 -1
- package/cli/selftune/publish.ts +35 -0
- package/cli/selftune/routes/actions.ts +81 -15
- package/cli/selftune/routes/overview.ts +1 -1
- package/cli/selftune/routes/skill-report.ts +147 -2
- package/cli/selftune/run.ts +18 -0
- package/cli/selftune/schedule.ts +3 -3
- package/cli/selftune/search-run.ts +703 -0
- package/cli/selftune/status.ts +35 -11
- package/cli/selftune/testing-readiness.ts +431 -40
- package/cli/selftune/types.ts +316 -0
- package/cli/selftune/utils/eval-readiness.ts +1 -0
- package/cli/selftune/utils/json-output.ts +11 -0
- package/cli/selftune/utils/lifecycle-surface.ts +48 -0
- package/cli/selftune/utils/query-filter.ts +82 -1
- package/cli/selftune/utils/tui.ts +85 -2
- package/cli/selftune/verify.ts +205 -0
- package/cli/selftune/workflows/proposals.ts +1 -1
- package/cli/selftune/workflows/skill-scaffold.ts +141 -63
- package/cli/selftune/workflows/workflows.ts +4 -4
- package/package.json +1 -1
- package/skill/SKILL.md +148 -85
- package/skill/references/cli-quick-reference.md +16 -1
- package/skill/references/creator-playbook.md +31 -10
- package/skill/workflows/Baseline.md +8 -9
- package/skill/workflows/Contributions.md +4 -4
- package/skill/workflows/Create.md +173 -0
- package/skill/workflows/CreateTestDeploy.md +34 -30
- package/skill/workflows/Cron.md +2 -2
- package/skill/workflows/Dashboard.md +3 -3
- package/skill/workflows/Evals.md +13 -7
- package/skill/workflows/Evolve.md +75 -32
- package/skill/workflows/EvolveBody.md +22 -15
- package/skill/workflows/Hook.md +1 -1
- package/skill/workflows/Improve.md +168 -0
- package/skill/workflows/Initialize.md +3 -3
- package/skill/workflows/Orchestrate.md +49 -12
- package/skill/workflows/Publish.md +100 -0
- package/skill/workflows/Run.md +72 -0
- package/skill/workflows/Schedule.md +2 -2
- package/skill/workflows/SearchRun.md +89 -0
- package/skill/workflows/SignalsDashboard.md +2 -2
- package/skill/workflows/UnitTest.md +13 -4
- package/skill/workflows/Verify.md +136 -0
- package/skill/workflows/Watch.md +114 -47
- package/skill/workflows/Workflows.md +13 -8
- package/apps/local-dashboard/dist/assets/index-B7v_o1WC.js +0 -15
- package/apps/local-dashboard/dist/assets/index-CrO77SVi.css +0 -1
- 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
|
+
}
|