@farmslot/agent-runtime 0.12.0 → 0.14.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.
Files changed (91) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/bin/farmslot-agent.mjs +7 -0
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/native/registry.d.ts.map +1 -1
  8. package/dist/native/registry.js +6 -1
  9. package/dist/native/registry.js.map +1 -1
  10. package/dist/task-init/index.d.ts.map +1 -1
  11. package/dist/task-init/index.js +1 -0
  12. package/dist/task-init/index.js.map +1 -1
  13. package/dist/task-init/task-document.d.ts +16 -2
  14. package/dist/task-init/task-document.d.ts.map +1 -1
  15. package/dist/task-init/task-document.js +21 -8
  16. package/dist/task-init/task-document.js.map +1 -1
  17. package/package.json +7 -3
  18. package/scripts/acceptance-cli.cjs +139 -0
  19. package/scripts/acceptance-ledger.cjs +413 -0
  20. package/scripts/check-task-artifact-contract.mjs +25 -1
  21. package/scripts/checklist-target.cjs +56 -0
  22. package/scripts/mark-checklist-step.cjs +111 -61
  23. package/scripts/mark-io.cjs +118 -0
  24. package/scripts/review-terminal.cjs +6 -1
  25. package/scripts/subtask-unit.cjs +725 -0
  26. package/scripts/task-init-cli.mjs +4 -0
  27. package/scripts/worker-terminal-contract.cjs +25 -1
  28. package/dist/execution-template/execution-template.test.d.ts +0 -2
  29. package/dist/execution-template/execution-template.test.d.ts.map +0 -1
  30. package/dist/execution-template/execution-template.test.js +0 -795
  31. package/dist/execution-template/execution-template.test.js.map +0 -1
  32. package/dist/native/account-observation.test.d.ts +0 -2
  33. package/dist/native/account-observation.test.d.ts.map +0 -1
  34. package/dist/native/account-observation.test.js +0 -256
  35. package/dist/native/account-observation.test.js.map +0 -1
  36. package/dist/native/account-profiles.test.d.ts +0 -2
  37. package/dist/native/account-profiles.test.d.ts.map +0 -1
  38. package/dist/native/account-profiles.test.js +0 -172
  39. package/dist/native/account-profiles.test.js.map +0 -1
  40. package/dist/native/acp.test.d.ts +0 -2
  41. package/dist/native/acp.test.d.ts.map +0 -1
  42. package/dist/native/acp.test.js +0 -257
  43. package/dist/native/acp.test.js.map +0 -1
  44. package/dist/native/affinity.test.d.ts +0 -2
  45. package/dist/native/affinity.test.d.ts.map +0 -1
  46. package/dist/native/affinity.test.js +0 -30
  47. package/dist/native/affinity.test.js.map +0 -1
  48. package/dist/native/cursor-model.test.d.ts +0 -2
  49. package/dist/native/cursor-model.test.d.ts.map +0 -1
  50. package/dist/native/cursor-model.test.js +0 -40
  51. package/dist/native/cursor-model.test.js.map +0 -1
  52. package/dist/native/durability.test.d.ts +0 -2
  53. package/dist/native/durability.test.d.ts.map +0 -1
  54. package/dist/native/durability.test.js +0 -976
  55. package/dist/native/durability.test.js.map +0 -1
  56. package/dist/native/filesystem-policy.test.d.ts +0 -2
  57. package/dist/native/filesystem-policy.test.d.ts.map +0 -1
  58. package/dist/native/filesystem-policy.test.js +0 -136
  59. package/dist/native/filesystem-policy.test.js.map +0 -1
  60. package/dist/native/process-tree.test.d.ts +0 -2
  61. package/dist/native/process-tree.test.d.ts.map +0 -1
  62. package/dist/native/process-tree.test.js +0 -174
  63. package/dist/native/process-tree.test.js.map +0 -1
  64. package/dist/native/process.test.d.ts +0 -2
  65. package/dist/native/process.test.d.ts.map +0 -1
  66. package/dist/native/process.test.js +0 -56
  67. package/dist/native/process.test.js.map +0 -1
  68. package/dist/native/registry.test.d.ts +0 -2
  69. package/dist/native/registry.test.d.ts.map +0 -1
  70. package/dist/native/registry.test.js +0 -13
  71. package/dist/native/registry.test.js.map +0 -1
  72. package/dist/native/review-sandbox.test.d.ts +0 -2
  73. package/dist/native/review-sandbox.test.d.ts.map +0 -1
  74. package/dist/native/review-sandbox.test.js +0 -40
  75. package/dist/native/review-sandbox.test.js.map +0 -1
  76. package/dist/native/worker-history.test.d.ts +0 -2
  77. package/dist/native/worker-history.test.d.ts.map +0 -1
  78. package/dist/native/worker-history.test.js +0 -195
  79. package/dist/native/worker-history.test.js.map +0 -1
  80. package/dist/native/worker-launch.test.d.ts +0 -2
  81. package/dist/native/worker-launch.test.d.ts.map +0 -1
  82. package/dist/native/worker-launch.test.js +0 -61
  83. package/dist/native/worker-launch.test.js.map +0 -1
  84. package/dist/native/worker-relocation.test.d.ts +0 -2
  85. package/dist/native/worker-relocation.test.d.ts.map +0 -1
  86. package/dist/native/worker-relocation.test.js +0 -110
  87. package/dist/native/worker-relocation.test.js.map +0 -1
  88. package/dist/task-init/discover.test.d.ts +0 -2
  89. package/dist/task-init/discover.test.d.ts.map +0 -1
  90. package/dist/task-init/discover.test.js +0 -58
  91. package/dist/task-init/discover.test.js.map +0 -1
