@farmslot/agent-runtime 0.8.1 → 0.10.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 (253) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +2 -2
  3. package/bin/farmslot-agent.mjs +3 -43
  4. package/dist/execution-template/create.d.ts.map +1 -1
  5. package/dist/execution-template/create.js +4 -9
  6. package/dist/execution-template/create.js.map +1 -1
  7. package/dist/execution-template/execution-template.test.js +85 -34
  8. package/dist/execution-template/execution-template.test.js.map +1 -1
  9. package/dist/execution-template/frontmatter.d.ts +6 -3
  10. package/dist/execution-template/frontmatter.d.ts.map +1 -1
  11. package/dist/execution-template/frontmatter.js +8 -10
  12. package/dist/execution-template/frontmatter.js.map +1 -1
  13. package/dist/execution-template/index.d.ts +2 -2
  14. package/dist/execution-template/index.d.ts.map +1 -1
  15. package/dist/execution-template/index.js +2 -2
  16. package/dist/execution-template/index.js.map +1 -1
  17. package/dist/execution-template/infer.d.ts +2 -3
  18. package/dist/execution-template/infer.d.ts.map +1 -1
  19. package/dist/execution-template/infer.js +2 -19
  20. package/dist/execution-template/infer.js.map +1 -1
  21. package/dist/execution-template/lint.d.ts.map +1 -1
  22. package/dist/execution-template/lint.js +12 -14
  23. package/dist/execution-template/lint.js.map +1 -1
  24. package/dist/execution-template/resolve.d.ts.map +1 -1
  25. package/dist/execution-template/resolve.js +0 -2
  26. package/dist/execution-template/resolve.js.map +1 -1
  27. package/dist/execution-template/select.d.ts.map +1 -1
  28. package/dist/execution-template/select.js +4 -8
  29. package/dist/execution-template/select.js.map +1 -1
  30. package/dist/execution-template/snapshot.d.ts.map +1 -1
  31. package/dist/execution-template/snapshot.js +0 -1
  32. package/dist/execution-template/snapshot.js.map +1 -1
  33. package/dist/execution-template/types.d.ts +10 -5
  34. package/dist/execution-template/types.d.ts.map +1 -1
  35. package/dist/execution-template/types.js.map +1 -1
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +2 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/native/account-claude.d.ts +5 -0
  41. package/dist/native/account-claude.d.ts.map +1 -0
  42. package/dist/native/account-claude.js +42 -0
  43. package/dist/native/account-claude.js.map +1 -0
  44. package/dist/native/account-codex.d.ts +4 -0
  45. package/dist/native/account-codex.d.ts.map +1 -0
  46. package/dist/native/account-codex.js +51 -0
  47. package/dist/native/account-codex.js.map +1 -0
  48. package/dist/native/account-common.d.ts +30 -0
  49. package/dist/native/account-common.d.ts.map +1 -0
  50. package/dist/native/account-common.js +155 -0
  51. package/dist/native/account-common.js.map +1 -0
  52. package/dist/native/account-cursor.d.ts +5 -0
  53. package/dist/native/account-cursor.d.ts.map +1 -0
  54. package/dist/native/account-cursor.js +48 -0
  55. package/dist/native/account-cursor.js.map +1 -0
  56. package/dist/native/account-grok.d.ts +4 -0
  57. package/dist/native/account-grok.d.ts.map +1 -0
  58. package/dist/native/account-grok.js +55 -0
  59. package/dist/native/account-grok.js.map +1 -0
  60. package/dist/native/account-observation.test.d.ts +2 -0
  61. package/dist/native/account-observation.test.d.ts.map +1 -0
  62. package/dist/native/account-observation.test.js +256 -0
  63. package/dist/native/account-observation.test.js.map +1 -0
  64. package/dist/native/account-profiles.d.ts +11 -0
  65. package/dist/native/account-profiles.d.ts.map +1 -0
  66. package/dist/native/account-profiles.js +172 -0
  67. package/dist/native/account-profiles.js.map +1 -0
  68. package/dist/native/account-profiles.test.d.ts +2 -0
  69. package/dist/native/account-profiles.test.d.ts.map +1 -0
  70. package/dist/native/account-profiles.test.js +172 -0
  71. package/dist/native/account-profiles.test.js.map +1 -0
  72. package/dist/native/account-types.d.ts +11 -0
  73. package/dist/native/account-types.d.ts.map +1 -0
  74. package/dist/native/account-types.js +2 -0
  75. package/dist/native/account-types.js.map +1 -0
  76. package/dist/native/acp-requests.d.ts +11 -0
  77. package/dist/native/acp-requests.d.ts.map +1 -0
  78. package/dist/native/acp-requests.js +109 -0
  79. package/dist/native/acp-requests.js.map +1 -0
  80. package/dist/native/acp-rpc.d.ts +15 -0
  81. package/dist/native/acp-rpc.d.ts.map +1 -0
  82. package/dist/native/acp-rpc.js +73 -0
  83. package/dist/native/acp-rpc.js.map +1 -0
  84. package/dist/native/acp.d.ts +15 -0
  85. package/dist/native/acp.d.ts.map +1 -0
  86. package/dist/native/acp.js +285 -0
  87. package/dist/native/acp.js.map +1 -0
  88. package/dist/native/acp.test.d.ts +2 -0
  89. package/dist/native/acp.test.d.ts.map +1 -0
  90. package/dist/native/acp.test.js +257 -0
  91. package/dist/native/acp.test.js.map +1 -0
  92. package/dist/native/affinity.test.d.ts +2 -0
  93. package/dist/native/affinity.test.d.ts.map +1 -0
  94. package/dist/native/affinity.test.js +30 -0
  95. package/dist/native/affinity.test.js.map +1 -0
  96. package/dist/native/claude.d.ts +11 -0
  97. package/dist/native/claude.d.ts.map +1 -0
  98. package/dist/native/claude.js +504 -0
  99. package/dist/native/claude.js.map +1 -0
  100. package/dist/native/client.d.ts +34 -0
  101. package/dist/native/client.d.ts.map +1 -0
  102. package/dist/native/client.js +217 -0
  103. package/dist/native/client.js.map +1 -0
  104. package/dist/native/codex.d.ts +3 -0
  105. package/dist/native/codex.d.ts.map +1 -0
  106. package/dist/native/codex.js +324 -0
  107. package/dist/native/codex.js.map +1 -0
  108. package/dist/native/cursor-model.d.ts +6 -0
  109. package/dist/native/cursor-model.d.ts.map +1 -0
  110. package/dist/native/cursor-model.js +74 -0
  111. package/dist/native/cursor-model.js.map +1 -0
  112. package/dist/native/cursor-model.test.d.ts +2 -0
  113. package/dist/native/cursor-model.test.d.ts.map +1 -0
  114. package/dist/native/cursor-model.test.js +40 -0
  115. package/dist/native/cursor-model.test.js.map +1 -0
  116. package/dist/native/cursor.d.ts +2 -0
  117. package/dist/native/cursor.d.ts.map +1 -0
  118. package/dist/native/cursor.js +30 -0
  119. package/dist/native/cursor.js.map +1 -0
  120. package/dist/native/durability.test.d.ts +2 -0
  121. package/dist/native/durability.test.d.ts.map +1 -0
  122. package/dist/native/durability.test.js +976 -0
  123. package/dist/native/durability.test.js.map +1 -0
  124. package/dist/native/filesystem-policy.test.d.ts +2 -0
  125. package/dist/native/filesystem-policy.test.d.ts.map +1 -0
  126. package/dist/native/filesystem-policy.test.js +136 -0
  127. package/dist/native/filesystem-policy.test.js.map +1 -0
  128. package/dist/native/grok.d.ts +2 -0
  129. package/dist/native/grok.d.ts.map +1 -0
  130. package/dist/native/grok.js +17 -0
  131. package/dist/native/grok.js.map +1 -0
  132. package/dist/native/host.d.ts +2 -0
  133. package/dist/native/host.d.ts.map +1 -0
  134. package/dist/native/host.js +129 -0
  135. package/dist/native/host.js.map +1 -0
  136. package/dist/native/index.d.ts +7 -0
  137. package/dist/native/index.d.ts.map +1 -0
  138. package/dist/native/index.js +6 -0
  139. package/dist/native/index.js.map +1 -0
  140. package/dist/native/ipc.d.ts +108 -0
  141. package/dist/native/ipc.d.ts.map +1 -0
  142. package/dist/native/ipc.js +181 -0
  143. package/dist/native/ipc.js.map +1 -0
  144. package/dist/native/manager.d.ts +61 -0
  145. package/dist/native/manager.d.ts.map +1 -0
  146. package/dist/native/manager.js +850 -0
  147. package/dist/native/manager.js.map +1 -0
  148. package/dist/native/process-tree.d.ts +26 -0
  149. package/dist/native/process-tree.d.ts.map +1 -0
  150. package/dist/native/process-tree.js +231 -0
  151. package/dist/native/process-tree.js.map +1 -0
  152. package/dist/native/process-tree.test.d.ts +2 -0
  153. package/dist/native/process-tree.test.d.ts.map +1 -0
  154. package/dist/native/process-tree.test.js +174 -0
  155. package/dist/native/process-tree.test.js.map +1 -0
  156. package/dist/native/process.d.ts +38 -0
  157. package/dist/native/process.d.ts.map +1 -0
  158. package/dist/native/process.js +246 -0
  159. package/dist/native/process.js.map +1 -0
  160. package/dist/native/process.test.d.ts +2 -0
  161. package/dist/native/process.test.d.ts.map +1 -0
  162. package/dist/native/process.test.js +56 -0
  163. package/dist/native/process.test.js.map +1 -0
  164. package/dist/native/profile-service.d.ts +5 -0
  165. package/dist/native/profile-service.d.ts.map +1 -0
  166. package/dist/native/profile-service.js +76 -0
  167. package/dist/native/profile-service.js.map +1 -0
  168. package/dist/native/registry.d.ts +20 -0
  169. package/dist/native/registry.d.ts.map +1 -0
  170. package/dist/native/registry.js +89 -0
  171. package/dist/native/registry.js.map +1 -0
  172. package/dist/native/registry.test.d.ts +2 -0
  173. package/dist/native/registry.test.d.ts.map +1 -0
  174. package/dist/native/registry.test.js +13 -0
  175. package/dist/native/registry.test.js.map +1 -0
  176. package/dist/native/review-sandbox.d.ts +12 -0
  177. package/dist/native/review-sandbox.d.ts.map +1 -0
  178. package/dist/native/review-sandbox.js +6 -0
  179. package/dist/native/review-sandbox.js.map +1 -0
  180. package/dist/native/review-sandbox.test.d.ts +2 -0
  181. package/dist/native/review-sandbox.test.d.ts.map +1 -0
  182. package/dist/native/review-sandbox.test.js +40 -0
  183. package/dist/native/review-sandbox.test.js.map +1 -0
  184. package/dist/native/service.d.ts +7 -0
  185. package/dist/native/service.d.ts.map +1 -0
  186. package/dist/native/service.js +151 -0
  187. package/dist/native/service.js.map +1 -0
  188. package/dist/native/storage.d.ts +11 -0
  189. package/dist/native/storage.d.ts.map +1 -0
  190. package/dist/native/storage.js +102 -0
  191. package/dist/native/storage.js.map +1 -0
  192. package/dist/native/supervisor.d.ts +2 -0
  193. package/dist/native/supervisor.d.ts.map +1 -0
  194. package/dist/native/supervisor.js +215 -0
  195. package/dist/native/supervisor.js.map +1 -0
  196. package/dist/native/types.d.ts +36 -0
  197. package/dist/native/types.d.ts.map +1 -0
  198. package/dist/native/types.js +2 -0
  199. package/dist/native/types.js.map +1 -0
  200. package/dist/native/worker-history.d.ts +16 -0
  201. package/dist/native/worker-history.d.ts.map +1 -0
  202. package/dist/native/worker-history.js +77 -0
  203. package/dist/native/worker-history.js.map +1 -0
  204. package/dist/native/worker-history.test.d.ts +2 -0
  205. package/dist/native/worker-history.test.d.ts.map +1 -0
  206. package/dist/native/worker-history.test.js +195 -0
  207. package/dist/native/worker-history.test.js.map +1 -0
  208. package/dist/native/worker-launch.d.ts +72 -0
  209. package/dist/native/worker-launch.d.ts.map +1 -0
  210. package/dist/native/worker-launch.js +134 -0
  211. package/dist/native/worker-launch.js.map +1 -0
  212. package/dist/native/worker-launch.test.d.ts +2 -0
  213. package/dist/native/worker-launch.test.d.ts.map +1 -0
  214. package/dist/native/worker-launch.test.js +61 -0
  215. package/dist/native/worker-launch.test.js.map +1 -0
  216. package/dist/native/worker-relocation.test.d.ts +2 -0
  217. package/dist/native/worker-relocation.test.d.ts.map +1 -0
  218. package/dist/native/worker-relocation.test.js +110 -0
  219. package/dist/native/worker-relocation.test.js.map +1 -0
  220. package/dist/native/worker-service.d.ts +4 -0
  221. package/dist/native/worker-service.d.ts.map +1 -0
  222. package/dist/native/worker-service.js +85 -0
  223. package/dist/native/worker-service.js.map +1 -0
  224. package/dist/native/workspace-files.d.ts +6 -0
  225. package/dist/native/workspace-files.d.ts.map +1 -0
  226. package/dist/native/workspace-files.js +62 -0
  227. package/dist/native/workspace-files.js.map +1 -0
  228. package/dist/native/workspace.d.ts +5 -0
  229. package/dist/native/workspace.d.ts.map +1 -0
  230. package/dist/native/workspace.js +120 -0
  231. package/dist/native/workspace.js.map +1 -0
  232. package/dist/task-init/index.d.ts +70 -0
  233. package/dist/task-init/index.d.ts.map +1 -0
  234. package/dist/task-init/index.js +124 -0
  235. package/dist/task-init/index.js.map +1 -0
  236. package/dist/task-init/task-document.d.ts +98 -0
  237. package/dist/task-init/task-document.d.ts.map +1 -0
  238. package/dist/task-init/task-document.js +184 -0
  239. package/dist/task-init/task-document.js.map +1 -0
  240. package/dist/task-init/write.d.ts +42 -0
  241. package/dist/task-init/write.d.ts.map +1 -0
  242. package/dist/task-init/write.js +63 -0
  243. package/dist/task-init/write.js.map +1 -0
  244. package/package.json +13 -4
  245. package/scripts/checklist-target.cjs +5 -3
  246. package/scripts/cursor-session-discovery.cjs +48 -0
  247. package/scripts/execution-template-cli.mjs +17 -18
  248. package/scripts/mark-checklist-step.cjs +1 -1
  249. package/scripts/review-filesystem.cjs +39 -0
  250. package/scripts/review-session.cjs +45 -0
  251. package/scripts/review-terminal.cjs +134 -0
  252. package/scripts/task-init-cli.mjs +166 -0
  253. package/scripts/worker-terminal-contract.cjs +15 -3
