@farmslot/agent-runtime 0.2.0 → 0.3.1

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 (43) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/bin/farmslot-agent.mjs +6 -0
  3. package/dist/execution-template/create.d.ts +7 -0
  4. package/dist/execution-template/create.d.ts.map +1 -0
  5. package/dist/execution-template/create.js +63 -0
  6. package/dist/execution-template/create.js.map +1 -0
  7. package/dist/execution-template/execution-template.test.d.ts +2 -0
  8. package/dist/execution-template/execution-template.test.d.ts.map +1 -0
  9. package/dist/execution-template/execution-template.test.js +340 -0
  10. package/dist/execution-template/execution-template.test.js.map +1 -0
  11. package/dist/execution-template/frontmatter.d.ts +16 -0
  12. package/dist/execution-template/frontmatter.d.ts.map +1 -0
  13. package/dist/execution-template/frontmatter.js +110 -0
  14. package/dist/execution-template/frontmatter.js.map +1 -0
  15. package/dist/execution-template/index.d.ts +7 -0
  16. package/dist/execution-template/index.d.ts.map +1 -0
  17. package/dist/execution-template/index.js +6 -0
  18. package/dist/execution-template/index.js.map +1 -0
  19. package/dist/execution-template/infer.d.ts +25 -0
  20. package/dist/execution-template/infer.d.ts.map +1 -0
  21. package/dist/execution-template/infer.js +122 -0
  22. package/dist/execution-template/infer.js.map +1 -0
  23. package/dist/execution-template/lint.d.ts +6 -0
  24. package/dist/execution-template/lint.d.ts.map +1 -0
  25. package/dist/execution-template/lint.js +203 -0
  26. package/dist/execution-template/lint.js.map +1 -0
  27. package/dist/execution-template/resolve.d.ts +12 -0
  28. package/dist/execution-template/resolve.d.ts.map +1 -0
  29. package/dist/execution-template/resolve.js +126 -0
  30. package/dist/execution-template/resolve.js.map +1 -0
  31. package/dist/execution-template/types.d.ts +66 -0
  32. package/dist/execution-template/types.d.ts.map +1 -0
  33. package/dist/execution-template/types.js +3 -0
  34. package/dist/execution-template/types.js.map +1 -0
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/package.json +6 -4
  40. package/scripts/check-task-artifact-contract.mjs +202 -66
  41. package/scripts/execution-template-cli.mjs +203 -0
  42. package/scripts/mark-checklist-step.cjs +26 -8
  43. package/scripts/worker-terminal-contract.cjs +23 -21
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone execution-template CLI for ADR-049.
4
+ * Used by `farmslot-agent execution-template` and thin Consensys wrappers.
5
+ */
6
+ import { existsSync, statSync } from 'node:fs';
7
+ import { dirname, resolve } from 'node:path';
8
+ import { fileURLToPath, pathToFileURL } from 'node:url';
9
+
10
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
11
+ const distEntry = resolve(packageRoot, 'dist', 'index.js');
12
+
13
+ function usage(exitCode = 0) {
14
+ const text = [
15
+ 'Usage: execution-template <list|lint|new> [options]',
16
+ '',
17
+ 'list --dir <path> --project-worker <path> --package-templates <path>',
18
+ ' [--project-name name] [--package-id id] [--flow f] [--run-mode m]',
19
+ ' [--platform p] [--no-include-shadowed] [--json]',
20
+ 'lint <file-or-dir> [--json]',
21
+ 'new <path> [--flow f] [--run-mode m] [--platform p] [--title t] [--force] [--json]',
22
+ ].join('\n');
23
+ (exitCode === 0 ? console.log : console.error)(text);
24
+ process.exit(exitCode);
25
+ }
26
+
27
+ function takeValue(args, i, flag) {
28
+ const value = args[i + 1];
29
+ if (value === undefined) throw new Error(`${flag} requires a value`);
30
+ return value;
31
+ }
32
+
33
+ async function loadRuntime() {
34
+ if (!existsSync(distEntry)) {
35
+ throw new Error(
36
+ `compiled package missing at ${distEntry}; run yarn workspace @farmslot/agent-runtime build`,
37
+ );
38
+ }
39
+ return import(pathToFileURL(distEntry).href);
40
+ }
41
+
42
+ function parseRunMode(value) {
43
+ if (value === 'autonomous' || value === 'interactive' || value === 'validation') return value;
44
+ throw new Error(`--run-mode must be autonomous|interactive|validation (got ${value})`);
45
+ }
46
+
47
+ async function cmdList(args, runtime) {
48
+ const opts = {
49
+ dirs: [],
50
+ projectWorker: null,
51
+ projectName: 'project',
52
+ packageTemplates: null,
53
+ packageId: 'shared',
54
+ flow: undefined,
55
+ runMode: undefined,
56
+ platform: undefined,
57
+ includeShadowed: true,
58
+ json: false,
59
+ };
60
+ for (let i = 0; i < args.length; i += 1) {
61
+ const arg = args[i];
62
+ if (arg === '--dir') opts.dirs.push(takeValue(args, i++, arg));
63
+ else if (arg === '--project-worker') opts.projectWorker = takeValue(args, i++, arg);
64
+ else if (arg === '--project-name') opts.projectName = takeValue(args, i++, arg);
65
+ else if (arg === '--package-templates') opts.packageTemplates = takeValue(args, i++, arg);
66
+ else if (arg === '--package-id') opts.packageId = takeValue(args, i++, arg);
67
+ else if (arg === '--flow') opts.flow = takeValue(args, i++, arg);
68
+ else if (arg === '--run-mode') opts.runMode = parseRunMode(takeValue(args, i++, arg));
69
+ else if (arg === '--platform') opts.platform = takeValue(args, i++, arg);
70
+ else if (arg === '--include-shadowed') opts.includeShadowed = true;
71
+ else if (arg === '--no-include-shadowed') opts.includeShadowed = false;
72
+ else if (arg === '--json') opts.json = true;
73
+ else if (arg === '-h' || arg === '--help') usage(0);
74
+ else throw new Error(`unknown option ${arg}`);
75
+ }
76
+
77
+ const sources = [];
78
+ for (const [index, dir] of opts.dirs.entries()) {
79
+ const root = resolve(dir);
80
+ if (!existsSync(root) || !statSync(root).isDirectory()) {
81
+ throw new Error(`--dir is not a directory: ${root}`);
82
+ }
83
+ sources.push(runtime.customTemplateSource(`dir-${index + 1}`, root, 'flow-tree'));
84
+ }
85
+ if (opts.projectWorker) {
86
+ const input = resolve(opts.projectWorker);
87
+ const projectTemplatesDir =
88
+ input.endsWith('/worker') || input.endsWith('\\worker') ? resolve(input, '..') : input;
89
+ // An explicitly named source that does not exist is operator error — a
90
+ // silent empty catalog hides typos (worker/ is appended by the source).
91
+ const workerRoot = resolve(projectTemplatesDir, 'worker');
92
+ if (!existsSync(workerRoot) || !statSync(workerRoot).isDirectory()) {
93
+ throw new Error(`--project-worker is not a directory: ${workerRoot}`);
94
+ }
95
+ sources.push(runtime.projectWorkerTemplateSource(opts.projectName, projectTemplatesDir));
96
+ }
97
+ if (opts.packageTemplates) {
98
+ const root = resolve(opts.packageTemplates);
99
+ if (!existsSync(root) || !statSync(root).isDirectory()) {
100
+ throw new Error(`--package-templates is not a directory: ${root}`);
101
+ }
102
+ sources.push(runtime.packageFlowTreeTemplateSource(opts.packageId, root));
103
+ }
104
+ if (sources.length === 0) {
105
+ throw new Error('provide --dir, --project-worker, and/or --package-templates');
106
+ }
107
+
108
+ const templates = runtime.listExecutionTemplates({
109
+ sources,
110
+ flow: opts.flow,
111
+ runMode: opts.runMode,
112
+ platform: opts.platform,
113
+ includeShadowed: opts.includeShadowed,
114
+ });
115
+ if (opts.json) {
116
+ process.stdout.write(`${JSON.stringify({ templates }, null, 2)}\n`);
117
+ return;
118
+ }
119
+ for (const entry of templates) {
120
+ const shadow = entry.shadowedBy ? ` (shadowed by ${entry.shadowedBy})` : '';
121
+ process.stdout.write(
122
+ `${entry.id}\t${entry.flow}\t${entry.runMode ?? '-'}\t${entry.platforms.join(',')}\t${entry.sourceId}\t${entry.path}${shadow}\n`,
123
+ );
124
+ }
125
+ }
126
+
127
+ async function cmdLint(args, runtime) {
128
+ let target = null;
129
+ let json = false;
130
+ for (let i = 0; i < args.length; i += 1) {
131
+ const arg = args[i];
132
+ if (arg === '--json') json = true;
133
+ else if (arg === '-h' || arg === '--help') usage(0);
134
+ else if (!arg.startsWith('-') && !target) target = arg;
135
+ else throw new Error(`unknown option ${arg}`);
136
+ }
137
+ if (!target) usage(2);
138
+ const result = runtime.lintExecutionTemplates(target);
139
+ if (json) {
140
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
141
+ } else if (result.ok) {
142
+ process.stdout.write(`pass ${result.filesChecked} template(s)\n`);
143
+ } else {
144
+ for (const issue of result.issues) {
145
+ process.stderr.write(`${issue.severity} ${issue.path}: ${issue.message}\n`);
146
+ }
147
+ }
148
+ if (!result.ok) process.exitCode = 1;
149
+ }
150
+
151
+ async function cmdNew(args, runtime) {
152
+ let pathArg = null;
153
+ const opts = {
154
+ flow: undefined,
155
+ runMode: undefined,
156
+ platforms: undefined,
157
+ title: undefined,
158
+ force: false,
159
+ json: false,
160
+ };
161
+ for (let i = 0; i < args.length; i += 1) {
162
+ const arg = args[i];
163
+ if (arg === '--flow') opts.flow = takeValue(args, i++, arg);
164
+ else if (arg === '--run-mode') opts.runMode = parseRunMode(takeValue(args, i++, arg));
165
+ else if (arg === '--platform') {
166
+ opts.platforms = takeValue(args, i++, arg)
167
+ .split(',')
168
+ .map((p) => p.trim())
169
+ .filter(Boolean);
170
+ } else if (arg === '--title') opts.title = takeValue(args, i++, arg);
171
+ else if (arg === '--force') opts.force = true;
172
+ else if (arg === '--json') opts.json = true;
173
+ else if (arg === '-h' || arg === '--help') usage(0);
174
+ else if (!arg.startsWith('-') && !pathArg) pathArg = arg;
175
+ else throw new Error(`unknown option ${arg}`);
176
+ }
177
+ if (!pathArg) usage(2);
178
+ const created = runtime.createExecutionTemplate({
179
+ path: pathArg,
180
+ flow: opts.flow,
181
+ runMode: opts.runMode,
182
+ platforms: opts.platforms,
183
+ title: opts.title,
184
+ force: opts.force,
185
+ });
186
+ if (opts.json) process.stdout.write(`${JSON.stringify(created, null, 2)}\n`);
187
+ else process.stdout.write(`created ${created.path}\n`);
188
+ }
189
+
190
+ async function main() {
191
+ const [command, ...rest] = process.argv.slice(2);
192
+ if (!command || command === '-h' || command === '--help') usage(0);
193
+ const runtime = await loadRuntime();
194
+ if (command === 'list') await cmdList(rest, runtime);
195
+ else if (command === 'lint') await cmdLint(rest, runtime);
196
+ else if (command === 'new') await cmdNew(rest, runtime);
197
+ else usage(2);
198
+ }
199
+
200
+ main().catch((error) => {
201
+ console.error(error instanceof Error ? error.message : String(error));
202
+ process.exit(1);
203
+ });
@@ -19,13 +19,16 @@ const FLOW_REPORT_ARTIFACTS = {
19
19
  'review-pr': ['review.md', 'report.md'],
20
20
  dev: ['pr-description.md', 'report.md'],
21
21
  'pr-complete': ['comments-report.md', 'report.md'],
22
- 'merge-main': ['report.md', 'merge-report.md'],
22
+ 'update-branch': ['branch-update-report.md', 'report.md'],
23
+ 'ci-fix': ['report.md'],
24
+ 'self-review': ['review-feedback.md', 'report.md'],
25
+ 'self-review-fix': ['report.md'],
23
26
  };
