@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.1.1",
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": ">=0.7.4"
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') &&