@@ -0,0 +1,48 @@
1
+ // Read-only recovery for older terminal launches that did not persist their chat ID.
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const input = JSON.parse(process.argv.at(-1));
5
+ const root = input.root.replace(/^~(?=\/|$)/, process.env.HOME);
6
+ const matches = [];
7
+ if (
8
+ !path.isAbsolute(input.cwd) ||
9
+ !Number.isFinite(input.startedAt) ||
10
+ !Number.isFinite(input.completedAt) ||
11
+ input.completedAt < input.startedAt
12
+ )
13
+ throw Error('Invalid session discovery bounds');
14
+ function directories(parent) {
15
+ try {
16
+ return fs
17
+ .readdirSync(parent, { withFileTypes: true })
18
+ .filter((entry) => entry.isDirectory())
19
+ .map((entry) => path.join(parent, entry.name));
20
+ } catch (error) {
21
+ if (error.code === 'ENOENT') return [];
22
+ throw error;
23
+ }
24
+ }
25
+ for (const workspace of directories(root))
26
+ for (const chat of directories(workspace)) {
27
+ if (!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(path.basename(chat))) continue;
28
+ let meta;
29
+ try {
30
+ meta = JSON.parse(fs.readFileSync(path.join(chat, 'meta.json'), 'utf8'));
31
+ } catch (error) {
32
+ // Concurrently created or truncated metadata is not evidence of a recoverable chat.
33
+ if (error.code === 'ENOENT' || error instanceof SyntaxError) continue;
34
+ throw error;
35
+ }
36
+ if (
37
+ meta.schemaVersion === 1 &&
38
+ meta.hasConversation === true &&
39
+ meta.cwd === input.cwd &&
40
+ Number.isFinite(meta.createdAtMs) &&
41
+ meta.createdAtMs >= input.startedAt &&
42
+ meta.createdAtMs <= input.completedAt
43
+ )
44
+ matches.push({ sessionId: path.basename(chat) });
45
+ }
46
+ if (matches.length > 1)
47
+ throw Error('Several chats match the review workspace; session recovery is ambiguous');
48
+ process.stdout.write(JSON.stringify(matches[0] ?? null));
@@ -17,10 +17,12 @@ function usage(exitCode = 0) {
17
17
  'list --dir <path> --domain-dir <domain=path> --project-worker <path> --package-templates <path>',
18
18
  ' [--project-name name] [--package-id id] [--flow f] [--run-mode m]',
19
19
  ' [--platform p] [--domain d] [--no-include-shadowed] [--json]',
20
- 'materialize <output> --flow f --run-mode m --platform p [--domain d] [--id id]',
20
+ 'materialize <output> --flow f --platform p [--run-mode m] [--domain d] [--id id]',
21
21
  ' [the same source options as list] [--provenance path] [--json]',
22
22
  'lint <file-or-dir> [--json]',
23
- 'new <path> [--flow f] [--run-mode m] [--platform p] [--title t] [--force] [--json]',
23
+ 'new <path> [--flow f] [--platform p] [--title t] [--force] [--json]',
24
+ '',
25
+ '--run-mode is the run mode used to match project default rules; templates have none.',
24
26
  ].join('\n');
