@farmslot/skills 0.1.2 → 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
@@ -6,9 +6,19 @@ All notable changes to `@farmslot/skills` are tracked here.
6
6
 
7
7
  - Active-development baseline; add user-facing changes here before release or package publication.
8
8
 
9
+ ## 0.2.0 - 2026-07-19
10
+
11
+ - Teach recipe authors to inspect declared actions and reusable flows before creating new capabilities
12
+
13
+ ## 0.1.3 - 2026-07-08
14
+
15
+ - Update recipe-quality skill guidance to delegate runtime artifact generation to `@farmslot/agent-runtime`
16
+ - `packet`, a skill for packet-shaped operator communication aligned with ADR-048
17
+ - Convert task lifecycle scripts to compatibility shims over `@farmslot/agent-runtime`
18
+
9
19
  ## 0.1.2 - 2026-07-03
10
20
 
11
- - Colocate finish-contract scripts in `packages/skills/scripts/` (`mark-checklist-step.cjs`, `worker-terminal-contract.cjs`, `check-task-artifact-contract.mjs`) as the canonical source for `@farmslot/skills` and consensys-skills delegation
21
+ - Colocate finish-contract scripts in `packages/skills/scripts/` (`mark-checklist-step.cjs`, `worker-terminal-contract.cjs`, `check-task-artifact-contract.mjs`) for the initial `@farmslot/skills` and consensys-skills delegation path
12
22
  - **dev / fix-bug:** terminal `./mark complete` requires `artifacts/learnings.md` + `artifacts/pr-description.md` (not `report.md`)
13
23
  - `./mark` infers dev/fix-bug flow from embedded checklist `Skill:` header (standalone recipe skills)
14
24
  - Export `./scripts/worker-terminal-contract.cjs` and `./scripts/check-task-artifact-contract.mjs` from the package
package/README.md CHANGED
@@ -42,6 +42,7 @@ Supported layouts:
42
42
  | `recipe-doctor` | Check local readiness for recipe authoring or execution. |
43
43
  | `recipe-harness` | Help install or run the local recipe runtime when the project has one. |
44
44
  | `project-adopt` | Recommend the smallest useful Farmslot integration level for the project. |
45
+ | `packet` | Communicate plans, blockers, decisions, and evidence reviews as compact operator packets. |
45
46
 
46
47
  ## Adoption Ladder
47
48
 
@@ -55,13 +56,13 @@ Supported layouts:
55
56
 
56
57
  - `src/`: TypeScript CLI and install-library entry points.
57
58
  - `skills/`: Packaged skill directories copied into consumer repos.
58
- - `scripts/`: CommonJS recipe-cook runner utilities exposed through package bin scripts.
59
+ - `scripts/`: Compatibility shims for legacy runtime paths. Runtime logic lives in `@farmslot/agent-runtime`.
59
60
  - `templates/`: Scenario fixtures and task templates used by the recipe-cook workflow.
60
61
  - `test/`: Node TAP-style smoke and behavior tests for installers and runner scripts.
61
62
 
62
63
  ## Maintenance rules
63
64
 
64
- - `scripts/mark-checklist-step.cjs` mirrors consensys-skills `recipe-harness/scripts/mark-checklist-step.cjs`. Edit the recipe-harness copy first; sync the mirror before shipping Farmslot gateway or npm changes.
65
+ - `scripts/mark-checklist-step.cjs`, `scripts/worker-terminal-contract.cjs`, and `scripts/check-task-artifact-contract.mjs` are compatibility shims that delegate to `@farmslot/agent-runtime`.
65
66
  - Keep package payloads self-contained; consumers should not need the Farmslot monorepo.
66
67
  - Update `CHANGELOG.md` before packaging or publishing.
67
68
  - Run the package readiness guard when files move, bins change, or exports are added.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export declare const FARMSLOT_SKILLS_PACKAGE = "@farmslot/skills";
2
- export declare const FARMSLOT_SKILL_NAMES: readonly ["recipe-cook", "recipe-quality", "recipe-doctor", "recipe-harness", "project-adopt"];
2
+ export declare const FARMSLOT_SKILL_NAMES: readonly ["recipe-cook", "recipe-quality", "recipe-doctor", "recipe-harness", "project-adopt", "packet"];
3
3
  export type FarmslotSkillName = (typeof FARMSLOT_SKILL_NAMES)[number];
