@farmslot/skills 0.1.0 → 0.1.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.
package/CHANGELOG.md CHANGED
@@ -4,4 +4,10 @@ All notable changes to `@farmslot/skills` are tracked here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.1.1 - 2026-06-30
8
+
9
+ - Single-path mark CLI for worker terminal signals.
10
+
11
+ ## 0.1.0 - 2026-06-28
12
+
7
13
  - Add the generic recipe-first skills package and migrate the former top-level recipe cooking kit into `skills/recipe-cook`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arthur Breton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Public guide: https://farmslot.io/docs/guides/recipe-skills-adoption
6
6
 
7
7
  ## What This Package Is
8
8
 
9
- `@farmslot/skills` is the recipe-first adoption kit. It teaches an agent how to write, review, diagnose, and adopt validation recipes before a team installs the full Farmslot OS.
9
+ `@farmslot/skills` is the recipe-first adoption kit. It teaches an agent how to write, review, diagnose, and adopt validation recipes before a team installs the full Farmslot framework.
10
10
 
11
11
  It does not require Gateway, Command Center, a pool file, slots, or Companion for first use.
12
12
 
@@ -49,7 +49,7 @@ Supported layouts:
49
49
  2. Recipe runner: add a local command that can execute one recipe and emit artifacts.
50
50
  3. Project recipe layer: add recipes, runner manifests, project actions, and artifact conventions.
51
51
  4. Farmslot project integration: add `project.json` hooks for Command Center and slots.
52
- 5. Full OS: adopt multi-slot dispatch, live steering, Companion supervision, and retrospectives.
52
+ 5. Full framework: adopt multi-slot dispatch, live steering, Companion supervision, and retrospectives.
53
53
 
54
54
  ## Source layout
55
55
 
@@ -61,6 +61,7 @@ Supported layouts:
61
61
 
62
62
  ## Maintenance rules
63
63
 
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.
64
65
  - Keep package payloads self-contained; consumers should not need the Farmslot monorepo.
65
66
  - Update `CHANGELOG.md` before packaging or publishing.
66
67
  - Run the package readiness guard when files move, bins change, or exports are added.
@@ -72,3 +73,7 @@ yarn workspace @farmslot/skills build
72
73
  yarn workspace @farmslot/skills test
73
74
  node scripts/quality/check-farmslot-package-readiness.mjs --packages @farmslot/skills