@@ -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);
@@ -363,7 +393,9 @@ function assertTerminalPackagedEvidence(taskPath, taskDir, terminalCommand) {
363
393
  function buildSignalUpdate(signal, terminal, target, timing, events, now, taskPath, _taskDir) {
364
394
  const base = {
365
395
  ...pickSignalPassthrough(signal),
366
- ...(isStartCommand ? { attemptId: randomUUID() } : {}),
396
+ ...(isStartCommand
397
+ ? { attemptId: process.env.FARMSLOT_SIGNAL_ATTEMPT_ID || randomUUID() }
398
+ : {}),
367
399
  step: target?.label ?? signal.step ?? 'complete',
368
400
  checklistTiming: {
369
401
  schemaVersion: 1,
@@ -403,45 +435,6 @@ function buildSignalUpdate(signal, terminal, target, timing, events, now, taskPa
403
435
  return next;
404
436
  }
405
437
 
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
438
  function resolveTarget(taskPath, stepNumber, markLast) {
446
439
  const original = fs.readFileSync(taskPath, 'utf8');
447
440
  const parsed = parseChecklist(original);
@@ -458,6 +451,9 @@ function resolveTarget(taskPath, stepNumber, markLast) {
458
451
  }
459
452
  let updated = original;
460
453
  if (item) {
454
+ // Also covers `--mark-last`: the parent must not tick a box whose child is
455
+ // still open, whichever way the row was chosen.
456
+ assertStepNotOwnedBySubtask(item.stepNumber);
461
457
  const nextLines = [...parsed.lines];
462
458
  markStepInLines(nextLines, item);
463
459
  updated = nextLines.join('\n');
@@ -469,6 +465,36 @@ function resolveTarget(taskPath, stepNumber, markLast) {
469
465
  }
470
466
 
471
467
  const taskDir = path.dirname(signalPath);
468
+
469
+ /**
470
+ * A step owned by a child unit belongs to that child until it is settled
471
+ * (`complete` or `done` — a `blocked` child keeps ownership). The child's
472
+ * `complete` ticks the box, so a later `mark N` on it is an idempotent no-op.
473
+ */
474
+ function assertStepNotOwnedBySubtask(stepNumber) {
475
+ if (stepNumber == null) return;
476
+ const owner = subtaskOwningStep(taskDir, path.basename(taskPath), stepNumber);
477
+ if (!owner) return;
478
+ const open = openSubtaskUnits(taskDir).find((entry) => entry.unit.id === owner.id);
479
+ if (!open) return;
480
+ console.error(
481
+ `step ${stepNumber} is owned by subtask ${owner.id}; finish it with ./mark sub ${owner.id} complete`,
482
+ );
483
+ process.exit(1);
484
+ }
485
+
486
+ /** Every registered child must be settled before the parent reports success. */
487
+ function assertNoOpenSubtasks(command) {
488
+ const open = openSubtaskUnits(taskDir);
489
+ if (open.length === 0) return;
490
+ console.error(openSubtaskRefusal(open, command));
491
+ process.exit(1);
492
+ }
493
+
494
+ // `blocked` stays available while a child is open: the run is blocked either way.
495
+ if (terminalCommand === 'complete' || terminalCommand === 'no-change') {
496
+ assertNoOpenSubtasks(terminalCommand);
497
+ }
472
498
  if (
473
499
  terminalCommand &&
474
500
  (terminalCommand === 'complete' || terminalCommand === 'no-change') &&
@@ -483,6 +509,17 @@ const target = isStartCommand
483
509
 
484
510
  const now = new Date().toISOString();
485
511
  const signal = readJson(signalPath);
512
+ // The launcher owns the attempt. Descendants may repeat bootstrap, but must not
513
+ // replace the parent's identity or reset progress within that same attempt.
514
+ if (
515
+ isStartCommand &&
516
+ process.env.FARMSLOT_SIGNAL_ATTEMPT_ID &&
517
+ signal.attemptId === process.env.FARMSLOT_SIGNAL_ATTEMPT_ID &&
518
+ signal.status === 'running'
519
+ ) {
520
+ console.log('signal already started');
521
+ process.exit(0);
522
+ }
486
523
  const timing =
487
524
  signal.checklistTiming && typeof signal.checklistTiming === 'object'
488
525
  ? signal.checklistTiming
@@ -514,14 +551,27 @@ const next = buildSignalUpdate(
514
551
  taskPath,
515
552
  taskDir,
516
553
  );
517
- atomicWrite(signalPath, `${JSON.stringify(next, null, 2)}\n`, 0o644);
518
-
519
- if (isStartCommand) {
520
- console.log('signal started');
521
- } else if (terminalCommand) {
522
- console.log(
523
- `signal ${terminalCommand}: status=${next.status} disposition=${next.disposition ?? 'n/a'}`,
524
- );
554
+ // Re-marking a step that is already done changes nothing: the box was checked
555
+ // and its timing event already exists, so the only difference from the stored
556
+ // signal would be a fresh `timestamp`. Compare content, never the command: that
557
+ // keeps `start` (new attemptId), a terminal mark (new status), a resume from
558
+ // `blocked`, and a box checked by hand without an event all writing as before,
559
+ // while a genuinely empty re-mark stays a no-op.
560
+ const alreadyRecorded = !isStartCommand && !terminalCommand && signalContentUnchanged(signal, next);
561
+
562
+ if (alreadyRecorded) {
563
+ console.log(`already marked ${target.stepNumber}: ${target.label}`);
525
564
  } else {
526
- console.log(`marked ${target.stepNumber}: ${target.label}`);
565
+ // One signal writer for the parent and child paths, so their bytes and mode
566
+ // cannot drift (mark-io.cjs writeSignal).
567
+ writeSignal(signalPath, next);
568
+ if (isStartCommand) {
569
+ console.log('signal started');
570
+ } else if (terminalCommand) {
571
+ console.log(
572
+ `signal ${terminalCommand}: status=${next.status} disposition=${next.disposition ?? 'n/a'}`,
573
+ );
574
+ } else {
575
+ console.log(`marked ${target.stepNumber}: ${target.label}`);
576
+ }
527
577
  }
@@ -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
+ };
@@ -75,7 +75,6 @@ async function main() {
75
75
  const guard = await sandbox(policy, runtimeRoots);
76
76
  const promptFile = path.join(task, '.terminal-prompt.txt');
77
77
  fs.writeFileSync(promptFile, input.prompt, { mode: 0o600 });
78
- if (input.setup) check(cp.spawnSync('/bin/sh', ['-c', input.setup], { cwd, encoding: 'utf8' }));
79
78
  const environment = {
80
79
  ...process.env,
81
80
  ...input.environment.set,
@@ -91,12 +90,18 @@ async function main() {
91
90
  'FARMSLOT_GATEWAY_PASSWORD',
92
91
  ])
93
92
  delete environment[key];
93
+ environment.FARMSLOT_SIGNAL_ATTEMPT_ID = require('node:crypto').randomUUID();
94
+ if (input.setup)
95
+ check(
96
+ cp.spawnSync('/bin/sh', ['-c', input.setup], { cwd, env: environment, encoding: 'utf8' }),
97
+ );
94
98
  const commandFile = path.join(task, '.terminal-start.cjs');
95
99
  const record = {
96
100
  runId: input.runId,
97
101
  workspaceId: input.workspaceId,
98
102
  session: input.session,
99
103
  startedAt: new Date().toISOString(),
104
+ signalAttemptId: environment.FARMSLOT_SIGNAL_ATTEMPT_ID,
100
105
  };
101
106
  const launch = `const cp=require('node:child_process');const env={...process.env};for(const key of ['FARMSLOT_NODE_TOKEN','FARMSLOT_GATEWAY_TOKEN','FARMSLOT_GATEWAY_PASSWORD','CLAUDECODE'])delete env[key];const r=cp.spawnSync(${JSON.stringify(guard.sandbox.executable)},${JSON.stringify([...guard.sandbox.args, '/bin/sh', '-c', input.command])},{cwd:${JSON.stringify(cwd)},env,stdio:'inherit'});process.exit(r.status??1);`;
102
107
  fs.writeFileSync(commandFile, launch, { mode: 0o600 });