@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.
@@ -1,382 +1,17 @@
1
- /** @typedef {'complete' | 'no-change' | 'blocked'} WorkerTerminalCommand */
1
+ 'use strict';
2
2
 
3
- /**
4
- * @typedef {object} WorkerTerminalCommandSpec
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
- * @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);
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.