@farmslot/agent-runtime 0.12.0 → 0.13.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 +20 -0
- package/bin/farmslot-agent.mjs +7 -0
- package/dist/execution-template/subtask-render-parity.test.d.ts +2 -0
- package/dist/execution-template/subtask-render-parity.test.d.ts.map +1 -0
- package/dist/execution-template/subtask-render-parity.test.js +63 -0
- package/dist/execution-template/subtask-render-parity.test.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/task-init/index.d.ts.map +1 -1
- package/dist/task-init/index.js +1 -0
- package/dist/task-init/index.js.map +1 -1
- package/dist/task-init/task-document.d.ts +16 -2
- package/dist/task-init/task-document.d.ts.map +1 -1
- package/dist/task-init/task-document.js +21 -8
- package/dist/task-init/task-document.js.map +1 -1
- package/package.json +7 -3
- package/scripts/acceptance-cli.cjs +139 -0
- package/scripts/acceptance-ledger.cjs +412 -0
- package/scripts/check-task-artifact-contract.mjs +25 -1
- package/scripts/checklist-target.cjs +56 -0
- package/scripts/mark-checklist-step.cjs +97 -60
- package/scripts/mark-io.cjs +118 -0
- package/scripts/subtask-unit.cjs +724 -0
- package/scripts/task-init-cli.mjs +4 -0
- package/scripts/worker-terminal-contract.cjs +21 -0
|
@@ -8,11 +8,25 @@ const {
|
|
|
8
8
|
resolveWorkerTerminalContract,
|
|
9
9
|
} = require('./worker-terminal-contract.cjs');
|
|
10
10
|
const {
|
|
11
|
-
checklistStepName,
|
|
12
|
-
enumerateChecklistCheckboxes,
|
|
13
11
|
parseTaskDirMarkArgs,
|
|
14
12
|
terminalContractInputForChecklist,
|
|
15
13
|
} = require('./checklist-target.cjs');
|
|
14
|
+
const {
|
|
15
|
+
atomicWrite,
|
|
16
|
+
markStepInLines,
|
|
17
|
+
parseChecklist,
|
|
18
|
+
pickSignalPassthrough,
|
|
19
|
+
readJson,
|
|
20
|
+
signalContentUnchanged,
|
|
21
|
+
writeSignal,
|
|
22
|
+
} = require('./mark-io.cjs');
|
|
23
|
+
const { AcceptanceRefusal, requireHandoffAcceptanceCriteria } = require('./acceptance-ledger.cjs');
|
|
24
|
+
const {
|
|
25
|
+
openSubtaskRefusal,
|
|
26
|
+
openSubtaskUnits,
|
|
27
|
+
runSubtaskCommand,
|
|
28
|
+
subtaskOwningStep,
|
|
29
|
+
} = require('./subtask-unit.cjs');
|
|
16
30
|
|
|
17
31
|
const START_COMMANDS = new Set(['start']);
|
|
18
32
|
const TERMINAL_COMMANDS = new Set(['complete', 'no-change', 'blocked']);
|
|
@@ -49,6 +63,8 @@ function printHelp() {
|
|
|
49
63
|
'Override: farmslot-agent mark <task-dir> --checklist SELF-REVIEW.md <step> — optional --signal; signal defaults from checklist name.',
|
|
50
64
|
'Bootstrap: ./mark start — role-owned signal with status running (no checklist box).',
|
|
51
65
|
'Progress: ./mark 1, ./mark 2, ... — checks the box and appends checklistTiming.',
|
|
66
|
+
'Child units: ./mark sub start <id> --step N --from <path|inline:text>, then ./mark sub <id> <n> | complete [--report PATH] [--mark-last] | blocked --reason "..." | status.',
|
|
67
|
+
' While a child owns step N, ./mark N is refused; the child complete ticks it. Run ./mark sub --help for the child verbs.',
|
|
52
68
|
'Terminal:',
|
|
53
69
|
' ./mark complete [--mark-last] [--no-self-review] [--skip-learnings] [--skip-checklist]',
|
|
54
70
|
' ./mark no-change --reason "..." [--already-fixed] [--mark-last] [--skip-learnings] [--skip-checklist]',
|
|
@@ -86,6 +102,11 @@ function resolveMarkInvocation(rawArgs) {
|
|
|
86
102
|
const firstPath = path.resolve(first);
|
|
87
103
|
const firstIsDir = fs.existsSync(firstPath) && fs.statSync(firstPath).isDirectory();
|
|
88
104
|
if (firstIsDir) {
|
|
105
|
+
// Child-unit verbs own their own argument grammar (`sub start <id> …`), so
|
|
106
|
+
// they are routed before the parent step parser sees a non-step token.
|
|
107
|
+
if (rawArgs[1] === 'sub') {
|
|
108
|
+
process.exit(runSubtaskCommand(firstPath, rawArgs.slice(2)));
|
|
109
|
+
}
|
|
89
110
|
const parsed = parseTaskDirMarkArgs(firstPath, rawArgs.slice(1), {
|
|
90
111
|
isMarkStepToken,
|
|
91
112
|
usage,
|
|
@@ -139,16 +160,6 @@ if (terminalCommand === 'no-change' || terminalCommand === 'blocked') {
|
|
|
139
160
|
}
|
|
140
161
|
}
|
|
141
162
|
|
|
142
|
-
const SIGNAL_PASSTHROUGH_KEYS = ['role', 'contextId', 'attemptId', 'prNumber'];
|
|
143
|
-
|
|
144
|
-
function pickSignalPassthrough(signal) {
|
|
145
|
-
const out = {};
|
|
146
|
-
for (const key of SIGNAL_PASSTHROUGH_KEYS) {
|
|
147
|
-
if (signal[key] !== undefined) out[key] = signal[key];
|
|
148
|
-
}
|
|
149
|
-
return out;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
163
|
function resolveTerminalPreset(command) {
|
|
153
164
|
switch (command) {
|
|
154
165
|
case 'complete':
|
|
@@ -309,6 +320,25 @@ function assertArtifactContract(taskDir, taskPath, contract, terminalCommand) {
|
|
|
309
320
|
}
|
|
310
321
|
const contractPath = terminalContractPath(taskDir, taskPath);
|
|
311
322
|
const args = [ARTIFACT_CONTRACT_SCRIPT, taskDir];
|
|
323
|
+
// The ledger blocks `complete` only where the project opted in
|
|
324
|
+
// (`worker_terminal.acceptance.require`) AND the task registered criteria
|
|
325
|
+
// (ADR-060). Without the opt-in a template that does not write a ledger yet
|
|
326
|
+
// still completes; the ledger is watched, projected and preferred for coverage
|
|
327
|
+
// either way.
|
|
328
|
+
if (terminalCommand === 'complete' && contract?.acceptance?.require === true) {
|
|
329
|
+
let criteria;
|
|
330
|
+
try {
|
|
331
|
+
criteria = requireHandoffAcceptanceCriteria(taskDir);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
// Fail closed, the same way the gateway's backup check does: a handoff the
|
|
334
|
+
// engine cannot read hides whether the criteria were judged, and dropping
|
|
335
|
+
// the rule the project asked for is how an unproven run closes.
|
|
336
|
+
if (!(err instanceof AcceptanceRefusal)) throw err;
|
|
337
|
+
console.error(err.message);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
if (criteria.length > 0) args.push('--require-acceptance-status');
|
|
341
|
+
}
|
|
312
342
|
if (fs.existsSync(contractPath)) {
|
|
313
343
|
args.push('--contract', contractPath);
|
|
314
344
|
if (terminalCommand) args.push('--terminal', terminalCommand);
|
|
@@ -403,45 +433,6 @@ function buildSignalUpdate(signal, terminal, target, timing, events, now, taskPa
|
|
|
403
433
|
return next;
|
|
404
434
|
}
|
|
405
435
|
|
|
406
|
-
function atomicWrite(file, content, mode) {
|
|
407
|
-
const dir = path.dirname(file);
|
|
408
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
409
|
-
const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.tmp`);
|
|
410
|
-
fs.writeFileSync(tmp, content, mode ? { mode } : undefined);
|
|
411
|
-
fs.renameSync(tmp, file);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function readJson(file) {
|
|
415
|
-
try {
|
|
416
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
417
|
-
} catch (err) {
|
|
418
|
-
if (err && err.code === 'ENOENT') return {};
|
|
419
|
-
throw err;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Step enumeration is shared with the gateway parsers (generateTaskSchema in
|
|
424
|
-
// tasks/writer.ts and parseCheckboxStates in methods/task.ts) via the
|
|
425
|
-
// checklist-target enumerator: same skip sections, <details> handling, and
|
|
426
|
-
// checkbox shape. Any divergence makes `mark N` check a different box than
|
|
427
|
-
// the one progress reporting counts as step N (checkbox-formatted Acceptance
|
|
428
|
-
// Criteria used to shift every step by the AC count).
|
|
429
|
-
function parseChecklist(markdown) {
|
|
430
|
-
const lines = markdown.split(/\n/);
|
|
431
|
-
const items = enumerateChecklistCheckboxes(markdown).map((item) => ({
|
|
432
|
-
...item,
|
|
433
|
-
label: checklistStepName(item.rawLabel),
|
|
434
|
-
}));
|
|
435
|
-
return { lines, items };
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function markStepInLines(lines, item) {
|
|
439
|
-
if (item.checked) return false;
|
|
440
|
-
const before = lines[item.lineIndex];
|
|
441
|
-
lines[item.lineIndex] = before.replace(/^(\s*- \[)( |x|X)(\])/, '$1x$3');
|
|
442
|
-
return lines[item.lineIndex] !== before;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
436
|
function resolveTarget(taskPath, stepNumber, markLast) {
|
|
446
437
|
const original = fs.readFileSync(taskPath, 'utf8');
|
|
447
438
|
const parsed = parseChecklist(original);
|
|
@@ -458,6 +449,9 @@ function resolveTarget(taskPath, stepNumber, markLast) {
|
|
|
458
449
|
}
|
|
459
450
|
let updated = original;
|
|
460
451
|
if (item) {
|
|
452
|
+
// Also covers `--mark-last`: the parent must not tick a box whose child is
|
|
453
|
+
// still open, whichever way the row was chosen.
|
|
454
|
+
assertStepNotOwnedBySubtask(item.stepNumber);
|
|
461
455
|
const nextLines = [...parsed.lines];
|
|
462
456
|
markStepInLines(nextLines, item);
|
|
463
457
|
updated = nextLines.join('\n');
|
|
@@ -469,6 +463,36 @@ function resolveTarget(taskPath, stepNumber, markLast) {
|
|
|
469
463
|
}
|
|
470
464
|
|
|
471
465
|
const taskDir = path.dirname(signalPath);
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* A step owned by a child unit belongs to that child until it is settled
|
|
469
|
+
* (`complete` or `done` — a `blocked` child keeps ownership). The child's
|
|
470
|
+
* `complete` ticks the box, so a later `mark N` on it is an idempotent no-op.
|
|
471
|
+
*/
|
|
472
|
+
function assertStepNotOwnedBySubtask(stepNumber) {
|
|
473
|
+
if (stepNumber == null) return;
|
|
474
|
+
const owner = subtaskOwningStep(taskDir, path.basename(taskPath), stepNumber);
|
|
475
|
+
if (!owner) return;
|
|
476
|
+
const open = openSubtaskUnits(taskDir).find((entry) => entry.unit.id === owner.id);
|
|
477
|
+
if (!open) return;
|
|
478
|
+
console.error(
|
|
479
|
+
`step ${stepNumber} is owned by subtask ${owner.id}; finish it with ./mark sub ${owner.id} complete`,
|
|
480
|
+
);
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Every registered child must be settled before the parent reports success. */
|
|
485
|
+
function assertNoOpenSubtasks(command) {
|
|
486
|
+
const open = openSubtaskUnits(taskDir);
|
|
487
|
+
if (open.length === 0) return;
|
|
488
|
+
console.error(openSubtaskRefusal(open, command));
|
|
489
|
+
process.exit(1);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// `blocked` stays available while a child is open: the run is blocked either way.
|
|
493
|
+
if (terminalCommand === 'complete' || terminalCommand === 'no-change') {
|
|
494
|
+
assertNoOpenSubtasks(terminalCommand);
|
|
495
|
+
}
|
|
472
496
|
if (
|
|
473
497
|
terminalCommand &&
|
|
474
498
|
(terminalCommand === 'complete' || terminalCommand === 'no-change') &&
|
|
@@ -514,14 +538,27 @@ const next = buildSignalUpdate(
|
|
|
514
538
|
taskPath,
|
|
515
539
|
taskDir,
|
|
516
540
|
);
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
541
|
+
// Re-marking a step that is already done changes nothing: the box was checked
|
|
542
|
+
// and its timing event already exists, so the only difference from the stored
|
|
543
|
+
// signal would be a fresh `timestamp`. Compare content, never the command: that
|
|
544
|
+
// keeps `start` (new attemptId), a terminal mark (new status), a resume from
|
|
545
|
+
// `blocked`, and a box checked by hand without an event all writing as before,
|
|
546
|
+
// while a genuinely empty re-mark stays a no-op.
|
|
547
|
+
const alreadyRecorded = !isStartCommand && !terminalCommand && signalContentUnchanged(signal, next);
|
|
548
|
+
|
|
549
|
+
if (alreadyRecorded) {
|
|
550
|
+
console.log(`already marked ${target.stepNumber}: ${target.label}`);
|
|
525
551
|
} else {
|
|
526
|
-
|
|
552
|
+
// One signal writer for the parent and child paths, so their bytes and mode
|
|
553
|
+
// cannot drift (mark-io.cjs writeSignal).
|
|
554
|
+
writeSignal(signalPath, next);
|
|
555
|
+
if (isStartCommand) {
|
|
556
|
+
console.log('signal started');
|
|
557
|
+
} else if (terminalCommand) {
|
|
558
|
+
console.log(
|
|
559
|
+
`signal ${terminalCommand}: status=${next.status} disposition=${next.disposition ?? 'n/a'}`,
|
|
560
|
+
);
|
|
561
|
+
} else {
|
|
562
|
+
console.log(`marked ${target.stepNumber}: ${target.label}`);
|
|
563
|
+
}
|
|
527
564
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const { checklistStepName, enumerateChecklistCheckboxes } = require('./checklist-target.cjs');
|
|
5
|
+
|
|
6
|
+
// One definition of how the mark engine reads and writes task-dir files, shared
|
|
7
|
+
// by the parent mark path and the child-unit (`sub`) verbs so both write the
|
|
8
|
+
// same bytes the same way.
|
|
9
|
+
|
|
10
|
+
function atomicWrite(file, content, mode) {
|
|
11
|
+
const dir = path.dirname(file);
|
|
12
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
13
|
+
const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.tmp`);
|
|
14
|
+
fs.writeFileSync(tmp, content, mode ? { mode } : undefined);
|
|
15
|
+
fs.renameSync(tmp, file);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readJson(file) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (err && err.code === 'ENOENT') return {};
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Signal JSON with a trailing newline and the mode every signal file carries. */
|
|
28
|
+
function writeSignal(signalPath, signal) {
|
|
29
|
+
atomicWrite(signalPath, `${JSON.stringify(signal, null, 2)}\n`, 0o644);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Registry JSON with a trailing newline and the same mode as a signal file. */
|
|
33
|
+
function writeIndex(indexPath, index) {
|
|
34
|
+
atomicWrite(indexPath, `${JSON.stringify(index, null, 2)}\n`, 0o644);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Stable string for "is this the same signal content?" — key order and object
|
|
39
|
+
* nesting normalized, so two writers that agree on values but not on insertion
|
|
40
|
+
* order compare equal.
|
|
41
|
+
*/
|
|
42
|
+
function canonicalJson(value) {
|
|
43
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
44
|
+
if (value && typeof value === 'object') {
|
|
45
|
+
return `{${Object.keys(value)
|
|
46
|
+
.sort()
|
|
47
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
|
48
|
+
.join(',')}}`;
|
|
49
|
+
}
|
|
50
|
+
return JSON.stringify(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** True when two signals differ only by `timestamp` (or not at all). */
|
|
54
|
+
function signalContentUnchanged(stored, candidate) {
|
|
55
|
+
const withoutTimestamp = (signal) => {
|
|
56
|
+
const { timestamp: _timestamp, ...rest } = signal;
|
|
57
|
+
return canonicalJson(rest);
|
|
58
|
+
};
|
|
59
|
+
return withoutTimestamp(stored) === withoutTimestamp(candidate);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const SIGNAL_PASSTHROUGH_KEYS = ['role', 'contextId', 'attemptId', 'prNumber'];
|
|
63
|
+
|
|
64
|
+
function pickSignalPassthrough(signal) {
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const key of SIGNAL_PASSTHROUGH_KEYS) {
|
|
67
|
+
if (signal[key] !== undefined) out[key] = signal[key];
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Step enumeration is shared with the gateway parsers (generateTaskSchema in
|
|
73
|
+
// tasks/writer.ts and parseCheckboxStates in methods/task.ts) via the
|
|
74
|
+
// checklist-target enumerator: same skip sections, <details> handling, and
|
|
75
|
+
// checkbox shape. Any divergence makes `mark N` check a different box than
|
|
76
|
+
// the one progress reporting counts as step N (checkbox-formatted Acceptance
|
|
77
|
+
// Criteria used to shift every step by the AC count).
|
|
78
|
+
function parseChecklist(markdown) {
|
|
79
|
+
const lines = markdown.split(/\n/);
|
|
80
|
+
const items = enumerateChecklistCheckboxes(markdown).map((item) => ({
|
|
81
|
+
...item,
|
|
82
|
+
label: checklistStepName(item.rawLabel),
|
|
83
|
+
}));
|
|
84
|
+
return { lines, items };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function markStepInLines(lines, item) {
|
|
88
|
+
if (item.checked) return false;
|
|
89
|
+
const before = lines[item.lineIndex];
|
|
90
|
+
lines[item.lineIndex] = before.replace(/^(\s*- \[)( |x|X)(\])/, '$1x$3');
|
|
91
|
+
return lines[item.lineIndex] !== before;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Tick one enumerated row in a checklist file. Returns true when the file changed. */
|
|
95
|
+
function markStepInFile(filePath, item) {
|
|
96
|
+
const original = fs.readFileSync(filePath, 'utf8');
|
|
97
|
+
const parsed = parseChecklist(original);
|
|
98
|
+
const lines = [...parsed.lines];
|
|
99
|
+
markStepInLines(lines, item);
|
|
100
|
+
const updated = lines.join('\n');
|
|
101
|
+
if (updated === original) return false;
|
|
102
|
+
atomicWrite(filePath, updated);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
atomicWrite,
|
|
108
|
+
readJson,
|
|
109
|
+
writeSignal,
|
|
110
|
+
writeIndex,
|
|
111
|
+
canonicalJson,
|
|
112
|
+
signalContentUnchanged,
|
|
113
|
+
SIGNAL_PASSTHROUGH_KEYS,
|
|
114
|
+
pickSignalPassthrough,
|
|
115
|
+
parseChecklist,
|
|
116
|
+
markStepInLines,
|
|
117
|
+
markStepInFile,
|
|
118
|
+
};
|