@farmslot/agent-runtime 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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +58 -0
- package/bin/farmslot-agent.mjs +481 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/recipe-quality.d.ts +27 -0
- package/dist/recipe-quality.d.ts.map +1 -0
- package/dist/recipe-quality.js +178 -0
- package/dist/recipe-quality.js.map +1 -0
- package/package.json +73 -0
- package/scripts/check-task-artifact-contract.mjs +400 -0
- package/scripts/mark-checklist-step.cjs +488 -0
- package/scripts/worker-terminal-contract.cjs +382 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/** @typedef {'complete' | 'no-change' | 'blocked'} WorkerTerminalCommand */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {object} WorkerTerminalCommandSpec
|
|
5
|
+
* @property {string} [report]
|
|
6
|
+
* @property {string[]} artifacts
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {object} WorkerTerminalWhenPresentRule
|
|
11
|
+
* @property {string} path
|
|
12
|
+
* @property {string[]} alsoRequire
|
|
13
|
+
* @property {boolean} [requireRecipeQuality]
|
|
14
|
+
* @property {boolean} [requireRecipeCoverage]
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {object} WorkerTerminalProjectConfig
|
|
19
|
+
* @property {boolean} [requireSignal]
|
|
20
|
+
* @property {Partial<Record<WorkerTerminalCommand, WorkerTerminalCommandSpec>>} [complete]
|
|
21
|
+
* @property {Partial<Record<WorkerTerminalCommand, WorkerTerminalCommandSpec>>} [no-change]
|
|
22
|
+
* @property {Partial<Record<WorkerTerminalCommand, WorkerTerminalCommandSpec>>} [blocked]
|
|
23
|
+
* @property {Record<string, Partial<Record<WorkerTerminalCommand, WorkerTerminalCommandSpec>>>} [flows]
|
|
24
|
+
* @property {WorkerTerminalWhenPresentRule[]} [whenPresent]
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {object} WorkerTerminalContractDocument
|
|
29
|
+
* @property {1} schemaVersion
|
|
30
|
+
* @property {string} flowType
|
|
31
|
+
* @property {string} [mode]
|
|
32
|
+
* @property {boolean} requireSignal
|
|
33
|
+
* @property {Record<WorkerTerminalCommand, WorkerTerminalCommandSpec>} commands
|
|
34
|
+
* @property {WorkerTerminalWhenPresentRule[]} whenPresent
|
|
35
|
+
* @property {string} resolvedAt
|
|
36
|
+
* @property {'builtin' | 'project'} source
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const TERMINAL_MARK_RE = /(?:\.\/|\{\{TASK_DIR\}\}\/)mark\s+(complete|no-change|blocked)\b/;
|
|
40
|
+
const TERMINAL_COMMANDS = /** @type {const} */ (['complete', 'no-change', 'blocked']);
|
|
41
|
+
|
|
42
|
+
const LEARNINGS = 'artifacts/learnings.md';
|
|
43
|
+
|
|
44
|
+
const PR_DESCRIPTION = 'artifacts/pr-description.md';
|
|
45
|
+
|
|
46
|
+
/** @type {Record<string, Record<WorkerTerminalCommand, WorkerTerminalCommandSpec>>} */
|
|
47
|
+
const BUILTIN_FLOW_COMMANDS = {
|
|
48
|
+
dev: {
|
|
49
|
+
complete: { report: PR_DESCRIPTION, artifacts: [LEARNINGS, PR_DESCRIPTION] },
|
|
50
|
+
'no-change': {
|
|
51
|
+
report: 'artifacts/no-change-report.md',
|
|
52
|
+
artifacts: [LEARNINGS, 'artifacts/no-change-report.md'],
|
|
53
|
+
},
|
|
54
|
+
blocked: { artifacts: [] },
|
|
55
|
+
},
|
|
56
|
+
'fix-bug': {
|
|
57
|
+
complete: { report: PR_DESCRIPTION, artifacts: [LEARNINGS, PR_DESCRIPTION] },
|
|
58
|
+
'no-change': {
|
|
59
|
+
report: 'artifacts/no-change-report.md',
|
|
60
|
+
artifacts: [LEARNINGS, 'artifacts/no-change-report.md'],
|
|
61
|
+
},
|
|
62
|
+
blocked: { artifacts: [] },
|
|
63
|
+
},
|
|
64
|
+
'review-pr': {
|
|
65
|
+
complete: {
|
|
66
|
+
report: 'artifacts/review.md',
|
|
67
|
+
artifacts: [LEARNINGS, 'artifacts/review.md', 'artifacts/line-comments.json'],
|
|
68
|
+
},
|
|
69
|
+
'no-change': {
|
|
70
|
+
report: 'artifacts/no-change-report.md',
|
|
71
|
+
artifacts: [LEARNINGS, 'artifacts/no-change-report.md'],
|
|
72
|
+
},
|
|
73
|
+
blocked: { artifacts: [] },
|
|
74
|
+
},
|
|
75
|
+
'pr-complete': {
|
|
76
|
+
complete: {
|
|
77
|
+
report: 'artifacts/comments-report.md',
|
|
78
|
+
artifacts: [LEARNINGS, 'artifacts/comments-report.md'],
|
|
79
|
+
},
|
|
80
|
+
'no-change': {
|
|
81
|
+
report: 'artifacts/no-change-report.md',
|
|
82
|
+
artifacts: [LEARNINGS, 'artifacts/no-change-report.md'],
|
|
83
|
+
},
|
|
84
|
+
blocked: { artifacts: [] },
|
|
85
|
+
},
|
|
86
|
+
'merge-main': {
|
|
87
|
+
complete: {
|
|
88
|
+
report: 'artifacts/report.md',
|
|
89
|
+
artifacts: [LEARNINGS, 'artifacts/report.md'],
|
|
90
|
+
},
|
|
91
|
+
'no-change': {
|
|
92
|
+
report: 'artifacts/no-change-report.md',
|
|
93
|
+
artifacts: [LEARNINGS, 'artifacts/no-change-report.md'],
|
|
94
|
+
},
|
|
95
|
+
blocked: { artifacts: [] },
|
|
96
|
+
},
|
|
97
|
+
'ci-fix': {
|
|
98
|
+
complete: { report: 'artifacts/report.md', artifacts: [LEARNINGS, 'artifacts/report.md'] },
|
|
99
|
+
'no-change': {
|
|
100
|
+
report: 'artifacts/no-change-report.md',
|
|
101
|
+
artifacts: [LEARNINGS, 'artifacts/no-change-report.md'],
|
|
102
|
+
},
|
|
103
|
+
blocked: { artifacts: [] },
|
|
104
|
+
},
|
|
105
|
+
'self-review': {
|
|
106
|
+
complete: { artifacts: [LEARNINGS] },
|
|
107
|
+
'no-change': {
|
|
108
|
+
report: 'artifacts/no-change-report.md',
|
|
109
|
+
artifacts: [LEARNINGS, 'artifacts/no-change-report.md'],
|
|
110
|
+
},
|
|
111
|
+
blocked: { artifacts: [] },
|
|
112
|
+
},
|
|
113
|
+
'self-review-fix': {
|
|
114
|
+
complete: { report: 'artifacts/report.md', artifacts: [LEARNINGS, 'artifacts/report.md'] },
|
|
115
|
+
'no-change': {
|
|
116
|
+
report: 'artifacts/no-change-report.md',
|
|
117
|
+
artifacts: [LEARNINGS, 'artifacts/no-change-report.md'],
|
|
118
|
+
},
|
|
119
|
+
blocked: { artifacts: [] },
|
|
120
|
+
},
|
|
121
|
+
'validate-dep': {
|
|
122
|
+
complete: { report: 'artifacts/report.md', artifacts: [LEARNINGS, 'artifacts/report.md'] },
|
|
123
|
+
'no-change': {
|
|
124
|
+
report: 'artifacts/no-change-report.md',
|
|
125
|
+
artifacts: [LEARNINGS, 'artifacts/no-change-report.md'],
|
|
126
|
+
},
|
|
127
|
+
blocked: { artifacts: [] },
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const DEFAULT_WHEN_PRESENT = [
|
|
132
|
+
{
|
|
133
|
+
path: 'artifacts/recipe.json',
|
|
134
|
+
alsoRequire: ['artifacts/recipe-coverage.md', 'artifacts/evidence-manifest.json'],
|
|
135
|
+
requireRecipeQuality: true,
|
|
136
|
+
requireRecipeCoverage: true,
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
const DEFAULT_BLOCKED = { artifacts: [] };
|
|
141
|
+
const DEFAULT_NO_CHANGE = {
|
|
142
|
+
report: 'artifacts/no-change-report.md',
|
|
143
|
+
artifacts: [LEARNINGS, 'artifacts/no-change-report.md'],
|
|
144
|
+
};
|
|
145
|
+
const DEFAULT_COMPLETE = {
|
|
146
|
+
report: 'artifacts/report.md',
|
|
147
|
+
artifacts: [LEARNINGS, 'artifacts/report.md'],
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** @param {WorkerTerminalCommandSpec | undefined} spec */
|
|
151
|
+
function normalizeCommandSpec(spec) {
|
|
152
|
+
if (!spec) return { artifacts: [] };
|
|
153
|
+
const artifacts = [...new Set((spec.artifacts ?? []).filter(Boolean))];
|
|
154
|
+
if (spec.report && !artifacts.includes(spec.report)) artifacts.unshift(spec.report);
|
|
155
|
+
return {
|
|
156
|
+
...(spec.report ? { report: spec.report } : {}),
|
|
157
|
+
artifacts,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** @param {Partial<Record<WorkerTerminalCommand, WorkerTerminalCommandSpec>> | undefined} layer */
|
|
162
|
+
function normalizeCommandLayer(layer) {
|
|
163
|
+
/** @type {Record<WorkerTerminalCommand, WorkerTerminalCommandSpec>} */
|
|
164
|
+
const out = {
|
|
165
|
+
complete: normalizeCommandSpec(layer?.complete ?? DEFAULT_COMPLETE),
|
|
166
|
+
'no-change': normalizeCommandSpec(layer?.['no-change'] ?? DEFAULT_NO_CHANGE),
|
|
167
|
+
blocked: normalizeCommandSpec(layer?.blocked ?? DEFAULT_BLOCKED),
|
|
168
|
+
};
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @param {WorkerTerminalProjectConfig | null | undefined} projectConfig
|
|
174
|
+
* @param {string} flowType
|
|
175
|
+
* @param {{ mode?: string | null, now?: string }} [options]
|
|
176
|
+
* @returns {WorkerTerminalContractDocument}
|
|
177
|
+
*/
|
|
178
|
+
function resolveWorkerTerminalContract(projectConfig, flowType, options = {}) {
|
|
179
|
+
const mode = options.mode ?? undefined;
|
|
180
|
+
const now = options.now ?? new Date().toISOString();
|
|
181
|
+
const flowKey = String(flowType || '').trim() || 'unknown';
|
|
182
|
+
|
|
183
|
+
const defaultsLayer = normalizeCommandLayer({
|
|
184
|
+
complete: projectConfig?.complete,
|
|
185
|
+
'no-change': projectConfig?.['no-change'],
|
|
186
|
+
blocked: projectConfig?.blocked,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const builtin = BUILTIN_FLOW_COMMANDS[flowKey];
|
|
190
|
+
const flowLayer = projectConfig?.flows?.[flowKey];
|
|
191
|
+
/** @type {Record<WorkerTerminalCommand, WorkerTerminalCommandSpec>} */
|
|
192
|
+
const commands = {
|
|
193
|
+
complete: normalizeCommandSpec(
|
|
194
|
+
flowLayer?.complete ?? builtin?.complete ?? defaultsLayer.complete,
|
|
195
|
+
),
|
|
196
|
+
'no-change': normalizeCommandSpec(
|
|
197
|
+
flowLayer?.['no-change'] ?? builtin?.['no-change'] ?? defaultsLayer['no-change'],
|
|
198
|
+
),
|
|
199
|
+
blocked: normalizeCommandSpec(flowLayer?.blocked ?? builtin?.blocked ?? defaultsLayer.blocked),
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
let requireSignal = projectConfig?.requireSignal ?? true;
|
|
203
|
+
if (flowKey === 'pr-complete' && mode === 'interactive') {
|
|
204
|
+
requireSignal = false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const whenPresent = Array.isArray(projectConfig?.whenPresent)
|
|
208
|
+
? projectConfig.whenPresent.map((rule) => ({
|
|
209
|
+
path: rule.path,
|
|
210
|
+
alsoRequire: [...(rule.alsoRequire ?? [])],
|
|
211
|
+
...(rule.requireRecipeQuality ? { requireRecipeQuality: true } : {}),
|
|
212
|
+
...(rule.requireRecipeCoverage ? { requireRecipeCoverage: true } : {}),
|
|
213
|
+
}))
|
|
214
|
+
: DEFAULT_WHEN_PRESENT.map((rule) => ({ ...rule, alsoRequire: [...rule.alsoRequire] }));
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
schemaVersion: 1,
|
|
218
|
+
flowType: flowKey,
|
|
219
|
+
...(mode ? { mode } : {}),
|
|
220
|
+
requireSignal,
|
|
221
|
+
commands,
|
|
222
|
+
whenPresent,
|
|
223
|
+
resolvedAt: now,
|
|
224
|
+
source: projectConfig ? 'project' : 'builtin',
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** @param {string} content */
|
|
229
|
+
function templateUsesTerminalMark(content) {
|
|
230
|
+
return (
|
|
231
|
+
TERMINAL_MARK_RE.test(content) ||
|
|
232
|
+
/mark-checklist-step\.cjs[\s\S]{0,400}?\bcomplete\b/.test(content)
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** @param {string} content */
|
|
237
|
+
function templateTerminalCommands(content) {
|
|
238
|
+
let scope = content;
|
|
239
|
+
const checklistIdx = content.indexOf('## Checklist');
|
|
240
|
+
if (checklistIdx >= 0) {
|
|
241
|
+
scope = content.slice(checklistIdx);
|
|
242
|
+
} else {
|
|
243
|
+
const completionIdx = content.indexOf('## Completion signal');
|
|
244
|
+
if (completionIdx >= 0) scope = content.slice(completionIdx);
|
|
245
|
+
else {
|
|
246
|
+
const taskIdx = content.indexOf('## Task');
|
|
247
|
+
if (taskIdx >= 0) scope = content.slice(taskIdx);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const commands = new Set();
|
|
251
|
+
for (const match of scope.matchAll(
|
|
252
|
+
/(?:\.\/|\{\{TASK_DIR\}\}\/)mark\s+(complete|no-change|blocked)\b/g,
|
|
253
|
+
)) {
|
|
254
|
+
commands.add(match[1]);
|
|
255
|
+
}
|
|
256
|
+
if (commands.size === 0 && /mark-checklist-step\.cjs[\s\S]{0,400}?\bcomplete\b/.test(scope)) {
|
|
257
|
+
commands.add('complete');
|
|
258
|
+
}
|
|
259
|
+
return [...commands];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @param {string} templateContent
|
|
264
|
+
* @param {WorkerTerminalContractDocument} contract
|
|
265
|
+
*/
|
|
266
|
+
/** @param {string} content */
|
|
267
|
+
function templateBodyScope(content) {
|
|
268
|
+
const checklistIdx = content.indexOf('## Checklist');
|
|
269
|
+
if (checklistIdx >= 0) return content.slice(checklistIdx);
|
|
270
|
+
const completionIdx = content.indexOf('## Completion signal');
|
|
271
|
+
if (completionIdx >= 0) return content.slice(completionIdx);
|
|
272
|
+
const taskIdx = content.indexOf('## Task');
|
|
273
|
+
if (taskIdx >= 0) return content.slice(taskIdx);
|
|
274
|
+
return content;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Deterministic structure checks beyond terminal artifact contract.
|
|
279
|
+
* @param {string} templateContent
|
|
280
|
+
*/
|
|
281
|
+
function lintWorkerTemplateStructure(templateContent) {
|
|
282
|
+
/** @type {string[]} */
|
|
283
|
+
const issues = [];
|
|
284
|
+
if (/(?<!\{)\{TASK_DIR\}(?!\})/.test(templateContent)) {
|
|
285
|
+
issues.push('uses `{TASK_DIR}` — must be `{{TASK_DIR}}` (double braces)');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (
|
|
289
|
+
templateUsesTerminalMark(templateContent) &&
|
|
290
|
+
!templateContent.includes('## Checklist') &&
|
|
291
|
+
!templateContent.includes('## Completion signal')
|
|
292
|
+
) {
|
|
293
|
+
issues.push('terminal template missing `## Checklist` or `## Completion signal` section');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!templateContent.includes('## Task') && !templateContent.includes('TASK_DIR:')) {
|
|
297
|
+
issues.push('missing `## Task` block or `TASK_DIR:` metadata');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const scope = templateBodyScope(templateContent);
|
|
301
|
+
const steps = [...scope.matchAll(/\*\*(\d+)\./g)].map((match) => match[1]);
|
|
302
|
+
const duplicateSteps = [...new Set(steps.filter((step, index) => steps.indexOf(step) !== index))];
|
|
303
|
+
if (duplicateSteps.length > 0) {
|
|
304
|
+
issues.push(`duplicate checklist step number(s): ${duplicateSteps.join(', ')}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return issues;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function lintWorkerTemplateAgainstContract(templateContent, contract) {
|
|
311
|
+
/** @type {string[]} */
|
|
312
|
+
const issues = [];
|
|
313
|
+
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)) {
|
|
322
|
+
issues.push(
|
|
323
|
+
'requireSignal is true but template has no terminal `./mark` or mark-checklist-step command',
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
if (/never hand-write `SIGNAL\.json`|Do not write.*SIGNAL\.json/i.test(templateContent)) {
|
|
327
|
+
// allowed when paired with mark instructions
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const commands = templateTerminalCommands(templateContent);
|
|
332
|
+
const commandSet = new Set(commands.length > 0 ? commands : ['complete']);
|
|
333
|
+
for (const command of commandSet) {
|
|
334
|
+
const spec = contract.commands[/** @type {WorkerTerminalCommand} */ (command)];
|
|
335
|
+
if (!spec) continue;
|
|
336
|
+
for (const artifactPath of spec.artifacts) {
|
|
337
|
+
if (!templateContent.includes(artifactPath)) {
|
|
338
|
+
issues.push(
|
|
339
|
+
`missing checklist/reference to required artifact \`${artifactPath}\` for \`${command}\``,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return issues;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @param {WorkerTerminalContractDocument} contract
|
|
350
|
+
* @param {WorkerTerminalCommand} command
|
|
351
|
+
* @param {(relPath: string) => boolean} fileExists
|
|
352
|
+
*/
|
|
353
|
+
function expandedArtifactsForCommand(contract, command, fileExists) {
|
|
354
|
+
const spec = contract.commands[command] ?? { artifacts: [] };
|
|
355
|
+
const required = new Set(spec.artifacts);
|
|
356
|
+
for (const rule of contract.whenPresent ?? []) {
|
|
357
|
+
if (!rule.path || !fileExists(rule.path)) continue;
|
|
358
|
+
for (const path of rule.alsoRequire ?? []) required.add(path);
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
report: spec.report,
|
|
362
|
+
artifacts: [...required],
|
|
363
|
+
requireRecipeQuality: (contract.whenPresent ?? []).some(
|
|
364
|
+
(rule) => rule.path && fileExists(rule.path) && rule.requireRecipeQuality,
|
|
365
|
+
),
|
|
366
|
+
requireRecipeCoverage: (contract.whenPresent ?? []).some(
|
|
367
|
+
(rule) => rule.path && fileExists(rule.path) && rule.requireRecipeCoverage,
|
|
368
|
+
),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
module.exports = {
|
|
373
|
+
TERMINAL_COMMANDS,
|
|
374
|
+
BUILTIN_FLOW_COMMANDS,
|
|
375
|
+
resolveWorkerTerminalContract,
|
|
376
|
+
lintWorkerTemplateAgainstContract,
|
|
377
|
+
lintWorkerTemplateStructure,
|
|
378
|
+
templateUsesTerminalMark,
|
|
379
|
+
templateTerminalCommands,
|
|
380
|
+
templateBodyScope,
|
|
381
|
+
expandedArtifactsForCommand,
|
|
382
|
+
};
|