74
75
  ```
76
+
77
+ ## License
78
+
79
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../scripts/mark-checklist-step.cjs';
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@farmslot/skills",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "bin": {
8
+ "farmslot-checklist": "./bin/farmslot-checklist.mjs",
8
9
  "farmslot-skills": "./bin/farmslot-skills.mjs"
9
10
  },
10
11
  "exports": {
@@ -14,11 +15,12 @@
14
15
  "default": "./dist/index.js"
15
16
  },
16
17
  "./skills/*": "./skills/*/SKILL.md",
17
- "./schemas/*": "./schemas/*"
18
+ "./schemas/*": "./schemas/*",
19
+ "./scripts/mark-checklist-step.cjs": "./scripts/mark-checklist-step.cjs"
18
20
  },
19
21
  "scripts": {
20
22
  "typecheck": "tsc --noEmit --project tsconfig.json",
21
- "test": "node test/prepare-cooking-run.test.cjs && node test/run-cooking-lane.test.cjs && node test/validator-loop.test.cjs && node test/package-exports.test.cjs",
23
+ "test": "node test/mark-checklist-step.test.cjs && node test/prepare-cooking-run.test.cjs && node test/run-cooking-lane.test.cjs && node test/validator-loop.test.cjs && node test/package-exports.test.cjs",
22
24
  "format": "prettier --write --ignore-path ../../.prettierignore .",
23
25
  "format:check": "prettier --check --ignore-path ../../.prettierignore .",
24
26
  "lint": "eslint \"src/**/*.ts\"",
@@ -53,6 +55,7 @@
53
55
  ],
54
56
  "files": [
55
57
  "README.md",
58
+ "LICENSE",
56
59
  "CHANGELOG.md",
57
60
  "bin/**/*.mjs",
58
61
  "dist/**/*.js",
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+
5
+ const START_COMMANDS = new Set(['start']);
6
+ const TERMINAL_COMMANDS = new Set(['complete', 'no-change', 'blocked']);
7
+ const NO_CHANGE_REPORT = path.join('artifacts', 'no-change-report.md');
8
+
9
+ const usageLine =
10
+ 'usage: mark <step-number|start|complete|no-change|blocked> [--reason text] [--already-fixed] [--mark-last] [--no-self-review]';
11
+
12
+ function printHelp() {
13
+ console.log(
14
+ [
15
+ usageLine,
16
+ '',
17
+ 'Bootstrap: ./mark start — worker-owned SIGNAL.json with status running (no checklist box).',
18
+ 'Progress: ./mark 1, ./mark 2, ... — checks the box and appends checklistTiming.',
19
+ 'Terminal:',
20
+ ' ./mark complete [--mark-last] [--no-self-review]',
21
+ ' ./mark no-change --reason "..." [--already-fixed] [--mark-last]',
22
+ ' ./mark blocked --reason "..." [--mark-last]',
23
+ 'Do not write SIGNAL.json by hand or with echo.',
24
+ ].join('\n'),
25
+ );
26
+ }
27
+
28
+ function usage() {
29
+ console.error(usageLine);
30
+ console.error('run ./mark --help for details');
31
+ process.exit(2);
32
+ }
33
+
34
+ const args = process.argv.slice(2);
35
+ if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) {
36
+ printHelp();
37
+ process.exit(0);
38
+ }
39
+
40
+ const [taskPath, signalPath, stepRaw, ...rest] = args;
41
+ if (stepRaw === '--help' || stepRaw === '-h') {
42
+ printHelp();
43
+ process.exit(0);
44
+ }
45
+ if (!taskPath || !signalPath || !stepRaw) usage();
46
+
47
+ const opts = {};
48
+ for (let i = 0; i < rest.length; i += 1) {
49
+ const key = rest[i];
50
+ if (key === '--mark-last' || key === '--already-fixed' || key === '--no-self-review') {
51
+ opts[key.slice(2)] = true;
52
+ continue;
53
+ }
54
+ const value = rest[i + 1];
55
+ if (!key?.startsWith('--') || value === undefined) usage();
56
+ opts[key.slice(2)] = value;
57
+ i += 1;
58
+ }
59
+
60
+ const stepToken = String(stepRaw).toLowerCase();
61
+ const isStartCommand = START_COMMANDS.has(stepToken);
62
+ const terminalCommand = TERMINAL_COMMANDS.has(stepToken) ? stepToken : null;
63
+ let stepNumber = null;
64
+ if (!isStartCommand && !terminalCommand) {
65
+ stepNumber = Number(stepRaw);
66
+ if (!Number.isInteger(stepNumber) || stepNumber < 1) usage();
67
+ }
68
+
69
+ if (isStartCommand && Object.keys(opts).length > 0) usage();
70
+ if (terminalCommand === 'no-change' || terminalCommand === 'blocked') {
71
+ if (!opts.reason?.trim()) {
72
+ console.error(`${terminalCommand} requires --reason`);
73
+ process.exit(1);
74
+ }
75
+ }
76
+
77
+ const SIGNAL_PASSTHROUGH_KEYS = ['role', 'contextId', 'prNumber'];
78
+
79
+ function pickSignalPassthrough(signal) {
80
+ const out = {};
81
+ for (const key of SIGNAL_PASSTHROUGH_KEYS) {
82
+ if (signal[key] !== undefined) out[key] = signal[key];
83
+ }
84
+ return out;
85
+ }
86
+
87
+ function resolveTerminalPreset(command) {
88
+ switch (command) {
89
+ case 'complete':
90
+ return { status: 'complete', outcome: 'success', disposition: 'fixed' };
91
+ case 'no-change':
92
+ return {
93
+ status: 'complete',
94
+ outcome: 'success',
95
+ disposition: opts['already-fixed'] ? 'already_fixed' : 'not_reproducible',
96
+ };
97
+ case 'blocked':
98
+ return { status: 'blocked', outcome: 'partial', disposition: 'blocked' };
99
+ default:
100
+ usage();
101
+ return null;
102
+ }
103
+ }
104
+
105
+ function assertNoChangeReport(taskDir) {
106
+ const reportAbs = path.join(taskDir, NO_CHANGE_REPORT);
107
+ if (!fs.existsSync(reportAbs) || fs.statSync(reportAbs).size === 0) {
108
+ console.error(`missing required report: ${NO_CHANGE_REPORT}`);
109
+ process.exit(1);
110
+ }
111
+ return NO_CHANGE_REPORT;
112
+ }
113
+
114
+ function buildSignalUpdate(signal, terminal, target, timing, events, now, taskPath, taskDir) {
115
+ const base = {
116
+ ...pickSignalPassthrough(signal),
117
+ step: target?.label ?? signal.step ?? 'complete',
118
+ checklistTiming: {
119
+ schemaVersion: 1,
120
+ source: timing.source || path.basename(taskPath),
121
+ events,
122
+ },
123
+ timestamp: now,
124
+ };
125
+
126
+ if (!terminal) {
127
+ return { ...base, status: 'running' };
128
+ }
129
+
130
+ const preset = resolveTerminalPreset(terminal.command);
131
+ const next = {
132
+ ...base,
133
+ status: preset.status,
134
+ outcome: preset.outcome,
135
+ disposition: preset.disposition,
136
+ ...(opts.reason ? { reason: opts.reason } : {}),
137
+ ...(terminal.command === 'complete' && opts['no-self-review']
138
+ ? { needsSelfReview: false }
139
+ : {}),
140
+ };
141
+
142
+ if (terminal.command === 'no-change') {
143
+ next.evidence = { reportPath: assertNoChangeReport(taskDir) };
144
+ }
145
+
146
+ return next;
147
+ }
148
+
149
+ function atomicWrite(file, content, mode) {
150
+ const dir = path.dirname(file);
151
+ fs.mkdirSync(dir, { recursive: true });
152
+ const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.tmp`);
153
+ fs.writeFileSync(tmp, content, mode ? { mode } : undefined);
154
+ fs.renameSync(tmp, file);
155
+ }
156
+
157
+ function readJson(file) {
158
+ try {
159
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
160
+ } catch (err) {
161
+ if (err && err.code === 'ENOENT') return {};
162
+ throw err;
163
+ }
164
+ }
165
+
166
+ function stripLabel(raw) {
167
+ return raw
168
+ .replace(/^\*\*(.*?)\*\*\s*[—-]?\s*/, '$1 ')
169
+ .replace(/`([^`]+)`/g, '$1')
170
+ .replace(/\s+/g, ' ')
171
+ .trim();
172
+ }
173
+
174
+ function parseChecklist(markdown) {
175
+ const lines = markdown.split(/\n/);
176
+ const items = [];
177
+ let inFence = false;
178
+ let seen = 0;
179
+ for (let i = 0; i < lines.length; i += 1) {
180
+ if (/^\s*```/.test(lines[i])) {
181
+ inFence = !inFence;
182
+ continue;
183
+ }
184
+ if (inFence) continue;
185
+ const match = lines[i].match(/^(\s*[-*]\s+\[)( |x|X)(\]\s+)(.*)$/);
186
+ if (!match) continue;
187
+ seen += 1;
188
+ items.push({
189
+ lineIndex: i,
190
+ stepNumber: seen,
191
+ checked: match[2].toLowerCase() === 'x',
192
+ prefix: match[1],
193
+ suffix: match[3],
194
+ rawLabel: match[4],
195
+ label: stripLabel(match[4]),
196
+ });
197
+ }
198
+ return { lines, items };
199
+ }
200
+
201
+ function markStepInLines(lines, item) {
202
+ if (item.checked) return false;
203
+ lines[item.lineIndex] = `${item.prefix}x${item.suffix}${item.rawLabel}`;
204
+ return true;
205
+ }
206
+
207
+ function resolveTarget(taskPath, stepNumber, markLast) {
208
+ const original = fs.readFileSync(taskPath, 'utf8');
209
+ const parsed = parseChecklist(original);
210
+ let item = null;
211
+ if (stepNumber != null) {
212
+ item = parsed.items.find((entry) => entry.stepNumber === stepNumber) ?? null;
213
+ if (!item) {
214
+ console.error(`checklist step ${stepNumber} not found in ${taskPath}`);
215
+ process.exit(1);
216
+ }
217
+ } else if (markLast) {
218
+ const unchecked = parsed.items.filter((entry) => !entry.checked);
219
+ item = unchecked.length ? unchecked[unchecked.length - 1] : null;
220
+ }
221
+ let updated = original;
222
+ if (item) {
223
+ const nextLines = [...parsed.lines];
224
+ markStepInLines(nextLines, item);
225
+ updated = nextLines.join('\n');
226
+ if (updated !== original) atomicWrite(taskPath, updated);
227
+ }
228
+ return item
229
+ ? { stepNumber: item.stepNumber, label: item.label }
230
+ : { stepNumber: null, label: 'complete' };
231
+ }
232
+
233
+ const taskDir = path.dirname(signalPath);
234
+ const target = isStartCommand
235
+ ? { stepNumber: null, label: 'started' }
236
+ : resolveTarget(taskPath, stepNumber, Boolean(terminalCommand && opts['mark-last']));
237
+
238
+ const now = new Date().toISOString();
239
+ const signal = readJson(signalPath);
240
+ const timing =
241
+ signal.checklistTiming && typeof signal.checklistTiming === 'object'
242
+ ? signal.checklistTiming
243
+ : { schemaVersion: 1, source: path.basename(taskPath), events: [] };
244
+ const events = Array.isArray(timing.events) ? [...timing.events] : [];
245
+
246
+ if (
247
+ !isStartCommand &&
248
+ target.stepNumber != null &&
249
+ !events.some(
250
+ (event) =>
251
+ event && (event.stepNumber === target.stepNumber || event.index === target.stepNumber - 1),
252
+ )
253
+ ) {
254
+ events.push({ stepNumber: target.stepNumber, label: target.label, checkedAt: now });
255
+ }
256
+
257
+ const next = buildSignalUpdate(
258
+ signal,
259
+ terminalCommand ? { command: terminalCommand } : null,
260
+ target,
261
+ timing,
262
+ events,
263
+ now,
264
+ taskPath,
265
+ taskDir,
266
+ );
267
+ atomicWrite(signalPath, `${JSON.stringify(next, null, 2)}\n`, 0o644);
268
+
269
+ if (isStartCommand) {
270
+ console.log('signal started');
271
+ } else if (terminalCommand) {
272
+ console.log(
273
+ `signal ${terminalCommand}: status=${next.status} disposition=${next.disposition ?? 'n/a'}`,
274
+ );
275
+ } else {
276
+ console.log(`marked ${target.stepNumber}: ${target.label}`);
277
+ }
@@ -17,7 +17,7 @@ Recommend a pragmatic adoption path for recipe-first validation.
17
17
  1. Recipe runner: add a local command that executes one recipe and emits artifacts.
