@zerwiz/ymir 0.1.3 → 0.1.5

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 (850) hide show
  1. package/.agents/README.md +291 -0
  2. package/.agents/agents/bragi-marketer.md +111 -0
  3. package/.agents/agents/brokk.md +352 -0
  4. package/.agents/agents/forseti-reviewer.md +93 -0
  5. package/.agents/agents/frigg-consent.md +77 -0
  6. package/.agents/agents/groa-update.md +77 -0
  7. package/.agents/agents/hnoss-designer.md +69 -0
  8. package/.agents/agents/huginn-researcher.md +99 -0
  9. package/.agents/agents/jord-projects.md +78 -0
  10. package/.agents/agents/kvasir-scout.md +91 -0
  11. package/.agents/agents/mimir-planner.md +92 -0
  12. package/.agents/agents/muninn-stow.md +77 -0
  13. package/.agents/agents/saga-bearings.md +78 -0
  14. package/.agents/agents/sindri-developer.md +114 -0
  15. package/.agents/agents/snotra-documenter.md +105 -0
  16. package/.agents/agents/syn-recovery.md +78 -0
  17. package/.agents/agents/tyr-check.md +78 -0
  18. package/.agents/agents/urdh-hold.md +77 -0
  19. package/.agents/agents/volundr-smith.md +80 -0
  20. package/.agents/agents/vor-diagnostics.md +77 -0
  21. package/.agents/assets/agents/naming.md +50 -0
  22. package/.agents/assets/agents/registry.md +93 -0
  23. package/.agents/assets/agents/runtime.md +36 -0
  24. package/.agents/assets/agents/toon-tasks-cli.md +42 -0
  25. package/.agents/assets/templates/company_entity.template.md +37 -0
  26. package/.agents/backend/backends/cmux.sh +649 -0
  27. package/.agents/backend/backends/herdr-eventwait.py +157 -0
  28. package/.agents/backend/backends/herdr-workspace-move.py +114 -0
  29. package/.agents/backend/backends/herdr.sh +3386 -0
  30. package/.agents/backend/backends/orca.sh +290 -0
  31. package/.agents/backend/backends/tmux.sh +357 -0
  32. package/.agents/backend/backends/zellij.sh +700 -0
  33. package/.agents/backend/fm-afk-launch.sh +654 -0
  34. package/.agents/backend/fm-afk-return.sh +241 -0
  35. package/.agents/backend/fm-afk-start.sh +172 -0
  36. package/.agents/backend/fm-arm-command-policy.mjs +960 -0
  37. package/.agents/backend/fm-arm-pretool-check.sh +200 -0
  38. package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
  39. package/.agents/backend/fm-backend.sh +981 -0
  40. package/.agents/backend/fm-backlog-handoff.sh +909 -0
  41. package/.agents/backend/fm-backlog-receive.sh +187 -0
  42. package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
  43. package/.agents/backend/fm-bearings-board.sh +204 -0
  44. package/.agents/backend/fm-bearings-snapshot.sh +546 -0
  45. package/.agents/backend/fm-bootstrap.sh +1568 -0
  46. package/.agents/backend/fm-branch-outcome.sh +203 -0
  47. package/.agents/backend/fm-branch-prompt.sh +101 -0
  48. package/.agents/backend/fm-brief.sh +452 -0
  49. package/.agents/backend/fm-busy-event.sh +234 -0
  50. package/.agents/backend/fm-busy-lib.sh +986 -0
  51. package/.agents/backend/fm-captain-hold.sh +1001 -0
  52. package/.agents/backend/fm-cd-command-policy.mjs +153 -0
  53. package/.agents/backend/fm-cd-pretool-check.sh +190 -0
  54. package/.agents/backend/fm-check-lib.sh +74 -0
  55. package/.agents/backend/fm-check-register.sh +42 -0
  56. package/.agents/backend/fm-check-unregister.sh +52 -0
  57. package/.agents/backend/fm-classify-lib.sh +1770 -0
  58. package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
  59. package/.agents/backend/fm-composer-lib.sh +1416 -0
  60. package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
  61. package/.agents/backend/fm-config-push.sh +254 -0
  62. package/.agents/backend/fm-control-lib.sh +251 -0
  63. package/.agents/backend/fm-control.sh +878 -0
  64. package/.agents/backend/fm-crew-state.sh +625 -0
  65. package/.agents/backend/fm-cursor-lib.sh +243 -0
  66. package/.agents/backend/fm-decision-hold.sh +232 -0
  67. package/.agents/backend/fm-doc-audience-check.sh +269 -0
  68. package/.agents/backend/fm-dod-lib.sh +67 -0
  69. package/.agents/backend/fm-ensure-agents-md.sh +253 -0
  70. package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
  71. package/.agents/backend/fm-extension.mjs +2577 -0
  72. package/.agents/backend/fm-extension.sh +16 -0
  73. package/.agents/backend/fm-ff-lib.sh +420 -0
  74. package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
  75. package/.agents/backend/fm-fleet-sync.sh +458 -0
  76. package/.agents/backend/fm-fleet-view.sh +96 -0
  77. package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
  78. package/.agents/backend/fm-guard.sh +242 -0
  79. package/.agents/backend/fm-harness.sh +195 -0
  80. package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
  81. package/.agents/backend/fm-herdr-lab.sh +346 -0
  82. package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
  83. package/.agents/backend/fm-home-seed.sh +984 -0
  84. package/.agents/backend/fm-home-summary-refresh.sh +255 -0
  85. package/.agents/backend/fm-hook-host-lib.sh +36 -0
  86. package/.agents/backend/fm-inactive-reconcile.sh +531 -0
  87. package/.agents/backend/fm-inbox.sh +399 -0
  88. package/.agents/backend/fm-install-actionlint.sh +84 -0
  89. package/.agents/backend/fm-install-herdr.sh +96 -0
  90. package/.agents/backend/fm-install-shellcheck.sh +84 -0
  91. package/.agents/backend/fm-install-treehouse.sh +95 -0
  92. package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
  93. package/.agents/backend/fm-lease-lib.sh +218 -0
  94. package/.agents/backend/fm-lease.sh +189 -0
  95. package/.agents/backend/fm-line-cap-lib.sh +51 -0
  96. package/.agents/backend/fm-lint-workflows.sh +137 -0
  97. package/.agents/backend/fm-lint.sh +591 -0
  98. package/.agents/backend/fm-lock-lib.sh +104 -0
  99. package/.agents/backend/fm-lock.sh +107 -0
  100. package/.agents/backend/fm-marker-lib.sh +12 -0
  101. package/.agents/backend/fm-merge-local.sh +74 -0
  102. package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
  103. package/.agents/backend/fm-nm-run-lib.sh +124 -0
  104. package/.agents/backend/fm-on.sh +125 -0
  105. package/.agents/backend/fm-operational-input.sh +253 -0
  106. package/.agents/backend/fm-peek.sh +46 -0
  107. package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
  108. package/.agents/backend/fm-pr-check.sh +135 -0
  109. package/.agents/backend/fm-pr-lib.sh +1016 -0
  110. package/.agents/backend/fm-pr-merge.sh +707 -0
  111. package/.agents/backend/fm-pr-poll.sh +110 -0
  112. package/.agents/backend/fm-primary-scope-lib.sh +33 -0
  113. package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
  114. package/.agents/backend/fm-procevent-lavish.sh +628 -0
  115. package/.agents/backend/fm-procevent-lib.sh +892 -0
  116. package/.agents/backend/fm-procevent-quota.sh +290 -0
  117. package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
  118. package/.agents/backend/fm-procevent-when.sh +504 -0
  119. package/.agents/backend/fm-procevent.sh +1466 -0
  120. package/.agents/backend/fm-project-mode.sh +92 -0
  121. package/.agents/backend/fm-project-origin-lib.sh +180 -0
  122. package/.agents/backend/fm-promote.sh +282 -0
  123. package/.agents/backend/fm-public-followup-emit.sh +260 -0
  124. package/.agents/backend/fm-public-followup-lib.sh +398 -0
  125. package/.agents/backend/fm-public-followup.sh +1330 -0
  126. package/.agents/backend/fm-push-transition-lib.sh +168 -0
  127. package/.agents/backend/fm-quota-axi-lib.sh +93 -0
  128. package/.agents/backend/fm-quota-choose.sh +384 -0
  129. package/.agents/backend/fm-remote-delta-read.sh +189 -0
  130. package/.agents/backend/fm-remote-doctor.sh +800 -0
  131. package/.agents/backend/fm-remote-entrypoint.sh +193 -0
  132. package/.agents/backend/fm-remote-file.sh +234 -0
  133. package/.agents/backend/fm-remote-home-provision.sh +259 -0
  134. package/.agents/backend/fm-remote-home-seed.sh +259 -0
  135. package/.agents/backend/fm-remote-inherit-push.sh +90 -0
  136. package/.agents/backend/fm-remote-inherit.sh +179 -0
  137. package/.agents/backend/fm-remote-job-lib.sh +1241 -0
  138. package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
  139. package/.agents/backend/fm-remote-job-worker.sh +1124 -0
  140. package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
  141. package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
  142. package/.agents/backend/fm-review-diff.sh +158 -0
  143. package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
  144. package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
  145. package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
  146. package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
  147. package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
  148. package/.agents/backend/fm-secondmate-report.sh +88 -0
  149. package/.agents/backend/fm-send.sh +1059 -0
  150. package/.agents/backend/fm-session-lock-lib.sh +176 -0
  151. package/.agents/backend/fm-session-start.sh +970 -0
  152. package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
  153. package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
  154. package/.agents/backend/fm-sessionstart-run.sh +145 -0
  155. package/.agents/backend/fm-spawn.sh +3153 -0
  156. package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
  157. package/.agents/backend/fm-startup-memory-budget.sh +94 -0
  158. package/.agents/backend/fm-startup-network.sh +642 -0
  159. package/.agents/backend/fm-stow-cascade.sh +251 -0
  160. package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
  161. package/.agents/backend/fm-supervise-daemon.sh +1732 -0
  162. package/.agents/backend/fm-supervision-instructions.sh +215 -0
  163. package/.agents/backend/fm-supervision-lib.sh +90 -0
  164. package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
  165. package/.agents/backend/fm-tangle-lib.sh +53 -0
  166. package/.agents/backend/fm-task-inbox-lib.sh +402 -0
  167. package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
  168. package/.agents/backend/fm-teardown.sh +2922 -0
  169. package/.agents/backend/fm-test-isolation-proof.sh +588 -0
  170. package/.agents/backend/fm-test-run.sh +2191 -0
  171. package/.agents/backend/fm-timeout-lib.sh +141 -0
  172. package/.agents/backend/fm-timing-lib.sh +179 -0
  173. package/.agents/backend/fm-tmux-lib.sh +291 -0
  174. package/.agents/backend/fm-tool-update-check.sh +898 -0
  175. package/.agents/backend/fm-trace-context-lib.sh +227 -0
  176. package/.agents/backend/fm-transition-lib.sh +103 -0
  177. package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
  178. package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
  179. package/.agents/backend/fm-turnend-guard.sh +432 -0
  180. package/.agents/backend/fm-update.sh +110 -0
  181. package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
  182. package/.agents/backend/fm-voice-client.py +1373 -0
  183. package/.agents/backend/fm-voice-relay.py +1256 -0
  184. package/.agents/backend/fm-wake-drain.sh +592 -0
  185. package/.agents/backend/fm-wake-grant.sh +127 -0
  186. package/.agents/backend/fm-wake-lib.sh +1849 -0
  187. package/.agents/backend/fm-watch-arm.sh +602 -0
  188. package/.agents/backend/fm-watch-checkpoint.sh +109 -0
  189. package/.agents/backend/fm-watch.sh +1962 -0
  190. package/.agents/backend/fm-x-dismiss.sh +116 -0
  191. package/.agents/backend/fm-x-followup.sh +286 -0
  192. package/.agents/backend/fm-x-lib.sh +1000 -0
  193. package/.agents/backend/fm-x-link.sh +234 -0
  194. package/.agents/backend/fm-x-poll.sh +185 -0
  195. package/.agents/backend/fm-x-reply.sh +385 -0
  196. package/.agents/backend/fm_voice_frame.py +166 -0
  197. package/.agents/backend/fm_voice_records.py +574 -0
  198. package/.agents/backend/model-bridge.py +290 -0
  199. package/.agents/backend/opencode-go-bridge.py +30 -0
  200. package/.agents/bus/README.md +12 -0
  201. package/.agents/bus/messages.json +1 -0
  202. package/.agents/config/agents.machine.example.yaml +16 -0
  203. package/.agents/config/agents.yaml +50 -0
  204. package/.agents/config/agents.yaml.example +72 -0
  205. package/.agents/config/crew-dispatch.json +22 -0
  206. package/.agents/config/cron.yaml +9 -0
  207. package/.agents/config/eindri-dispatch.json +67 -0
  208. package/.agents/config/eindri-harness +1 -0
  209. package/.agents/config/startup-memory-budget +1 -0
  210. package/.agents/config/tailscale-sync.yaml +27 -0
  211. package/.agents/filebrowser/.filebrowser.json +6 -0
  212. package/.agents/filebrowser/README.md +11 -0
  213. package/.agents/gateway/README.md +12 -0
  214. package/.agents/github/README.md +15 -0
  215. package/.agents/github/webhooks/.gitkeep +0 -0
  216. package/.agents/github/workflows/.gitkeep +0 -0
  217. package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
  218. package/.agents/harness/opencode/plugins/package.json +4 -0
  219. package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
  220. package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
  221. package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
  222. package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
  223. package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
  224. package/.agents/memory/README.md +46 -0
  225. package/.agents/memory/kaia.engram +0 -0
  226. package/.agents/memory/kaia.engram-shm +0 -0
  227. package/.agents/memory/kaia.engram-wal +0 -0
  228. package/.agents/memory/runes_audit.md +5 -0
  229. package/.agents/memory/well/episodes.jsonl +367 -0
  230. package/.agents/memory/well/workspace.jsonl +27 -0
  231. package/.agents/migrations/0001-hodd-layout.sh +38 -0
  232. package/.agents/migrations/0002-a2a-mcp.sh +27 -0
  233. package/.agents/migrations/0003-private-data-separation.sh +111 -0
  234. package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
  235. package/.agents/sandbox/Dockerfile.utgard +19 -0
  236. package/.agents/sandbox/README.md +14 -0
  237. package/.agents/sandbox/utgard.config.json +14 -0
  238. package/.agents/skills/README.md +59 -0
  239. package/.agents/skills/bragi-marketing/README.md +3 -0
  240. package/.agents/skills/bragi-marketing/SKILL.md +82 -0
  241. package/.agents/skills/eindri-homes/README.md +3 -0
  242. package/.agents/skills/eindri-homes/SKILL.md +291 -0
  243. package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
  244. package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
  245. package/.agents/skills/frigg-consent/README.md +3 -0
  246. package/.agents/skills/frigg-consent/SKILL.md +56 -0
  247. package/.agents/skills/galdr-ymirsystem/README.md +3 -0
  248. package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
  249. package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
  250. package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
  251. package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
  252. package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
  253. package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
  254. package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
  255. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
  256. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
  257. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
  258. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
  259. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
  260. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
  261. package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
  262. package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
  263. package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
  264. package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
  265. package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
  266. package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
  267. package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
  268. package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
  269. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
  270. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
  271. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
  272. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
  273. package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
  274. package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
  275. package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
  276. package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
  277. package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
  278. package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
  279. package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
  280. package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
  281. package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
  282. package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
  283. package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
  284. package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
  285. package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
  286. package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
  287. package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
  288. package/.agents/skills/gjallarhorn-relay/README.md +3 -0
  289. package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
  290. package/.agents/skills/groa-update/SKILL.md +70 -0
  291. package/.agents/skills/hamr-adapters/README.md +3 -0
  292. package/.agents/skills/hamr-adapters/SKILL.md +123 -0
  293. package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
  294. package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
  295. package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
  296. package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
  297. package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
  298. package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
  299. package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
  300. package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
  301. package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
  302. package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
  303. package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
  304. package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
  305. package/.agents/skills/herdr-panes/README.md +3 -0
  306. package/.agents/skills/herdr-panes/SKILL.md +92 -0
  307. package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
  308. package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
  309. package/.agents/skills/hnoss-design/DESIGN.md +19 -0
  310. package/.agents/skills/hnoss-design/README.md +3 -0
  311. package/.agents/skills/hnoss-design/SKILL.md +83 -0
  312. package/.agents/skills/hvild-afk/README.md +3 -0
  313. package/.agents/skills/hvild-afk/SKILL.md +241 -0
  314. package/.agents/skills/jord-projects/README.md +3 -0
  315. package/.agents/skills/jord-projects/SKILL.md +92 -0
  316. package/.agents/skills/lifecycle/SKILL.md +56 -0
  317. package/.agents/skills/lifecycle/smoke_test.sh +52 -0
  318. package/.agents/skills/lifecycle/start.sh +21 -0
  319. package/.agents/skills/lifecycle/status.sh +35 -0
  320. package/.agents/skills/lifecycle/stop.sh +16 -0
  321. package/.agents/skills/muninn-stow/README.md +3 -0
  322. package/.agents/skills/muninn-stow/SKILL.md +309 -0
  323. package/.agents/skills/no-mistakes/SKILL.md +379 -0
  324. package/.agents/skills/nornir-schedule/README.md +3 -0
  325. package/.agents/skills/nornir-schedule/SKILL.md +25 -0
  326. package/.agents/skills/nornir-schedule/assets/events.md +137 -0
  327. package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
  328. package/.agents/skills/nsr-compliance/README.md +3 -0
  329. package/.agents/skills/nsr-compliance/SKILL.md +29 -0
  330. package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
  331. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
  332. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
  333. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
  334. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
  335. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
  336. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
  337. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
  338. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
  339. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
  340. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
  341. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
  342. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
  343. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
  344. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
  345. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
  346. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
  347. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
  348. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
  349. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
  350. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
  351. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
  352. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
  353. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  354. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
  355. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
  356. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
  357. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
  358. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
  359. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
  360. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
  361. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
  362. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
  363. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
  364. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
  365. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
  366. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
  367. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
  368. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
  369. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
  370. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
  371. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
  372. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
  373. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
  374. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
  375. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
  376. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
  377. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
  378. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
  379. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
  380. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
  381. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
  382. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
  383. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
  384. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
  385. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
  386. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
  387. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
  388. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
  389. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
  390. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
  391. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
  392. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
  393. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
  394. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
  395. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
  396. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
  397. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
  398. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
  399. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
  400. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
  401. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
  402. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
  403. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
  404. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
  405. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
  406. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
  407. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
  408. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
  409. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
  410. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
  411. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
  412. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
  413. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
  414. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
  415. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
  416. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
  417. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
  418. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
  419. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
  420. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
  421. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
  422. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
  423. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
  424. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
  425. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
  426. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
  427. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
  428. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
  429. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
  430. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
  431. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
  432. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
  433. package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
  434. package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
  435. package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
  436. package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
  437. package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
  438. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
  439. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
  440. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
  441. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
  442. package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
  443. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
  444. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
  445. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
  446. package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
  447. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
  448. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
  449. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
  450. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
  451. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
  452. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
  453. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
  454. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  455. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
  456. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
  457. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
  458. package/.agents/skills/pr-ops/README.md +3 -0
  459. package/.agents/skills/pr-ops/SKILL.md +109 -0
  460. package/.agents/skills/ratatoskr-a2a/README.md +3 -0
  461. package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
  462. package/.agents/skills/rules-check-drift/SKILL.md +81 -0
  463. package/.agents/skills/saga-bearings/README.md +3 -0
  464. package/.agents/skills/saga-bearings/SKILL.md +26 -0
  465. package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
  466. package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
  467. package/.agents/skills/saga-bearings/assets/recap.md +56 -0
  468. package/.agents/skills/syn-recovery/README.md +3 -0
  469. package/.agents/skills/syn-recovery/SKILL.md +54 -0
  470. package/.agents/skills/tyr-check/README.md +3 -0
  471. package/.agents/skills/tyr-check/SKILL.md +218 -0
  472. package/.agents/skills/urdh-hold/README.md +3 -0
  473. package/.agents/skills/urdh-hold/SKILL.md +25 -0
  474. package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
  475. package/.agents/skills/urdh-hold/assets/hold.md +54 -0
  476. package/.agents/skills/vor-diagnostics/README.md +3 -0
  477. package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
  478. package/.agents/skills/ymir-host/README.md +3 -0
  479. package/.agents/skills/ymir-host/SKILL.md +34 -0
  480. package/.agents/skills/ymir-host/assets/desktop.md +36 -0
  481. package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
  482. package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
  483. package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
  484. package/.agents/skills/ymir-host/assets/update.md +64 -0
  485. package/.agents/tests/assets/board-render-harness.mjs +117 -0
  486. package/.agents/tests/cmux-test-safety.sh +51 -0
  487. package/.agents/tests/fixtures.sh +291 -0
  488. package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
  489. package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
  490. package/.agents/tests/fm-afk-launch.test.sh +961 -0
  491. package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
  492. package/.agents/tests/fm-afk-return.test.sh +283 -0
  493. package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
  494. package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
  495. package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
  496. package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
  497. package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
  498. package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
  499. package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
  500. package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
  501. package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
  502. package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
  503. package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
  504. package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
  505. package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
  506. package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
  507. package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
  508. package/.agents/tests/fm-backend-orca.test.sh +1357 -0
  509. package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
  510. package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
  511. package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
  512. package/.agents/tests/fm-backend.test.sh +1142 -0
  513. package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
  514. package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
  515. package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
  516. package/.agents/tests/fm-bearings-board.test.sh +414 -0
  517. package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
  518. package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
  519. package/.agents/tests/fm-bootstrap.test.sh +1178 -0
  520. package/.agents/tests/fm-branch-supervision.test.sh +552 -0
  521. package/.agents/tests/fm-brief.test.sh +785 -0
  522. package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
  523. package/.agents/tests/fm-busy-state.test.sh +465 -0
  524. package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
  525. package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
  526. package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
  527. package/.agents/tests/fm-check-unregister.test.sh +198 -0
  528. package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
  529. package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
  530. package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
  531. package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
  532. package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
  533. package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
  534. package/.agents/tests/fm-composer-ghost.test.sh +716 -0
  535. package/.agents/tests/fm-composer-lib.test.sh +666 -0
  536. package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
  537. package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
  538. package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
  539. package/.agents/tests/fm-control.test.sh +910 -0
  540. package/.agents/tests/fm-crew-state.test.sh +1610 -0
  541. package/.agents/tests/fm-cursor-harness.test.sh +404 -0
  542. package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
  543. package/.agents/tests/fm-cursor-primary.test.sh +708 -0
  544. package/.agents/tests/fm-daemon.test.sh +2740 -0
  545. package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
  546. package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
  547. package/.agents/tests/fm-extension-binding.test.sh +2187 -0
  548. package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
  549. package/.agents/tests/fm-fleet-sync.test.sh +721 -0
  550. package/.agents/tests/fm-gate-refuse.test.sh +356 -0
  551. package/.agents/tests/fm-gitignore-config.test.sh +89 -0
  552. package/.agents/tests/fm-gotmp.test.sh +230 -0
  553. package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
  554. package/.agents/tests/fm-grok-harness.test.sh +117 -0
  555. package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
  556. package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
  557. package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
  558. package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
  559. package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
  560. package/.agents/tests/fm-herdr-lab.test.sh +243 -0
  561. package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
  562. package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
  563. package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
  564. package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
  565. package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
  566. package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
  567. package/.agents/tests/fm-kimi-harness.test.sh +687 -0
  568. package/.agents/tests/fm-lint-workflows.test.sh +532 -0
  569. package/.agents/tests/fm-lint.test.sh +1023 -0
  570. package/.agents/tests/fm-muse-harness.test.sh +932 -0
  571. package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
  572. package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
  573. package/.agents/tests/fm-on.test.sh +507 -0
  574. package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
  575. package/.agents/tests/fm-operational-input.test.sh +160 -0
  576. package/.agents/tests/fm-peek-remote.test.sh +110 -0
  577. package/.agents/tests/fm-pending-reply.test.sh +1288 -0
  578. package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
  579. package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
  580. package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
  581. package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
  582. package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
  583. package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
  584. package/.agents/tests/fm-pr-merge.test.sh +2135 -0
  585. package/.agents/tests/fm-procevent-quota.test.sh +225 -0
  586. package/.agents/tests/fm-procevent-when.test.sh +406 -0
  587. package/.agents/tests/fm-procevent.test.sh +1745 -0
  588. package/.agents/tests/fm-project-origin.test.sh +110 -0
  589. package/.agents/tests/fm-public-followup.test.sh +2272 -0
  590. package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
  591. package/.agents/tests/fm-quota-choose.test.sh +626 -0
  592. package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
  593. package/.agents/tests/fm-remote-doctor.test.sh +625 -0
  594. package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
  595. package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
  596. package/.agents/tests/fm-remote-job.test.sh +768 -0
  597. package/.agents/tests/fm-remote-reply.test.sh +520 -0
  598. package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
  599. package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
  600. package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
  601. package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
  602. package/.agents/tests/fm-review-diff.test.sh +176 -0
  603. package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
  604. package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
  605. package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
  606. package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
  607. package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
  608. package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
  609. package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
  610. package/.agents/tests/fm-send-inbox.test.sh +352 -0
  611. package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
  612. package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
  613. package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
  614. package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
  615. package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
  616. package/.agents/tests/fm-send-settle.test.sh +147 -0
  617. package/.agents/tests/fm-send-strict.test.sh +241 -0
  618. package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
  619. package/.agents/tests/fm-session-start.test.sh +2515 -0
  620. package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
  621. package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
  622. package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
  623. package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
  624. package/.agents/tests/fm-spawn-batch.test.sh +148 -0
  625. package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
  626. package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
  627. package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
  628. package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
  629. package/.agents/tests/fm-startup-network.test.sh +712 -0
  630. package/.agents/tests/fm-stow-cascade.test.sh +370 -0
  631. package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
  632. package/.agents/tests/fm-supervision-events.test.sh +156 -0
  633. package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
  634. package/.agents/tests/fm-tangle-guard.test.sh +277 -0
  635. package/.agents/tests/fm-task-delivery.test.sh +420 -0
  636. package/.agents/tests/fm-task-inbox.test.sh +513 -0
  637. package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
  638. package/.agents/tests/fm-teardown.test.sh +2665 -0
  639. package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
  640. package/.agents/tests/fm-test-fixtures.test.sh +132 -0
  641. package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
  642. package/.agents/tests/fm-test-run.test.sh +1196 -0
  643. package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
  644. package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
  645. package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
  646. package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
  647. package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
  648. package/.agents/tests/fm-transition-lib.test.sh +54 -0
  649. package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
  650. package/.agents/tests/fm-update.test.sh +304 -0
  651. package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
  652. package/.agents/tests/fm-voice-relay.test.sh +4741 -0
  653. package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
  654. package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
  655. package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
  656. package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
  657. package/.agents/tests/fm-wake-queue.test.sh +1224 -0
  658. package/.agents/tests/fm-watch-arm.test.sh +821 -0
  659. package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
  660. package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
  661. package/.agents/tests/fm-watch-triage.test.sh +3889 -0
  662. package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
  663. package/.agents/tests/fm-x-mode.test.sh +3042 -0
  664. package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
  665. package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
  666. package/.agents/tests/herdr-test-safety.sh +42 -0
  667. package/.agents/tests/lib.sh +315 -0
  668. package/.agents/tests/remote-herdr-fixture.sh +125 -0
  669. package/.agents/tests/secondmate-helpers.sh +193 -0
  670. package/.agents/tests/smoke.test.sh +93 -0
  671. package/.agents/tests/wake-helpers.sh +338 -0
  672. package/.agents/tests/zellij-test-safety.sh +60 -0
  673. package/.agents/tools/README.md +10 -0
  674. package/.agents/tools/bin/tasks-cli +267 -0
  675. package/.agents/tools/tasks-cli.ts +216 -0
  676. package/AGENTS.md +454 -0
  677. package/CHANGELOG.md +881 -0
  678. package/LICENSE +1 -1
  679. package/NOTICE +63 -0
  680. package/README.md +113 -39
  681. package/RULES/01-domains.md +51 -0
  682. package/RULES/02-agents.md +78 -0
  683. package/RULES/03-houses.md +61 -0
  684. package/RULES/04-hoard.md +65 -0
  685. package/RULES/05-platforms.md +67 -0
  686. package/RULES/06-append-only.md +58 -0
  687. package/RULES/07-config.md +45 -0
  688. package/RULES/08-delivery-gate.md +75 -0
  689. package/RULES/README.md +20 -0
  690. package/bin/README.md +107 -0
  691. package/bin/a2a-mcp.sh +98 -0
  692. package/bin/a2a-serve.py +103 -0
  693. package/bin/a2a-serve.sh +8 -0
  694. package/bin/a2a-talk.sh +50 -0
  695. package/bin/a2abridge-ensure.sh +108 -0
  696. package/bin/agent-run.sh +43 -0
  697. package/bin/agents-config.sh +233 -0
  698. package/bin/apodex-smoke-test.sh +92 -0
  699. package/bin/bifrost-bridge.sh +112 -0
  700. package/bin/bifrost-ingress.sh +77 -0
  701. package/bin/bootstrap-macos.sh +107 -0
  702. package/bin/bootstrap-windows.ps1 +112 -0
  703. package/bin/branch-guard.sh +64 -0
  704. package/bin/brokk +73 -0
  705. package/bin/brokk-classify-lib.sh +1770 -0
  706. package/bin/brokk-lease-lib.sh +222 -0
  707. package/bin/brokk-lease.sh +193 -0
  708. package/bin/brokk-lint.sh +65 -0
  709. package/bin/brokk-send.sh +38 -0
  710. package/bin/brokk-update.sh +9 -0
  711. package/bin/brokk-wake-grant.sh +127 -0
  712. package/bin/brokk-wake-lib.sh +1849 -0
  713. package/bin/changelog-guard.sh +165 -0
  714. package/bin/crash-sense.sh +160 -0
  715. package/bin/desktop-place.sh +224 -0
  716. package/bin/docs-guard.sh +39 -0
  717. package/bin/editor-place.sh +207 -0
  718. package/bin/eindri-acclaim.sh +34 -0
  719. package/bin/eindri-control.sh +28 -0
  720. package/bin/eindri-role.sh +83 -0
  721. package/bin/eindri-seen.sh +41 -0
  722. package/bin/eindri-send.sh +15 -0
  723. package/bin/eindri-start.sh +85 -0
  724. package/bin/eindri-watch.sh +57 -0
  725. package/bin/einherjar-spawn.sh +576 -0
  726. package/bin/eir-doctor.sh +112 -0
  727. package/bin/erindi-brief.sh +271 -0
  728. package/bin/fleet-apply.sh +68 -0
  729. package/bin/github-deploy.sh +81 -0
  730. package/bin/gjallarhorn-expose.sh +102 -0
  731. package/bin/gjallarhorn-notify.sh +58 -0
  732. package/bin/gjallarhorn-purge.sh +54 -0
  733. package/bin/gjallarhorn-tunnel.sh +72 -0
  734. package/bin/gleipnir-lock-lib.sh +284 -0
  735. package/bin/groa-update.sh +122 -0
  736. package/bin/hall-snapshot.sh +92 -0
  737. package/bin/hamr-harness.sh +246 -0
  738. package/bin/heimdall-ssh-keys.sh +374 -0
  739. package/bin/herdr-agents.py +44 -0
  740. package/bin/herdr-ensure.sh +104 -0
  741. package/bin/herdr-run.sh +431 -0
  742. package/bin/hermes-ensure.sh +88 -0
  743. package/bin/hoard-lib.sh +25 -0
  744. package/bin/hodd.sh +78 -0
  745. package/bin/host-sense.sh +199 -0
  746. package/bin/huginn-research-worker.sh +108 -0
  747. package/bin/local-model-lock.sh +51 -0
  748. package/bin/mimir-bridge.py +226 -0
  749. package/bin/mimir-bridge.sh +87 -0
  750. package/bin/mimir-ingest.sh +131 -0
  751. package/bin/mimir.sh +113 -0
  752. package/bin/mjollnir-webhook.sh +59 -0
  753. package/bin/mjollnir.sh +85 -0
  754. package/bin/model-hardware.sh +82 -0
  755. package/bin/model-resolve.sh +147 -0
  756. package/bin/models-detect.sh +178 -0
  757. package/bin/nornir-cron-start.sh +162 -0
  758. package/bin/nornir-job-daily-briefing.sh +191 -0
  759. package/bin/nornir-job-git-sync.sh +152 -0
  760. package/bin/nornir-job-memory-housekeeping.sh +114 -0
  761. package/bin/nornir-job-observer.sh +166 -0
  762. package/bin/nornir-job-skillopt-sleep.sh +76 -0
  763. package/bin/omarchy-hook-install.sh +60 -0
  764. package/bin/omarchy-install.sh +148 -0
  765. package/bin/omarchy-plugins.sh +174 -0
  766. package/bin/omarchy-sense.sh +149 -0
  767. package/bin/perm-guard.sh +30 -0
  768. package/bin/pi-agent.sh +68 -0
  769. package/bin/pi-ensure.sh +77 -0
  770. package/bin/pi-local.sh +37 -0
  771. package/bin/pi-seat.sh +63 -0
  772. package/bin/prereq-ensure.sh +166 -0
  773. package/bin/private-guard.sh +38 -0
  774. package/bin/project-git.sh +55 -0
  775. package/bin/public-guard.sh +70 -0
  776. package/bin/ratatoskr.sh +49 -0
  777. package/bin/realm-lib.sh +32 -0
  778. package/bin/repo-scrub.sh +62 -0
  779. package/bin/rodd-operational-input.sh +250 -0
  780. package/bin/runes-append.sh +198 -0
  781. package/bin/saga-session-start.sh +172 -0
  782. package/bin/saga-sessionstart-run.sh +79 -0
  783. package/bin/saga-wake-drain.sh +41 -0
  784. package/bin/secret-guard.sh +63 -0
  785. package/bin/sessrumnir-ensure.sh +100 -0
  786. package/bin/sessrumnir-sync.sh +237 -0
  787. package/bin/sessrumnir.sh +91 -0
  788. package/bin/skillopt-setup.sh +76 -0
  789. package/bin/skuld-branch-outcome.sh +203 -0
  790. package/bin/skuld-branch-prompt.sh +101 -0
  791. package/bin/smidja-bootstrap.sh +90 -0
  792. package/bin/smidja-observe.sh +86 -0
  793. package/bin/syn-arm-pretool-check.sh +57 -0
  794. package/bin/syn-asset-pretool-check.sh +76 -0
  795. package/bin/syn-cd-pretool-check.sh +28 -0
  796. package/bin/syn-guard-pretool-check.sh +83 -0
  797. package/bin/syn-turnend-guard.sh +36 -0
  798. package/bin/syn-watch-arm.sh +99 -0
  799. package/bin/tailscale-sync.sh +190 -0
  800. package/bin/telegram-bot.sh +69 -0
  801. package/bin/toolchain.sh +74 -0
  802. package/bin/utgard.sh +111 -0
  803. package/bin/valhalla.sh +115 -0
  804. package/bin/valknut-load.sh +260 -0
  805. package/bin/vor-crew-state.sh +124 -0
  806. package/bin/wedge-notify.sh +47 -0
  807. package/bin/workspace-provision.sh +80 -0
  808. package/bin/workspace-rag.sh +150 -0
  809. package/bin/wyrd-db.sh +47 -0
  810. package/bin/yggdrasil.sh +152 -0
  811. package/bin/ymir-edit.sh +145 -0
  812. package/bin/ymir-install.sh +641 -0
  813. package/bin/ymir-invite.sh +93 -0
  814. package/bin/ymir-isolation.sh +88 -0
  815. package/bin/ymir-migrate.sh +61 -0
  816. package/bin/ymir-platform.sh +348 -0
  817. package/bin/ymir-say.sh +97 -0
  818. package/bin/ymir-setup-auth.sh +108 -0
  819. package/bin/ymir-validate.sh +173 -0
  820. package/bin/ymir.js +74 -16
  821. package/docs/Architecture.md +339 -0
  822. package/docs/design.md +351 -0
  823. package/docs/installations/README.md +63 -0
  824. package/docs/installations/ymir-home.md +93 -0
  825. package/docs/integration.md +105 -0
  826. package/docs/lore.md +1056 -0
  827. package/docs/research/agent-harnesses.md +290 -0
  828. package/docs/runbooks/README.md +19 -0
  829. package/docs/runbooks/a2a.md +91 -0
  830. package/docs/runbooks/agent-permissions.md +99 -0
  831. package/docs/runbooks/agents.md +57 -0
  832. package/docs/runbooks/models.md +69 -0
  833. package/docs/runbooks/realm-onboarding.md +60 -0
  834. package/docs/runbooks/secrets-and-hoard.md +60 -0
  835. package/docs/runbooks/updates-and-migrations.md +110 -0
  836. package/docs/runbooks/worktrees-isolation.md +72 -0
  837. package/docs/session-start.md +105 -0
  838. package/docs/skillopt-integration.md +321 -0
  839. package/docs/sync-system.md +66 -0
  840. package/docs/workspaces.md +104 -0
  841. package/docs/ymir-rut.md +260 -0
  842. package/package.json +44 -14
  843. package/packaging/build.sh +74 -0
  844. package/packaging/macos/Ymir Installer.command +43 -0
  845. package/packaging/windows/ymir-setup.nsi +74 -0
  846. package/scripts/README.md +22 -0
  847. package/scripts/electron.sh +210 -0
  848. package/scripts/start.sh +167 -0
  849. package/scripts/stop.sh +113 -0
  850. package/install.sh +0 -35
