@farmslot/skills 0.1.2 → 0.1.3
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 +7 -1
- package/README.md +3 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
- package/scripts/check-task-artifact-contract.mjs +14 -300
- package/scripts/mark-checklist-step.cjs +10 -480
- package/scripts/worker-terminal-contract.cjs +11 -376
- package/skills/packet/SKILL.md +136 -0
- package/skills/recipe-quality/SKILL.md +2 -0
|
@@ -1,382 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* @property {string} [report]
|
|
6
|
-
* @property {string[]} artifacts
|
|
7
|
-
*/
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { createRequire } = require('node:module');
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
* @typedef {object} WorkerTerminalWhenPresentRule
|
|
11
|
-
* @property {string} path
|
|
12
|
-
* @property {string[]} alsoRequire
|
|
13
|
-
* @property {boolean} [requireRecipeQuality]
|
|
14
|
-
* @property {boolean} [requireRecipeCoverage]
|
|
15
|
-
*/
|
|
6
|
+
const requireFromHere = createRequire(__filename);
|
|
16
7
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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);
|
|
8
|
+
function resolveRuntimeScript() {
|
|
9
|
+
try {
|
|
10
|
+
return requireFromHere.resolve('@farmslot/agent-runtime/scripts/worker-terminal-contract.cjs');
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (error.code !== 'MODULE_NOT_FOUND') throw error;
|
|
13
|
+
return path.resolve(__dirname, '../../agent-runtime/scripts/worker-terminal-contract.cjs');
|
|
359
14
|
}
|
|
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
15
|
}
|
|
371
16
|
|
|
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
|
-
};
|
|
17
|
+
module.exports = require(resolveRuntimeScript());
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: packet
|
|
3
|
+
description: Communicate complex agent output as compact, reviewable Farmslot operator packets with artifact anchors and safe action requests
|
|
4
|
+
compatibility: Claude, Codex, Cursor, and Markdown skill runners with access to the working directory
|
|
5
|
+
metadata:
|
|
6
|
+
package: '@farmslot/skills'
|
|
7
|
+
adr: ADR-048
|
|
8
|
+
outputs: artifacts/interactive/*.packet.json and optional sibling markdown when supported
|
|
9
|
+
allowed-tools: Read Write Bash(rg:*) Bash(node:*) Bash(git:*)
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Interactive Operator Packets
|
|
13
|
+
|
|
14
|
+
Use this skill when a plan, decision, blocker, comparison, or evidence review would be easier for the operator to inspect as a structured review packet than as terminal prose.
|
|
15
|
+
|
|
16
|
+
This skill teaches the communication pattern. It does not assume Farmslot runtime support is installed. If the current project does not support Interactive Operator Packets yet, produce a concise markdown fallback and do not claim that Command Center or Companion can render it.
|
|
17
|
+
|
|
18
|
+
## Use When
|
|
19
|
+
|
|
20
|
+
- You need the operator to review a plan before implementation.
|
|
21
|
+
- You need a decision with clear options and consequences.
|
|
22
|
+
- You found a blocker and need targeted human input.
|
|
23
|
+
- You want feedback anchored to a diff, artifact, screenshot, recipe result, or report section.
|
|
24
|
+
- You are summarizing evidence and want the operator to approve, reject, or request a revision.
|
|
25
|
+
|
|
26
|
+
## Do Not Use When
|
|
27
|
+
|
|
28
|
+
- A one-sentence answer is enough.
|
|
29
|
+
- The operator asked for raw terminal output.
|
|
30
|
+
- The action would be unsafe without an existing Farmslot confirmation surface.
|
|
31
|
+
- You would need to execute arbitrary HTML or JavaScript to make the packet useful.
|
|
32
|
+
- The packet would only restate a normal `SIGNAL.json`, recipe manifest, or PR description.
|
|
33
|
+
|
|
34
|
+
## Packet Contract
|
|
35
|
+
|
|
36
|
+
When the project supports packets, write JSON to:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
artifacts/interactive/<short-id>.packet.json
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Use this shape:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"schema": "farmslot.interactive.operator-packet.v1",
|
|
47
|
+
"id": "review-plan-1",
|
|
48
|
+
"title": "Review implementation plan",
|
|
49
|
+
"intent": "plan",
|
|
50
|
+
"summary": "Confirm the scoped approach before changes start.",
|
|
51
|
+
"body": {
|
|
52
|
+
"format": "markdown",
|
|
53
|
+
"path": "artifacts/interactive/review-plan-1.md"
|
|
54
|
+
},
|
|
55
|
+
"anchors": [
|
|
56
|
+
{
|
|
57
|
+
"id": "diff-risk",
|
|
58
|
+
"label": "Risky diff area",
|
|
59
|
+
"artifactPath": "artifacts/diff.txt",
|
|
60
|
+
"line": 42
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"actions": [
|
|
64
|
+
{
|
|
65
|
+
"id": "approve",
|
|
66
|
+
"label": "Approve plan",
|
|
67
|
+
"kind": "terminal.send",
|
|
68
|
+
"safety": "operator-confirmed",
|
|
69
|
+
"payload": {
|
|
70
|
+
"text": "Approved packet review-plan-1. Proceed with the scoped plan."
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
"createdAt": "2026-07-03T00:00:00.000Z"
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Keep `body.text` or the sibling markdown short. A packet is a review surface, not a report dump.
|
|
79
|
+
|
|
80
|
+
## Writing Rules
|
|
81
|
+
|
|
82
|
+
1. Start with the operator's next decision or review task.
|
|
83
|
+
2. Use a short title and one-sentence summary.
|
|
84
|
+
3. Pick one intent: `plan`, `decision`, `review`, `blocker`, `evidence`, or `comparison`.
|
|
85
|
+
4. Anchor feedback to concrete artifacts when possible.
|
|
86
|
+
5. Offer only actions the operator can understand and confirm.
|
|
87
|
+
6. Print a terminal fallback after writing the packet.
|
|
88
|
+
|
|
89
|
+
Terminal fallback format:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
Interactive packet ready: <title>
|
|
93
|
+
Packet: artifacts/interactive/<short-id>.packet.json
|
|
94
|
+
Fallback: artifacts/interactive/<short-id>.md
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
If Farmslot deep links are known to be supported, also print the route. Never encode a mutating action in the link.
|
|
98
|
+
|
|
99
|
+
## Action Safety
|
|
100
|
+
|
|
101
|
+
Allowed v1 action kinds:
|
|
102
|
+
|
|
103
|
+
- `terminal.send` — send a concise approved response back to the worker.
|
|
104
|
+
- `decision.resolve` — resolve an existing Farmslot decision.
|
|
105
|
+
- `copy` — copy text for manual use.
|
|
106
|
+
- `open-artifact` — open a referenced artifact.
|
|
107
|
+
|
|
108
|
+
Rules:
|
|
109
|
+
|
|
110
|
+
- Mark mutating actions as `operator-confirmed`.
|
|
111
|
+
- Keep generated `terminal.send` text short and specific.
|
|
112
|
+
- Do not hide shell commands inside action payloads.
|
|
113
|
+
- Do not ask the UI to execute arbitrary RPC methods.
|
|
114
|
+
- Do not treat a deep link as approval.
|
|
115
|
+
|
|
116
|
+
## Fallback Mode
|
|
117
|
+
|
|
118
|
+
If packet runtime support is absent:
|
|
119
|
+
|
|
120
|
+
1. Write the same information as markdown in the conversation or artifact directory.
|
|
121
|
+
2. Include anchors as plain file paths and line numbers.
|
|
122
|
+
3. Ask for the operator response in normal chat/terminal.
|
|
123
|
+
4. Do not say that a packet has been opened, rendered, or sent to Companion.
|
|
124
|
+
|
|
125
|
+
## Response Handling
|
|
126
|
+
|
|
127
|
+
When the operator responds to a packet, continue with the referenced packet id and anchor ids:
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
Received response for packet review-plan-1:
|
|
131
|
+
- action: approve
|
|
132
|
+
- anchor: diff-risk
|
|
133
|
+
- note: Keep the protocol typed; defer HTML.
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Then act on the feedback through the normal task flow. The packet is a communication aid; the runner session remains the source of execution truth.
|
|
@@ -43,3 +43,5 @@ Return:
|
|
|
43
43
|
- weak or missing claims
|
|
44
44
|
- evidence gaps
|
|
45
45
|
- concrete recipe delta
|
|
46
|
+
|
|
47
|
+
When the task requires `artifacts/recipe-quality.json`, do not hand-author the full schema. Provide the compact fields to `farmslot-agent recipe-quality build` (or `buildRecipeQualityArtifact()` from `@farmslot/agent-runtime`) so the artifact passes the `@farmslot/protocol` validator.
|