@farmslot/skills 0.1.0

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.
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { spawnSync } = require('node:child_process');
7
+ const {
8
+ appendJsonl,
9
+ assert,
10
+ buildLaneResult,
11
+ computeAggregateScore,
12
+ computeConvergenceState,
13
+ decisionFromScores,
14
+ normalizeRepoPath,
15
+ readJsonIfExists,
16
+ validateLearningArtifact,
17
+ writeJson,
18
+ } = require('./lib.cjs');
19
+
20
+ const RECIPE_COOK_PATCH_ROOTS = [
21
+ 'packages/skills/skills/recipe-cook',
22
+ 'recipe-cook',
23
+ '.agents/skills/recipe-cook',
24
+ '.codex/skills/recipe-cook',
25
+ '.claude/skills/recipe-cook',
26
+ '.cursor/skills/recipe-cook',
27
+ ];
28
+
29
+ const RECIPE_COOK_PATCH_FILES = new Set([
30
+ 'SKILL.md',
31
+ 'TASK.md',
32
+ 'references/TASK.md',
33
+ 'references/VALIDATOR-LOOP.md',
34
+ ]);
35
+
36
+ function splitRecipeCookPatchPath(filePath) {
37
+ const normalized = normalizeRepoPath(filePath);
38
+ for (const root of RECIPE_COOK_PATCH_ROOTS) {
39
+ const prefix = `${root}/`;
40
+ if (normalized.startsWith(prefix)) {
41
+ return {
42
+ root,
43
+ relativePath: normalized.slice(prefix.length),
44
+ };
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+
50
+ function isAllowedRecipeCookPatchPath(filePath) {
51
+ const parsed = splitRecipeCookPatchPath(filePath);
52
+ return Boolean(
53
+ parsed &&
54
+ (RECIPE_COOK_PATCH_FILES.has(parsed.relativePath) ||
55
+ /^repos\/[^/]+\.md$/.test(parsed.relativePath)),
56
+ );
57
+ }
58
+
59
+ function parseArgs(argv = process.argv.slice(2)) {
60
+ const result = {
61
+ versionId: '',
62
+ parentVersionId: '',
63
+ iterationId: '',
64
+ runId: '',
65
+ scenarioConfig: path.resolve(__dirname, '../templates/recipe-cook/scenarios.json'),
66
+ touchedFiles: [],
67
+ };
68
+ for (let i = 0; i < argv.length; i += 1) {
69
+ switch (argv[i]) {
70
+ case '--version-id':
71
+ result.versionId = argv[++i] || '';
72
+ break;
73
+ case '--parent-version-id':
74
+ result.parentVersionId = argv[++i] || '';
75
+ break;
76
+ case '--iteration-id':
77
+ result.iterationId = argv[++i] || '';
78
+ break;
79
+ case '--run-id':
80
+ result.runId = argv[++i] || '';
81
+ break;
82
+ case '--scenario-config':
83
+ result.scenarioConfig = path.resolve(argv[++i] || '');
84
+ break;
85
+ case '--touched-file':
86
+ result.touchedFiles.push(argv[++i] || '');
87
+ break;
88
+ default:
89
+ break;
90
+ }
91
+ }
92
+ return result;
93
+ }
94
+
95
+ function createPatchArtifact(
96
+ baseDir,
97
+ versionId,
98
+ parentVersionId,
99
+ touchedFiles,
100
+ scenarioIds,
101
+ expectedScoreImpact,
102
+ ) {
103
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
104
+ const slug = `${versionId || 'version'}-${scenarioIds.join('-') || 'scenario'}`.replace(
105
+ /[^a-zA-Z0-9-_]/g,
106
+ '-',
107
+ );
108
+ const patchPath = path.join(
109
+ baseDir,
110
+ '.omx',
111
+ 'artifacts',
112
+ 'recipe-cook-patches',
113
+ `${timestamp}-${slug}.patch`,
114
+ );
115
+ fs.mkdirSync(path.dirname(patchPath), { recursive: true });
116
+ const normalizedTouched = touchedFiles.filter(Boolean);
117
+ const patchParts = [];
118
+ normalizedTouched.forEach((file) => {
119
+ const absFile = path.resolve(baseDir, file);
120
+ if (!fs.existsSync(absFile)) return;
121
+ const tracked = spawnSync('git', ['ls-files', '--error-unmatch', file], {
122
+ cwd: baseDir,
123
+ encoding: 'utf8',
124
+ });
125
+ const diff =
126
+ tracked.status === 0
127
+ ? spawnSync('git', ['diff', '--binary', '--', file], { cwd: baseDir, encoding: 'utf8' })
128
+ : spawnSync('git', ['diff', '--binary', '--no-index', '--', '/dev/null', file], {
129
+ cwd: baseDir,
130
+ encoding: 'utf8',
131
+ });
132
+ if (diff.stdout) patchParts.push(diff.stdout);
133
+ });
134
+ const header = [
135
+ '# Recipe Cook validator patch',
136
+ `# before_version_id: ${parentVersionId}`,
137
+ `# after_version_id: ${versionId}`,
138
+ `# scenario_ids: ${scenarioIds.join(',')}`,
139
+ `# expected_score_impact: ${expectedScoreImpact}`,
140
+ ].join('\n');
141
+ fs.writeFileSync(patchPath, `${header}\n\n${patchParts.join('\n')}`, 'utf8');
142
+
143
+ const patchMetaPath = `${patchPath}.json`;
144
+ writeJson(patchMetaPath, {
145
+ before_version_id: parentVersionId,
146
+ after_version_id: versionId,
147
+ scenario_ids: scenarioIds,
148
+ touched_files: normalizedTouched,
149
+ expected_score_impact: expectedScoreImpact,
150
+ });
151
+
152
+ return { patchPath, patchMetaPath };
153
+ }
154
+
155
+ function loadVersionLedger(filePath) {
156
+ if (!fs.existsSync(filePath)) {
157
+ return { versions: [] };
158
+ }
159
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
160
+ }
161
+
162
+ function main() {
163
+ const args = parseArgs();
164
+ const baseDir = process.cwd();
165
+ const illegalTouched = args.touchedFiles.filter((file) => !isAllowedRecipeCookPatchPath(file));
166
+ assert(
167
+ illegalTouched.length === 0,
168
+ `Validator touched-file outside approved write scope: ${illegalTouched.join(', ')}`,
169
+ );
170
+ const scenarioConfig = readJsonIfExists(args.scenarioConfig);
171
+ if (!scenarioConfig || !Array.isArray(scenarioConfig.scenarios)) {
172
+ throw new Error(`Invalid scenario config: ${args.scenarioConfig}`);
173
+ }
174
+ const lanes = new Set(scenarioConfig.scenarios.map((scenario) => scenario.lane));
175
+ ['review', 'fix', 'investigation'].forEach((lane) => {
176
+ assert(lanes.has(lane), `Scenario config missing required lane: ${lane}`);
177
+ });
178
+ ['review-explore-markets-live', 'fix-zero-balance-cta', 'investigation-probe-output'].forEach(
179
+ (scenarioId) => {
180
+ assert(
181
+ scenarioConfig.scenarios.some((scenario) => scenario.id === scenarioId),
182
+ `Scenario config missing required canonical scenario: ${scenarioId}`,
183
+ );
184
+ },
185
+ );
186
+ assert(
187
+ scenarioConfig.scenarios.length === 3,
188
+ 'Scenario config must contain exactly the v1 canonical scenario bundle',
189
+ );
190
+
191
+ const laneResults = scenarioConfig.scenarios.map((scenario) => {
192
+ const artifactDir = path.resolve(baseDir, scenario.task_artifact_dir);
193
+ const learningArtifactPath = path.join(artifactDir, 'recipe-cook-learning.json');
194
+ const learningArtifact = readJsonIfExists(learningArtifactPath);
195
+ assert(learningArtifact, `Missing learning artifact: ${learningArtifactPath}`);
196
+ validateLearningArtifact(learningArtifact, artifactDir);
197
+ return buildLaneResult({
198
+ lane: scenario.lane,
199
+ scenarioId: scenario.id,
200
+ artifactDir,
201
+ learningArtifactPath,
202
+ });
203
+ });
204
+
205
+ const manifestPath = path.join(
206
+ baseDir,
207
+ '.omx',
208
+ 'artifacts',
209
+ 'recipe-cook-runs',
210
+ `${args.runId}.json`,
211
+ );
212
+ const runsPath = path.join(baseDir, '.omx', 'state', 'recipe-cook', 'runs.jsonl');
213
+ const versionsPath = path.join(baseDir, '.omx', 'state', 'recipe-cook', 'versions.json');
214
+ const versionLedger = loadVersionLedger(versionsPath);
215
+ const previousKept = [...versionLedger.versions]
216
+ .reverse()
217
+ .find((entry) => entry.decision === 'keep');
218
+ const currentRawCost = computeAggregateScore(laneResults, null).raw_inference_cost_usd;
219
+ const previousRawCost =
220
+ previousKept && previousKept.aggregate_score_vector
221
+ ? previousKept.aggregate_score_vector.raw_inference_cost_usd
222
+ : null;
223
+ const costBaseline =
224
+ currentRawCost == null && previousRawCost == null
225
+ ? null
226
+ : Math.max(currentRawCost ?? 0, previousRawCost ?? 0);
227
+ const scoreVector = computeAggregateScore(
228
+ laneResults,
229
+ costBaseline && costBaseline > 0 ? costBaseline : null,
230
+ );
231
+ if (
232
+ previousKept &&
233
+ previousKept.aggregate_score_vector &&
234
+ previousRawCost != null &&
235
+ costBaseline &&
236
+ costBaseline > 0
237
+ ) {
238
+ previousKept.aggregate_score_vector = {
239
+ ...previousKept.aggregate_score_vector,
240
+ cost_efficiency: 1 - Math.min(previousRawCost / costBaseline, 1),
241
+ balanced_score:
242
+ previousKept.aggregate_score_vector.quality * 0.45 +
243
+ previousKept.aggregate_score_vector.pass_rate * 0.4 +
244
+ (1 - Math.min(previousRawCost / costBaseline, 1)) * 0.15,
245
+ };
246
+ }
247
+ const autoDecision = decisionFromScores(scoreVector, laneResults, previousKept);
248
+ const finalDecision = autoDecision.decision;
249
+ const finalReason = autoDecision.reason;
250
+ const diffArgs = ['diff', '--name-only', '--', ...RECIPE_COOK_PATCH_ROOTS];
251
+ const currentRecipeCookDiff = spawnSync('git', diffArgs, { cwd: baseDir, encoding: 'utf8' });
252
+ const currentTouchedRecipeCook = currentRecipeCookDiff.stdout
253
+ .split('\n')
254
+ .map((line) => line.trim())
255
+ .filter(Boolean);
256
+ const allowedTouched = new Set(args.touchedFiles.filter(Boolean).map(normalizeRepoPath));
257
+ const unexpectedRecipeCookChanges = currentTouchedRecipeCook.filter(
258
+ (file) => !allowedTouched.has(normalizeRepoPath(file)),
259
+ );
260
+ assert(
261
+ unexpectedRecipeCookChanges.length === 0,
262
+ `Unexpected recipe-cook diff outside validator touched-files: ${unexpectedRecipeCookChanges.join(', ')}`,
263
+ );
264
+
265
+ const { patchPath } = createPatchArtifact(
266
+ baseDir,
267
+ args.versionId,
268
+ args.parentVersionId,
269
+ args.touchedFiles,
270
+ scenarioConfig.scenarios.map((scenario) => scenario.id),
271
+ finalReason,
272
+ );
273
+
274
+ const manifest = {
275
+ run_id: args.runId,
276
+ iteration_id: args.iterationId,
277
+ base_version_id: args.parentVersionId,
278
+ task_artifact_dir: laneResults[0].task_artifact_dir,
279
+ learning_artifact_path: laneResults[0].learning_artifact_path,
280
+ recipe_path: laneResults[0].recipe_path,
281
+ recipe_cook_path: laneResults[0].recipe_cook_path,
282
+ candidate_patch_path: patchPath,
283
+ validator_manifest_path: manifestPath,
284
+ touched_files: args.touchedFiles.filter(Boolean),
285
+ scenario_bundle: scenarioConfig.scenarios.map((scenario) => scenario.id),
286
+ lane_results: laneResults,
287
+ score_vector: scoreVector,
288
+ decision: finalDecision,
289
+ decision_reason: finalReason,
290
+ };
291
+ writeJson(manifestPath, manifest);
292
+
293
+ laneResults.forEach((laneResult) => {
294
+ appendJsonl(runsPath, {
295
+ run_id: args.runId,
296
+ version_id_under_test: args.versionId,
297
+ task_artifact_dir: laneResult.task_artifact_dir,
298
+ learning_artifact_path: laneResult.learning_artifact_path,
299
+ recipe_path: laneResult.recipe_path,
300
+ recipe_cook_path: laneResult.recipe_cook_path,
301
+ validator_manifest_path: manifestPath,
302
+ scenario_lane: laneResult.lane,
303
+ result_summary: finalReason,
304
+ quality_semantic: laneResult.quality_semantic,
305
+ quality: laneResult.quality,
306
+ pass_rate: laneResult.pass_rate,
307
+ inference_cost_usd: laneResult.inference_cost_usd,
308
+ source_run_id: args.runId,
309
+ parent_version_id: args.parentVersionId,
310
+ });
311
+ });
312
+
313
+ versionLedger.versions.push({
314
+ version_id: args.versionId,
315
+ parent_version_id: args.parentVersionId,
316
+ patch_artifact_path: patchPath,
317
+ source_run_id: args.runId,
318
+ source_validator_manifest_path: manifestPath,
319
+ source_task_artifact_dirs: laneResults.map((lane) => lane.task_artifact_dir),
320
+ source_learning_artifact_paths: laneResults.map((lane) => lane.learning_artifact_path),
321
+ scenarios_covered: scenarioConfig.scenarios.map((scenario) => scenario.id),
322
+ aggregate_score_vector: scoreVector,
323
+ decision: finalDecision,
324
+ decision_rationale: finalReason,
325
+ });
326
+ const updatedRuns = fs.existsSync(runsPath)
327
+ ? fs
328
+ .readFileSync(runsPath, 'utf8')
329
+ .split('\n')
330
+ .filter(Boolean)
331
+ .map((line) => JSON.parse(line))
332
+ : [];
333
+ versionLedger.convergence = computeConvergenceState(versionLedger, updatedRuns);
334
+ writeJson(versionsPath, versionLedger);
335
+
336
+ process.stdout.write(`${manifestPath}\n`);
337
+ }
338
+
339
+ main();