@farmslot/agent-runtime 0.1.1 → 0.2.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.
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
- Active-development baseline; add user-facing changes here before release or package publication.
|
|
6
6
|
|
|
7
|
+
## 0.2.0 - 2026-07-12
|
|
8
|
+
|
|
9
|
+
- fix: use workspace-linked `@farmslot/protocol` during local development so package builds cannot resolve a stale published sibling package.
|
|
10
|
+
- feat: the task-artifact contract check validates `artifacts/recipe.json` (envelope-only) and, when present, `artifacts/resolved-recipe.json` (in full, including flow-call resolution) against the shared Recipe Protocol v1 validator, with a local minimum-envelope fallback when `@farmslot/protocol` dist is not yet built.
|
|
11
|
+
|
|
7
12
|
## 0.1.1 - 2026-07-06
|
|
8
13
|
|
|
9
14
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farmslot/agent-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"prepublishOnly": "node ../../scripts/quality/check-farmslot-package-readiness.mjs --publish --packages @farmslot/agent-runtime"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@farmslot/protocol": "
|
|
34
|
+
"@farmslot/protocol": "0.8.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^22.0.0",
|
|
@@ -7,6 +7,11 @@ const require = createRequire(import.meta.url);
|
|
|
7
7
|
const { expandedArtifactsForCommand } = require('./worker-terminal-contract.cjs');
|
|
8
8
|
|
|
9
9
|
let sharedRecipeQualityValidator = null;
|
|
10
|
+
let sharedRecipeDocumentValidator = null;
|
|
11
|
+
// Mirror @farmslot/protocol recipeProtocolSchemaUrlForVersion: only v1 is a
|
|
12
|
+
// supported schema_version, so do not synthesize URLs for other versions.
|
|
13
|
+
let sharedRecipeSchemaUrlForVersion = (version) =>
|
|
14
|
+
version === 1 ? 'https://farmslot.io/schemas/recipe-v1.schema.json' : null;
|
|
10
15
|
try {
|
|
11
16
|
({ isRecipeQualityArtifact: sharedRecipeQualityValidator } =
|
|
12
17
|
await import('@farmslot/protocol/contracts/recipes'));
|
|
@@ -17,6 +22,20 @@ try {
|
|
|
17
22
|
console.warn(`[agent-runtime] using local RecipeQualityArtifact fallback: ${error.message}`);
|
|
18
23
|
}
|
|
19
24
|
}
|
|
25
|
+
try {
|
|
26
|
+
const recipeProtocol = await import('@farmslot/protocol/recipe');
|
|
27
|
+
sharedRecipeDocumentValidator = recipeProtocol.validateRecipeDocument;
|
|
28
|
+
if (typeof recipeProtocol.recipeProtocolSchemaUrlForVersion === 'function') {
|
|
29
|
+
sharedRecipeSchemaUrlForVersion = recipeProtocol.recipeProtocolSchemaUrlForVersion;
|
|
30
|
+
}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
// Compatibility fail-safe: source checkouts can run before @farmslot/protocol
|
|
33
|
+
// has emitted dist/. In that degraded state, do not bypass recipe checks;
|
|
34
|
+
// enforce the minimum executable envelope locally until the shared validator loads.
|
|
35
|
+
if (process.env.FARMSLOT_DEBUG_AGENT_RUNTIME) {
|
|
36
|
+
console.warn(`[agent-runtime] using local Recipe v1 fallback: ${error.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
20
39
|
|
|
21
40
|
const taskDir = process.argv[2];
|
|
22
41
|
const rawArgs = process.argv.slice(3);
|
|
@@ -226,6 +245,83 @@ function validateRecipeQualityArtifact() {
|
|
|
226
245
|
}
|
|
227
246
|
}
|
|
228
247
|
|
|
248
|
+
function validateRecipeDocumentArtifact() {
|
|
249
|
+
// Mirror the artifact-package contract (protocol/recipe/artifact.ts): a failed run
|
|
250
|
+
// keeps recipe.json envelope-only and does not re-validate the composition, so a
|
|
251
|
+
// graceful failure (e.g. a flow cycle) is not rejected here.
|
|
252
|
+
const summaryText = readText('artifacts/summary.json');
|
|
253
|
+
let runFailed = false;
|
|
254
|
+
if (summaryText) {
|
|
255
|
+
try {
|
|
256
|
+
runFailed = JSON.parse(summaryText)?.status === 'fail';
|
|
257
|
+
} catch {
|
|
258
|
+
// A malformed summary is another check's concern; without a confirmed failure
|
|
259
|
+
// status, enforce the stricter (passing-run) validation as the safe default.
|
|
260
|
+
runFailed = false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (runFailed) {
|
|
264
|
+
validateRecipeDocumentArtifactAt('artifacts/recipe.json', 'recipe.json', {
|
|
265
|
+
skipFlowCallResolution: true,
|
|
266
|
+
});
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// Passing/unknown run: the composition must be proven — recipe.json's call.refs are
|
|
270
|
+
// proven either by resolved-recipe.json (validated in full, so recipe.json is
|
|
271
|
+
// envelope-only) or by recipe.json being self-contained (validated in full).
|
|
272
|
+
const hasResolvedRecipe = fileExists('artifacts/resolved-recipe.json');
|
|
273
|
+
validateRecipeDocumentArtifactAt('artifacts/recipe.json', 'recipe.json', {
|
|
274
|
+
skipFlowCallResolution: hasResolvedRecipe,
|
|
275
|
+
externalCatalogsResolvable: false,
|
|
276
|
+
});
|
|
277
|
+
validateRecipeDocumentArtifactAt('artifacts/resolved-recipe.json', 'resolved-recipe.json', {});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function validateRecipeDocumentArtifactAt(relPath, label, options) {
|
|
281
|
+
const text = readText(relPath);
|
|
282
|
+
if (!text) return;
|
|
283
|
+
let recipe;
|
|
284
|
+
try {
|
|
285
|
+
recipe = JSON.parse(text);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
issues.push(`${label}: invalid JSON: ${error.message}`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (sharedRecipeDocumentValidator) {
|
|
291
|
+
const result = sharedRecipeDocumentValidator(recipe, options);
|
|
292
|
+
for (const finding of result.findings) {
|
|
293
|
+
if (finding.severity === 'error') {
|
|
294
|
+
issues.push(`${label}: ${finding.code} ${finding.path}: ${finding.message}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (!isRecord(recipe)) {
|
|
300
|
+
issues.push(`${label}: expected object`);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (recipe.schema_version !== 1) {
|
|
304
|
+
issues.push(`${label}: schema_version must equal 1`);
|
|
305
|
+
}
|
|
306
|
+
const expectedSchemaUrl = sharedRecipeSchemaUrlForVersion(recipe.schema_version);
|
|
307
|
+
if (recipe.$schema != null && recipe.$schema !== expectedSchemaUrl) {
|
|
308
|
+
issues.push(
|
|
309
|
+
`${label}: $schema must match schema_version (${expectedSchemaUrl ?? 'unknown schema_version'})`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
if (!isRecord(recipe.validate) || !isRecord(recipe.validate.workflow)) {
|
|
313
|
+
issues.push(`${label}: validate.workflow is required`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const workflow = recipe.validate.workflow;
|
|
317
|
+
if (typeof workflow.entry !== 'string' || !workflow.entry.trim()) {
|
|
318
|
+
issues.push(`${label}: validate.workflow.entry must be a non-empty string`);
|
|
319
|
+
}
|
|
320
|
+
if (!isRecord(workflow.nodes) || Object.keys(workflow.nodes).length === 0) {
|
|
321
|
+
issues.push(`${label}: validate.workflow.nodes must be a non-empty object`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
229
325
|
function parseManifest() {
|
|
230
326
|
const text = readText('artifacts/evidence-manifest.json');
|
|
231
327
|
if (!text) return null;
|
|
@@ -339,6 +435,7 @@ function parseManifest() {
|
|
|
339
435
|
}
|
|
340
436
|
|
|
341
437
|
const hasRecipe = fileExists('artifacts/recipe.json');
|
|
438
|
+
validateRecipeDocumentArtifact();
|
|
342
439
|
if (
|
|
343
440
|
hasRecipe &&
|
|
344
441
|
flags.has('--require-recipe-quality-if-recipe') &&
|