18
18
  2. Project recipe layer: add recipes, runner manifest, project actions, and artifact conventions.
19
19
  3. Farmslot project integration: add `project.json` hooks for slot-based execution.
20
- 4. Full OS: adopt Gateway, Command Center, multi-slot dispatch, Companion, and retrospectives.
20
+ 4. Full framework: adopt Gateway, Command Center, multi-slot dispatch, Companion, and retrospectives.
21
21
 
22
22
  ## Workflow
23
23
 
@@ -28,6 +28,6 @@ Recommend a pragmatic adoption path for recipe-first validation.
28
28
 
29
29
  ## Hard Rules
30
30
 
31
- - Do not prescribe the full Farmslot OS when a local recipe runner is enough.
31
+ - Do not prescribe the full Farmslot framework when a local recipe runner is enough.
32
32
  - Do not add domain-specific assumptions.
33
33
  - Do not require a project to change its test stack before proving the first useful recipe.
@@ -61,7 +61,7 @@ If an interactive slot exists, do not stop after pasting or emitting JSON in cha
61
61
 
62
62
  The harness order is mandatory.
63
63
  Do not silently batch or skip steps.
64
- Mark each completed checklist step `[x]` immediately. Do not batch checkbox updates.
64
+ Mark each completed checklist step immediately. If the run-local task has a `mark` helper, use `./mark N` (1-based); otherwise change `[ ]` to `[x]` yourself. Do not batch checkbox updates.
65
65
 
66
66
  ## Output Contract
67
67
 
@@ -10,7 +10,7 @@ Do not edit the template in-place during a cooking run.
10
10
 
11
11
  Every time you complete a checklist step:
12
12
 
13
- 1. immediately change `- [ ]` to `- [x]`
13
+ 1. run `./mark N` if present (1-based); otherwise immediately change `- [ ]` to `- [x]`
14
14
  2. update `STATUS` if needed
15
15
  3. only then move to the next step
16
16
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: recipe-harness
3
- description: Help run or install a local recipe runtime without assuming the full Farmslot OS
3
+ description: Help run or install a local recipe runtime without assuming the full Farmslot framework
4
4
  compatibility: Claude, Codex, Cursor, and Markdown skill runners with shell access
5
5
  metadata:
6
6
  package: '@farmslot/skills'