4
4
  export declare const FARMSLOT_SKILL_INSTALL_LAYOUTS: readonly ["agents", "claude", "codex", "cursor"];
5
5
  export type FarmslotSkillInstallLayout = (typeof FARMSLOT_SKILL_INSTALL_LAYOUTS)[number];
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,uBAAuB,qBAAqB,CAAC;AAE1D,eAAO,MAAM,oBAAoB,gGAMvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,eAAO,MAAM,8BAA8B,kDAAmD,CAAC;AAE/F,MAAM,MAAM,0BAA0B,GAAG,CAAC,OAAO,8BAA8B,CAAC,CAAC,MAAM,CAAC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,uBAAuB,qBAAqB,CAAC;AAE1D,eAAO,MAAM,oBAAoB,0GAOvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,eAAO,MAAM,8BAA8B,kDAAmD,CAAC;AAE/F,MAAM,MAAM,0BAA0B,GAAG,CAAC,OAAO,8BAA8B,CAAC,CAAC,MAAM,CAAC,CAAC"}
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export const FARMSLOT_SKILL_NAMES = [
5
5
  'recipe-doctor',
6
6
  'recipe-harness',
7
7
  'project-adopt',
8
+ 'packet',
8
9
  ];
9
10
  export const FARMSLOT_SKILL_INSTALL_LAYOUTS = ['agents', 'claude', 'codex', 'cursor'];
10
11
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,uBAAuB,GAAG,kBAAkB,CAAC;AAE1D,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,aAAa;IACb,gBAAgB;IAChB,eAAe;IACf,gBAAgB;IAChB,eAAe;CACP,CAAC;AAIX,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAU,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,uBAAuB,GAAG,kBAAkB,CAAC;AAE1D,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,aAAa;IACb,gBAAgB;IAChB,eAAe;IACf,gBAAgB;IAChB,eAAe;IACf,QAAQ;CACA,CAAC;AAIX,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAU,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farmslot/skills",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -32,6 +32,9 @@
32
32
  "prepack": "cd ../.. && node scripts/quality/check-farmslot-package-readiness.mjs --require-changelog --packages @farmslot/skills && yarn workspace @farmslot/skills build",
33
33
  "prepublishOnly": "node ../../scripts/quality/check-farmslot-package-readiness.mjs --publish --packages @farmslot/skills"
34
34
  },
35
+ "dependencies": {
36
+ "@farmslot/agent-runtime": "0.2.0"
37
+ },
35
38
  "devDependencies": {
36
39
  "@types/node": "^22.0.0",
37
40
  "eslint": "^10.3.0",
@@ -1,310 +1,24 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readFileSync, statSync } from 'node:fs';
2
+ import { spawnSync } from 'node:child_process';
3
3
  import { createRequire } from 'node:module';
4
4
  import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
 
6
- const require = createRequire(import.meta.url);
7
- const { expandedArtifactsForCommand } = require('./worker-terminal-contract.cjs');
7
+ const requireFromHere = createRequire(import.meta.url);
8
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
8
9
 