24
27
  const FALLBACK_REPORT_ARTIFACTS = [
25
28
  'report.md',
26
29
  'review.md',
27
30
  'comments-report.md',
28
- 'merge-report.md',
31
+ 'branch-update-report.md',
29
32
  ];
30
33
 
31
34
  const usageLine =
@@ -44,8 +47,8 @@ function printHelp() {
44
47
  ' ./mark complete [--mark-last] [--no-self-review] [--skip-learnings] [--skip-checklist]',
45
48
  ' ./mark no-change --reason "..." [--already-fixed] [--mark-last] [--skip-learnings] [--skip-checklist]',
46
49
  ' ./mark blocked --reason "..." [--mark-last]',
47
- 'Terminal success paths require non-empty artifacts/learnings.md and a flow outcome artifact (complete) or no-change-report (no-change).',
48
- 'PR flows (dev, fix-bug): write artifacts/pr-description.md. Other flows: review.md, merge-report.md, report.md, etc.',
50
+ 'Terminal success paths require the flow contract artifacts: contribution flows need non-empty artifacts/learnings.md plus a flow outcome artifact (complete) or no-change-report (no-change); reviewer flows (self-review, self-review-fix) need their feedback/report artifact instead of learnings.',
51
+ 'PR flows (dev, fix-bug): write artifacts/pr-description.md. Other flows: review.md, review-feedback.md, branch-update-report.md, report.md, etc.',
49
52
  'With --mark-last, every checklist box must be [x] unless --skip-checklist. complete also runs check-task-artifact-contract.mjs.',
50
53
  'Do not write SIGNAL.json by hand or with echo.',
51
54
  ].join('\n'),