@@ -0,0 +1,1241 @@
1
+ #!/usr/bin/env bash
2
+ # Shared remote fm-on job-worker protocol.
3
+ #
4
+ # Source this file from the fixed SSH entrypoint, the long-lived worker, or the
5
+ # remote doctor. It owns the per-account queue at
6
+ # ~/.firstmate/remote-job (override only with FM_REMOTE_JOB_STATE_ROOT for
7
+ # isolated tests), the bounded job record, worker installation, and the remote
8
+ # runtime PATH.
9
+ #
10
+ # A published job directory is mode 0700 and contains root, home, argv
11
+ # (NUL-delimited), stdin, seq, stdout, stderr, queue_deadline, timeout, and
12
+ # state; deadline and exit are added as execution advances, cancel is an
13
+ # optional caller-cancellation marker, and .claim may hold owner, owner_start,
14
+ # supervisor, supervisor_start, group, group_start, and armed records while
15
+ # work executes.
16
+ # Stage writes state=queued last. seq is a queue-wide monotonic staging
17
+ # sequence reserved atomically by its persistent .seq-claims directory; the
18
+ # counter is only a forward-moving allocation hint. If the bounded hint walk
19
+ # is exhausted, allocation rescans the claims for the maximum and continues
20
+ # above it. Expired claims are reaped by an independently hourly-rate-limited
21
+ # sweep. seq is the worker's FIFO ordering key within a home, with the job id
22
+ # as the deterministic tiebreak.
23
+ # FIFO is defined over completed stagings: a stage that returns before another
24
+ # begins executes first; concurrently overlapping stagings have no relative
25
+ # ordering contract.
26
+ # The worker atomically claims a job with .claim, establishes its execution
27
+ # deadline, changes state to running, writes bounded stdout/stderr and exit,
28
+ # then publishes state=done last. Callers wait for done, relay stdout and
29
+ # stderr separately, then reap only their completed record. Input, argv,
30
+ # stdout, and stderr are each capped at 1048576 bytes.
31
+ #
32
+ # The worker serves one lane per staged home: jobs for the same home run
33
+ # strictly FIFO in seq order while lanes for different homes run concurrently,
34
+ # so one home's long job never delays another home's commands. Within a lane a
35
+ # deliberately long-blocking poll would still serialize that home's short
36
+ # interactive commands behind its wait window.
37
+ # fm_remote_job_command_preemptible names the read-only long-poll class
38
+ # (fm-remote-delta-read.sh, the reply-log delta read). The worker preempts a
39
+ # running preemptible job as soon as a non-preemptible job is queued for the
40
+ # same home and publishes exit 76 with emptied stdout and stderr, distinct from
41
+ # the poll's exit 75 elapsed-window-with-no-data result. The delta read is
42
+ # non-destructive and cursor-anchored, so the caller's normal re-arm re-reads
43
+ # the same data and a preempted poll loses nothing.
44
+ #
45
+ # A caller that disconnects before its job completes cancels it instead of
46
+ # abandoning it: fm_remote_job_cancel writes a cancel marker into the record,
47
+ # the worker skips a cancelled queued job and terminates a running cancelled
48
+ # job's process group, and whichever side observes terminal publication reaps
49
+ # the finalized record because no result consumer remains. fm_remote_job_wait
50
+ # honors an optional FM_REMOTE_JOB_DISCONNECT_PROBE function name. When set,
51
+ # the probe runs about once per second; a failure cancels the job and fails
52
+ # the wait. The staging entrypoint arms it with a parent-liveness probe so an
53
+ # ssh channel
54
+ # that dies without delivering a signal still cancels the abandoned job.
55
+ # Abandoned .stage.* staging litter older than
56
+ # FM_REMOTE_JOB_STAGE_REAP_SECONDS is reaped by the worker's stale sweep.
57
+ #
58
+ # The worker accepts only a tracked, non-symlink executable named fm-*.sh below
59
+ # its configured FM_ROOT/bin. Every child receives env -i with the composed
60
+ # PATH, HOME, FM_HOME, FM_ROOT_OVERRIDE, and FM_REMOTE_JOB_ACTIVE=1. The PATH
61
+ # is intentionally filesystem-discovered rather than login-shell-derived:
62
+ # ~/.local/bin; nvm, asdf, and mise shims/install bins; Nix; Homebrew; and the
63
+ # system tail. No shell startup files are evaluated. Each discovered set is
64
+ # appended in the shell's own sorted pathname-expansion order, so which install
65
+ # of a multi-version tool wins is fixed by this composition rather than by the
66
+ # order the filesystem happens to return.
67
+ #
68
+ # On macOS the worker is Firstmate's Aqua LaunchAgent
69
+ # dev.firstmate.remote-job at ~/Library/LaunchAgents/dev.firstmate.remote-job.plist
70
+ # with logs under ~/Library/Logs. Linux starts the same worker process without
71
+ # an Aqua requirement. The launch-agent renderer and repair helpers here are
72
+ # shared by the entrypoint and remote doctor so their ownership cannot drift.
73
+ #
74
+ # The Linux start path puts the worker tree in its own process group, so
75
+ # stopping a worker signals its restart supervisor, its serving child, and any
76
+ # job descendant together instead of leaving a supervisor to restart what was
77
+ # just killed. fm_remote_job_stop_worker_tree owns that stop and refuses to
78
+ # signal a group whose leader is not itself a worker, so a worker inherited
79
+ # from an older build or from launchd's own session is still stopped safely as
80
+ # a single process. fm_remote_job_root_is_live is the shared predicate for
81
+ # whether a worker's code root still exists; bin/fm-remote-job-worker.sh uses
82
+ # it to stop itself once its root is pruned, and
83
+ # bin/fm-remote-job-reap-orphans.sh uses it to reap workers that were already
84
+ # orphaned that way.
85
+
86
+ FM_REMOTE_JOB_LABEL=dev.firstmate.remote-job
87
+ FM_REMOTE_JOB_MAX_BYTES=${FM_REMOTE_JOB_MAX_BYTES:-1048576}
88
+ FM_REMOTE_JOB_QUEUE_TIMEOUT=${FM_REMOTE_JOB_QUEUE_TIMEOUT:-360}
89
+ FM_REMOTE_JOB_TIMEOUT=${FM_REMOTE_JOB_TIMEOUT:-360}
90
+ FM_REMOTE_JOB_WAIT_GRACE=${FM_REMOTE_JOB_WAIT_GRACE:-30}
91
+ FM_REMOTE_JOB_POLL_SECONDS=${FM_REMOTE_JOB_POLL_SECONDS:-0.05}
92
+ FM_REMOTE_JOB_REAP_SECONDS=${FM_REMOTE_JOB_REAP_SECONDS:-3600}
93
+ FM_REMOTE_JOB_STAGE_REAP_SECONDS=${FM_REMOTE_JOB_STAGE_REAP_SECONDS:-600}
94
+ FM_REMOTE_JOB_SEQ_CLAIM_REAP_SECONDS=86400
95
+ FM_REMOTE_JOB_SEQ_CLAIM_REAP_INTERVAL=3600
96
+ # shellcheck disable=SC2034 # Shared protocol constant consumed by the worker and sourcing callers.
97
+ FM_REMOTE_JOB_PREEMPTED_EXIT=76
98
+ FM_REMOTE_JOB_OPERATOR_PATH=
99
+ FM_REMOTE_JOB_CHILD_PATH=
100
+ FM_REMOTE_JOB_STATE=
101
+ FM_REMOTE_JOB_JOBS=
102
+ FM_REMOTE_JOB_SEQ_CLAIMS=
103
+ FM_REMOTE_JOB_ID=
104
+ FM_REMOTE_JOB_STDOUT=
105
+ FM_REMOTE_JOB_STDERR=
106
+ FM_REMOTE_JOB_EXIT=
107
+ FM_REMOTE_JOB_ERROR=
108
+ FM_REMOTE_JOB_REPAIRED=0
109
+
110
+ fm_remote_job_die() {
111
+ printf 'error: %s\n' "$1" >&2
112
+ return 1
113
+ }
114
+
115
+ fm_remote_job_safe_id() {
116
+ case "$1" in ''|*[!A-Za-z0-9._-]*) return 1 ;; esac
117
+ }
118
+
119
+ fm_remote_job_command_preemptible() { # <staged argv command>
120
+ case "${1:-}" in fm-remote-delta-read.sh) return 0 ;; *) return 1 ;; esac
121
+ }
122
+
123
+ fm_remote_job_validate_settings() {
124
+ case "$FM_REMOTE_JOB_MAX_BYTES" in ''|*[!0-9]*|0) return 1 ;; esac
125
+ [ "$FM_REMOTE_JOB_MAX_BYTES" -le 1048576 ] || return 1
126
+ case "$FM_REMOTE_JOB_QUEUE_TIMEOUT" in ''|*[!0-9]*|0) return 1 ;; esac
127
+ [ "$FM_REMOTE_JOB_QUEUE_TIMEOUT" -le 3600 ] || return 1
128
+ case "$FM_REMOTE_JOB_TIMEOUT" in ''|*[!0-9]*|0) return 1 ;; esac
129
+ [ "$FM_REMOTE_JOB_TIMEOUT" -le 3600 ] || return 1
130
+ case "$FM_REMOTE_JOB_WAIT_GRACE" in ''|*[!0-9]*) return 1 ;; esac
131
+ [ "$FM_REMOTE_JOB_WAIT_GRACE" -le 300 ] || return 1
132
+ case "$FM_REMOTE_JOB_REAP_SECONDS" in ''|*[!0-9]*|0) return 1 ;; esac
133
+ case "$FM_REMOTE_JOB_STAGE_REAP_SECONDS" in ''|*[!0-9]*|0) return 1 ;; esac
134
+ return 0
135
+ }
136
+
137
+ fm_remote_job_platform() {
138
+ local raw=${FM_REMOTE_JOB_PLATFORM_OVERRIDE:-}
139
+ [ -n "$raw" ] || raw=$(uname -s 2>/dev/null || true)
140
+ case "$raw" in
141
+ Darwin|darwin) printf 'darwin\n' ;;
142
+ Linux|linux) printf 'linux\n' ;;
143
+ '') printf 'unknown\n' ;;
144
+ *) printf '%s\n' "$raw" ;;
145
+ esac
146
+ }
147
+
148
+ fm_remote_job_path_append() { # <directory>
149
+ case ":$FM_REMOTE_JOB_OPERATOR_PATH:" in *":$1:"*) return 0 ;; esac
150
+ FM_REMOTE_JOB_OPERATOR_PATH="${FM_REMOTE_JOB_OPERATOR_PATH:+$FM_REMOTE_JOB_OPERATOR_PATH:}$1"
151
+ }
152
+
153
+ fm_remote_job_path_append_if_dir() { # <directory>
154
+ [ -d "$1" ] && [ ! -L "$1" ] || return 0
155
+ fm_remote_job_path_append "$1"
156
+ }
157
+
158
+ fm_remote_job_path_append_resolved_dir() { # <directory>
159
+ local directory physical
160
+ directory=$1
161
+ [ -d "$directory" ] || return 0
162
+ if [ ! -L "$directory" ]; then
163
+ fm_remote_job_path_append "$directory"
164
+ return 0
165
+ fi
166
+ physical=$(CDPATH='' cd -- "$directory" 2>/dev/null && pwd -P) || return 0
167
+ [ -d "$physical" ] && [ ! -L "$physical" ] || return 0
168
+ fm_remote_job_path_append "$physical"
169
+ }
170
+
171
+ # Callers pass an already-expanded glob rather than the pattern, because only
172
+ # the shell's own pathname expansion sorts its matches: bash sorts
173
+ # glob_filename's result in pathexp.c, while `compgen -G` reaches the same
174
+ # glob_filename through pcomplete.c, which does not sort. On bash 3.2 (macOS
175
+ # /bin/bash) that handed back raw readdir order, so which install of a
176
+ # multi-version tool a remote job resolved depended on the filesystem instead
177
+ # of on this composition.
178
+ fm_remote_job_append_dirs() { # <expanded glob matches>
179
+ local directory
180
+ for directory in "$@"; do
181
+ fm_remote_job_path_append_if_dir "$directory"
182
+ done
183
+ }
184
+
185
+ fm_remote_job_nvm_default_selector() { # <account-home>
186
+ local account_home=$1 alias_root selector alias_file next depth=0 suffix
187
+ alias_root="$account_home/.nvm/alias"
188
+ selector=$(fm_remote_job_read_single_line "$alias_root/default" 256 2>/dev/null || true)
189
+ while [ -n "$selector" ] && [ "$depth" -lt 8 ]; do
190
+ case "$selector" in ''|/*|*..*|*[!A-Za-z0-9._*/-]*) return 1 ;; esac
191
+ case "$selector" in
192
+ lts/*) suffix=${selector#lts/}; case "$suffix" in ''|*/*) return 1 ;; esac ;;
193
+ */*) return 1 ;;
194
+ esac
195
+ alias_file="$alias_root/$selector"
196
+ if [ -f "$alias_file" ] && [ ! -L "$alias_file" ]; then
197
+ next=$(fm_remote_job_read_single_line "$alias_file" 256 2>/dev/null || true)
198
+ [ -n "$next" ] && [ "$next" != "$selector" ] || return 1
199
+ selector=$next
200
+ depth=$((depth + 1))
201
+ continue
202
+ fi
203
+ printf '%s\n' "$selector"
204
+ return 0
205
+ done
206
+ return 1
207
+ }
208
+
209
+ fm_remote_job_nvm_selected_bin() { # <account-home>
210
+ local account_home=$1 selector normalized directory base version major minor patch extra
211
+ local selected='' fallback='' selected_major=-1 selected_minor=-1 selected_patch=-1
212
+ local fallback_major=-1 fallback_minor=-1 fallback_patch=-1 matches
213
+ selector=$(fm_remote_job_nvm_default_selector "$account_home" 2>/dev/null || true)
214
+ [ "$selector" != system ] || return 0
215
+ case "$selector" in node|stable|unstable) normalized= ;; v*) normalized=${selector#v} ;; *) normalized=$selector ;; esac
216
+ case "$normalized" in *[!0-9.]*|.*|*.|*..*) normalized=invalid ;; esac
217
+ for directory in "$account_home"/.nvm/versions/node/*/bin; do
218
+ [ -d "$directory" ] && [ ! -L "$directory" ] || continue
219
+ base=${directory%/bin}
220
+ version=${base##*/}
221
+ version=${version#v}
222
+ IFS=. read -r major minor patch extra <<< "$version"
223
+ case "$major:$minor:$patch:$extra" in *[!0-9:]*) continue ;; esac
224
+ [ -n "$major" ] && [ -n "$minor" ] && [ -n "$patch" ] && [ -z "$extra" ] || continue
225
+ if [ "$major" -gt "$fallback_major" ] ||
226
+ { [ "$major" -eq "$fallback_major" ] && [ "$minor" -gt "$fallback_minor" ]; } ||
227
+ { [ "$major" -eq "$fallback_major" ] && [ "$minor" -eq "$fallback_minor" ] && [ "$patch" -gt "$fallback_patch" ]; }; then
228
+ fallback=$directory
229
+ fallback_major=$major
230
+ fallback_minor=$minor
231
+ fallback_patch=$patch
232
+ fi
233
+ matches=0
234
+ if [ -z "$normalized" ]; then
235
+ matches=1
236
+ elif [ "$normalized" != invalid ]; then
237
+ case ".$version." in
238
+ ."$normalized".|."$normalized".*) matches=1 ;;
239
+ esac
240
+ fi
241
+ [ "$matches" -eq 1 ] || continue
242
+ if [ "$major" -gt "$selected_major" ] ||
243
+ { [ "$major" -eq "$selected_major" ] && [ "$minor" -gt "$selected_minor" ]; } ||
244
+ { [ "$major" -eq "$selected_major" ] && [ "$minor" -eq "$selected_minor" ] && [ "$patch" -gt "$selected_patch" ]; }; then
245
+ selected=$directory
246
+ selected_major=$major
247
+ selected_minor=$minor
248
+ selected_patch=$patch
249
+ fi
250
+ done
251
+ if [ -n "$selected" ]; then
252
+ printf '%s\n' "$selected"
253
+ elif [ -n "$fallback" ]; then
254
+ printf '%s\n' "$fallback"
255
+ fi
256
+ }
257
+
258
+ fm_remote_job_compose_operator_path() { # <account-home>
259
+ local account_home=$1 account_user nvm_bin
260
+ FM_REMOTE_JOB_OPERATOR_PATH=
261
+ fm_remote_job_path_append_if_dir "$account_home/.local/bin"
262
+ nvm_bin=$(fm_remote_job_nvm_selected_bin "$account_home" 2>/dev/null || true)
263
+ [ -z "$nvm_bin" ] || fm_remote_job_path_append "$nvm_bin"
264
+ fm_remote_job_path_append_if_dir "$account_home/.asdf/shims"
265
+ fm_remote_job_append_dirs "$account_home"/.asdf/installs/*/*/bin
266
+ fm_remote_job_path_append_if_dir "$account_home/.local/share/mise/shims"
267
+ fm_remote_job_path_append_if_dir "$account_home/.mise/shims"
268
+ fm_remote_job_append_dirs "$account_home"/.local/share/mise/installs/*/*/bin
269
+ fm_remote_job_append_dirs "$account_home"/.mise/installs/*/*/bin
270
+ fm_remote_job_path_append_resolved_dir "$account_home/.nix-profile/bin"
271
+ account_user=$(id -un 2>/dev/null || true)
272
+ if [ -n "$account_user" ]; then
273
+ fm_remote_job_path_append_resolved_dir "/etc/profiles/per-user/$account_user/bin"
274
+ fi
275
+ fm_remote_job_path_append_resolved_dir /run/current-system/sw/bin
276
+ fm_remote_job_path_append_if_dir /opt/homebrew/bin
277
+ fm_remote_job_path_append_if_dir /usr/local/bin
278
+ fm_remote_job_path_append /usr/bin
279
+ fm_remote_job_path_append /bin
280
+ fm_remote_job_path_append /usr/sbin
281
+ fm_remote_job_path_append /sbin
282
+ printf '%s\n' "$FM_REMOTE_JOB_OPERATOR_PATH"
283
+ }
284
+
285
+ fm_remote_job_build_child_path() { # <remote-root>
286
+ local root=$1 directory old_ifs
287
+ FM_REMOTE_JOB_CHILD_PATH="$root/bin"
288
+ old_ifs=$IFS
289
+ IFS=:
290
+ for directory in $FM_REMOTE_JOB_OPERATOR_PATH; do
291
+ case ":$FM_REMOTE_JOB_CHILD_PATH:" in *":$directory:"*) continue ;; esac
292
+ FM_REMOTE_JOB_CHILD_PATH="$FM_REMOTE_JOB_CHILD_PATH:$directory"
293
+ done
294
+ IFS=$old_ifs
295
+ printf '%s\n' "$FM_REMOTE_JOB_CHILD_PATH"
296
+ }
297
+
298
+ fm_remote_job_operator_tool() { # <tool>; resolves only outside the checkout bin
299
+ local tool=$1 resolved
300
+ resolved=$(PATH="$FM_REMOTE_JOB_OPERATOR_PATH" command -v "$tool" 2>/dev/null || true)
301
+ case "$resolved" in
302
+ /*)
303
+ [ -x "$resolved" ] || return 1
304
+ case ":$FM_REMOTE_JOB_OPERATOR_PATH:" in *":${resolved%/*}:"*) printf '%s\n' "$resolved" ;; *) return 1 ;; esac
305
+ ;;
306
+ *) return 1 ;;
307
+ esac
308
+ }
309
+
310
+ fm_remote_job_manager_tool() { # <account-home> <tool>
311
+ local account_home=$1 tool=$2 directory candidate
312
+ for directory in \
313
+ "$account_home"/.nvm/versions/node/*/bin \
314
+ "$account_home"/.asdf/shims \
315
+ "$account_home"/.asdf/installs/*/*/bin \
316
+ "$account_home"/.local/share/mise/shims \
317
+ "$account_home"/.mise/shims \
318
+ "$account_home"/.local/share/mise/installs/*/*/bin \
319
+ "$account_home"/.mise/installs/*/*/bin; do
320
+ [ -d "$directory" ] && [ ! -L "$directory" ] || continue
321
+ candidate="$directory/$tool"
322
+ [ -f "$candidate" ] && [ ! -L "$candidate" ] && [ -x "$candidate" ] || continue
323
+ printf '%s\n' "$candidate"
324
+ return 0
325
+ done
326
+ return 1
327
+ }
328
+
329
+ fm_remote_job_has_forbidden_text_bytes() { # <file>
330
+ LC_ALL=C tr -cd '\000\012\015' < "$1" | LC_ALL=C wc -c | tr -d ' '
331
+ }
332
+
333
+ fm_remote_job_normalize_absolute_path() { # <absolute-path>
334
+ local path=$1 part old_ifs out=/
335
+ case "$path" in /*) ;; *) return 1 ;; esac
336
+ case "$path" in *'//'*) return 1 ;; esac
337
+ old_ifs=$IFS
338
+ IFS=/
339
+ for part in $path; do
340
+ case "$part" in
341
+ '') ;;
342
+ .|..) IFS=$old_ifs; return 1 ;;
343
+ *)
344
+ case "$part" in *$'\n'*|*$'\r'*|*$'\t'*) IFS=$old_ifs; return 1 ;; esac
345
+ if [ "$out" = / ]; then out="/$part"; else out="$out/$part"; fi
346
+ ;;
347
+ esac
348
+ done
349
+ IFS=$old_ifs
350
+ printf '%s\n' "$out"
351
+ }
352
+
353
+ fm_remote_job_canonical_existing_dir() { # <path>
354
+ local path=$1 normalized physical
355
+ normalized=$(fm_remote_job_normalize_absolute_path "$path") || return 1
356
+ [ "$normalized" != / ] || return 1
357
+ [ -d "$normalized" ] && [ ! -L "$normalized" ] || return 1
358
+ physical=$(CDPATH='' cd -- "$normalized" 2>/dev/null && pwd -P) || return 1
359
+ [ "$physical" = "$normalized" ] || return 1
360
+ printf '%s\n' "$physical"
361
+ }
362
+
363
+ fm_remote_job_canonical_home() { # <path>; one absent leaf is allowed
364
+ local path=$1 normalized parent base parent_real
365
+ normalized=$(fm_remote_job_normalize_absolute_path "$path") || return 1
366
+ [ "$normalized" != / ] || return 1
367
+ if [ -e "$normalized" ] || [ -L "$normalized" ]; then
368
+ fm_remote_job_canonical_existing_dir "$normalized"
369
+ return
370
+ fi
371
+ parent=$(dirname "$normalized")
372
+ base=$(basename "$normalized")
373
+ case "$base" in ''|.|..) return 1 ;; esac
374
+ parent_real=$(fm_remote_job_canonical_existing_dir "$parent") || return 1
375
+ [ "$parent_real/$base" = "$normalized" ] || return 1
376
+ printf '%s\n' "$normalized"
377
+ }
378
+
379
+ fm_remote_job_safe_child_dir() { # <canonical-parent> <single child basename>
380
+ local parent=$1 base=$2 candidate physical
381
+ case "$base" in ''|*/*|.|..) return 1 ;; esac
382
+ [ -d "$parent" ] && [ ! -L "$parent" ] || return 1
383
+ candidate="$parent/$base"
384
+ if [ -e "$candidate" ] || [ -L "$candidate" ]; then
385
+ [ -d "$candidate" ] && [ ! -L "$candidate" ] || return 1
386
+ else
387
+ (umask 077; mkdir "$candidate") || return 1
388
+ fi
389
+ chmod 700 "$candidate" 2>/dev/null || return 1
390
+ physical=$(CDPATH='' cd -- "$candidate" 2>/dev/null && pwd -P) || return 1
391
+ [ "$physical" = "$candidate" ] || return 1
392
+ printf '%s\n' "$physical"
393
+ }
394
+
395
+ fm_remote_job_prepare_state() { # <account-home>
396
+ local account_home=$1 root parent base firstmate
397
+ fm_remote_job_validate_settings || {
398
+ FM_REMOTE_JOB_ERROR="remote job bounds or timeout are invalid"
399
+ return 1
400
+ }
401
+ account_home=$(fm_remote_job_canonical_existing_dir "$account_home") || {
402
+ FM_REMOTE_JOB_ERROR="remote account home is unavailable or unsafe"
403
+ return 1
404
+ }
405
+ if [ -n "${FM_REMOTE_JOB_STATE_ROOT:-}" ]; then
406
+ root=$(fm_remote_job_normalize_absolute_path "$FM_REMOTE_JOB_STATE_ROOT") || {
407
+ FM_REMOTE_JOB_ERROR="remote job state root is not a safe absolute path"
408
+ return 1
409
+ }
410
+ parent=$(dirname "$root")
411
+ base=$(basename "$root")
412
+ parent=$(fm_remote_job_canonical_existing_dir "$parent") || {
413
+ FM_REMOTE_JOB_ERROR="remote job state parent is unavailable or unsafe"
414
+ return 1
415
+ }
416
+ [ "$parent/$base" = "$root" ] || return 1
417
+ root=$(fm_remote_job_safe_child_dir "$parent" "$base") || {
418
+ FM_REMOTE_JOB_ERROR="remote job state root is unsafe"
419
+ return 1
420
+ }
421
+ else
422
+ firstmate=$(fm_remote_job_safe_child_dir "$account_home" .firstmate) || {
423
+ FM_REMOTE_JOB_ERROR="cannot prepare $account_home/.firstmate for remote jobs"
424
+ return 1
425
+ }
426
+ root=$(fm_remote_job_safe_child_dir "$firstmate" remote-job) || {
427
+ FM_REMOTE_JOB_ERROR="cannot prepare remote job state"
428
+ return 1
429
+ }
430
+ fi
431
+ FM_REMOTE_JOB_STATE=$root
432
+ FM_REMOTE_JOB_JOBS=$(fm_remote_job_safe_child_dir "$FM_REMOTE_JOB_STATE" jobs) || {
433
+ FM_REMOTE_JOB_ERROR="remote job queue is unsafe"
434
+ return 1
435
+ }
436
+ FM_REMOTE_JOB_SEQ_CLAIMS=$(fm_remote_job_safe_child_dir "$FM_REMOTE_JOB_STATE" .seq-claims) || {
437
+ FM_REMOTE_JOB_ERROR="remote job sequence claims are unsafe"
438
+ return 1
439
+ }
440
+ fm_remote_job_safe_child_dir "$FM_REMOTE_JOB_STATE" logs >/dev/null || {
441
+ FM_REMOTE_JOB_ERROR="remote job log directory is unsafe"
442
+ return 1
443
+ }
444
+ }
445
+
446
+ fm_remote_job_job_dir() { # <id>
447
+ local id=$1 dir physical
448
+ fm_remote_job_safe_id "$id" || return 1
449
+ [ -n "$FM_REMOTE_JOB_JOBS" ] || return 1
450
+ dir="$FM_REMOTE_JOB_JOBS/$id"
451
+ [ -d "$dir" ] && [ ! -L "$dir" ] || return 1
452
+ physical=$(CDPATH='' cd -- "$dir" 2>/dev/null && pwd -P) || return 1
453
+ [ "$physical" = "$dir" ] || return 1
454
+ printf '%s\n' "$physical"
455
+ }
456
+
457
+ fm_remote_job_regular_bounded() { # <file> <max-bytes>
458
+ local file=$1 max=$2 bytes
459
+ [ -f "$file" ] && [ ! -L "$file" ] || return 1
460
+ bytes=$(LC_ALL=C wc -c < "$file" | tr -d ' ') || return 1
461
+ case "$bytes" in ''|*[!0-9]*) return 1 ;; esac
462
+ [ "$bytes" -le "$max" ]
463
+ }
464
+
465
+ fm_remote_job_remove_claim_records() { # <claim-dir>
466
+ local claim=$1 file
467
+ [ -d "$claim" ] && [ ! -L "$claim" ] || return 1
468
+ for file in "$claim"/owner "$claim"/owner_start "$claim"/supervisor \
469
+ "$claim"/supervisor_start "$claim"/group "$claim"/group_start "$claim"/armed \
470
+ "$claim"/.owner.* "$claim"/.owner_start.* "$claim"/.supervisor.* \
471
+ "$claim"/.supervisor_start.* "$claim"/.group.* "$claim"/.group_start.* \
472
+ "$claim"/.armed.*; do
473
+ [ -e "$file" ] || [ -L "$file" ] || continue
474
+ fm_remote_job_regular_bounded "$file" 256 || return 1
475
+ rm -f -- "$file" || return 1
476
+ done
477
+ }
478
+
479
+ fm_remote_job_write_state() { # <job-dir> queued|running|done
480
+ local job=$1 value=$2 tmp
481
+ case "$value" in queued|running|done) ;; *) return 1 ;; esac
482
+ [ -d "$job" ] && [ ! -L "$job" ] || return 1
483
+ tmp=$(umask 077; mktemp "$job/.state.XXXXXX") || return 1
484
+ printf '%s\n' "$value" > "$tmp" || { rm -f -- "$tmp"; return 1; }
485
+ chmod 600 "$tmp" || { rm -f -- "$tmp"; return 1; }
486
+ mv -f -- "$tmp" "$job/state"
487
+ }
488
+
489
+ fm_remote_job_read_state() { # <job-dir>
490
+ local job=$1 value extra
491
+ fm_remote_job_regular_bounded "$job/state" 64 || return 1
492
+ IFS= read -r value < "$job/state" || return 1
493
+ if IFS= read -r extra < <(tail -n +2 "$job/state"); then
494
+ : "$extra"
495
+ return 1
496
+ fi
497
+ case "$value" in queued|running|'done') printf '%s\n' "$value" ;; *) return 1 ;; esac
498
+ }
499
+
500
+ fm_remote_job_read_number() { # <job-dir> queue_deadline|timeout|deadline|seq
501
+ local job=$1 field=$2 value
502
+ case "$field" in queue_deadline|timeout|deadline|seq) ;; *) return 1 ;; esac
503
+ fm_remote_job_regular_bounded "$job/$field" 32 || return 1
504
+ value=$(tr -d '\n' < "$job/$field")
505
+ case "$value" in ''|*[!0-9]*) return 1 ;; esac
506
+ [ "$value" -gt 0 ] || return 1
507
+ printf '%s\n' "$value"
508
+ }
509
+
510
+ fm_remote_job_write_number() { # <job-dir> queue_deadline|timeout|deadline|seq <value>
511
+ local job=$1 field=$2 value=$3 tmp
512
+ case "$field" in queue_deadline|timeout|deadline|seq) ;; *) return 1 ;; esac
513
+ case "$value" in ''|*[!0-9]*|0) return 1 ;; esac
514
+ [ -d "$job" ] && [ ! -L "$job" ] || return 1
515
+ tmp=$(umask 077; mktemp "$job/.$field.XXXXXX") || return 1
516
+ printf '%s\n' "$value" > "$tmp" || { rm -f -- "$tmp"; return 1; }
517
+ chmod 600 "$tmp" || { rm -f -- "$tmp"; return 1; }
518
+ mv -f -- "$tmp" "$job/$field"
519
+ }
520
+
521
+ fm_remote_job_read_deadline() { # <job-dir>
522
+ fm_remote_job_read_number "$1" deadline
523
+ }
524
+
525
+ fm_remote_job_advance_seq_hint() { # <value>
526
+ local value=$1 counter current tmp
527
+ counter="$FM_REMOTE_JOB_STATE/seq"
528
+ current=$(cat "$counter" 2>/dev/null || true)
529
+ case "$current" in ''|*[!0-9]*) current=0 ;; esac
530
+ [ "$value" -gt "$current" ] || return 0
531
+ tmp=$(umask 077; mktemp "$FM_REMOTE_JOB_STATE/.seqhint.XXXXXX") || return 1
532
+ printf '%s\n' "$value" > "$tmp" || { rm -f -- "$tmp"; return 1; }
533
+ chmod 600 "$tmp" || { rm -f -- "$tmp"; return 1; }
534
+ current=$(cat "$counter" 2>/dev/null || true)
535
+ case "$current" in ''|*[!0-9]*) current=0 ;; esac
536
+ if [ "$value" -gt "$current" ]; then
537
+ mv -f -- "$tmp" "$counter" || { rm -f -- "$tmp"; return 1; }
538
+ else
539
+ rm -f -- "$tmp"
540
+ fi
541
+ }
542
+
543
+ fm_remote_job_next_seq() { # [stage-dir destination]
544
+ local stage=${1:-} destination=${2:-} counter value claim attempt=0 recovered=0 maximum entry
545
+ [ -n "$FM_REMOTE_JOB_STATE" ] && [ -n "$FM_REMOTE_JOB_SEQ_CLAIMS" ] || return 1
546
+ counter="$FM_REMOTE_JOB_STATE/seq"
547
+ value=$(cat "$counter" 2>/dev/null || true)
548
+ case "$value" in ''|*[!0-9]*) value=0 ;; esac
549
+ while :; do
550
+ if [ "$attempt" -ge 100000 ]; then
551
+ [ "$recovered" -eq 0 ] || return 1
552
+ maximum=0
553
+ for entry in "$FM_REMOTE_JOB_SEQ_CLAIMS"/*; do
554
+ [ -d "$entry" ] && [ ! -L "$entry" ] || continue
555
+ entry=${entry##*/}
556
+ case "$entry" in ''|*[!0-9]*|0) continue ;; esac
557
+ [ "$entry" -le "$maximum" ] || maximum=$entry
558
+ done
559
+ value=$maximum
560
+ attempt=0
561
+ recovered=1
562
+ fi
563
+ attempt=$((attempt + 1))
564
+ value=$((value + 1))
565
+ claim="$FM_REMOTE_JOB_SEQ_CLAIMS/$value"
566
+ if (umask 077; mkdir "$claim") 2>/dev/null; then
567
+ chmod 700 "$claim" || return 1
568
+ fm_remote_job_advance_seq_hint "$value" || true
569
+ if [ -n "$stage" ]; then
570
+ if ! fm_remote_job_write_number "$stage" seq "$value" \
571
+ || ! fm_remote_job_write_state "$stage" queued \
572
+ || ! mv -- "$stage" "$destination"; then
573
+ rm -f -- "$stage/state" "$stage/seq"
574
+ return 1
575
+ fi
576
+ rm -f -- "$destination/.owner-pid" "$destination/.owner-start" || true
577
+ fi
578
+ printf '%s\n' "$value"
579
+ return 0
580
+ fi
581
+ [ -d "$claim" ] && [ ! -L "$claim" ] || return 1
582
+ done
583
+ }
584
+
585
+ fm_remote_job_cancelled() { # <job-dir>
586
+ [ -f "$1/cancel" ] && [ ! -L "$1/cancel" ]
587
+ }
588
+
589
+ # Mark a job cancelled on behalf of a disconnected or abandoning caller. The
590
+ # marker never rewrites state: the worker observes it, skips a cancelled queued
591
+ # job, and stops a running cancelled job's process group. The worker reaps after
592
+ # terminal publication; if publication already won the race, this function
593
+ # reaps instead. Cancelling a job that disappeared is a harmless no-op.
594
+ fm_remote_job_cancel() { # <account-home> <id>
595
+ local account_home=$1 id=$2 job state tmp
596
+ fm_remote_job_prepare_state "$account_home" || return 1
597
+ job=$(fm_remote_job_job_dir "$id" 2>/dev/null) || return 0
598
+ state=$(fm_remote_job_read_state "$job" 2>/dev/null || true)
599
+ if [ "$state" = 'done' ]; then
600
+ fm_remote_job_reap "$account_home" "$id" 2>/dev/null || true
601
+ return 0
602
+ fi
603
+ tmp=$(umask 077; mktemp "$job/.cancel.XXXXXX") || return 1
604
+ printf 'cancelled: caller disconnected or abandoned the job\n' > "$tmp" || { rm -f -- "$tmp"; return 1; }
605
+ chmod 600 "$tmp" || { rm -f -- "$tmp"; return 1; }
606
+ mv -f -- "$tmp" "$job/cancel" || return 1
607
+ state=$(fm_remote_job_read_state "$job" 2>/dev/null || true)
608
+ if [ "$state" = 'done' ]; then
609
+ fm_remote_job_reap "$account_home" "$id" 2>/dev/null || true
610
+ fi
611
+ }
612
+
613
+ fm_remote_job_stage() { # <account-home> <root> <home> <command> [args...]; stdin is captured
614
+ local account_home=$1 root=$2 home=$3 command=$4 stage id destination bytes queue_deadline owner_start
615
+ shift 4
616
+ fm_remote_job_prepare_state "$account_home" || return 1
617
+ root=$(fm_remote_job_canonical_existing_dir "$root") || {
618
+ FM_REMOTE_JOB_ERROR="remote job root is unavailable or unsafe"
619
+ return 1
620
+ }
621
+ home=$(fm_remote_job_canonical_home "$home") || {
622
+ FM_REMOTE_JOB_ERROR="remote job home is unavailable or unsafe"
623
+ return 1
624
+ }
625
+ case "$command" in fm-*.sh) ;; *) FM_REMOTE_JOB_ERROR="remote job command is outside the fm-*.sh namespace"; return 1 ;; esac
626
+ case "$command" in */*|*..*) FM_REMOTE_JOB_ERROR="remote job command contains a path or traversal"; return 1 ;; esac
627
+ owner_start=$(fm_remote_job_process_start "$$") || {
628
+ FM_REMOTE_JOB_ERROR="cannot establish remote job staging ownership"
629
+ return 1
630
+ }
631
+ stage=$(umask 077; mktemp -d "$FM_REMOTE_JOB_JOBS/.stage.XXXXXX") || {
632
+ FM_REMOTE_JOB_ERROR="cannot stage remote job"
633
+ return 1
634
+ }
635
+ chmod 700 "$stage" || { rm -rf -- "$stage"; return 1; }
636
+ queue_deadline=$(( $(date +%s) + FM_REMOTE_JOB_QUEUE_TIMEOUT ))
637
+ if ! printf '%s\n' "$$" > "$stage/.owner-pid" ||
638
+ ! printf '%s\n' "$owner_start" > "$stage/.owner-start" ||
639
+ ! chmod 600 "$stage/.owner-pid" "$stage/.owner-start" ||
640
+ ! printf '%s\n' "$root" > "$stage/root" ||
641
+ ! printf '%s\n' "$home" > "$stage/home" ||
642
+ ! printf '%s\n' "$queue_deadline" > "$stage/queue_deadline" ||
643
+ ! printf '%s\n' "$FM_REMOTE_JOB_TIMEOUT" > "$stage/timeout" ||
644
+ ! printf '%s\0' "$command" "$@" > "$stage/argv" ||
645
+ ! head -c "$((FM_REMOTE_JOB_MAX_BYTES + 1))" > "$stage/stdin"; then
646
+ rm -rf -- "$stage"
647
+ FM_REMOTE_JOB_ERROR="cannot capture remote job input"
648
+ return 1
649
+ fi
650
+ for bytes in root home queue_deadline timeout argv stdin; do chmod 600 "$stage/$bytes" || { rm -rf -- "$stage"; return 1; }; done
651
+ fm_remote_job_regular_bounded "$stage/argv" "$FM_REMOTE_JOB_MAX_BYTES" || {
652
+ rm -rf -- "$stage"
653
+ FM_REMOTE_JOB_ERROR="remote job argv exceeds the ${FM_REMOTE_JOB_MAX_BYTES}-byte bound"
654
+ return 1
655
+ }
656
+ fm_remote_job_regular_bounded "$stage/stdin" "$FM_REMOTE_JOB_MAX_BYTES" || {
657
+ rm -rf -- "$stage"
658
+ FM_REMOTE_JOB_ERROR="remote job stdin exceeds the ${FM_REMOTE_JOB_MAX_BYTES}-byte bound"
659
+ return 1
660
+ }
661
+ : > "$stage/stdout"
662
+ : > "$stage/stderr"
663
+ chmod 600 "$stage/stdout" "$stage/stderr" || { rm -rf -- "$stage"; return 1; }
664
+ id="job-${stage##*/.stage.}"
665
+ fm_remote_job_safe_id "$id" || { rm -rf -- "$stage"; return 1; }
666
+ destination="$FM_REMOTE_JOB_JOBS/$id"
667
+ [ ! -e "$destination" ] && [ ! -L "$destination" ] || { rm -rf -- "$stage"; return 1; }
668
+ if ! fm_remote_job_next_seq "$stage" "$destination" >/dev/null; then
669
+ rm -rf -- "$stage"
670
+ FM_REMOTE_JOB_ERROR="cannot allocate and publish a remote job staging sequence"
671
+ return 1
672
+ fi
673
+ # shellcheck disable=SC2034 # Sourceable API consumed by callers that do not use command substitution.
674
+ FM_REMOTE_JOB_ID=$id
675
+ printf '%s\n' "$id"
676
+ }
677
+
678
+ fm_remote_job_wait() { # <account-home> <id>; honors FM_REMOTE_JOB_DISCONNECT_PROBE
679
+ local account_home=$1 id=$2 job state queue_deadline execution_timeout wait_deadline exit_value
680
+ local now next_probe=0
681
+ fm_remote_job_prepare_state "$account_home" || return 1
682
+ job=$(fm_remote_job_job_dir "$id") || {
683
+ FM_REMOTE_JOB_ERROR="remote job record disappeared or became unsafe"
684
+ return 1
685
+ }
686
+ queue_deadline=$(fm_remote_job_read_number "$job" queue_deadline) || {
687
+ FM_REMOTE_JOB_ERROR="remote job queue deadline is invalid"
688
+ return 1
689
+ }
690
+ execution_timeout=$(fm_remote_job_read_number "$job" timeout) || {
691
+ FM_REMOTE_JOB_ERROR="remote job execution timeout is invalid"
692
+ return 1
693
+ }
694
+ [ "$execution_timeout" -le 3600 ] || {
695
+ FM_REMOTE_JOB_ERROR="remote job execution timeout is invalid"
696
+ return 1
697
+ }
698
+ wait_deadline=$((queue_deadline + execution_timeout + FM_REMOTE_JOB_WAIT_GRACE))
699
+ while :; do
700
+ state=$(fm_remote_job_read_state "$job" 2>/dev/null || true)
701
+ case "$state" in
702
+ 'done')
703
+ if ! fm_remote_job_regular_bounded "$job/stdout" "$FM_REMOTE_JOB_MAX_BYTES" ||
704
+ ! fm_remote_job_regular_bounded "$job/stderr" "$FM_REMOTE_JOB_MAX_BYTES" ||
705
+ ! fm_remote_job_regular_bounded "$job/exit" 32; then
706
+ FM_REMOTE_JOB_ERROR="remote job result is unsafe or exceeds its byte bound"
707
+ return 1
708
+ fi
709
+ exit_value=$(tr -d '\n' < "$job/exit")
710
+ case "$exit_value" in ''|*[!0-9]*) FM_REMOTE_JOB_ERROR="remote job exit status is invalid"; return 1 ;; esac
711
+ [ "$exit_value" -le 255 ] || { FM_REMOTE_JOB_ERROR="remote job exit status is invalid"; return 1; }
712
+ # shellcheck disable=SC2034 # Sourceable API consumed by the entrypoint after this function returns.
713
+ FM_REMOTE_JOB_STDOUT="$job/stdout"
714
+ # shellcheck disable=SC2034 # Sourceable API consumed by the entrypoint after this function returns.
715
+ FM_REMOTE_JOB_STDERR="$job/stderr"
716
+ # shellcheck disable=SC2034 # Sourceable API consumed by the entrypoint after this function returns.
717
+ FM_REMOTE_JOB_EXIT=$exit_value
718
+ return 0
719
+ ;;
720
+ queued|running) ;;
721
+ *) FM_REMOTE_JOB_ERROR="remote job state is invalid"; return 1 ;;
722
+ esac
723
+ now=$(date +%s)
724
+ if [ "$now" -ge "$wait_deadline" ]; then
725
+ FM_REMOTE_JOB_ERROR="remote job did not complete within its bounded wait"
726
+ return 1
727
+ fi
728
+ if [ -n "${FM_REMOTE_JOB_DISCONNECT_PROBE:-}" ] && [ "$now" -ge "$next_probe" ]; then
729
+ next_probe=$((now + 1))
730
+ if ! "$FM_REMOTE_JOB_DISCONNECT_PROBE"; then
731
+ fm_remote_job_cancel "$account_home" "$id" 2>/dev/null || true
732
+ FM_REMOTE_JOB_ERROR="remote job caller disconnected; the job was cancelled"
733
+ return 1
734
+ fi
735
+ fi
736
+ sleep "$FM_REMOTE_JOB_POLL_SECONDS"
737
+ done
738
+ }
739
+
740
+ fm_remote_job_reap() { # <account-home> <id>; only removes an exact completed record
741
+ local account_home=$1 id=$2 job file
742
+ fm_remote_job_prepare_state "$account_home" || return 1
743
+ job=$(fm_remote_job_job_dir "$id") || return 1
744
+ [ "$(fm_remote_job_read_state "$job")" = 'done' ] || return 1
745
+ for file in root home queue_deadline timeout deadline seq cancel argv stdin stdout stderr exit state .owner-pid .owner-start; do
746
+ [ -e "$job/$file" ] || continue
747
+ [ ! -L "$job/$file" ] || return 1
748
+ rm -f -- "$job/$file" || return 1
749
+ done
750
+ if [ -e "$job/.claim" ] || [ -L "$job/.claim" ]; then
751
+ [ -d "$job/.claim" ] && [ ! -L "$job/.claim" ] || return 1
752
+ fm_remote_job_remove_claim_records "$job/.claim" || return 1
753
+ rmdir "$job/.claim" || return 1
754
+ fi
755
+ rmdir "$job"
756
+ }
757
+
758
+ fm_remote_job_path_mtime() { # <path>
759
+ # The platform override controls worker shape in isolated tests, not the host
760
+ # kernel's stat syntax.
761
+ if [ "$(uname -s 2>/dev/null || true)" = Darwin ]; then stat -f %m "$1" 2>/dev/null; else stat -c %Y "$1" 2>/dev/null; fi
762
+ }
763
+
764
+ fm_remote_job_stage_owner_alive() { # <stage-dir>
765
+ local stage=$1 pid recorded_start actual_start
766
+ pid=$(fm_remote_job_read_single_line "$stage/.owner-pid" 64 2>/dev/null) || return 1
767
+ case "$pid" in ''|*[!0-9]*) return 1 ;; esac
768
+ [ "$pid" -gt 1 ] || return 1
769
+ recorded_start=$(fm_remote_job_read_single_line "$stage/.owner-start" 256 2>/dev/null) || return 1
770
+ actual_start=$(fm_remote_job_process_start "$pid" 2>/dev/null) || return 1
771
+ [ "$recorded_start" = "$actual_start" ]
772
+ }
773
+
774
+ fm_remote_job_reap_stale() { # <account-home>
775
+ local account_home=$1 job id state mtime now stage claim value marker tmp reap_claims=0
776
+ fm_remote_job_prepare_state "$account_home" || return 1
777
+ now=$(date +%s)
778
+ for job in "$FM_REMOTE_JOB_JOBS"/job-*; do
779
+ [ -d "$job" ] && [ ! -L "$job" ] || continue
780
+ id=${job##*/}
781
+ fm_remote_job_safe_id "$id" || continue
782
+ state=$(fm_remote_job_read_state "$job" 2>/dev/null || true)
783
+ [ "$state" = 'done' ] || continue
784
+ mtime=$(fm_remote_job_path_mtime "$job" 2>/dev/null || true)
785
+ case "$mtime" in ''|*[!0-9]*) continue ;; esac
786
+ [ $((now - mtime)) -ge "$FM_REMOTE_JOB_REAP_SECONDS" ] || continue
787
+ fm_remote_job_reap "$account_home" "$id" || true
788
+ done
789
+ marker="$FM_REMOTE_JOB_STATE/.seq-claims-reaped"
790
+ mtime=$(fm_remote_job_path_mtime "$marker" 2>/dev/null || true)
791
+ case "$mtime" in
792
+ ''|*[!0-9]*) reap_claims=1 ;;
793
+ *) [ $((now - mtime)) -lt "$FM_REMOTE_JOB_SEQ_CLAIM_REAP_INTERVAL" ] || reap_claims=1 ;;
794
+ esac
795
+ if [ "$reap_claims" -eq 1 ]; then
796
+ tmp=$(umask 077; mktemp "$FM_REMOTE_JOB_STATE/.seqreap.XXXXXX") || tmp=
797
+ if [ -n "$tmp" ] && printf '%s\n' "$now" > "$tmp" && chmod 600 "$tmp" \
798
+ && mv -f -- "$tmp" "$marker"; then
799
+ for claim in "$FM_REMOTE_JOB_SEQ_CLAIMS"/*; do
800
+ [ -d "$claim" ] && [ ! -L "$claim" ] || continue
801
+ value=${claim##*/}
802
+ case "$value" in ''|*[!0-9]*|0) continue ;; esac
803
+ mtime=$(fm_remote_job_path_mtime "$claim" 2>/dev/null || true)
804
+ case "$mtime" in ''|*[!0-9]*) continue ;; esac
805
+ [ $((now - mtime)) -ge "$FM_REMOTE_JOB_SEQ_CLAIM_REAP_SECONDS" ] || continue
806
+ rmdir "$claim" 2>/dev/null || true
807
+ done
808
+ else
809
+ [ -z "$tmp" ] || rm -f -- "$tmp"
810
+ fi
811
+ fi
812
+ # Staging litter a killed caller left behind is reaped after its owner is no
813
+ # longer the process that created it and the stage has exceeded the age bound.
814
+ for stage in "$FM_REMOTE_JOB_JOBS"/.stage.*; do
815
+ [ -d "$stage" ] && [ ! -L "$stage" ] || continue
816
+ fm_remote_job_stage_owner_alive "$stage" && continue
817
+ mtime=$(fm_remote_job_path_mtime "$stage" 2>/dev/null || true)
818
+ case "$mtime" in ''|*[!0-9]*) continue ;; esac
819
+ [ $((now - mtime)) -ge "$FM_REMOTE_JOB_STAGE_REAP_SECONDS" ] || continue
820
+ rm -rf -- "$stage"
821
+ done
822
+ }
823
+
824
+ fm_remote_job_launchagent_paths() { # <account-home>
825
+ local account_home=$1
826
+ FM_REMOTE_JOB_LAUNCH_AGENT_DIR="$account_home/Library/LaunchAgents"
827
+ FM_REMOTE_JOB_LAUNCH_AGENT_PLIST="$FM_REMOTE_JOB_LAUNCH_AGENT_DIR/$FM_REMOTE_JOB_LABEL.plist"
828
+ FM_REMOTE_JOB_LAUNCH_AGENT_LOG_DIR="$account_home/Library/Logs"
829
+ FM_REMOTE_JOB_LAUNCH_AGENT_LOG="$FM_REMOTE_JOB_LAUNCH_AGENT_LOG_DIR/$FM_REMOTE_JOB_LABEL.log"
830
+ }
831
+
832
+ fm_remote_job_plist_safe_path() {
833
+ case "$1" in *'&'*|*'<'*|*'>'*|*'"'*|*"'"*) return 1 ;; esac
834
+ }
835
+
836
+ fm_remote_job_render_launchagent() { # <remote-root> <account-home>
837
+ local root=$1 account_home=$2 worker
838
+ worker="$root/bin/fm-remote-job-worker.sh"
839
+ fm_remote_job_launchagent_paths "$account_home"
840
+ fm_remote_job_plist_safe_path "$worker" && fm_remote_job_plist_safe_path "$account_home" &&
841
+ fm_remote_job_plist_safe_path "$FM_REMOTE_JOB_LAUNCH_AGENT_LOG" || return 1
842
+ cat <<XML
843
+ <?xml version="1.0" encoding="UTF-8"?>
844
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
845
+ <plist version="1.0">
846
+ <dict>
847
+ <key>Label</key>
848
+ <string>$FM_REMOTE_JOB_LABEL</string>
849
+ <key>ProgramArguments</key>
850
+ <array>
851
+ <string>$worker</string>
852
+ </array>
853
+ <key>EnvironmentVariables</key>
854
+ <dict>
855
+ <key>HOME</key>
856
+ <string>$account_home</string>
857
+ <key>FM_ROOT_OVERRIDE</key>
858
+ <string>$root</string>
859
+ </dict>
860
+ <key>LimitLoadToSessionType</key>
861
+ <string>Aqua</string>
862
+ <key>RunAtLoad</key>
863
+ <true/>
864
+ <key>KeepAlive</key>
865
+ <true/>
866
+ <key>StandardOutPath</key>
867
+ <string>$FM_REMOTE_JOB_LAUNCH_AGENT_LOG</string>
868
+ <key>StandardErrorPath</key>
869
+ <string>$FM_REMOTE_JOB_LAUNCH_AGENT_LOG</string>
870
+ </dict>
871
+ </plist>
872
+ XML
873
+ }
874
+
875
+ fm_remote_job_launchagent_contract_matches() { # <remote-root> <account-home>
876
+ local root=$1 account_home=$2 actual expected
877
+ fm_remote_job_launchagent_paths "$account_home"
878
+ [ -f "$FM_REMOTE_JOB_LAUNCH_AGENT_PLIST" ] && [ ! -L "$FM_REMOTE_JOB_LAUNCH_AGENT_PLIST" ] || return 1
879
+ actual=$(tr -d ' \t\r\n' < "$FM_REMOTE_JOB_LAUNCH_AGENT_PLIST" 2>/dev/null) || return 1
880
+ expected=$(fm_remote_job_render_launchagent "$root" "$account_home" | tr -d ' \t\r\n') || return 1
881
+ [ "$actual" = "$expected" ]
882
+ }
883
+
884
+ fm_remote_job_gui_available() { # <uid>
885
+ local uid=$1
886
+ command -v launchctl >/dev/null 2>&1 && launchctl print "gui/$uid" >/dev/null 2>&1
887
+ }
888
+
889
+ fm_remote_job_launchagent_loaded() { # <remote-root> <account-home> <uid>
890
+ local root=$1 account_home=$2 uid=$3 worker loaded compact
891
+ fm_remote_job_launchagent_paths "$account_home"
892
+ worker="$root/bin/fm-remote-job-worker.sh"
893
+ loaded=$(launchctl print "gui/$uid/$FM_REMOTE_JOB_LABEL" 2>/dev/null) || return 1
894
+ compact=$(printf '%s' "$loaded" | tr -d ' \t\r\n') || return 1
895
+ [[ "$compact" == *"$FM_REMOTE_JOB_LABEL"* ]] || return 1
896
+ [[ "$compact" == *"$worker"* ]] || return 1
897
+ [[ "$compact" == *"$FM_REMOTE_JOB_LAUNCH_AGENT_PLIST"* ]] || return 1
898
+ }
899
+
900
+ fm_remote_job_worker_pid_path() { printf '%s\n' "$FM_REMOTE_JOB_STATE/worker.pid"; }
901
+ fm_remote_job_worker_ready_path() { printf '%s\n' "$FM_REMOTE_JOB_STATE/worker.ready"; }
902
+ fm_remote_job_worker_identity_path() { printf '%s\n' "$FM_REMOTE_JOB_STATE/worker.identity"; }
903
+ fm_remote_job_worker_lock_path() { printf '%s\n' "$FM_REMOTE_JOB_STATE/worker.lock"; }
904
+
905
+ fm_remote_job_process_start() {
906
+ local pid=$1 ps_bin value
907
+ if [ -x /bin/ps ]; then ps_bin=/bin/ps; elif [ -x /usr/bin/ps ]; then ps_bin=/usr/bin/ps; else return 1; fi
908
+ value=$("$ps_bin" -p "$pid" -o lstart= 2>/dev/null) || return 1
909
+ [ -n "$value" ] || return 1
910
+ case "$value" in *$'\n'*|*$'\r'*) return 1 ;; esac
911
+ printf '%s\n' "$value"
912
+ }
913
+
914
+ fm_remote_job_process_command() {
915
+ local pid=$1 ps_bin value
916
+ if [ -x /bin/ps ]; then ps_bin=/bin/ps; elif [ -x /usr/bin/ps ]; then ps_bin=/usr/bin/ps; else return 1; fi
917
+ value=$("$ps_bin" -p "$pid" -o command= 2>/dev/null) || return 1
918
+ [ -n "$value" ] || return 1
919
+ case "$value" in *$'\n'*|*$'\r'*) return 1 ;; esac
920
+ printf '%s\n' "$value"
921
+ }
922
+
923
+ fm_remote_job_process_pgid() { # <pid>
924
+ local pid=$1 ps_bin value
925
+ if [ -x /bin/ps ]; then ps_bin=/bin/ps; elif [ -x /usr/bin/ps ]; then ps_bin=/usr/bin/ps; else return 1; fi
926
+ value=$("$ps_bin" -p "$pid" -o pgid= 2>/dev/null) || return 1
927
+ value=$(printf '%s' "$value" | tr -d '[:space:]')
928
+ case "$value" in ''|*[!0-9]*) return 1 ;; esac
929
+ printf '%s\n' "$value"
930
+ }
931
+
932
+ # The code root a worker serves is still a genuine Firstmate checkout. A worker
933
+ # whose root fails this can never claim, validate, or execute another job, so
934
+ # the same predicate decides both self-termination and orphan reaping.
935
+ fm_remote_job_root_is_live() { # <remote-root>
936
+ local root=$1
937
+ [ -n "$root" ] || return 1
938
+ [ -d "$root" ] && [ ! -L "$root" ] || return 1
939
+ [ -f "$root/AGENTS.md" ] && [ ! -L "$root/AGENTS.md" ] || return 1
940
+ [ -f "$root/bin/fm-remote-job-worker.sh" ] && [ ! -L "$root/bin/fm-remote-job-worker.sh" ]
941
+ }
942
+
943
+ # The isolated process group that owns <pid>'s whole worker tree, echoed only
944
+ # when signalling it is provably safe: the group is not this shell's own, not a
945
+ # reserved id, and its leader is itself a remote job worker. A worker started
946
+ # without group isolation (an older build, or launchd's own session) therefore
947
+ # never yields a group, and callers fall back to signalling the single process.
948
+ fm_remote_job_worker_process_group() { # <pid>
949
+ local pid=$1 pgid own_pgid leader_command
950
+ pgid=$(fm_remote_job_process_pgid "$pid") || return 1
951
+ case "$pgid" in 0|1) return 1 ;; esac
952
+ own_pgid=$(fm_remote_job_process_pgid "$$") || return 1
953
+ [ "$pgid" != "$own_pgid" ] || return 1
954
+ leader_command=$(fm_remote_job_process_command "$pgid" 2>/dev/null || true)
955
+ case "$leader_command" in *fm-remote-job-worker.sh*) ;; *) return 1 ;; esac
956
+ printf '%s\n' "$pgid"
957
+ }
958
+
959
+ # Stop a worker and every descendant it leaked, TERM first and KILL only for a
960
+ # survivor. Signals the isolated worker group when one is provable and the lone
961
+ # process otherwise. Returns non-zero when any verified worker-group member is
962
+ # still alive afterwards.
963
+ fm_remote_job_stop_worker_tree() { # <pid>
964
+ local pid=$1 pgid i=0
965
+ case "$pid" in ''|*[!0-9]*) return 1 ;; esac
966
+ [ "$pid" -gt 1 ] || return 1
967
+ pgid=$(fm_remote_job_worker_process_group "$pid" 2>/dev/null || true)
968
+ if [ -n "$pgid" ]; then kill -TERM -- "-$pgid" 2>/dev/null || true; else kill -TERM "$pid" 2>/dev/null || true; fi
969
+ while { [ -n "$pgid" ] && kill -0 -- "-$pgid" 2>/dev/null || [ -z "$pgid" ] && kill -0 "$pid" 2>/dev/null; } \
970
+ && [ "$i" -lt 50 ]; do
971
+ i=$((i + 1))
972
+ sleep 0.1
973
+ done
974
+ if [ -n "$pgid" ]; then
975
+ kill -0 -- "-$pgid" 2>/dev/null || return 0
976
+ else
977
+ kill -0 "$pid" 2>/dev/null || return 0
978
+ fi
979
+ if [ -n "$pgid" ]; then kill -KILL -- "-$pgid" 2>/dev/null || true; else kill -KILL "$pid" 2>/dev/null || true; fi
980
+ i=0
981
+ while { [ -n "$pgid" ] && kill -0 -- "-$pgid" 2>/dev/null || [ -z "$pgid" ] && kill -0 "$pid" 2>/dev/null; } \
982
+ && [ "$i" -lt 50 ]; do
983
+ i=$((i + 1))
984
+ sleep 0.1
985
+ done
986
+ if [ -n "$pgid" ]; then
987
+ ! kill -0 -- "-$pgid" 2>/dev/null
988
+ else
989
+ ! kill -0 "$pid" 2>/dev/null
990
+ fi
991
+ }
992
+
993
+ fm_remote_job_read_single_line() {
994
+ local file=$1 max=$2 value extra
995
+ fm_remote_job_regular_bounded "$file" "$max" || return 1
996
+ IFS= read -r value < "$file" || return 1
997
+ if IFS= read -r extra < <(tail -n +2 "$file"); then
998
+ : "$extra"
999
+ return 1
1000
+ fi
1001
+ [ -n "$value" ] || return 1
1002
+ printf '%s\n' "$value"
1003
+ }
1004
+
1005
+ fm_remote_job_lock_owner_matches_process() {
1006
+ local account_home=$1 lock pid recorded_start actual_start recorded_command actual_command
1007
+ fm_remote_job_prepare_state "$account_home" || return 1
1008
+ lock=$(fm_remote_job_worker_lock_path)
1009
+ [ -d "$lock" ] && [ ! -L "$lock" ] || return 1
1010
+ pid=$(fm_remote_job_read_single_line "$lock/pid" 64) || return 1
1011
+ case "$pid" in ''|*[!0-9]*) return 1 ;; esac
1012
+ [ "$pid" -gt 1 ] || return 1
1013
+ recorded_start=$(fm_remote_job_read_single_line "$lock/start" 256) || return 1
1014
+ actual_start=$(fm_remote_job_process_start "$pid") || return 1
1015
+ [ "$recorded_start" = "$actual_start" ] || return 1
1016
+ recorded_command=$(fm_remote_job_read_single_line "$lock/command" 8192) || return 1
1017
+ actual_command=$(fm_remote_job_process_command "$pid") || return 1
1018
+ [ "$recorded_command" = "$actual_command" ] || return 1
1019
+ FM_REMOTE_JOB_OWNER_PID=$pid
1020
+ }
1021
+
1022
+ fm_remote_job_worker_owned_alive() {
1023
+ local root=$1 account_home=$2 lock pid pid_file identity_file command ps_bin
1024
+ [ "${FM_REMOTE_JOB_ACTIVE:-}" != 1 ] || return 0
1025
+ fm_remote_job_prepare_state "$account_home" || return 1
1026
+ lock=$(fm_remote_job_worker_lock_path)
1027
+ [ -d "$lock" ] && [ ! -L "$lock" ] || return 1
1028
+ [ ! -e "$lock/quarantine" ] && [ ! -L "$lock/quarantine" ] || return 1
1029
+ pid_file=$(fm_remote_job_worker_pid_path)
1030
+ pid=$(fm_remote_job_read_single_line "$pid_file" 64) || return 1
1031
+ case "$pid" in ''|*[!0-9]*) return 1 ;; esac
1032
+ identity_file=$(fm_remote_job_worker_identity_path)
1033
+ fm_remote_job_regular_bounded "$identity_file" 256 || return 1
1034
+ fm_remote_job_probe "$account_home" || return 1
1035
+ if fm_remote_job_lock_owner_matches_process "$account_home"; then
1036
+ [ "$pid" = "$FM_REMOTE_JOB_OWNER_PID" ] || return 1
1037
+ return 0
1038
+ fi
1039
+ [ ! -e "$lock/pid" ] && [ ! -L "$lock/pid" ] &&
1040
+ [ ! -e "$lock/start" ] && [ ! -L "$lock/start" ] &&
1041
+ [ ! -e "$lock/command" ] && [ ! -L "$lock/command" ] || return 1
1042
+ if [ -x /bin/ps ]; then ps_bin=/bin/ps; elif [ -x /usr/bin/ps ]; then ps_bin=/usr/bin/ps; else return 1; fi
1043
+ command=$("$ps_bin" -p "$pid" -o command= 2>/dev/null) || return 1
1044
+ case "$command" in *"$root/bin/fm-remote-job-worker.sh"*) FM_REMOTE_JOB_OWNER_PID=$pid; return 0 ;; esac
1045
+ return 1
1046
+ }
1047
+
1048
+ fm_remote_job_code_identity() { # <remote-root> <account-home>
1049
+ local root=$1 account_home=$2 git_bin root_hash library_hash worker_hash
1050
+ root=$(fm_remote_job_canonical_existing_dir "$root") || return 1
1051
+ [ -f "$root/bin/fm-remote-job-lib.sh" ] && [ ! -L "$root/bin/fm-remote-job-lib.sh" ] || return 1
1052
+ [ -f "$root/bin/fm-remote-job-worker.sh" ] && [ ! -L "$root/bin/fm-remote-job-worker.sh" ] || return 1
1053
+ fm_remote_job_compose_operator_path "$account_home" >/dev/null
1054
+ git_bin=$(fm_remote_job_operator_tool git 2>/dev/null || true)
1055
+ [ -n "$git_bin" ] || return 1
1056
+ root_hash=$(printf '%s' "$root" | "$git_bin" hash-object --stdin 2>/dev/null) || return 1
1057
+ library_hash=$("$git_bin" hash-object -- "$root/bin/fm-remote-job-lib.sh" 2>/dev/null) || return 1
1058
+ worker_hash=$("$git_bin" hash-object -- "$root/bin/fm-remote-job-worker.sh" 2>/dev/null) || return 1
1059
+ case "$root_hash:$library_hash:$worker_hash" in *[!0-9a-f:]*) return 1 ;; esac
1060
+ [ -n "$root_hash" ] && [ -n "$library_hash" ] && [ -n "$worker_hash" ] || return 1
1061
+ printf '%s:%s:%s\n' "$root_hash" "$library_hash" "$worker_hash"
1062
+ }
1063
+
1064
+ fm_remote_job_worker_identity_matches() { # <remote-root> <account-home>
1065
+ local root=$1 account_home=$2 identity_file expected actual extra
1066
+ [ "${FM_REMOTE_JOB_ACTIVE:-}" != 1 ] || return 0
1067
+ fm_remote_job_prepare_state "$account_home" || return 1
1068
+ identity_file=$(fm_remote_job_worker_identity_path)
1069
+ fm_remote_job_regular_bounded "$identity_file" 256 || return 1
1070
+ IFS= read -r actual < "$identity_file" || return 1
1071
+ if IFS= read -r extra < <(tail -n +2 "$identity_file"); then
1072
+ : "$extra"
1073
+ return 1
1074
+ fi
1075
+ expected=$(fm_remote_job_code_identity "$root" "$account_home") || return 1
1076
+ [ "$actual" = "$expected" ]
1077
+ }
1078
+
1079
+ fm_remote_job_worker_alive() { # <account-home>
1080
+ local account_home=$1 pid
1081
+ fm_remote_job_prepare_state "$account_home" || return 1
1082
+ pid=$(cat "$(fm_remote_job_worker_pid_path)" 2>/dev/null || true)
1083
+ case "$pid" in ''|*[!0-9]*) return 1 ;; esac
1084
+ kill -0 "$pid" 2>/dev/null
1085
+ }
1086
+
1087
+ fm_remote_job_probe() { # <account-home>; a fresh worker heartbeat or active job proves readiness
1088
+ local account_home=$1 ready lock mtime now
1089
+ [ "${FM_REMOTE_JOB_ACTIVE:-}" = 1 ] && return 0
1090
+ fm_remote_job_prepare_state "$account_home" || return 1
1091
+ lock=$(fm_remote_job_worker_lock_path)
1092
+ [ ! -e "$lock/quarantine" ] && [ ! -L "$lock/quarantine" ] || return 1
1093
+ ready=$(fm_remote_job_worker_ready_path)
1094
+ [ -f "$ready" ] && [ ! -L "$ready" ] || return 1
1095
+ mtime=$(fm_remote_job_path_mtime "$ready" 2>/dev/null || true)
1096
+ case "$mtime" in ''|*[!0-9]*) return 1 ;; esac
1097
+ now=$(date +%s)
1098
+ [ $((now - mtime)) -le 10 ]
1099
+ }
1100
+
1101
+ fm_remote_job_wait_for_probe() { # <remote-root> <account-home>
1102
+ local root=$1 account_home=$2 i=0
1103
+ while [ "$i" -lt 200 ]; do
1104
+ fm_remote_job_probe "$account_home" && fm_remote_job_worker_identity_matches "$root" "$account_home" && return 0
1105
+ i=$((i + 1))
1106
+ sleep 0.1
1107
+ done
1108
+ return 1
1109
+ }
1110
+
1111
+ fm_remote_job_write_launchagent() { # <remote-root> <account-home>
1112
+ local root=$1 account_home=$2 tmp
1113
+ fm_remote_job_launchagent_paths "$account_home"
1114
+ if ! mkdir -p "$FM_REMOTE_JOB_LAUNCH_AGENT_DIR" 2>/dev/null ||
1115
+ ! mkdir -p "$FM_REMOTE_JOB_LAUNCH_AGENT_LOG_DIR" 2>/dev/null; then
1116
+ FM_REMOTE_JOB_ERROR="cannot create the remote job LaunchAgent directories"
1117
+ return 1
1118
+ fi
1119
+ [ -d "$FM_REMOTE_JOB_LAUNCH_AGENT_DIR" ] && [ ! -L "$FM_REMOTE_JOB_LAUNCH_AGENT_DIR" ] || return 1
1120
+ [ -d "$FM_REMOTE_JOB_LAUNCH_AGENT_LOG_DIR" ] && [ ! -L "$FM_REMOTE_JOB_LAUNCH_AGENT_LOG_DIR" ] || return 1
1121
+ tmp="$FM_REMOTE_JOB_LAUNCH_AGENT_DIR/.$FM_REMOTE_JOB_LABEL.plist.tmp.$$"
1122
+ fm_remote_job_render_launchagent "$root" "$account_home" > "$tmp" || {
1123
+ rm -f -- "$tmp"
1124
+ FM_REMOTE_JOB_ERROR="remote job paths cannot be embedded safely in a property list"
1125
+ return 1
1126
+ }
1127
+ chmod 0644 "$tmp" 2>/dev/null || true
1128
+ mv -f -- "$tmp" "$FM_REMOTE_JOB_LAUNCH_AGENT_PLIST" || {
1129
+ rm -f -- "$tmp"
1130
+ FM_REMOTE_JOB_ERROR="cannot publish $FM_REMOTE_JOB_LAUNCH_AGENT_PLIST"
1131
+ return 1
1132
+ }
1133
+ }
1134
+
1135
+ fm_remote_job_reload_launchagent() { # <account-home> <uid>
1136
+ local account_home=$1 uid=$2 out
1137
+ fm_remote_job_launchagent_paths "$account_home"
1138
+ launchctl bootout "gui/$uid/$FM_REMOTE_JOB_LABEL" >/dev/null 2>&1 || true
1139
+ if ! out=$(launchctl bootstrap "gui/$uid" "$FM_REMOTE_JOB_LAUNCH_AGENT_PLIST" 2>&1); then
1140
+ FM_REMOTE_JOB_ERROR="launchctl bootstrap gui/$uid refused: ${out:-no diagnostic}"
1141
+ return 1
1142
+ fi
1143
+ if ! out=$(launchctl kickstart -k "gui/$uid/$FM_REMOTE_JOB_LABEL" 2>&1); then
1144
+ FM_REMOTE_JOB_ERROR="launchctl kickstart gui/$uid/$FM_REMOTE_JOB_LABEL refused: ${out:-no diagnostic}"
1145
+ return 1
1146
+ fi
1147
+ }
1148
+
1149
+ fm_remote_job_start_linux_worker() { # <remote-root> <account-home>
1150
+ local root=$1 account_home=$2 worker pid
1151
+ worker="$root/bin/fm-remote-job-worker.sh"
1152
+ [ -f "$worker" ] && [ ! -L "$worker" ] && [ -x "$worker" ] || {
1153
+ FM_REMOTE_JOB_ERROR="remote job worker is not a genuine executable in the configured code root"
1154
+ return 1
1155
+ }
1156
+ fm_remote_job_prepare_state "$account_home" || return 1
1157
+ if fm_remote_job_worker_owned_alive "$root" "$account_home"; then
1158
+ if fm_remote_job_worker_identity_matches "$root" "$account_home"; then return 0; fi
1159
+ # The owner pid is the serving child; its restart supervisor sits above it
1160
+ # and would immediately replace a lone process kill, so stop the whole
1161
+ # worker tree through its isolated group.
1162
+ pid=$FM_REMOTE_JOB_OWNER_PID
1163
+ fm_remote_job_stop_worker_tree "$pid" || {
1164
+ FM_REMOTE_JOB_ERROR="stale remote job worker did not stop safely"
1165
+ return 1
1166
+ }
1167
+ wait "$pid" 2>/dev/null || true
1168
+ FM_REMOTE_JOB_REPAIRED=1
1169
+ fi
1170
+ # Job control puts the worker tree in its own process group, so a later stop
1171
+ # can signal every descendant at once without ever reaching the caller's own
1172
+ # group. Without this the group of a leaked worker is the launching command's.
1173
+ set -m
1174
+ nohup env \
1175
+ HOME="$account_home" \
1176
+ FM_ROOT_OVERRIDE="$root" \
1177
+ FM_REMOTE_JOB_STATE_ROOT="$FM_REMOTE_JOB_STATE" \
1178
+ FM_REMOTE_JOB_PLATFORM_OVERRIDE="${FM_REMOTE_JOB_PLATFORM_OVERRIDE:-}" \
1179
+ "$worker" >> "$FM_REMOTE_JOB_STATE/logs/$FM_REMOTE_JOB_LABEL.log" 2>&1 < /dev/null &
1180
+ pid=$!
1181
+ set +m
1182
+ case "$pid" in ''|*[!0-9]*) FM_REMOTE_JOB_ERROR="could not start the remote job worker"; return 1 ;; esac
1183
+ FM_REMOTE_JOB_REPAIRED=1
1184
+ }
1185
+
1186
+ fm_remote_job_ensure_worker() { # <remote-root> <account-home>
1187
+ local root=$1 account_home=$2 platform uid identity_matches=0
1188
+ FM_REMOTE_JOB_ERROR=
1189
+ FM_REMOTE_JOB_REPAIRED=0
1190
+ root=$(fm_remote_job_canonical_existing_dir "$root") || {
1191
+ FM_REMOTE_JOB_ERROR="configured remote root is unavailable or unsafe"
1192
+ return 1
1193
+ }
1194
+ account_home=$(fm_remote_job_canonical_existing_dir "$account_home") || {
1195
+ FM_REMOTE_JOB_ERROR="remote account home is unavailable or unsafe"
1196
+ return 1
1197
+ }
1198
+ [ -f "$root/bin/fm-remote-job-worker.sh" ] && [ ! -L "$root/bin/fm-remote-job-worker.sh" ] &&
1199
+ [ -x "$root/bin/fm-remote-job-worker.sh" ] || {
1200
+ FM_REMOTE_JOB_ERROR="configured remote root has no safe executable remote job worker"
1201
+ return 1
1202
+ }
1203
+ platform=$(fm_remote_job_platform)
1204
+ fm_remote_job_worker_identity_matches "$root" "$account_home" && identity_matches=1
1205
+ if [ "$platform" = darwin ]; then
1206
+ uid=$(id -u 2>/dev/null || true)
1207
+ case "$uid" in ''|*[!0-9]*) FM_REMOTE_JOB_ERROR="remote account uid is unavailable; run fm-on.sh <route> fm-remote-doctor.sh --fix"; return 1 ;; esac
1208
+ if ! fm_remote_job_gui_available "$uid"; then
1209
+ FM_REMOTE_JOB_ERROR="no Aqua login session exists for uid $uid; log that account in at the console, then run fm-on.sh <route> fm-remote-doctor.sh --fix"
1210
+ return 1
1211
+ fi
1212
+ if ! fm_remote_job_launchagent_contract_matches "$root" "$account_home"; then
1213
+ fm_remote_job_write_launchagent "$root" "$account_home" || return 1
1214
+ FM_REMOTE_JOB_REPAIRED=1
1215
+ fi
1216
+ if ! fm_remote_job_launchagent_loaded "$root" "$account_home" "$uid" ||
1217
+ [ "$FM_REMOTE_JOB_REPAIRED" -eq 1 ] || [ "$identity_matches" -eq 0 ]; then
1218
+ fm_remote_job_reload_launchagent "$account_home" "$uid" || return 1
1219
+ FM_REMOTE_JOB_REPAIRED=1
1220
+ fi
1221
+ else
1222
+ fm_remote_job_start_linux_worker "$root" "$account_home" || return 1
1223
+ fi
1224
+ fm_remote_job_wait_for_probe "$root" "$account_home" && return 0
1225
+ if [ "$platform" = darwin ]; then
1226
+ fm_remote_job_reload_launchagent "$account_home" "$uid" || return 1
1227
+ FM_REMOTE_JOB_REPAIRED=1
1228
+ fm_remote_job_wait_for_probe "$root" "$account_home" && return 0
1229
+ else
1230
+ # A replaced Linux supervisor can lose its first ownership race while the
1231
+ # prior supervisor finishes releasing the shared worker lock. Retry the
1232
+ # idempotent start once, matching the bounded recovery already used above
1233
+ # for launchd, before reporting a startup failure.
1234
+ fm_remote_job_start_linux_worker "$root" "$account_home" || return 1
1235
+ FM_REMOTE_JOB_REPAIRED=1
1236
+ fm_remote_job_wait_for_probe "$root" "$account_home" && return 0
1237
+ fi
1238
+ # shellcheck disable=SC2034 # Sourceable API consumed by the entrypoint and remote doctor.
1239
+ FM_REMOTE_JOB_ERROR="remote job worker did not report ready after startup"
1240
+ return 1
1241
+ }