25
27
  (exitCode === 0 ? console.log : console.error)(text);
26
28
  process.exit(exitCode);
@@ -32,7 +34,7 @@ function takeValue(args, i, flag) {
32
34
  return value;
33
35
  }
34
36
 
35
- async function loadRuntime() {
37
+ export async function loadRuntime() {
36
38
  if (!existsSync(distEntry)) {
37
39
  throw new Error(
38
40
  `compiled package missing at ${distEntry}; run yarn workspace @farmslot/agent-runtime build`,
@@ -58,7 +60,7 @@ function parseDomainDir(value) {
58
60
  return { domain, root: value.slice(separator + 1) };
59
61
  }
60
62
 
61
- function parseCatalogArgs(args, { materialize = false } = {}) {
63
+ export function parseCatalogArgs(args, { materialize = false } = {}) {
62
64
  const opts = {
63
65
  dirs: [],
64
66
  domainDirs: [],
@@ -101,7 +103,7 @@ function parseCatalogArgs(args, { materialize = false } = {}) {
101
103
  return opts;
102
104
  }
103
105
 
104
- function buildSources(opts, runtime) {
106
+ export function buildSources(opts, runtime) {
105
107
  const sources = [];
106
108
  const domainSourceIds = new Set();
107
109
  for (const [index, dir] of opts.dirs.entries()) {
@@ -174,18 +176,17 @@ async function cmdList(args, runtime) {
174
176
  if (opts.id) throw new Error('--id is only valid with materialize');
175
177
  const sources = buildSources(opts, runtime);
176
178
  const templates =
177
- opts.flow && opts.platform && opts.runMode
179
+ opts.flow && opts.platform
178
180
  ? runtime.listCompatibleExecutionTemplates({
179
181
  sources,
180
182
  flow: opts.flow,
181
183
  platform: opts.platform,
182
- runMode: opts.runMode,
184
+ ...(opts.runMode ? { runMode: opts.runMode } : {}),
183
185
  ...(opts.domain ? { domain: opts.domain } : {}),
184
186
  })
185
187
  : runtime.listExecutionTemplates({
186
188
  sources,
187
189
  flow: opts.flow,
188
- runMode: opts.runMode,
189
190
  platform: opts.platform,
190
191
  domain: opts.domain,
191
192
  includeShadowed: opts.includeShadowed,
@@ -203,7 +204,7 @@ async function cmdList(args, runtime) {
203
204
  for (const entry of catalog) {
204
205
  const shadow = entry.shadowedBy ? ` (shadowed by ${entry.shadowedBy})` : '';
205
206
  process.stdout.write(
206
- `${entry.id}\t${entry.flow}\t${entry.runMode ?? '-'}\t${entry.platforms.join(',')}\t${entry.sourceId}${shadow}\n`,
207
+ `${entry.id}\t${entry.flow}\t${entry.platforms.join(',')}\t${entry.sourceId}${shadow}\n`,
207
208
  );
208
209
  }
209
210
  }
@@ -212,7 +213,6 @@ async function cmdMaterialize(args, runtime) {
212
213
  const opts = parseCatalogArgs(args, { materialize: true });
213
214
  if (!opts.output) throw new Error('materialize requires <output>');
214
215
  if (!opts.flow) throw new Error('materialize requires --flow');
215
- if (!opts.runMode) throw new Error('materialize requires --run-mode');
216
216
  if (!opts.platform) throw new Error('materialize requires --platform');
217
217
  const output = resolve(opts.output);
218
218
  if (existsSync(output)) {
@@ -227,7 +227,7 @@ async function cmdMaterialize(args, runtime) {
227
227
  sources: buildSources(opts, runtime),
228
228
  flow: opts.flow,
229
229
  platform: opts.platform,
230
- runMode: opts.runMode,
230
+ ...(opts.runMode ? { runMode: opts.runMode } : {}),
231
231
  ...(opts.domain ? { domain: opts.domain } : {}),
232
232
  ...(opts.id ? { explicitId: opts.id } : {}),
233
233
  });
@@ -282,7 +282,6 @@ async function cmdNew(args, runtime) {
282
282
  let pathArg = null;
283
283
  const opts = {
284
284
  flow: undefined,
285
- runMode: undefined,
286
285
  platforms: undefined,
287
286
  title: undefined,
288
287
  force: false,
@@ -291,7 +290,6 @@ async function cmdNew(args, runtime) {
291
290
  for (let i = 0; i < args.length; i += 1) {
292
291
  const arg = args[i];
293
292
  if (arg === '--flow') opts.flow = takeValue(args, i++, arg);
294
- else if (arg === '--run-mode') opts.runMode = parseRunMode(takeValue(args, i++, arg));
295
293
  else if (arg === '--platform') {
296
294
  opts.platforms = takeValue(args, i++, arg)
297
295
  .split(',')
@@ -308,7 +306,6 @@ async function cmdNew(args, runtime) {
308
306
  const created = runtime.createExecutionTemplate({
309
307
  path: pathArg,
310
308
  flow: opts.flow,
311
- runMode: opts.runMode,
312
309
  platforms: opts.platforms,
313
310
  title: opts.title,
314
311
  force: opts.force,
@@ -328,7 +325,9 @@ async function main() {
328
325
  else usage(2);
329
326
  }
330
327
 
331
- main().catch((error) => {
332
- console.error(error instanceof Error ? error.message : String(error));
333
- process.exit(1);
334
- });
328
+ if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
329
+ main().catch((error) => {
330
+ console.error(error instanceof Error ? error.message : String(error));
331
+ process.exit(1);
332
+ });
333
+ }
@@ -44,7 +44,7 @@ function printHelp() {
44
44
  [
45
45
  usageLine,
46
46
  '',
47
- 'Task-dir mode: farmslot-agent mark <task-dir> <step> — requires checklist-target.json written by the gateway at task creation or role switch.',
47
+ 'Task-dir mode: farmslot-agent mark <task-dir> <step> — marks CHECKLIST.md (or TASK.md) unless checklist-target.json, written by a role switch, points elsewhere.',
48
48
  'Override: farmslot-agent mark <task-dir> --checklist SELF-REVIEW.md <step> — optional --signal; signal defaults from checklist name.',
49
49
  'Bootstrap: ./mark start — role-owned signal with status running (no checklist box).',
50
50
  'Progress: ./mark 1, ./mark 2, ... — checks the box and appends checklistTiming.',
@@ -0,0 +1,39 @@
1
+ const { accessSync, constants } = require('node:fs');
2
+ const { mkdir, realpath } = require('node:fs/promises');
3
+ const { join } = require('node:path');
4
+
5
+ function available() {
6
+ if (process.platform !== 'darwin') return false;
7
+ try {
8
+ accessSync('/usr/bin/sandbox-exec', constants.X_OK);
9
+ return true;
10
+ } catch (error) {
11
+ if (['ENOENT', 'EACCES'].includes(error.code)) return false;
12
+ throw error;
13
+ }
14
+ }
15
+ async function sandbox(policy, runtimeRoots) {
16
+ if (!available())
17
+ throw new Error('This runner needs the macOS review sandbox on its execution node');
18
+ const temporaryDirectory = join(policy.writableRoots[0], '.review-runtime', 'tmp');
19
+ await mkdir(temporaryDirectory, { recursive: true, mode: 0o700 });
20
+ const writable = await Promise.all(policy.writableRoots.map((root) => realpath(root)));
21
+ const readOnly = await Promise.all(policy.readOnlyRoots.map((root) => realpath(root)));
22
+ for (const root of runtimeRoots) {
23
+ await mkdir(root, { recursive: true, mode: 0o700 });
24
+ writable.push(await realpath(root));
25
+ }
26
+ const profile = [
27
+ '(version 1)',
28
+ '(allow default)',
29
+ '(deny file-write*)',
30
+ ...writable.map((root) => `(allow file-write* (subpath ${JSON.stringify(root)}))`),
31
+ '(allow file-write* (literal "/dev/null") (literal "/dev/tty"))',
32
+ ...readOnly.map((root) => `(deny file-write* (subpath ${JSON.stringify(root)}))`),
33
+ ].join('\n');
34
+ return {
35
+ sandbox: { executable: '/usr/bin/sandbox-exec', args: ['-p', profile] },
36
+ temporaryDirectory,
37
+ };
38
+ }
39
+ module.exports = { available, sandbox };
@@ -0,0 +1,45 @@
1
+ // Persist a runner-owned chat identity before launching a managed review terminal.
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const cp = require('node:child_process');
5
+ const input = JSON.parse(process.argv[2]);
6
+ const file = path.join(input.task, '.runner-session.json');
7
+ const identity = { runId: input.runId, workspaceId: input.workspaceId, runner: input.runner };
8
+ if (!path.isAbsolute(input.task) || !path.isAbsolute(input.cwd))
9
+ throw Error('Invalid session workspace');
10
+ if (fs.existsSync(file)) {
11
+ const saved = JSON.parse(fs.readFileSync(file, 'utf8'));
12
+ for (const key of Object.keys(identity))
13
+ if (saved[key] !== identity[key]) throw Error('Runner session identity changed');
14
+ if (input.resumeSessionId && saved.sessionId !== input.resumeSessionId)
15
+ throw Error('Retained session changed');
16
+ process.stdout.write(JSON.stringify(saved));
17
+ } else {
18
+ let sessionId = input.resumeSessionId;
19
+ if (!sessionId) {
20
+ const env = { ...process.env, ...input.environment.set };
21
+ for (const key of [
22
+ ...input.environment.unset,
23
+ 'FARMSLOT_NODE_TOKEN',
24
+ 'FARMSLOT_GATEWAY_TOKEN',
25
+ 'FARMSLOT_GATEWAY_PASSWORD',
26
+ 'CLAUDECODE',
27
+ ])
28
+ delete env[key];
29
+ const result = cp.spawnSync(input.argv[0], input.argv.slice(1), {
30
+ cwd: input.cwd,
31
+ env,
32
+ encoding: 'utf8',
33
+ timeout: 20000,
34
+ });
35
+ if (result.error) throw result.error;
36
+ if (result.status !== 0) throw Error(result.stderr || 'Could not reserve runner session');
37
+ sessionId = result.stdout.trim();
38
+ }
39
+ // create-chat documents a UUID result. Never infer identity from terminal text.
40
+ if (!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(sessionId))
41
+ throw Error('Runner returned an invalid chat ID');
42
+ const saved = { ...identity, sessionId };
43
+ fs.writeFileSync(file, JSON.stringify(saved), { flag: 'wx', mode: 0o600 });
44
+ process.stdout.write(JSON.stringify(saved));
45
+ }
@@ -0,0 +1,134 @@
1
+ // Runner-neutral terminal lifecycle. Inputs come from the owning gateway, never PR content.
2
+ const fs = require('node:fs'),
3
+ path = require('node:path'),
4
+ cp = require('node:child_process');
5
+ const { sandbox } = require('./review-filesystem.cjs');
6
+ const input = JSON.parse(process.argv[2]);
7
+ const quote = (s) => "'" + s.replaceAll("'", "'\\''") + "'";
8
+ const tmux = (args) => cp.spawnSync('tmux', args, { encoding: 'utf8' });
9
+ const check = (result) => {
10
+ if (result.status !== 0)
11
+ throw Error(result.stderr || result.error?.message || 'Terminal operation failed');
12
+ return result.stdout.trim();
13
+ };
14
+ const target = input.session;
15
+ async function main() {
16
+ if (
17
+ !/^[a-zA-Z0-9_-]+$/.test(input.session) ||
18
+ !path.isAbsolute(input.task) ||
19
+ !path.isAbsolute(input.cwd)
20
+ )
21
+ throw Error('Invalid review terminal identity');
22
+ const marker = path.join(input.task, '.terminal-launch.json');
23
+ const cancelled = path.join(input.task, '.terminal-cancelled');
24
+ const existing = fs.existsSync(marker) ? JSON.parse(fs.readFileSync(marker, 'utf8')) : undefined;
25
+ if (
26
+ existing &&
27
+ (existing.runId !== input.runId ||
28
+ existing.workspaceId !== input.workspaceId ||
29
+ existing.session !== input.session)
30
+ )
31
+ throw Error('Review terminal identity changed');
32
+ const has = tmux(['has-session', '-t', '=' + target]);
33
+ if (![0, 1].includes(has.status)) check(has);
34
+ if (
35
+ has.status === 0 &&
36
+ check(tmux(['show-option', '-v', '-t', target, '@farmslot-review-workspace'])) !==
37
+ input.workspaceId
38
+ )
39
+ throw Error('Review terminal belongs to another workspace');
40
+ if (input.action === 'stop') {
41
+ fs.writeFileSync(cancelled, input.runId, { mode: 0o600 });
42
+ if (has.status === 0) check(tmux(['kill-session', '-t', target]));
43
+ process.stdout.write(JSON.stringify({ stopped: true }));
44
+ return;
45
+ }
46
+ if (input.action === 'inspect') {
47
+ process.stdout.write(JSON.stringify({ exists: has.status === 0, launch: existing }));
48
+ return;
49
+ }
50
+ if (fs.existsSync(cancelled)) throw new Error('Review terminal launch was cancelled');
51
+ if (existing) {
52
+ if (has.status !== 0) throw Error('Review terminal exited; explicit retry required');
53
+ process.stdout.write(JSON.stringify(existing));
54
+ return;
55
+ }
56
+ if (has.status === 0) throw Error('Review terminal exists without a launch record');
57
+ const cwd = fs.realpathSync(input.cwd),
58
+ task = fs.realpathSync(input.task);
59
+ const gitRoot = check(
60
+ cp.spawnSync('git', ['-C', cwd, 'rev-parse', '--path-format=absolute', '--git-common-dir'], {
61
+ encoding: 'utf8',
62
+ }),
63
+ );
64
+ const policy = {
65
+ readOnlyRoots: [
66
+ cwd,
67
+ fs.realpathSync(gitRoot),
68
+ ...(input.support ? [fs.realpathSync(input.support)] : []),
69
+ ],
70
+ writableRoots: [task],
71
+ };
72
+ const runtimeRoots = input.runtimeRoots.map((root) =>
73
+ root.replace(/^~(?=\/|$)/, process.env.HOME),
74
+ );
75
+ const guard = await sandbox(policy, runtimeRoots);
76
+ const promptFile = path.join(task, '.terminal-prompt.txt');
77
+ fs.writeFileSync(promptFile, input.prompt, { mode: 0o600 });
78
+ if (input.setup) check(cp.spawnSync('/bin/sh', ['-c', input.setup], { cwd, encoding: 'utf8' }));
79
+ const environment = {
80
+ ...process.env,
81
+ ...input.environment.set,
82
+ TMPDIR: guard.temporaryDirectory,
83
+ DISABLE_OMX: '1',
84
+ DISABLE_OMC: '1',
85
+ };
86
+ for (const key of [
87
+ ...input.environment.unset,
88
+ 'CLAUDECODE',
89
+ 'FARMSLOT_NODE_TOKEN',
90
+ 'FARMSLOT_GATEWAY_TOKEN',
91
+ 'FARMSLOT_GATEWAY_PASSWORD',
92
+ ])
93
+ delete environment[key];
94
+ const commandFile = path.join(task, '.terminal-start.cjs');
95
+ const record = {
96
+ runId: input.runId,
97
+ workspaceId: input.workspaceId,
98
+ session: input.session,
99
+ startedAt: new Date().toISOString(),
100
+ };
101
+ 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
+ fs.writeFileSync(commandFile, launch, { mode: 0o600 });
103
+ const envArgs = Object.entries(environment)
104
+ .filter(([, value]) => typeof value === 'string')
105
+ .flatMap(([name, value]) => ['-e', name + '=' + value]);
106
+ fs.writeFileSync(marker, JSON.stringify(record), { mode: 0o600, flag: 'wx' });
107
+ check(
108
+ tmux([
109
+ 'new-session',
110
+ '-d',
111
+ ...envArgs,
112
+ '-s',
113
+ input.session,
114
+ '-c',
115
+ cwd,
116
+ `${quote(process.execPath)} ${quote(commandFile)}`,
117
+ ';',
118
+ 'set-option',
119
+ '-t',
120
+ target,
121
+ '@farmslot-review-workspace',
122
+ input.workspaceId,
123
+ ]),
124
+ );
125
+ if (fs.existsSync(cancelled)) {
126
+ check(tmux(['kill-session', '-t', target]));
127
+ throw new Error('Review terminal launch was cancelled');
128
+ }
129
+ process.stdout.write(JSON.stringify(record));
130
+ }
131
+ main().catch((error) => {
132
+ process.stderr.write(String(error));
133
+ process.exitCode = 1;
134
+ });
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `farmslot-agent task init`: write a task directory from a selected execution
4
+ * template. Thin wrappers (mm-harness) add their own defaults and forward here.
5
+ */
6
+ import { execFileSync } from 'node:child_process';
7
+ import { readFileSync } from 'node:fs';
8
+ import path from 'node:path';
9
+ import { pathToFileURL } from 'node:url';
10
+
11
+ import { buildSources, loadRuntime, parseCatalogArgs } from './execution-template-cli.mjs';
12
+
13
+ function usage(exitCode = 0) {
14
+ const text = [
15
+ 'Usage: task init <task-dir> --flow f --platform p --template id --title t [options]',
16
+ '',
17
+ ' Template sources: --dir <path> --domain-dir <domain=path> --project-worker <path>',
18
+ ' --package-templates <path> [--package-id id] [--project-name name] [--domain d]',
19
+ ' Run: [--run-mode m] — run mode used to match project default rules',
20
+ ' Task: --title t [--task-text s | --task-file path] [--ticket key] [--source-ref url]',
21
+ ' Identity: [--surface s] [--project name] [--repo owner/name] [--attempt-id id]',
22
+ ' Rendering: [--var KEY=VALUE]... [--task-dir-label label] [--mode-preamble text]',
23
+ ' [--addendum-file path] [--mark-command "cmd"] [--json]',
24
+ '',
25
+ 'Writes TASK.md, CHECKLIST.md, mark, inputs/handoff.json, inputs/worker-terminal-contract.json.',
26
+ ].join('\n');
27
+ (exitCode === 0 ? console.log : console.error)(text);
28
+ process.exit(exitCode);
29
+ }
30
+
31
+ function takeValue(args, i, flag) {
32
+ const value = args[i + 1];
33
+ if (value === undefined) throw new Error(`${flag} requires a value`);
34
+ return value;
35
+ }
36
+
37
+ function parseArgs(args) {
38
+ const opts = {
39
+ taskDir: null,
40
+ template: null,
41
+ title: null,
42
+ taskText: null,
43
+ taskFile: null,
44
+ ticket: null,
45
+ sourceRef: null,
46
+ surface: 'cli',
47
+ project: null,
48
+ repo: null,
49
+ attemptId: null,
50
+ vars: {},
51
+ taskDirLabel: null,
52
+ modePreamble: null,
53
+ addendumFile: null,
54
+ markCommand: null,
55
+ json: false,
56
+ };
57
+ const catalogArgs = [];
58
+ // The task dir is the first positional, before any flag, as the usage line says.
59
+ if (args[0] && !args[0].startsWith('-')) opts.taskDir = args.shift();
60
+ for (let i = 0; i < args.length; i += 1) {
61
+ const arg = args[i];
62
+ if (arg === '--template') opts.template = takeValue(args, i++, arg);
63
+ else if (arg === '--title') opts.title = takeValue(args, i++, arg);
64
+ else if (arg === '--task-text') opts.taskText = takeValue(args, i++, arg);
65
+ else if (arg === '--task-file') opts.taskFile = takeValue(args, i++, arg);
66
+ else if (arg === '--ticket') opts.ticket = takeValue(args, i++, arg);
67
+ else if (arg === '--source-ref') opts.sourceRef = takeValue(args, i++, arg);
68
+ else if (arg === '--surface') opts.surface = takeValue(args, i++, arg);
69
+ else if (arg === '--project') opts.project = takeValue(args, i++, arg);
70
+ else if (arg === '--repo') opts.repo = takeValue(args, i++, arg);
71
+ else if (arg === '--attempt-id') opts.attemptId = takeValue(args, i++, arg);
72
+ else if (arg === '--task-dir-label') opts.taskDirLabel = takeValue(args, i++, arg);
73
+ else if (arg === '--mode-preamble') opts.modePreamble = takeValue(args, i++, arg);
74
+ else if (arg === '--addendum-file') opts.addendumFile = takeValue(args, i++, arg);
75
+ else if (arg === '--mark-command') opts.markCommand = takeValue(args, i++, arg);
76
+ else if (arg === '--var') {
77
+ const pair = takeValue(args, i++, arg);
78
+ const eq = pair.indexOf('=');
79
+ if (eq <= 0) throw new Error('--var must use KEY=VALUE');
80
+ opts.vars[pair.slice(0, eq)] = pair.slice(eq + 1);
81
+ } else if (arg === '--json') {
82
+ opts.json = true;
83
+ catalogArgs.push(arg);
84
+ } else if (arg === '-h' || arg === '--help') usage(0);
85
+ else catalogArgs.push(arg);
86
+ }
87
+ if (!opts.taskDir) throw new Error('task init requires <task-dir>');
88
+ if (!opts.template) throw new Error('task init requires --template <id>');
89
+ if (!opts.title) throw new Error('task init requires --title');
90
+ const catalog = parseCatalogArgs([...catalogArgs, '--id', opts.template]);
91
+ if (!catalog.flow) throw new Error('task init requires --flow');
92
+ if (!catalog.platform) throw new Error('task init requires --platform');
93
+ return { opts, catalog };
94
+ }
95
+
96
+ function gitOutput(args) {
97
+ try {
98
+ return execFileSync('git', args, {
99
+ encoding: 'utf8',
100
+ stdio: ['ignore', 'pipe', 'ignore'],
101
+ }).trim();
102
+ } catch {
103
+ // Not a git checkout, or no remote: identity falls back to the directory name.
104
+ return '';
105
+ }
106
+ }
107
+
108
+ async function main() {
109
+ const args = process.argv.slice(2);
110
+ if (args.length === 0 || args[0] === '-h' || args[0] === '--help') usage(0);
111
+ const { opts, catalog } = parseArgs(args);
112
+ const runtime = await loadRuntime();
113
+
114
+ const description = opts.taskFile
115
+ ? readFileSync(path.resolve(opts.taskFile), 'utf8').trim()
116
+ : (opts.taskText ?? '').trim();
117
+ const toplevel = gitOutput(['rev-parse', '--show-toplevel']);
118
+ const originUrl = gitOutput(['remote', 'get-url', 'origin']);
119
+ const repo = opts.repo ?? (originUrl ? runtime.portableRepoIdentity(originUrl) : undefined);
120
+ const project =
121
+ opts.project ??
122
+ (repo ? repo.split('/').pop() : path.basename(toplevel || process.cwd())).toLowerCase();
123
+ // Rendered inside taskInit with the same vars as the checklist, so an addendum
124
+ // that works on the farm works here.
125
+ const addendum = opts.addendumFile ? readFileSync(path.resolve(opts.addendumFile), 'utf8') : null;
126
+
127
+ const result = await runtime.taskInit({
128
+ taskDir: path.resolve(opts.taskDir),
129
+ taskDirLabel: opts.taskDirLabel ?? opts.taskDir,
130
+ flow: catalog.flow,
131
+ ...(catalog.runMode ? { runMode: catalog.runMode } : {}),
132
+ platform: catalog.platform,
133
+ ...(catalog.domain ? { domain: catalog.domain } : {}),
134
+ template: { sources: buildSources(catalog, runtime), explicitId: opts.template },
135
+ task: {
136
+ title: opts.title,
137
+ description,
138
+ sourceKind: opts.taskFile ? 'file' : 'text',
139
+ ...(opts.ticket ? { ticket: opts.ticket } : {}),
140
+ ...(opts.sourceRef ? { sourceRef: opts.sourceRef } : {}),
141
+ },
142
+ handoff: {
143
+ surface: opts.surface,
144
+ project,
145
+ ...(repo ? { repo } : {}),
146
+ ...(opts.attemptId ? { attemptId: opts.attemptId } : {}),
147
+ },
148
+ vars: opts.vars,
149
+ ...(opts.modePreamble ? { modePreamble: opts.modePreamble } : {}),
150
+ addendum,
151
+ ...(opts.markCommand ? { markCommand: opts.markCommand } : {}),
152
+ });
153
+
154
+ if (opts.json) {
155
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
156
+ } else {
157
+ process.stdout.write(`Task document: ${result.taskDocument}\nChecklist: ${result.checklist}\n`);
158
+ }
159
+ }
160
+
161
+ if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
162
+ main().catch((error) => {
163
+ console.error(error instanceof Error ? error.message : String(error));
164
+ process.exit(1);
165
+ });
166
+ }
@@ -286,8 +286,11 @@ function templateBodyScope(content) {
286
286
  /**
287
287
  * Deterministic structure checks beyond terminal artifact contract.
288
288
  * @param {string} templateContent
289
+ * @param {{ roleChecklist?: boolean }} [options] roleChecklist: a nested-loop role
290
+ * checklist (self-review, self-review-fix, ci-fix) rendered beside an existing
291
+ * task dir; it may keep its own `## Task` block.
289
292
  */
290
- function lintWorkerTemplateStructure(templateContent) {
293
+ function lintWorkerTemplateStructure(templateContent, options = {}) {
291
294
  /** @type {string[]} */
292
295
  const issues = [];
293
296
  if (/(?<!\{)\{TASK_DIR\}(?!\})/.test(templateContent)) {
@@ -302,8 +305,17 @@ function lintWorkerTemplateStructure(templateContent) {
302
305
  issues.push('terminal template missing `## Checklist` or `## Completion signal` section');
303
306
  }
304
307
 
305
- if (!templateContent.includes('## Task') && !templateContent.includes('TASK_DIR:')) {
306
- issues.push('missing `## Task` block or `TASK_DIR:` metadata');
308
+ // The task writer generates TASK.md (ticket block, description, acceptance
309
+ // criteria) and renders the flow template into CHECKLIST.md. A flow template
310
+ // that still carries the old `## Task` header duplicates that block inside
311
+ // the checklist.
312
+ if (
313
+ !options.roleChecklist &&
314
+ (/^## Task\s*$/m.test(templateContent) || /^TASK_DIR:\s/m.test(templateContent))
315
+ ) {
316
+ issues.push(
317
+ 'carries a `## Task` block or `TASK_DIR:` line — the task writer generates TASK.md; keep only the checklist',
318
+ );
307
319
  }
308
320
 
309
321
  const scope = templateBodyScope(templateContent);