9
- const taskDir = process.argv[2];
10
- const rawArgs = process.argv.slice(3);
11
- const flags = new Set();
12
- let contractPath;
13
- let terminalCommand;
14
- for (let i = 0; i < rawArgs.length; i += 1) {
15
- const arg = rawArgs[i];
16
- if (arg === '--contract') {
17
- contractPath = rawArgs[i + 1];
18
- i += 1;
19
- continue;
20
- }
21
- if (arg === '--terminal') {
22
- terminalCommand = rawArgs[i + 1];
23
- i += 1;
24
- continue;
25
- }
26
- flags.add(arg);
27
- }
28
-
29
- if (!taskDir) {
30
- console.error(
31
- 'usage: check-task-artifact-contract.mjs <task-dir> [--contract path] [--terminal complete|no-change|blocked] [--require-recipe-quality-if-recipe] [--require-recipe-coverage-if-recipe] [--require-learnings] [--skip-learnings]',
32
- );
33
- process.exit(2);
34
- }
35
-
36
- const issues = [];
37
- const mediaExt = /\.(png|jpe?g|gif|mp4|mov|webm)$/i;
38
- const internalArtifactPath =
39
- /^artifacts\/(?:harness-launch|harness-relaunch|harness-relaunch-node20|runtime-launch|runtime-relaunch|runner-blockers)\//;
40
- const allowedManifestKeys = new Set([
41
- 'version',
42
- 'preferred_mode',
43
- 'summary',
44
- 'before_after_pairs',
45
- 'standalone',
46
- 'omit',
47
- 'videos',
48
- 'before_state_capture',
49
- ]);
50
- const allowedPairKeys = new Set(['label', 'covers', 'before', 'after', 'note']);
51
- const allowedStandaloneKeys = new Set(['label', 'covers', 'file', 'note']);
52
- const allowedVideoKeys = new Set(['before', 'after', 'preferred', 'note']);
53
- const allowedOmitKeys = new Set(['file', 'reason']);
54
-
55
- function fileExists(rel) {
56
- try {
57
- return statSync(path.join(taskDir, rel)).isFile();
58
- } catch {
59
- return false;
60
- }
61
- }
62
-
63
- function readText(rel) {
64
- const p = path.join(taskDir, rel);
65
- return existsSync(p) ? readFileSync(p, 'utf8') : null;
66
- }
67
-
68
- function isRecord(value) {
69
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
70
- }
71
-
72
- function unknownKeys(record, allowed, prefix) {
73
- for (const key of Object.keys(record)) {
74
- if (!allowed.has(key)) issues.push(`${prefix}.${key}: unknown key`);
75
- }
76
- }
77
-
78
- function optionalString(record, key, prefix) {
79
- if (record[key] !== undefined && typeof record[key] !== 'string') {
80
- issues.push(`${prefix}.${key}: expected string`);
81
- }
82
- }
83
-
84
- function optionalStringArray(record, key, prefix) {
85
- if (record[key] === undefined) return;
86
- if (!Array.isArray(record[key]) || record[key].some((entry) => typeof entry !== 'string')) {
87
- issues.push(`${prefix}.${key}: expected string[]`);
88
- }
89
- }
90
-
91
- function normalizeArtifactPath(value) {
92
- if (typeof value !== 'string') return null;
93
- const trimmed = value.trim();
94
- if (!trimmed || /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(trimmed)) return null;
95
- const pathPart = trimmed.split(/[?#]/, 1)[0];
96
- const normalized = pathPart.replace(/\\/g, '/').replace(/^\.\/+/g, '');
97
- if (!normalized || path.posix.isAbsolute(normalized) || normalized.includes('\0')) return null;
98
- const segments = normalized.split('/').filter(Boolean);
99
- if (segments.some((segment) => segment === '.' || segment === '..')) return null;
100
- const withoutArtifacts =
101
- segments[0] === 'artifacts' ? segments.slice(1).join('/') : segments.join('/');
102
- if (!withoutArtifacts || !mediaExt.test(withoutArtifacts)) return null;
103
- return `artifacts/${withoutArtifacts}`;
104
- }
105
-
106
- function addRef(refs, value) {
107
- const normalized = normalizeArtifactPath(value);
108
- if (!normalized) return;
109
- if (internalArtifactPath.test(normalized)) {
110
- issues.push(`evidence-manifest references internal artifact: ${normalized}`);
111
- return;
112
- }
113
- refs.add(normalized);
114
- }
115
-
116
- function validateRecipeQualityArtifact() {
117
- const text = readText('artifacts/recipe-quality.json');
118
- if (!text) return;
119
- let artifact;
120
- try {
121
- artifact = JSON.parse(text);
122
- } catch (error) {
123
- issues.push(`recipe-quality.json: invalid JSON: ${error.message}`);
124
- return;
125
- }
126
- if (!isRecord(artifact)) {
127
- issues.push('recipe-quality.json: expected object');
128
- return;
129
- }
130
- if (artifact.version !== 1) issues.push('recipe-quality.json: version must be 1');
131
- if (!['pass', 'warn', 'fail'].includes(artifact.verdict)) {
132
- issues.push('recipe-quality.json: verdict must be pass, warn, or fail');
133
- }
134
- if (!isRecord(artifact.compact) || !['PASS', 'WARN', 'FAIL'].includes(artifact.compact.verdict)) {
135
- issues.push('recipe-quality.json: compact.verdict must be PASS, WARN, or FAIL');
136
- }
137
- }
138
-
139
- function parseManifest() {
140
- const text = readText('artifacts/evidence-manifest.json');
141
- if (!text) return null;
142
- let manifest;
10
+ function resolveRuntimeScript() {
143
11
  try {
144
- manifest = JSON.parse(text);
145
- } catch (error) {
146
- issues.push(`evidence-manifest.json: invalid JSON: ${error.message}`);
147
- return null;
148
- }
149
- if (!isRecord(manifest)) {
150
- issues.push('evidence-manifest.json: expected object');
151
- return null;
152
- }
153
- unknownKeys(manifest, allowedManifestKeys, 'manifest');
154
- if (manifest.version !== undefined && typeof manifest.version !== 'number') {
155
- issues.push('manifest.version: expected number');
156
- }
157
- if (
158
- manifest.preferred_mode !== undefined &&
159
- !['screenshots', 'video'].includes(manifest.preferred_mode)
160
- ) {
161
- issues.push('manifest.preferred_mode: expected screenshots or video');
162
- }
163
- optionalString(manifest, 'summary', 'manifest');
164
-
165
- if (manifest.before_after_pairs !== undefined) {
166
- if (!Array.isArray(manifest.before_after_pairs)) {
167
- issues.push('manifest.before_after_pairs: expected array');
168
- } else {
169
- manifest.before_after_pairs.forEach((entry, index) => {
170
- const prefix = `manifest.before_after_pairs[${index}]`;
171
- if (!isRecord(entry)) return issues.push(`${prefix}: expected object`);
172
- unknownKeys(entry, allowedPairKeys, prefix);
173
- if (typeof entry.label !== 'string' || !entry.label.trim())
174
- issues.push(`${prefix}.label: expected non-empty string`);
175
- optionalStringArray(entry, 'covers', prefix);
176
- optionalString(entry, 'before', prefix);
177
- optionalString(entry, 'after', prefix);
178
- optionalString(entry, 'note', prefix);
179
- if (entry.before === undefined && entry.after === undefined)
180
- issues.push(`${prefix}: expected before or after`);
181
- });
182
- }
183
- }
184
-
185
- if (manifest.standalone !== undefined) {
186
- if (!Array.isArray(manifest.standalone)) {
187
- issues.push('manifest.standalone: expected array');
188
- } else {
189
- manifest.standalone.forEach((entry, index) => {
190
- const prefix = `manifest.standalone[${index}]`;
191
- if (!isRecord(entry)) return issues.push(`${prefix}: expected object`);
192
- unknownKeys(entry, allowedStandaloneKeys, prefix);
193
- if (typeof entry.label !== 'string' || !entry.label.trim())
194
- issues.push(`${prefix}.label: expected non-empty string`);
195
- optionalStringArray(entry, 'covers', prefix);
196
- if (typeof entry.file !== 'string' || !entry.file.trim())
197
- issues.push(`${prefix}.file: expected non-empty string`);
198
- optionalString(entry, 'note', prefix);
199
- });
200
- }
201
- }
202
-
203
- if (manifest.videos !== undefined) {
204
- if (!isRecord(manifest.videos)) {
205
- issues.push('manifest.videos: expected object');
206
- } else {
207
- unknownKeys(manifest.videos, allowedVideoKeys, 'manifest.videos');
208
- optionalString(manifest.videos, 'before', 'manifest.videos');
209
- optionalString(manifest.videos, 'after', 'manifest.videos');
210
- optionalString(manifest.videos, 'note', 'manifest.videos');
211
- if (
212
- manifest.videos.preferred !== undefined &&
213
- typeof manifest.videos.preferred !== 'boolean'
214
- ) {
215
- issues.push('manifest.videos.preferred: expected boolean');
216
- }
217
- }
218
- }
219
-
220
- if (manifest.omit !== undefined) {
221
- if (!Array.isArray(manifest.omit)) {
222
- issues.push('manifest.omit: expected array');
223
- } else {
224
- manifest.omit.forEach((entry, index) => {
225
- const prefix = `manifest.omit[${index}]`;
226
- if (typeof entry === 'string') return;
227
- if (!isRecord(entry)) return issues.push(`${prefix}: expected string or object`);
228
- unknownKeys(entry, allowedOmitKeys, prefix);
229
- if (typeof entry.file !== 'string' || !entry.file.trim())
230
- issues.push(`${prefix}.file: expected non-empty string`);
231
- optionalString(entry, 'reason', prefix);
232
- });
233
- }
234
- }
235
-
236
- const refs = new Set();
237
- for (const pair of manifest.before_after_pairs ?? []) {
238
- addRef(refs, pair?.before);
239
- addRef(refs, pair?.after);
240
- }
241
- for (const entry of manifest.standalone ?? []) addRef(refs, entry?.file);
242
- addRef(refs, manifest.videos?.before);
243
- addRef(refs, manifest.videos?.after);
244
-
245
- for (const ref of [...refs].sort()) {
246
- if (!fileExists(ref)) issues.push(`evidence-manifest references missing artifact: ${ref}`);
247
- }
248
- return { manifest, refs };
249
- }
250
-
251
- const hasRecipe = fileExists('artifacts/recipe.json');
252
- if (
253
- hasRecipe &&
254
- flags.has('--require-recipe-quality-if-recipe') &&
255
- !fileExists('artifacts/recipe-quality.json')
256
- ) {
257
- issues.push('recipe.json exists but artifacts/recipe-quality.json is missing');
258
- }
259
- if (
260
- hasRecipe &&
261
- flags.has('--require-recipe-coverage-if-recipe') &&
262
- !fileExists('artifacts/recipe-coverage.md')
263
- ) {
264
- issues.push('recipe.json exists but artifacts/recipe-coverage.md is missing');
265
- }
266
- validateRecipeQualityArtifact();
267
-
268
- const parsed = parseManifest();
269
- const coverage = readText('artifacts/recipe-coverage.md');
270
- if (coverage && /\|\s*(visual|mixed)\s*\|/i.test(coverage)) {
271
- const refCount = parsed?.refs?.size ?? 0;
272
- if (refCount === 0)
273
- issues.push(
274
- 'recipe-coverage.md declares visual/mixed proof but evidence-manifest has no media references',
12
+ return requireFromHere.resolve(
13
+ '@farmslot/agent-runtime/scripts/check-task-artifact-contract.mjs',
275
14
  );
276
- }
277
-
278
- if (flags.has('--require-learnings')) {
279
- const learnings = readText('artifacts/learnings.md');
280
- if (!learnings?.trim()) {
281
- issues.push('artifacts/learnings.md: required non-empty learnings artifact');
282
- }
283
- }
284
-
285
- if (contractPath && existsSync(contractPath) && terminalCommand) {
286
- const contract = JSON.parse(readFileSync(contractPath, 'utf8'));
287
- const expanded = expandedArtifactsForCommand(contract, terminalCommand, fileExists);
288
- for (const artifactPath of expanded.artifacts) {
289
- if (flags.has('--skip-learnings') && artifactPath === 'artifacts/learnings.md') continue;
290
- if (artifactPath.endsWith('.md') && !readText(artifactPath)?.trim()) {
291
- issues.push(`${artifactPath}: required non-empty artifact (worker terminal contract)`);
292
- }
293
- if (!fileExists(artifactPath)) {
294
- issues.push(`${artifactPath}: required artifact missing (worker terminal contract)`);
295
- }
296
- }
297
- if (expanded.requireRecipeQuality && hasRecipe && !fileExists('artifacts/recipe-quality.json')) {
298
- issues.push('recipe.json exists but artifacts/recipe-quality.json is missing');
299
- }
300
- if (expanded.requireRecipeCoverage && hasRecipe && !fileExists('artifacts/recipe-coverage.md')) {
301
- issues.push('recipe.json exists but artifacts/recipe-coverage.md is missing');
15
+ } catch (error) {
16
+ if (error.code !== 'MODULE_NOT_FOUND') throw error;
17
+ return path.resolve(packageRoot, '../agent-runtime/scripts/check-task-artifact-contract.mjs');
302
18
  }
303
19
  }
304
20
 
305
- if (issues.length > 0) {
306
- console.error('TASK_ARTIFACT_CONTRACT_FAIL');
307
- for (const issue of issues) console.error(`- ${issue}`);
308
- process.exit(1);
309
- }
310
- console.log('TASK_ARTIFACT_CONTRACT_PASS');
21
+ const result = spawnSync(process.execPath, [resolveRuntimeScript(), ...process.argv.slice(2)], {
22
+ stdio: 'inherit',
23
+ });
24
+ process.exit(result.status ?? 1);