@@ -238,13 +241,20 @@ function inferFlowType(taskPath) {
238
241
  } catch {
239
242
  return null;
240
243
  }
241
- const workerMatch = head.match(/^#\s*Worker:\s*([^\n—-]+)/im);
244
+ const workerMatch = head.match(/^#\s*Worker:\s*([^\n]+)/im);
242
245
  if (workerMatch) {
243
- const label = workerMatch[1].trim().toLowerCase();
246
+ const label = workerMatch[1]
247
+ .split(/\s+[—-]\s+/)[0]
248
+ .trim()
249
+ .toLowerCase();
244
250
  if (label.includes('fix-bug') || label.includes('fix bug')) return 'fix-bug';
245
251
  if (label.includes('review-pr') || label.includes('review pr')) return 'review-pr';
246
252
  if (label.includes('pr-complete') || label.includes('pr complete')) return 'pr-complete';
247
- if (label.includes('merge-main') || label.includes('merge main')) return 'merge-main';
253
+ if (label.includes('update-branch') || label.includes('update branch')) return 'update-branch';
254
+ if (label.includes('ci-fix') || label.includes('ci fix')) return 'ci-fix';
255
+ if (label.includes('self-review fix') || label.includes('self-review-fix'))
256
+ return 'self-review-fix';
257
+ if (label.includes('self-review') || label.includes('self review')) return 'self-review';
248
258
  if (label.includes('interactive dev')) return 'dev';
249
259
  if (/\bdev\b/.test(label) && !label.includes('review')) return 'dev';
250
260
  }
@@ -292,7 +302,15 @@ function assertArtifactContract(taskDir, contract, terminalCommand) {
292
302
  if (terminalCommand) args.push('--terminal', terminalCommand);
293
303
  if (opts['skip-learnings']) args.push('--skip-learnings');
294
304
  } else {
295
- if (!opts['skip-learnings']) args.push('--require-learnings');
305
+ // No persisted contract file — honor the resolved builtin contract instead of
306
+ // assuming learnings: reviewer flows (self-review, self-review-fix) require their
307
+ // feedback/report artifact, not artifacts/learnings.md.
308
+ const contractRequiresLearnings = contract
309
+ ? (contract.commands?.[terminalCommand ?? 'complete']?.artifacts ?? []).includes(
310
+ LEARNINGS_ARTIFACT.split(path.sep).join('/'),
311
+ )
312
+ : true;
313
+ if (!opts['skip-learnings'] && contractRequiresLearnings) args.push('--require-learnings');
296
314
  if (fs.existsSync(path.join(taskDir, 'artifacts', 'recipe.json'))) {
297
315
  args.push('--require-recipe-coverage-if-recipe', '--require-recipe-quality-if-recipe');
298
316
  }
@@ -36,7 +36,11 @@
36
36
  * @property {'builtin' | 'project'} source
37
37
  */
38
38
 
39
- const TERMINAL_MARK_RE = /(?:\.\/|\{\{TASK_DIR\}\}\/)mark\s+(complete|no-change|blocked)\b/;
39
+ const TERMINAL_MARK_PATTERN =
40
+ String.raw`(?:\.\/|\{\{TASK_DIR\}\}\/)mark\b(?:\s+--(?:checklist|signal)\s+\S+)*\s+` +
41
+ String.raw`(complete|no-change|blocked)\b`;
42
+ const TERMINAL_MARK_RE = new RegExp(TERMINAL_MARK_PATTERN);
43
+ const TERMINAL_MARK_GLOBAL_RE = new RegExp(TERMINAL_MARK_PATTERN, 'g');
40
44
  const TERMINAL_COMMANDS = /** @type {const} */ (['complete', 'no-change', 'blocked']);
41
45
 
42
46
  const LEARNINGS = 'artifacts/learnings.md';
@@ -83,7 +87,7 @@ const BUILTIN_FLOW_COMMANDS = {
83
87
  },
84
88
  blocked: { artifacts: [] },
85
89
  },
86
- 'merge-main': {
90
+ 'update-branch': {
87
91
  complete: {
88
92
  report: 'artifacts/report.md',
89
93
  artifacts: [LEARNINGS, 'artifacts/report.md'],
@@ -102,19 +106,26 @@ const BUILTIN_FLOW_COMMANDS = {
102
106
  },
103
107
  blocked: { artifacts: [] },
104
108
  },
109
+ // Reviewer flows produce feedback/report artifacts, not learnings.md — the
110
+ // reviewer evaluates someone else's work, so the learnings requirement that
111
+ // applies to contribution flows does not apply here (matches the templates
112
+ // and the runtime FLOW_REPORT_ARTIFACTS mapping).
105
113
  'self-review': {
106
- complete: { artifacts: [LEARNINGS] },
114
+ complete: {
115
+ report: 'artifacts/review-feedback.md',
116
+ artifacts: ['artifacts/review-feedback.md'],
117
+ },
107
118
  'no-change': {
108
- report: 'artifacts/no-change-report.md',
109
- artifacts: [LEARNINGS, 'artifacts/no-change-report.md'],
119
+ report: 'artifacts/review-feedback.md',
120
+ artifacts: ['artifacts/review-feedback.md'],
110
121
  },
111
122
  blocked: { artifacts: [] },
112
123
  },
113
124
  'self-review-fix': {
114
- complete: { report: 'artifacts/report.md', artifacts: [LEARNINGS, 'artifacts/report.md'] },
125
+ complete: { report: 'artifacts/report.md', artifacts: ['artifacts/report.md'] },
115
126
  'no-change': {
116
127
  report: 'artifacts/no-change-report.md',
117
- artifacts: [LEARNINGS, 'artifacts/no-change-report.md'],
128
+ artifacts: ['artifacts/no-change-report.md'],
118
129
  },
119
130
  blocked: { artifacts: [] },
120
131
  },
@@ -248,9 +259,7 @@ function templateTerminalCommands(content) {
248
259
  }
249
260
  }
250
261
  const commands = new Set();
251
- for (const match of scope.matchAll(
252
- /(?:\.\/|\{\{TASK_DIR\}\}\/)mark\s+(complete|no-change|blocked)\b/g,
253
- )) {
262
+ for (const match of scope.matchAll(TERMINAL_MARK_GLOBAL_RE)) {
254
263
  commands.add(match[1]);
255
264
  }
256
265
  if (commands.size === 0 && /mark-checklist-step\.cjs[\s\S]{0,400}?\bcomplete\b/.test(scope)) {
@@ -311,21 +320,14 @@ function lintWorkerTemplateAgainstContract(templateContent, contract) {
311
320
  /** @type {string[]} */
312
321
  const issues = [];
313
322
  if (!templateUsesTerminalMark(templateContent)) {
314
- issues.push(
315
- 'terminal worker template must instruct `./mark complete`, `./mark no-change`, or `./mark blocked`',
316
- );
317
- return issues;
318
- }
319
-
320
- if (contract.requireSignal) {
321
- if (!templateUsesTerminalMark(templateContent)) {
323
+ // Omitting `./mark` entirely is only acceptable when the resolved contract
324
+ // does not require a terminal signal (e.g. pr-complete interactive).
325
+ if (contract.requireSignal) {
322
326
  issues.push(
323
327
  'requireSignal is true but template has no terminal `./mark` or mark-checklist-step command',
324
328
  );
325
329
  }
326
- if (/never hand-write `SIGNAL\.json`|Do not write.*SIGNAL\.json/i.test(templateContent)) {
327
- // allowed when paired with mark instructions
328
- }
330
+ return issues;
329
331
  }
330
332
 
331
333
  const commands = templateTerminalCommands(templateContent);