@zerwiz/ymir 0.1.4 → 0.1.7

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 (865) 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/README.md +74 -0
  27. package/.agents/backend/backends/cmux.sh +649 -0
  28. package/.agents/backend/backends/herdr-eventwait.py +157 -0
  29. package/.agents/backend/backends/herdr-workspace-move.py +114 -0
  30. package/.agents/backend/backends/herdr.sh +3386 -0
  31. package/.agents/backend/backends/orca.sh +290 -0
  32. package/.agents/backend/backends/tmux.sh +357 -0
  33. package/.agents/backend/backends/zellij.sh +700 -0
  34. package/.agents/backend/fm-afk-launch.sh +654 -0
  35. package/.agents/backend/fm-afk-return.sh +241 -0
  36. package/.agents/backend/fm-afk-start.sh +172 -0
  37. package/.agents/backend/fm-arm-command-policy.mjs +960 -0
  38. package/.agents/backend/fm-arm-pretool-check.sh +200 -0
  39. package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
  40. package/.agents/backend/fm-backend.sh +981 -0
  41. package/.agents/backend/fm-backlog-handoff.sh +909 -0
  42. package/.agents/backend/fm-backlog-receive.sh +187 -0
  43. package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
  44. package/.agents/backend/fm-bearings-board.sh +204 -0
  45. package/.agents/backend/fm-bearings-snapshot.sh +546 -0
  46. package/.agents/backend/fm-bootstrap.sh +1568 -0
  47. package/.agents/backend/fm-branch-outcome.sh +203 -0
  48. package/.agents/backend/fm-branch-prompt.sh +101 -0
  49. package/.agents/backend/fm-brief.sh +452 -0
  50. package/.agents/backend/fm-busy-event.sh +234 -0
  51. package/.agents/backend/fm-busy-lib.sh +986 -0
  52. package/.agents/backend/fm-captain-hold.sh +1001 -0
  53. package/.agents/backend/fm-cd-command-policy.mjs +153 -0
  54. package/.agents/backend/fm-cd-pretool-check.sh +190 -0
  55. package/.agents/backend/fm-check-lib.sh +74 -0
  56. package/.agents/backend/fm-check-register.sh +42 -0
  57. package/.agents/backend/fm-check-unregister.sh +52 -0
  58. package/.agents/backend/fm-classify-lib.sh +1770 -0
  59. package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
  60. package/.agents/backend/fm-composer-lib.sh +1416 -0
  61. package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
  62. package/.agents/backend/fm-config-push.sh +254 -0
  63. package/.agents/backend/fm-control-lib.sh +251 -0
  64. package/.agents/backend/fm-control.sh +878 -0
  65. package/.agents/backend/fm-crew-state.sh +625 -0
  66. package/.agents/backend/fm-cursor-lib.sh +243 -0
  67. package/.agents/backend/fm-decision-hold.sh +232 -0
  68. package/.agents/backend/fm-doc-audience-check.sh +269 -0
  69. package/.agents/backend/fm-dod-lib.sh +67 -0
  70. package/.agents/backend/fm-ensure-agents-md.sh +253 -0
  71. package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
  72. package/.agents/backend/fm-extension.mjs +2577 -0
  73. package/.agents/backend/fm-extension.sh +16 -0
  74. package/.agents/backend/fm-ff-lib.sh +420 -0
  75. package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
  76. package/.agents/backend/fm-fleet-sync.sh +458 -0
  77. package/.agents/backend/fm-fleet-view.sh +96 -0
  78. package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
  79. package/.agents/backend/fm-guard.sh +242 -0
  80. package/.agents/backend/fm-harness.sh +195 -0
  81. package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
  82. package/.agents/backend/fm-herdr-lab.sh +346 -0
  83. package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
  84. package/.agents/backend/fm-home-seed.sh +984 -0
  85. package/.agents/backend/fm-home-summary-refresh.sh +255 -0
  86. package/.agents/backend/fm-hook-host-lib.sh +36 -0
  87. package/.agents/backend/fm-inactive-reconcile.sh +531 -0
  88. package/.agents/backend/fm-inbox.sh +399 -0
  89. package/.agents/backend/fm-install-actionlint.sh +84 -0
  90. package/.agents/backend/fm-install-herdr.sh +96 -0
  91. package/.agents/backend/fm-install-shellcheck.sh +84 -0
  92. package/.agents/backend/fm-install-treehouse.sh +95 -0
  93. package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
  94. package/.agents/backend/fm-lease-lib.sh +218 -0
  95. package/.agents/backend/fm-lease.sh +189 -0
  96. package/.agents/backend/fm-line-cap-lib.sh +51 -0
  97. package/.agents/backend/fm-lint-workflows.sh +137 -0
  98. package/.agents/backend/fm-lint.sh +591 -0
  99. package/.agents/backend/fm-lock-lib.sh +104 -0
  100. package/.agents/backend/fm-lock.sh +107 -0
  101. package/.agents/backend/fm-marker-lib.sh +12 -0
  102. package/.agents/backend/fm-merge-local.sh +74 -0
  103. package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
  104. package/.agents/backend/fm-nm-run-lib.sh +124 -0
  105. package/.agents/backend/fm-on.sh +125 -0
  106. package/.agents/backend/fm-operational-input.sh +253 -0
  107. package/.agents/backend/fm-peek.sh +46 -0
  108. package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
  109. package/.agents/backend/fm-pr-check.sh +135 -0
  110. package/.agents/backend/fm-pr-lib.sh +1016 -0
  111. package/.agents/backend/fm-pr-merge.sh +707 -0
  112. package/.agents/backend/fm-pr-poll.sh +110 -0
  113. package/.agents/backend/fm-primary-scope-lib.sh +33 -0
  114. package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
  115. package/.agents/backend/fm-procevent-lavish.sh +628 -0
  116. package/.agents/backend/fm-procevent-lib.sh +892 -0
  117. package/.agents/backend/fm-procevent-quota.sh +290 -0
  118. package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
  119. package/.agents/backend/fm-procevent-when.sh +504 -0
  120. package/.agents/backend/fm-procevent.sh +1466 -0
  121. package/.agents/backend/fm-project-mode.sh +92 -0
  122. package/.agents/backend/fm-project-origin-lib.sh +180 -0
  123. package/.agents/backend/fm-promote.sh +282 -0
  124. package/.agents/backend/fm-public-followup-emit.sh +260 -0
  125. package/.agents/backend/fm-public-followup-lib.sh +398 -0
  126. package/.agents/backend/fm-public-followup.sh +1330 -0
  127. package/.agents/backend/fm-push-transition-lib.sh +168 -0
  128. package/.agents/backend/fm-quota-axi-lib.sh +93 -0
  129. package/.agents/backend/fm-quota-choose.sh +384 -0
  130. package/.agents/backend/fm-remote-delta-read.sh +189 -0
  131. package/.agents/backend/fm-remote-doctor.sh +800 -0
  132. package/.agents/backend/fm-remote-entrypoint.sh +193 -0
  133. package/.agents/backend/fm-remote-file.sh +234 -0
  134. package/.agents/backend/fm-remote-home-provision.sh +259 -0
  135. package/.agents/backend/fm-remote-home-seed.sh +259 -0
  136. package/.agents/backend/fm-remote-inherit-push.sh +90 -0
  137. package/.agents/backend/fm-remote-inherit.sh +179 -0
  138. package/.agents/backend/fm-remote-job-lib.sh +1241 -0
  139. package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
  140. package/.agents/backend/fm-remote-job-worker.sh +1124 -0
  141. package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
  142. package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
  143. package/.agents/backend/fm-review-diff.sh +158 -0
  144. package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
  145. package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
  146. package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
  147. package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
  148. package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
  149. package/.agents/backend/fm-secondmate-report.sh +88 -0
  150. package/.agents/backend/fm-send.sh +1059 -0
  151. package/.agents/backend/fm-session-lock-lib.sh +176 -0
  152. package/.agents/backend/fm-session-start.sh +970 -0
  153. package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
  154. package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
  155. package/.agents/backend/fm-sessionstart-run.sh +145 -0
  156. package/.agents/backend/fm-spawn.sh +3153 -0
  157. package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
  158. package/.agents/backend/fm-startup-memory-budget.sh +94 -0
  159. package/.agents/backend/fm-startup-network.sh +642 -0
  160. package/.agents/backend/fm-stow-cascade.sh +251 -0
  161. package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
  162. package/.agents/backend/fm-supervise-daemon.sh +1732 -0
  163. package/.agents/backend/fm-supervision-instructions.sh +215 -0
  164. package/.agents/backend/fm-supervision-lib.sh +90 -0
  165. package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
  166. package/.agents/backend/fm-tangle-lib.sh +53 -0
  167. package/.agents/backend/fm-task-inbox-lib.sh +402 -0
  168. package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
  169. package/.agents/backend/fm-teardown.sh +2922 -0
  170. package/.agents/backend/fm-test-isolation-proof.sh +588 -0
  171. package/.agents/backend/fm-test-run.sh +2191 -0
  172. package/.agents/backend/fm-timeout-lib.sh +141 -0
  173. package/.agents/backend/fm-timing-lib.sh +179 -0
  174. package/.agents/backend/fm-tmux-lib.sh +291 -0
  175. package/.agents/backend/fm-tool-update-check.sh +898 -0
  176. package/.agents/backend/fm-trace-context-lib.sh +227 -0
  177. package/.agents/backend/fm-transition-lib.sh +103 -0
  178. package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
  179. package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
  180. package/.agents/backend/fm-turnend-guard.sh +432 -0
  181. package/.agents/backend/fm-update.sh +110 -0
  182. package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
  183. package/.agents/backend/fm-voice-client.py +1373 -0
  184. package/.agents/backend/fm-voice-relay.py +1256 -0
  185. package/.agents/backend/fm-wake-drain.sh +592 -0
  186. package/.agents/backend/fm-wake-grant.sh +127 -0
  187. package/.agents/backend/fm-wake-lib.sh +1849 -0
  188. package/.agents/backend/fm-watch-arm.sh +602 -0
  189. package/.agents/backend/fm-watch-checkpoint.sh +109 -0
  190. package/.agents/backend/fm-watch.sh +1962 -0
  191. package/.agents/backend/fm-x-dismiss.sh +116 -0
  192. package/.agents/backend/fm-x-followup.sh +286 -0
  193. package/.agents/backend/fm-x-lib.sh +1000 -0
  194. package/.agents/backend/fm-x-link.sh +234 -0
  195. package/.agents/backend/fm-x-poll.sh +185 -0
  196. package/.agents/backend/fm-x-reply.sh +385 -0
  197. package/.agents/backend/fm_voice_frame.py +166 -0
  198. package/.agents/backend/fm_voice_records.py +574 -0
  199. package/.agents/backend/model-bridge.py +360 -0
  200. package/.agents/backend/opencode-go-bridge.py +30 -0
  201. package/.agents/bus/README.md +12 -0
  202. package/.agents/bus/messages.json +1 -0
  203. package/.agents/config/agents.machine.example.yaml +16 -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 +11 -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/filebrowser/.filebrowser.json +6 -0
  211. package/.agents/filebrowser/README.md +11 -0
  212. package/.agents/gateway/README.md +12 -0
  213. package/.agents/github/README.md +15 -0
  214. package/.agents/github/webhooks/.gitkeep +0 -0
  215. package/.agents/github/workflows/.gitkeep +0 -0
  216. package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
  217. package/.agents/harness/opencode/plugins/package.json +4 -0
  218. package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
  219. package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
  220. package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
  221. package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
  222. package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
  223. package/.agents/memory/README.md +46 -0
  224. package/.agents/memory/runes_audit.md +5 -0
  225. package/.agents/migrations/0001-hodd-layout.sh +38 -0
  226. package/.agents/migrations/0002-a2a-mcp.sh +27 -0
  227. package/.agents/migrations/0003-private-data-separation.sh +121 -0
  228. package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
  229. package/.agents/migrations/0005-roots-out-of-tree.sh +158 -0
  230. package/.agents/sandbox/Dockerfile.utgard +19 -0
  231. package/.agents/sandbox/README.md +14 -0
  232. package/.agents/sandbox/utgard.config.json +14 -0
  233. package/.agents/skills/README.md +59 -0
  234. package/.agents/skills/bragi-marketing/README.md +3 -0
  235. package/.agents/skills/bragi-marketing/SKILL.md +82 -0
  236. package/.agents/skills/eindri-homes/README.md +3 -0
  237. package/.agents/skills/eindri-homes/SKILL.md +291 -0
  238. package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
  239. package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
  240. package/.agents/skills/frigg-consent/README.md +3 -0
  241. package/.agents/skills/frigg-consent/SKILL.md +56 -0
  242. package/.agents/skills/galdr-ymirsystem/README.md +3 -0
  243. package/.agents/skills/galdr-ymirsystem/SKILL.md +159 -0
  244. package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
  245. package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
  246. package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
  247. package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
  248. package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
  249. package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
  250. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +736 -0
  251. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
  252. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
  253. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
  254. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
  255. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
  256. package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +539 -0
  257. package/.agents/skills/galdr-ymirsystem/assets/installation.md +763 -0
  258. package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
  259. package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
  260. package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +163 -0
  261. package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +415 -0
  262. package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
  263. package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +133 -0
  264. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
  265. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
  266. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +101 -0
  267. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
  268. package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
  269. package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
  270. package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
  271. package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
  272. package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
  273. package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
  274. package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
  275. package/.agents/skills/galdr-ymirsystem/assets/smidja.md +180 -0
  276. package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
  277. package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
  278. package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
  279. package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +503 -0
  280. package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
  281. package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
  282. package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
  283. package/.agents/skills/gjallarhorn-relay/README.md +3 -0
  284. package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
  285. package/.agents/skills/groa-update/SKILL.md +90 -0
  286. package/.agents/skills/hamr-adapters/README.md +3 -0
  287. package/.agents/skills/hamr-adapters/SKILL.md +123 -0
  288. package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
  289. package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
  290. package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
  291. package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
  292. package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
  293. package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
  294. package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
  295. package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
  296. package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
  297. package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
  298. package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
  299. package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
  300. package/.agents/skills/herdr-panes/README.md +3 -0
  301. package/.agents/skills/herdr-panes/SKILL.md +92 -0
  302. package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
  303. package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
  304. package/.agents/skills/hnoss-design/DESIGN.md +19 -0
  305. package/.agents/skills/hnoss-design/README.md +3 -0
  306. package/.agents/skills/hnoss-design/SKILL.md +83 -0
  307. package/.agents/skills/hvild-afk/README.md +3 -0
  308. package/.agents/skills/hvild-afk/SKILL.md +241 -0
  309. package/.agents/skills/jord-projects/README.md +3 -0
  310. package/.agents/skills/jord-projects/SKILL.md +92 -0
  311. package/.agents/skills/lifecycle/SKILL.md +56 -0
  312. package/.agents/skills/lifecycle/smoke_test.sh +57 -0
  313. package/.agents/skills/lifecycle/start.sh +21 -0
  314. package/.agents/skills/lifecycle/status.sh +35 -0
  315. package/.agents/skills/lifecycle/stop.sh +16 -0
  316. package/.agents/skills/muninn-stow/README.md +3 -0
  317. package/.agents/skills/muninn-stow/SKILL.md +309 -0
  318. package/.agents/skills/no-mistakes/SKILL.md +379 -0
  319. package/.agents/skills/nornir-schedule/README.md +3 -0
  320. package/.agents/skills/nornir-schedule/SKILL.md +25 -0
  321. package/.agents/skills/nornir-schedule/assets/events.md +137 -0
  322. package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
  323. package/.agents/skills/nsr-compliance/README.md +3 -0
  324. package/.agents/skills/nsr-compliance/SKILL.md +29 -0
  325. package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
  326. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
  327. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
  328. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
  329. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
  330. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
  331. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
  332. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
  333. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
  334. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
  335. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
  336. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
  337. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
  338. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
  339. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
  340. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
  341. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
  342. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
  343. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
  344. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
  345. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
  346. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
  347. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
  348. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  349. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
  350. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
  351. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
  352. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
  353. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
  354. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
  355. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
  356. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
  357. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
  358. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
  359. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
  360. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
  361. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
  362. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
  363. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
  364. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
  365. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
  366. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
  367. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
  368. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
  369. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
  370. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
  371. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
  372. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
  373. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
  374. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
  375. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
  376. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
  377. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
  378. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
  379. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
  380. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
  381. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
  382. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
  383. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
  384. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
  385. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
  386. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
  387. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
  388. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
  389. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
  390. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
  391. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
  392. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
  393. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
  394. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
  395. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
  396. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
  397. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
  398. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
  399. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
  400. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
  401. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
  402. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
  403. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
  404. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
  405. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
  406. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
  407. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
  408. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
  409. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
  410. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
  411. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
  412. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
  413. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
  414. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
  415. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
  416. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
  417. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
  418. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
  419. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
  420. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
  421. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
  422. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
  423. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
  424. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
  425. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
  426. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
  427. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
  428. package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
  429. package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
  430. package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
  431. package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
  432. package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
  433. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
  434. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
  435. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
  436. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
  437. package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
  438. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
  439. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
  440. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
  441. package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
  442. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
  443. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
  444. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
  445. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
  446. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
  447. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
  448. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
  449. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  450. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
  451. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
  452. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
  453. package/.agents/skills/pr-ops/README.md +3 -0
  454. package/.agents/skills/pr-ops/SKILL.md +109 -0
  455. package/.agents/skills/ratatoskr-a2a/README.md +3 -0
  456. package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
  457. package/.agents/skills/rules-check-drift/SKILL.md +81 -0
  458. package/.agents/skills/saga-bearings/README.md +3 -0
  459. package/.agents/skills/saga-bearings/SKILL.md +26 -0
  460. package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
  461. package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
  462. package/.agents/skills/saga-bearings/assets/recap.md +56 -0
  463. package/.agents/skills/syn-recovery/README.md +3 -0
  464. package/.agents/skills/syn-recovery/SKILL.md +54 -0
  465. package/.agents/skills/tyr-check/README.md +3 -0
  466. package/.agents/skills/tyr-check/SKILL.md +218 -0
  467. package/.agents/skills/urdh-hold/README.md +3 -0
  468. package/.agents/skills/urdh-hold/SKILL.md +25 -0
  469. package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
  470. package/.agents/skills/urdh-hold/assets/hold.md +54 -0
  471. package/.agents/skills/vor-diagnostics/README.md +3 -0
  472. package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
  473. package/.agents/skills/ymir-host/README.md +3 -0
  474. package/.agents/skills/ymir-host/SKILL.md +35 -0
  475. package/.agents/skills/ymir-host/assets/desktop.md +36 -0
  476. package/.agents/skills/ymir-host/assets/install.md +69 -0
  477. package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
  478. package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
  479. package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
  480. package/.agents/skills/ymir-host/assets/update.md +64 -0
  481. package/.agents/tests/assets/board-render-harness.mjs +117 -0
  482. package/.agents/tests/cmux-test-safety.sh +51 -0
  483. package/.agents/tests/fixtures.sh +291 -0
  484. package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
  485. package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
  486. package/.agents/tests/fm-afk-launch.test.sh +961 -0
  487. package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
  488. package/.agents/tests/fm-afk-return.test.sh +283 -0
  489. package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
  490. package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
  491. package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
  492. package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
  493. package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
  494. package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
  495. package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
  496. package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
  497. package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
  498. package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
  499. package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
  500. package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
  501. package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
  502. package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
  503. package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
  504. package/.agents/tests/fm-backend-orca.test.sh +1357 -0
  505. package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
  506. package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
  507. package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
  508. package/.agents/tests/fm-backend.test.sh +1142 -0
  509. package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
  510. package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
  511. package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
  512. package/.agents/tests/fm-bearings-board.test.sh +414 -0
  513. package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
  514. package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
  515. package/.agents/tests/fm-bootstrap.test.sh +1178 -0
  516. package/.agents/tests/fm-branch-supervision.test.sh +552 -0
  517. package/.agents/tests/fm-brief.test.sh +785 -0
  518. package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
  519. package/.agents/tests/fm-busy-state.test.sh +465 -0
  520. package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
  521. package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
  522. package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
  523. package/.agents/tests/fm-check-unregister.test.sh +198 -0
  524. package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
  525. package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
  526. package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
  527. package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
  528. package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
  529. package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
  530. package/.agents/tests/fm-composer-ghost.test.sh +716 -0
  531. package/.agents/tests/fm-composer-lib.test.sh +666 -0
  532. package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
  533. package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
  534. package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
  535. package/.agents/tests/fm-control.test.sh +910 -0
  536. package/.agents/tests/fm-crew-state.test.sh +1610 -0
  537. package/.agents/tests/fm-cursor-harness.test.sh +404 -0
  538. package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
  539. package/.agents/tests/fm-cursor-primary.test.sh +708 -0
  540. package/.agents/tests/fm-daemon.test.sh +2740 -0
  541. package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
  542. package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
  543. package/.agents/tests/fm-extension-binding.test.sh +2187 -0
  544. package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
  545. package/.agents/tests/fm-fleet-sync.test.sh +721 -0
  546. package/.agents/tests/fm-gate-refuse.test.sh +356 -0
  547. package/.agents/tests/fm-gitignore-config.test.sh +89 -0
  548. package/.agents/tests/fm-gotmp.test.sh +230 -0
  549. package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
  550. package/.agents/tests/fm-grok-harness.test.sh +117 -0
  551. package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
  552. package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
  553. package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
  554. package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
  555. package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
  556. package/.agents/tests/fm-herdr-lab.test.sh +243 -0
  557. package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
  558. package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
  559. package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
  560. package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
  561. package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
  562. package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
  563. package/.agents/tests/fm-kimi-harness.test.sh +687 -0
  564. package/.agents/tests/fm-lint-workflows.test.sh +532 -0
  565. package/.agents/tests/fm-lint.test.sh +1023 -0
  566. package/.agents/tests/fm-muse-harness.test.sh +932 -0
  567. package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
  568. package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
  569. package/.agents/tests/fm-on.test.sh +507 -0
  570. package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
  571. package/.agents/tests/fm-operational-input.test.sh +160 -0
  572. package/.agents/tests/fm-peek-remote.test.sh +110 -0
  573. package/.agents/tests/fm-pending-reply.test.sh +1288 -0
  574. package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
  575. package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
  576. package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
  577. package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
  578. package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
  579. package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
  580. package/.agents/tests/fm-pr-merge.test.sh +2135 -0
  581. package/.agents/tests/fm-procevent-quota.test.sh +225 -0
  582. package/.agents/tests/fm-procevent-when.test.sh +406 -0
  583. package/.agents/tests/fm-procevent.test.sh +1745 -0
  584. package/.agents/tests/fm-project-origin.test.sh +110 -0
  585. package/.agents/tests/fm-public-followup.test.sh +2272 -0
  586. package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
  587. package/.agents/tests/fm-quota-choose.test.sh +626 -0
  588. package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
  589. package/.agents/tests/fm-remote-doctor.test.sh +625 -0
  590. package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
  591. package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
  592. package/.agents/tests/fm-remote-job.test.sh +768 -0
  593. package/.agents/tests/fm-remote-reply.test.sh +520 -0
  594. package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
  595. package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
  596. package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
  597. package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
  598. package/.agents/tests/fm-review-diff.test.sh +176 -0
  599. package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
  600. package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
  601. package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
  602. package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
  603. package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
  604. package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
  605. package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
  606. package/.agents/tests/fm-send-inbox.test.sh +352 -0
  607. package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
  608. package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
  609. package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
  610. package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
  611. package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
  612. package/.agents/tests/fm-send-settle.test.sh +147 -0
  613. package/.agents/tests/fm-send-strict.test.sh +241 -0
  614. package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
  615. package/.agents/tests/fm-session-start.test.sh +2515 -0
  616. package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
  617. package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
  618. package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
  619. package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
  620. package/.agents/tests/fm-spawn-batch.test.sh +148 -0
  621. package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
  622. package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
  623. package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
  624. package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
  625. package/.agents/tests/fm-startup-network.test.sh +712 -0
  626. package/.agents/tests/fm-stow-cascade.test.sh +370 -0
  627. package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
  628. package/.agents/tests/fm-supervision-events.test.sh +156 -0
  629. package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
  630. package/.agents/tests/fm-tangle-guard.test.sh +277 -0
  631. package/.agents/tests/fm-task-delivery.test.sh +420 -0
  632. package/.agents/tests/fm-task-inbox.test.sh +513 -0
  633. package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
  634. package/.agents/tests/fm-teardown.test.sh +2665 -0
  635. package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
  636. package/.agents/tests/fm-test-fixtures.test.sh +132 -0
  637. package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
  638. package/.agents/tests/fm-test-run.test.sh +1196 -0
  639. package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
  640. package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
  641. package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
  642. package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
  643. package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
  644. package/.agents/tests/fm-transition-lib.test.sh +54 -0
  645. package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
  646. package/.agents/tests/fm-update.test.sh +304 -0
  647. package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
  648. package/.agents/tests/fm-voice-relay.test.sh +4741 -0
  649. package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
  650. package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
  651. package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
  652. package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
  653. package/.agents/tests/fm-wake-queue.test.sh +1224 -0
  654. package/.agents/tests/fm-watch-arm.test.sh +821 -0
  655. package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
  656. package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
  657. package/.agents/tests/fm-watch-triage.test.sh +3889 -0
  658. package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
  659. package/.agents/tests/fm-x-mode.test.sh +3042 -0
  660. package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
  661. package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
  662. package/.agents/tests/herdr-test-safety.sh +42 -0
  663. package/.agents/tests/lib.sh +315 -0
  664. package/.agents/tests/remote-herdr-fixture.sh +125 -0
  665. package/.agents/tests/secondmate-helpers.sh +193 -0
  666. package/.agents/tests/smoke.test.sh +93 -0
  667. package/.agents/tests/wake-helpers.sh +338 -0
  668. package/.agents/tests/zellij-test-safety.sh +60 -0
  669. package/.agents/tools/README.md +10 -0
  670. package/.agents/tools/bin/tasks-cli +267 -0
  671. package/.agents/tools/tasks-cli.ts +216 -0
  672. package/AGENTS.md +498 -0
  673. package/CHANGELOG.md +1913 -0
  674. package/LICENSE +1 -1
  675. package/NOTICE +63 -0
  676. package/README.md +114 -60
  677. package/RULES/01-domains.md +51 -0
  678. package/RULES/02-agents.md +78 -0
  679. package/RULES/03-houses.md +61 -0
  680. package/RULES/04-hoard.md +97 -0
  681. package/RULES/05-platforms.md +67 -0
  682. package/RULES/06-append-only.md +108 -0
  683. package/RULES/07-config.md +101 -0
  684. package/RULES/08-delivery-gate.md +75 -0
  685. package/RULES/09-electron.md +58 -0
  686. package/RULES/README.md +22 -0
  687. package/bin/README.md +107 -0
  688. package/bin/a2a-mcp.sh +133 -0
  689. package/bin/a2a-serve.py +103 -0
  690. package/bin/a2a-serve.sh +8 -0
  691. package/bin/a2a-talk.sh +50 -0
  692. package/bin/a2abridge-ensure.sh +108 -0
  693. package/bin/agent-run.sh +43 -0
  694. package/bin/agents-config.sh +256 -0
  695. package/bin/apodex-smoke-test.sh +105 -0
  696. package/bin/bifrost-bridge.sh +131 -0
  697. package/bin/bifrost-ingress.sh +77 -0
  698. package/bin/bootstrap-macos.sh +112 -0
  699. package/bin/bootstrap-windows.ps1 +112 -0
  700. package/bin/branch-guard.sh +64 -0
  701. package/bin/brokk +73 -0
  702. package/bin/brokk-classify-lib.sh +1770 -0
  703. package/bin/brokk-lease-lib.sh +222 -0
  704. package/bin/brokk-lease.sh +193 -0
  705. package/bin/brokk-lint.sh +65 -0
  706. package/bin/brokk-send.sh +51 -0
  707. package/bin/brokk-update.sh +9 -0
  708. package/bin/brokk-wake-grant.sh +127 -0
  709. package/bin/brokk-wake-lib.sh +1849 -0
  710. package/bin/changelog-assemble.sh +154 -0
  711. package/bin/changelog-guard.sh +197 -0
  712. package/bin/crash-sense.sh +173 -0
  713. package/bin/design-check.sh +80 -0
  714. package/bin/design-icon.sh +153 -0
  715. package/bin/desktop-place.sh +224 -0
  716. package/bin/docs-guard.sh +39 -0
  717. package/bin/editor-place.sh +225 -0
  718. package/bin/eindri-acclaim.sh +47 -0
  719. package/bin/eindri-control.sh +28 -0
  720. package/bin/eindri-role.sh +83 -0
  721. package/bin/eindri-seat.sh +83 -0
  722. package/bin/eindri-seen.sh +54 -0
  723. package/bin/eindri-send.sh +15 -0
  724. package/bin/eindri-start.sh +98 -0
  725. package/bin/eindri-watch.sh +57 -0
  726. package/bin/einherjar-spawn.sh +576 -0
  727. package/bin/eir-doctor.sh +158 -0
  728. package/bin/erindi-brief.sh +271 -0
  729. package/bin/feature-inventory.sh +73 -0
  730. package/bin/fleet-apply.sh +81 -0
  731. package/bin/github-deploy.sh +94 -0
  732. package/bin/gjallarhorn-expose.sh +136 -0
  733. package/bin/gjallarhorn-notify.sh +71 -0
  734. package/bin/gjallarhorn-purge.sh +67 -0
  735. package/bin/gjallarhorn-tunnel.sh +85 -0
  736. package/bin/gleipnir-lock-lib.sh +284 -0
  737. package/bin/groa-update.sh +130 -0
  738. package/bin/hall-snapshot.sh +111 -0
  739. package/bin/hamr-harness.sh +246 -0
  740. package/bin/heimdall-ssh-keys.sh +374 -0
  741. package/bin/herdr-agents.py +44 -0
  742. package/bin/herdr-ensure.sh +104 -0
  743. package/bin/herdr-run.sh +452 -0
  744. package/bin/hermes-ensure.sh +88 -0
  745. package/bin/hlidskjalf-agents.sh +172 -0
  746. package/bin/hlidskjalf-usage.sh +196 -0
  747. package/bin/hoard-guard.sh +135 -0
  748. package/bin/hoard-lib.sh +105 -0
  749. package/bin/hodd.sh +102 -0
  750. package/bin/host-sense.sh +199 -0
  751. package/bin/huginn-research-worker.sh +121 -0
  752. package/bin/local-model-lock.sh +70 -0
  753. package/bin/mimir-bridge.py +259 -0
  754. package/bin/mimir-bridge.sh +100 -0
  755. package/bin/mimir-ingest.sh +131 -0
  756. package/bin/mimir-reflect.sh +27 -0
  757. package/bin/mimir.sh +113 -0
  758. package/bin/mjollnir-webhook.sh +72 -0
  759. package/bin/mjollnir.sh +104 -0
  760. package/bin/model-hardware.sh +96 -0
  761. package/bin/model-resolve.sh +160 -0
  762. package/bin/models-detect.sh +178 -0
  763. package/bin/nornir-cron-start.sh +162 -0
  764. package/bin/nornir-job-daily-briefing.sh +191 -0
  765. package/bin/nornir-job-git-sync.sh +152 -0
  766. package/bin/nornir-job-hall-snapshot.sh +28 -0
  767. package/bin/nornir-job-memory-housekeeping.sh +114 -0
  768. package/bin/nornir-job-nsr-compliance.sh +54 -0
  769. package/bin/nornir-job-observer.sh +166 -0
  770. package/bin/nornir-job-skillopt-sleep.sh +76 -0
  771. package/bin/npm-publish.sh +136 -0
  772. package/bin/omarchy-hook-install.sh +60 -0
  773. package/bin/omarchy-install.sh +161 -0
  774. package/bin/omarchy-plugins.sh +187 -0
  775. package/bin/omarchy-sense.sh +162 -0
  776. package/bin/perm-guard.sh +30 -0
  777. package/bin/pi-agent.sh +68 -0
  778. package/bin/pi-ensure.sh +77 -0
  779. package/bin/pi-local.sh +37 -0
  780. package/bin/pi-model-agnostic.sh +71 -0
  781. package/bin/pi-seat.sh +63 -0
  782. package/bin/prereq-ensure.sh +166 -0
  783. package/bin/private-guard.sh +58 -0
  784. package/bin/project-git.sh +69 -0
  785. package/bin/public-guard.sh +71 -0
  786. package/bin/ratatoskr.sh +49 -0
  787. package/bin/realm-lib.sh +32 -0
  788. package/bin/repo-scrub.sh +62 -0
  789. package/bin/rodd-operational-input.sh +250 -0
  790. package/bin/runes-append.sh +198 -0
  791. package/bin/saga-session-start.sh +172 -0
  792. package/bin/saga-sessionstart-run.sh +79 -0
  793. package/bin/saga-wake-drain.sh +41 -0
  794. package/bin/secret-guard.sh +63 -0
  795. package/bin/sessrumnir-ensure.sh +100 -0
  796. package/bin/sessrumnir-sync.sh +237 -0
  797. package/bin/sessrumnir.sh +104 -0
  798. package/bin/skillopt-setup.sh +76 -0
  799. package/bin/skuld-branch-outcome.sh +203 -0
  800. package/bin/skuld-branch-prompt.sh +101 -0
  801. package/bin/smidja-bootstrap.sh +94 -0
  802. package/bin/smidja-observe.sh +86 -0
  803. package/bin/syn-arm-pretool-check.sh +57 -0
  804. package/bin/syn-asset-pretool-check.sh +89 -0
  805. package/bin/syn-cd-pretool-check.sh +28 -0
  806. package/bin/syn-guard-pretool-check.sh +83 -0
  807. package/bin/syn-spawn-pretool-check.sh +58 -0
  808. package/bin/syn-turnend-guard.sh +36 -0
  809. package/bin/syn-watch-arm.sh +99 -0
  810. package/bin/tailscale-sync.sh +203 -0
  811. package/bin/telegram-bot.sh +82 -0
  812. package/bin/toolchain.sh +74 -0
  813. package/bin/ui-truth-check.sh +75 -0
  814. package/bin/utgard.sh +111 -0
  815. package/bin/valhalla.sh +115 -0
  816. package/bin/valknut-load.sh +298 -0
  817. package/bin/vor-crew-state.sh +124 -0
  818. package/bin/wedge-notify.sh +60 -0
  819. package/bin/workspace-provision.sh +80 -0
  820. package/bin/workspace-rag.sh +163 -0
  821. package/bin/wyrd-db.sh +47 -0
  822. package/bin/yggdrasil.sh +152 -0
  823. package/bin/ymir-edit.sh +145 -0
  824. package/bin/ymir-install.sh +806 -0
  825. package/bin/ymir-invite.sh +93 -0
  826. package/bin/ymir-isolation.sh +88 -0
  827. package/bin/ymir-migrate.sh +61 -0
  828. package/bin/ymir-plan.sh +356 -0
  829. package/bin/ymir-platform.sh +348 -0
  830. package/bin/ymir-say.sh +110 -0
  831. package/bin/ymir-setup-auth.sh +119 -0
  832. package/bin/ymir-validate.sh +193 -0
  833. package/bin/ymir.js +74 -16
  834. package/docs/Architecture.md +339 -0
  835. package/docs/design.md +351 -0
  836. package/docs/installations/README.md +63 -0
  837. package/docs/installations/ymir-home.md +93 -0
  838. package/docs/integration.md +105 -0
  839. package/docs/lore.md +1060 -0
  840. package/docs/research/agent-harnesses.md +290 -0
  841. package/docs/runbooks/README.md +19 -0
  842. package/docs/runbooks/a2a.md +91 -0
  843. package/docs/runbooks/agent-permissions.md +99 -0
  844. package/docs/runbooks/agents.md +57 -0
  845. package/docs/runbooks/models.md +69 -0
  846. package/docs/runbooks/realm-onboarding.md +60 -0
  847. package/docs/runbooks/secrets-and-hoard.md +60 -0
  848. package/docs/runbooks/updates-and-migrations.md +110 -0
  849. package/docs/runbooks/worktrees-isolation.md +72 -0
  850. package/docs/session-start.md +105 -0
  851. package/docs/skillopt-integration.md +321 -0
  852. package/docs/sync-system.md +66 -0
  853. package/docs/workspaces.md +104 -0
  854. package/docs/ymir-rut.md +260 -0
  855. package/package.json +54 -14
  856. package/packaging/build.sh +74 -0
  857. package/packaging/macos/Ymir Installer.command +43 -0
  858. package/packaging/windows/ymir-setup.nsi +74 -0
  859. package/scripts/README.md +22 -0
  860. package/scripts/electron.sh +269 -0
  861. package/scripts/lower.sh +34 -0
  862. package/scripts/raise.sh +42 -0
  863. package/scripts/start.sh +217 -0
  864. package/scripts/stop.sh +113 -0
  865. package/install.sh +0 -35
@@ -0,0 +1,1256 @@
1
+ #!/usr/bin/env bash
2
+ # Full remote secondmate lifecycle over the deterministic generic SSH boundary.
3
+ set -u
4
+
5
+ # shellcheck source=tests/lib.sh
6
+ . "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
7
+ # shellcheck source=tests/remote-herdr-fixture.sh
8
+ . "$(dirname "${BASH_SOURCE[0]}")/remote-herdr-fixture.sh"
9
+
10
+ command -v jq >/dev/null 2>&1 || { echo "skip: jq not found"; exit 0; }
11
+ ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)
12
+ TMP_ROOT=$(fm_test_tmproot fm-remote-secondmate-e2e)
13
+ mkdir -p "$TMP_ROOT"
14
+ TMP_ROOT=$(cd "$TMP_ROOT" && pwd -P)
15
+ PARENT="$TMP_ROOT/parent"
16
+ REMOTE_ROOT="$TMP_ROOT/remote-root"
17
+ REMOTE_HOME="$TMP_ROOT/remote-home"
18
+ LOCAL_HOME="$TMP_ROOT/local-home"
19
+ FAKEBIN=$(fm_fakebin "$TMP_ROOT/fake")
20
+ SSH_COUNT="$TMP_ROOT/ssh.count"
21
+ DOCTOR_LOG="$TMP_ROOT/doctor.log"
22
+ HERDR_STATE="$TMP_ROOT/remote-herdr.state"
23
+ HERDR_LOG="$TMP_ROOT/remote-herdr.log"
24
+ TMUX_LOG="$TMP_ROOT/remote-tmux.log"
25
+ TMUX_STATE="$TMP_ROOT/remote-tmux.state"
26
+ CLAIMS="$TMP_ROOT/claims"
27
+ mkdir -p "$PARENT/data" "$PARENT/state" "$PARENT/config" "$PARENT/projects" "$REMOTE_ROOT" "$CLAIMS"
28
+ cleanup() {
29
+ local worker_pid='' wait_attempt=0
30
+ touch "$TMP_ROOT/provision.release" "$TMP_ROOT/seed.release" "$TMP_ROOT/handoff.release" \
31
+ "$TMP_ROOT/inherit.release" "$TMP_ROOT/launch.release" 2>/dev/null || true
32
+ FM_HOME="$PARENT" FM_PROCEVENT_CLAIM_ROOT="$CLAIMS" \
33
+ "$ROOT/bin/fm-procevent.sh" sweep-home >/dev/null 2>&1 || true
34
+ if [ -f "$TMP_ROOT/remote-jobs/worker.pid" ]; then
35
+ worker_pid=$(cat "$TMP_ROOT/remote-jobs/worker.pid")
36
+ kill "$worker_pid" 2>/dev/null || true
37
+ while kill -0 "$worker_pid" 2>/dev/null && [ "$wait_attempt" -lt 100 ]; do
38
+ wait_attempt=$((wait_attempt + 1))
39
+ sleep 0.05
40
+ done
41
+ fi
42
+ rm -rf -- "$TMP_ROOT"
43
+ }
44
+ trap cleanup EXIT
45
+
46
+ # Materialize the current branch as the remote host's tracked code root. The
47
+ # fixture is a real git repository because provisioning and guarded sync exercise
48
+ # the same clone and fast-forward path as a second Mac.
49
+ (
50
+ cd "$ROOT" || exit
51
+ tar --exclude=.git --exclude=.no-mistakes --exclude=data --exclude=state --exclude=config -cf - .
52
+ ) | (cd "$REMOTE_ROOT" && tar -xf -)
53
+ cat > "$REMOTE_ROOT/bin/tmux" <<SH
54
+ #!/usr/bin/env bash
55
+ set -u
56
+ log='$TMUX_LOG'
57
+ state='$TMUX_STATE'
58
+ fail_send='$TMP_ROOT/tmux-send-fail'
59
+ printf '%s\n' "\$*" >> "\$log"
60
+ case "\${1:-}" in
61
+ has-session|new-session|set-window-option) exit 0 ;;
62
+ list-windows)
63
+ [ -f "\$state" ] || exit 0
64
+ name=\$(cut -d'|' -f1 "\$state")
65
+ case "\$*" in *'#{session_name}:#{window_name}'*) printf 'firstmate:%s\n' "\$name" ;; *) printf '%s\n' "\$name" ;; esac
66
+ exit 0
67
+ ;;
68
+ new-window)
69
+ name=; cwd=
70
+ while [ "\$#" -gt 0 ]; do
71
+ case "\$1" in -n) shift; name=\$1 ;; -c) shift; cwd=\$1 ;; esac
72
+ shift
73
+ done
74
+ printf '%s|%s\n' "\$name" "\$cwd" > "\$state"
75
+ printf '@1\n'
76
+ exit 0
77
+ ;;
78
+ display-message)
79
+ case "\$*" in
80
+ *'#{pane_current_path}'*) cut -d'|' -f2- "\$state" ;;
81
+ *'#{pane_current_command}'*) printf 'codex\n' ;;
82
+ *'#{cursor_y}'*) printf '0\n' ;;
83
+ *'#S'*) printf 'firstmate\n' ;;
84
+ *) printf '%%1\n' ;;
85
+ esac
86
+ exit 0
87
+ ;;
88
+ capture-pane) printf '❯\n'; exit 0 ;;
89
+ send-keys) [ ! -f "\$fail_send" ] || exit 1; exit 0 ;;
90
+ kill-window) rm -f -- "\$state"; exit 0 ;;
91
+ list-panes) printf 'codex\n'; exit 0 ;;
92
+ esac
93
+ exit 0
94
+ SH
95
+ chmod +x "$REMOTE_ROOT/bin/tmux"
96
+ install_remote_herdr_fixture "$REMOTE_ROOT" "$HERDR_STATE" "$HERDR_LOG" \
97
+ "$TMP_ROOT/herdr-send-fail" "$TMP_ROOT/herdr.sock"
98
+ git -C "$REMOTE_ROOT" init -q -b main
99
+ git -C "$REMOTE_ROOT" config user.email test@example.com
100
+ git -C "$REMOTE_ROOT" config user.name Test
101
+ git -C "$REMOTE_ROOT" add .
102
+ git -C "$REMOTE_ROOT" commit -qm 'remote fixture root'
103
+ REMOTE_ORIGIN="$TMP_ROOT/firstmate-origin.git"
104
+ git init -q --bare "$REMOTE_ORIGIN"
105
+ git -C "$REMOTE_ROOT" remote add origin "file://$REMOTE_ORIGIN"
106
+ git -C "$REMOTE_ROOT" push -q -u origin main
107
+ git --git-dir="$REMOTE_ORIGIN" symbolic-ref HEAD refs/heads/main
108
+
109
+ # One remote-backed direct-PR project. The remote home clones its origin, never
110
+ # the primary working tree.
111
+ git init -q --bare "$TMP_ROOT/alpha.git"
112
+ git -C "$PARENT/projects" init -q -b main alpha
113
+ git -C "$PARENT/projects/alpha" config user.email test@example.com
114
+ git -C "$PARENT/projects/alpha" config user.name Test
115
+ printf 'alpha\n' > "$PARENT/projects/alpha/README.md"
116
+ git -C "$PARENT/projects/alpha" add README.md
117
+ git -C "$PARENT/projects/alpha" commit -qm init
118
+ git -C "$PARENT/projects/alpha" remote add origin "file://$TMP_ROOT/alpha.git"
119
+ git -C "$PARENT/projects/alpha" push -q -u origin main
120
+ cat > "$PARENT/data/projects.md" <<EOF
121
+ - alpha [direct-PR] - alpha project (added 2026-08-02)
122
+ EOF
123
+ printf 'codex\n' > "$PARENT/config/secondmate-harness"
124
+ printf 'tmux\n' > "$PARENT/config/backend"
125
+ printf 'primary harness defaults\n' > "$PARENT/config/crew-harness"
126
+
127
+ cat > "$FAKEBIN/fake-ssh" <<'SH'
128
+ #!/usr/bin/env bash
129
+ count=$(cat "$FM_FAKE_SSH_COUNT" 2>/dev/null || echo 0)
130
+ printf '%s\n' "$((count + 1))" > "$FM_FAKE_SSH_COUNT"
131
+ while [ "$#" -gt 0 ]; do
132
+ case "$1" in -o) shift 2 ;; --) shift; break ;; *) exit 90 ;; esac
133
+ done
134
+ host=$1
135
+ entry=$2
136
+ shift 2
137
+ [ "$host" = remote-mac ] || exit 91
138
+ [ "$entry" = fm-remote-entrypoint.sh ] || exit 92
139
+ cd "$FM_FAKE_REMOTE_CWD" || exit 93
140
+ argv_b64=$4
141
+ command_fields=$(perl -MMIME::Base64=decode_base64 -e '
142
+ my $data=decode_base64($ARGV[0]);
143
+ my @args=split(/\0/, $data);
144
+ print join("\t", map { defined $_ ? $_ : "" } @args[0..2]);
145
+ ' "$argv_b64")
146
+ IFS=$'\t' read -r command_name _command_action command_rel <<EOF
147
+ $command_fields
148
+ EOF
149
+ case "${FM_FAKE_SSH_MODE:-normal}:$command_name:$command_rel" in
150
+ inherit-partial:fm-remote-inherit.sh:config/crew-harness) exit 255 ;;
151
+ inherit-block:fm-remote-inherit.sh:data/captain-shared.md)
152
+ cat > "$FM_FAKE_INHERIT_PAYLOAD"
153
+ touch "$FM_FAKE_INHERIT_ENTERED"
154
+ while [ ! -f "$FM_FAKE_INHERIT_RELEASE" ]; do sleep 0.02; done
155
+ "$FM_FAKE_REMOTE_ENTRYPOINT" "$@" < "$FM_FAKE_INHERIT_PAYLOAD"
156
+ exit $?
157
+ ;;
158
+ esac
159
+ # The readiness gate is answered here rather than by the real doctor, which
160
+ # would inspect and repair the RUNNER's own account. tests/fm-remote-doctor.test.sh
161
+ # owns the doctor's real behavior against controlled account fixtures; this
162
+ # boundary owns only what the callers do with its verdict.
163
+ if [ "$command_name" = fm-remote-doctor.sh ]; then
164
+ printf '%s %s\n' "${FM_FAKE_SSH_MODE:-normal}" "${_command_action:--}" >> "$FM_FAKE_DOCTOR_LOG"
165
+ case "${FM_FAKE_SSH_MODE:-normal}" in
166
+ unreachable) exit 255 ;;
167
+ doctor-fix-unknown)
168
+ if [ "${_command_action:-}" = --fix ]; then
169
+ printf 'fix launchagent=applied: wrote the Aqua-scoped launch agent\n'
170
+ exit 255
171
+ fi
172
+ printf 'check launchagent=fixable: no Firstmate herdr launch agent\n'
173
+ printf 'error: this host is not ready for a remote second mate; unresolved: launchagent\n' >&2
174
+ exit 1
175
+ ;;
176
+ doctor-human)
177
+ printf 'check gui-session=human: no Aqua login session exists for uid 501\n'
178
+ printf 'action: gui-session: log that account in once at the console\n'
179
+ printf 'error: this host is not ready for a remote second mate; unresolved: gui-session\n' >&2
180
+ exit 1
181
+ ;;
182
+ doctor-fixable)
183
+ # Red until --fix runs on this host, green on every later read-only run.
184
+ if [ "${_command_action:-}" = --fix ]; then
185
+ touch "$FM_FAKE_DOCTOR_REPAIRED"
186
+ printf 'fix launchagent=applied: wrote the Aqua-scoped launch agent\n'
187
+ printf 'ok: remote second-mate readiness confirmed on this host\n'
188
+ exit 0
189
+ fi
190
+ [ -f "$FM_FAKE_DOCTOR_REPAIRED" ] || {
191
+ printf 'check launchagent=fixable: no Firstmate herdr launch agent\n'
192
+ printf 'error: this host is not ready for a remote second mate; unresolved: launchagent\n' >&2
193
+ exit 1
194
+ }
195
+ ;;
196
+ esac
197
+ printf 'check herdr=ok: /usr/bin/herdr\n'
198
+ printf 'ok: remote second-mate readiness confirmed on this host\n'
199
+ exit 0
200
+ fi
201
+ if [ "${FM_FAKE_SSH_MODE:-normal}" = doctor-fixable ] \
202
+ && [ "$command_name" = fm-remote-secondmate-control.sh ] \
203
+ && [ "$_command_action" = state ] \
204
+ && [ ! -f "$FM_FAKE_DOCTOR_REPAIRED" ]; then
205
+ printf 'unreadable\n'
206
+ exit 0
207
+ fi
208
+ case "${FM_FAKE_SSH_MODE:-normal}:$command_name:$command_rel" in
209
+ launch-nonherdr-route:fm-remote-secondmate-control.sh:*)
210
+ [ "$_command_action" = launch ] || exit 93
211
+ printf 'schema=fm-remote-secondmate-control.v1\n'
212
+ printf 'backend=tmux\n'
213
+ printf 'target=firstmate:fm-ios\n'
214
+ printf 'harness=codex\n'
215
+ exit 0
216
+ ;;
217
+ launch-default-session-route:fm-remote-secondmate-control.sh:*)
218
+ [ "$_command_action" = launch ] || exit 93
219
+ printf 'schema=fm-remote-secondmate-control.v1\n'
220
+ printf 'backend=herdr\n'
221
+ printf 'target=default:w1:p2\n'
222
+ printf 'herdr_session=default\n'
223
+ printf 'harness=codex\n'
224
+ exit 0
225
+ ;;
226
+ provision-block-fail:fm-remote-home-provision.sh:*)
227
+ touch "$FM_FAKE_SEED_ENTERED"
228
+ while [ ! -f "$FM_FAKE_SEED_RELEASE" ]; do sleep 0.02; done
229
+ exit 1
230
+ ;;
231
+ launch-block:fm-remote-secondmate-control.sh:*)
232
+ [ "$_command_action" = launch ] || exit 93
233
+ touch "$FM_FAKE_LAUNCH_ENTERED"
234
+ while [ ! -f "$FM_FAKE_LAUNCH_RELEASE" ]; do sleep 0.02; done
235
+ ;;
236
+ esac
237
+ case "${FM_FAKE_SSH_MODE:-normal}" in
238
+ unreachable) exit 255 ;;
239
+ ambiguous) "$FM_FAKE_REMOTE_ENTRYPOINT" "$@"; exit 255 ;;
240
+ *) exec "$FM_FAKE_REMOTE_ENTRYPOINT" "$@" ;;
241
+ esac
242
+ SH
243
+ chmod +x "$FAKEBIN/fake-ssh"
244
+
245
+ publish_healthy_watcher_identity() { # <state> <home> <watch-script>
246
+ local state=$1 home=$2 watch=$3 identity
247
+ identity=$(FM_HOME="$PARENT" FM_STATE_OVERRIDE="$PARENT/state" /bin/bash -c \
248
+ '. "$1"; fm_pid_identity "$2"' _ "$ROOT/bin/fm-wake-lib.sh" "$$") \
249
+ || fail "could not derive fixture watcher identity"
250
+ mkdir -p "$state/.watch.lock"
251
+ printf '%s\n' "$$" > "$state/.watch.lock/pid"
252
+ printf '%s\n' "$identity" > "$state/.watch.lock/pid-identity"
253
+ printf '%s\n' "$home" > "$state/.watch.lock/fm-home"
254
+ printf '%s\n' "$watch" > "$state/.watch.lock/watcher-path"
255
+ touch "$state/.last-watcher-beat"
256
+ }
257
+
258
+ remote_env() {
259
+ FM_HOME="$PARENT" \
260
+ FM_ROOT_OVERRIDE="$REMOTE_ROOT" \
261
+ FM_PROCEVENT_CLAIM_ROOT="$CLAIMS" \
262
+ FM_SSH_BIN="$FAKEBIN/fake-ssh" \
263
+ FM_FAKE_SSH_COUNT="$SSH_COUNT" \
264
+ FM_FAKE_REMOTE_ENTRYPOINT="$REMOTE_ROOT/bin/fm-remote-entrypoint.sh" \
265
+ FM_REMOTE_JOB_PLATFORM_OVERRIDE=Linux \
266
+ FM_REMOTE_JOB_STATE_ROOT="$TMP_ROOT/remote-jobs" \
267
+ FM_FAKE_SSH_MODE="${FM_FAKE_SSH_MODE:-normal}" \
268
+ FM_FAKE_REMOTE_CWD="$TMP_ROOT" \
269
+ FM_FAKE_SEED_ENTERED="$TMP_ROOT/seed.entered" \
270
+ FM_FAKE_SEED_RELEASE="$TMP_ROOT/seed.release" \
271
+ FM_FAKE_DOCTOR_LOG="$DOCTOR_LOG" \
272
+ FM_FAKE_DOCTOR_REPAIRED="$TMP_ROOT/doctor.repaired" \
273
+ FM_FAKE_INHERIT_ENTERED="$TMP_ROOT/inherit.entered" \
274
+ FM_FAKE_INHERIT_RELEASE="$TMP_ROOT/inherit.release" \
275
+ FM_FAKE_INHERIT_PAYLOAD="$TMP_ROOT/inherit.payload" \
276
+ FM_FAKE_LAUNCH_ENTERED="$TMP_ROOT/launch.entered" \
277
+ FM_FAKE_LAUNCH_RELEASE="$TMP_ROOT/launch.release" \
278
+ FM_SEND_SETTLE=0 FM_SEND_SLEEP=0 FM_REMOTE_REPLY_WAIT_SECONDS=10 \
279
+ "$@"
280
+ }
281
+
282
+ sha256_file() {
283
+ if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}'; else sha256sum "$1" | awk '{print $1}'; fi
284
+ }
285
+
286
+ # The correlation token of the newest record in the remote secondmate's
287
+ # steering inbox: a remote steer is delivered as a durable record there, so
288
+ # the corr a reply must echo is read from the record body, never from typed
289
+ # pane bytes.
290
+ newest_remote_inbox_corr() {
291
+ grep -Eoh 'corr=[a-f0-9]{16}' "$REMOTE_HOME"/state/parent-route/ios.inbox/*.msg 2>/dev/null \
292
+ | tail -1 | cut -d= -f2-
293
+ }
294
+
295
+ seed_env() {
296
+ FM_HOME="$TMP_ROOT/seed-parent" \
297
+ FM_ROOT_OVERRIDE="$REMOTE_ROOT" \
298
+ FM_PROCEVENT_CLAIM_ROOT="$CLAIMS" \
299
+ FM_SSH_BIN="$FAKEBIN/fake-ssh" \
300
+ FM_FAKE_SSH_COUNT="$SSH_COUNT" \
301
+ FM_FAKE_REMOTE_ENTRYPOINT="$REMOTE_ROOT/bin/fm-remote-entrypoint.sh" \
302
+ FM_REMOTE_JOB_PLATFORM_OVERRIDE=Linux \
303
+ FM_REMOTE_JOB_STATE_ROOT="$TMP_ROOT/remote-jobs" \
304
+ FM_FAKE_SSH_MODE="${FM_FAKE_SSH_MODE:-normal}" \
305
+ FM_FAKE_REMOTE_CWD="$TMP_ROOT" \
306
+ FM_FAKE_SEED_ENTERED="$TMP_ROOT/seed.entered" \
307
+ FM_FAKE_SEED_RELEASE="$TMP_ROOT/seed.release" \
308
+ FM_FAKE_DOCTOR_LOG="$DOCTOR_LOG" \
309
+ FM_FAKE_DOCTOR_REPAIRED="$TMP_ROOT/doctor.repaired" \
310
+ "$@"
311
+ }
312
+
313
+ REAL_GIT=$(command -v git)
314
+ cat > "$FAKEBIN/git" <<SH
315
+ #!/usr/bin/env bash
316
+ if [ "\${1:-}" = clone ] && [ "\${!#}" = "$TMP_ROOT/concurrent-home" ]; then
317
+ printf 'clone\n' >> "$TMP_ROOT/provision-clones"
318
+ if mkdir "$TMP_ROOT/provision-first" 2>/dev/null; then
319
+ touch "$TMP_ROOT/provision.entered"
320
+ while [ ! -f "$TMP_ROOT/provision.release" ]; do sleep 0.02; done
321
+ fi
322
+ fi
323
+ exec "$REAL_GIT" "\$@"
324
+ SH
325
+ chmod +x "$FAKEBIN/git"
326
+ printf 'schema=fm-remote-home-provision.v1\nid_b64=%s\ncharter_b64=%s\nproject_count=0\n' \
327
+ "$(printf ios | base64 | tr -d '\n')" \
328
+ "$(printf 'Concurrent provisioning charter.\n' | base64 | tr -d '\n')" \
329
+ > "$TMP_ROOT/provision.manifest"
330
+ PATH="$FAKEBIN:$PATH" FM_HOME="$TMP_ROOT/concurrent-home" FM_ROOT_OVERRIDE="$REMOTE_ROOT" \
331
+ "$REMOTE_ROOT/bin/fm-remote-home-provision.sh" < "$TMP_ROOT/provision.manifest" \
332
+ > "$TMP_ROOT/provision-one.out" 2>&1 &
333
+ provision_one=$!
334
+ provision_wait=0
335
+ while [ ! -f "$TMP_ROOT/provision.entered" ]; do
336
+ kill -0 "$provision_one" 2>/dev/null || fail "first provisioning attempt exited before cloning"
337
+ provision_wait=$((provision_wait + 1))
338
+ [ "$provision_wait" -le 250 ] || fail "first provisioning attempt never reached cloning"
339
+ sleep 0.02
340
+ done
341
+ PATH="$FAKEBIN:$PATH" FM_HOME="$TMP_ROOT/concurrent-home" FM_ROOT_OVERRIDE="$REMOTE_ROOT" \
342
+ "$REMOTE_ROOT/bin/fm-remote-home-provision.sh" < "$TMP_ROOT/provision.manifest" \
343
+ > "$TMP_ROOT/provision-two.out" 2>&1 &
344
+ provision_two=$!
345
+ sleep 0.2
346
+ [ "$(grep -cF clone "$TMP_ROOT/provision-clones")" -eq 1 ] \
347
+ || fail "overlapping provisioning reached home classification concurrently"
348
+ touch "$TMP_ROOT/provision.release"
349
+ wait "$provision_one" || fail "first serialized provisioning attempt failed"
350
+ wait "$provision_two" || fail "reconciled provisioning attempt failed"
351
+ [ "$(cat "$TMP_ROOT/concurrent-home/.fm-secondmate-home")" = ios ] \
352
+ || fail "serialized provisioning lost the published home"
353
+ [ "$(grep -cF clone "$TMP_ROOT/provision-clones")" -eq 1 ] \
354
+ || fail "reconciled provisioning cloned the already-published home"
355
+ pass "overlapping remote home provisioning serializes through publication and rollback"
356
+ if [ "${FM_TEST_PROVISION_ONLY:-0}" = 1 ]; then
357
+ echo "ALL TESTS PASSED"
358
+ exit 0
359
+ fi
360
+
361
+ mkdir -p "$TMP_ROOT/seed-parent/data" "$TMP_ROOT/seed-parent/state"
362
+ FM_SECONDMATE_CHARTER='Failing seed charter.' FM_SECONDMATE_SCOPE='failed seed' \
363
+ FM_FAKE_SSH_MODE=provision-block-fail seed_env "$ROOT/bin/fm-remote-home-seed.sh" \
364
+ seed-fail remote-mac "$REMOTE_ROOT" "$TMP_ROOT/seed-fail-home" --no-projects \
365
+ > "$TMP_ROOT/seed-fail.out" 2>&1 &
366
+ seed_fail_pid=$!
367
+ seed_wait=0
368
+ while [ ! -f "$TMP_ROOT/seed.entered" ]; do
369
+ kill -0 "$seed_fail_pid" 2>/dev/null || fail "failing seed exited before remote provisioning"
370
+ seed_wait=$((seed_wait + 1))
371
+ [ "$seed_wait" -le 250 ] || fail "failing seed never reached remote provisioning"
372
+ sleep 0.02
373
+ done
374
+ FM_SECONDMATE_CHARTER='Successful seed charter.' FM_SECONDMATE_SCOPE='successful seed' \
375
+ seed_env "$ROOT/bin/fm-remote-home-seed.sh" seed-keep remote-mac "$REMOTE_ROOT" \
376
+ "$TMP_ROOT/seed-keep-home" --no-projects > "$TMP_ROOT/seed-keep.out" 2>&1 &
377
+ seed_keep_pid=$!
378
+ sleep 0.2
379
+ kill -0 "$seed_keep_pid" 2>/dev/null || fail "competing seed bypassed the shared registry transaction"
380
+ touch "$TMP_ROOT/seed.release"
381
+ if wait "$seed_fail_pid"; then
382
+ fail "known-failing seed unexpectedly succeeded"
383
+ fi
384
+ wait "$seed_keep_pid" || fail "serialized successful seed failed"
385
+ assert_no_grep '- seed-fail ' "$TMP_ROOT/seed-parent/data/secondmates.md" "failed seed route survived rollback"
386
+ assert_grep '- seed-keep ' "$TMP_ROOT/seed-parent/data/secondmates.md" "failed seed rollback removed a competing successful route"
387
+ assert_present "$TMP_ROOT/seed-keep-home/.fm-secondmate-home" "serialized seed lost its published remote home"
388
+ pass "remote seed rollback preserves serialized competing routes"
389
+
390
+ : > "$DOCTOR_LOG"
391
+ if FM_SECONDMATE_CHARTER='Unknown readiness charter.' FM_SECONDMATE_SCOPE='unknown readiness' \
392
+ FM_FAKE_SSH_MODE=doctor-fix-unknown seed_env "$ROOT/bin/fm-remote-home-seed.sh" \
393
+ seed-unknown remote-mac "$REMOTE_ROOT" "$TMP_ROOT/seed-unknown-home" --no-projects \
394
+ > "$TMP_ROOT/seed-unknown.out" 2>&1; then
395
+ fail "seeding claimed success after readiness repair completion became unknown"
396
+ fi
397
+ assert_grep 'remote readiness completion is unknown' "$TMP_ROOT/seed-unknown.out" \
398
+ "unknown readiness did not report its distinct completion state"
399
+ assert_grep '- seed-unknown ' "$TMP_ROOT/seed-parent/data/secondmates.md" \
400
+ "unknown readiness removed the registered route"
401
+ assert_present "$TMP_ROOT/seed-parent/data/seed-unknown/brief.md" \
402
+ "unknown readiness removed the scaffolded brief"
403
+ assert_absent "$TMP_ROOT/seed-unknown-home" \
404
+ "unknown readiness proceeded into remote home provisioning"
405
+ [ "$(cat "$DOCTOR_LOG")" = 'doctor-fix-unknown -
406
+ doctor-fix-unknown --fix' ] || fail "unknown readiness did not occur during the repair stage"$'\n'"$(cat "$DOCTOR_LOG")"
407
+ pass "unknown readiness preserves its route and brief for reconciliation"
408
+
409
+ # A host that cannot hold a durable second mate must be rejected by the
410
+ # readiness gate before any home is created on it, and the operator must get the
411
+ # gap text rather than a bare refusal.
412
+ : > "$DOCTOR_LOG"
413
+ if FM_SECONDMATE_CHARTER='Unready host charter.' FM_SECONDMATE_SCOPE='unready host' \
414
+ FM_FAKE_SSH_MODE=doctor-human seed_env "$ROOT/bin/fm-remote-home-seed.sh" \
415
+ seed-toolless remote-mac "$REMOTE_ROOT" "$TMP_ROOT/seed-toolless-home" --no-projects \
416
+ > "$TMP_ROOT/seed-toolless.out" 2>&1; then
417
+ fail "seeding proceeded against a host that is not ready for a remote second mate"
418
+ fi
419
+ assert_grep 'check gui-session=human:' \
420
+ "$TMP_ROOT/seed-toolless.out" "the seed hid the remaining human gap"
421
+ assert_grep 'action: gui-session:' \
422
+ "$TMP_ROOT/seed-toolless.out" "the seed hid the operator step that closes the gap"
423
+ assert_grep 'remote runtime preflight failed' "$TMP_ROOT/seed-toolless.out" \
424
+ "the seed did not report the failing stage"
425
+ assert_absent "$TMP_ROOT/seed-toolless-home" "the seed provisioned a home despite a failing preflight"
426
+ assert_no_grep '- seed-toolless ' "$TMP_ROOT/seed-parent/data/secondmates.md" \
427
+ "the refused route survived the preflight rollback"
428
+ assert_absent "$TMP_ROOT/seed-parent/data/seed-toolless/brief.md" \
429
+ "the refused route left its scaffolded charter behind"
430
+ [ "$(cat "$DOCTOR_LOG")" = 'doctor-human -
431
+ doctor-human --fix
432
+ doctor-human -' ] || fail "the seed did not run the check, repair, re-check sequence"$'\n'"$(cat "$DOCTOR_LOG")"
433
+ pass "remote seeding checks, repairs, and re-checks readiness, then stops on a remaining gap"
434
+
435
+ # The same gate must accept a host whose only gaps were repairable.
436
+ : > "$DOCTOR_LOG"
437
+ rm -f "$TMP_ROOT/doctor.repaired"
438
+ out=$(FM_SECONDMATE_CHARTER='Repairable host charter.' FM_SECONDMATE_SCOPE='repairable host' \
439
+ FM_FAKE_SSH_MODE=doctor-fixable seed_env "$ROOT/bin/fm-remote-home-seed.sh" \
440
+ seed-repair remote-mac "$REMOTE_ROOT" "$TMP_ROOT/seed-repair-home" --no-projects 2>&1) \
441
+ || fail "seeding refused a host whose gaps the repair closed"$'\n'"$out"
442
+ assert_present "$TMP_ROOT/seed-repair-home/.fm-secondmate-home" "the repaired host was never provisioned"
443
+ assert_grep '- seed-repair ' "$TMP_ROOT/seed-parent/data/secondmates.md" "the repaired route was not registered"
444
+ [ "$(cat "$DOCTOR_LOG")" = 'doctor-fixable -
445
+ doctor-fixable --fix
446
+ doctor-fixable -' ] || fail "the repaired seed did not re-check after its repair"$'\n'"$(cat "$DOCTOR_LOG")"
447
+ pass "remote seeding proceeds once the repair closes every gap"
448
+
449
+ # Seeding must not need a copy of the project in this home: firstmate names the
450
+ # origin it already resolved, the seed validates and transports it, and the
451
+ # primary project tree is left exactly as it was found.
452
+ projects_snapshot() { # <dir>
453
+ local dir=$1 path
454
+ (
455
+ cd "$dir" 2>/dev/null || exit 0
456
+ find . -print | LC_ALL=C sort | while IFS= read -r path; do
457
+ if [ -f "$path" ] && [ ! -L "$path" ]; then
458
+ printf '%s %s\n' "$path" "$(sha256_file "$path")"
459
+ else
460
+ printf '%s\n' "$path"
461
+ fi
462
+ done
463
+ )
464
+ }
465
+ mkdir -p "$TMP_ROOT/seed-parent/projects"
466
+ fm_git_init_commit "$TMP_ROOT/seed-parent/projects/resident"
467
+ git init -q --bare "$TMP_ROOT/beta.git"
468
+ fm_git_init_commit "$TMP_ROOT/beta-src"
469
+ git -C "$TMP_ROOT/beta-src" remote add origin "file://$TMP_ROOT/beta.git"
470
+ git -C "$TMP_ROOT/beta-src" push -q -u origin HEAD
471
+ rm -rf "$TMP_ROOT/beta-src"
472
+ cat > "$TMP_ROOT/seed-parent/data/projects.md" <<'EOF'
473
+ - beta [direct-PR] - beta project (added 2026-08-06)
474
+ - delta [local-only] - delta project (added 2026-08-06)
475
+ EOF
476
+ BETA_ORIGIN="file://$TMP_ROOT/beta.git"
477
+ PROJECTS_BEFORE=$(projects_snapshot "$TMP_ROOT/seed-parent/projects")
478
+
479
+ if FM_SECONDMATE_CHARTER='Unsupplied origin charter.' FM_SECONDMATE_SCOPE='unsupplied origin' \
480
+ seed_env "$ROOT/bin/fm-remote-home-seed.sh" seed-noorigin remote-mac "$REMOTE_ROOT" \
481
+ "$TMP_ROOT/seed-noorigin-home" beta > "$TMP_ROOT/seed-noorigin.out" 2>&1; then
482
+ fail "seeding an uncloned project with no origin claimed success"
483
+ fi
484
+ assert_grep 'pass beta=<origin-url>' "$TMP_ROOT/seed-noorigin.out" \
485
+ "the refusal did not name how to supply the origin"
486
+ assert_absent "$TMP_ROOT/seed-noorigin-home" "the unresolvable origin still provisioned a remote home"
487
+
488
+ if FM_SECONDMATE_CHARTER='Unsafe origin charter.' FM_SECONDMATE_SCOPE='unsafe origin' \
489
+ seed_env "$ROOT/bin/fm-remote-home-seed.sh" seed-unsafe remote-mac "$REMOTE_ROOT" \
490
+ "$TMP_ROOT/seed-unsafe-home" 'beta=ext::git-upload-pack' \
491
+ > "$TMP_ROOT/seed-unsafe.out" 2>&1; then
492
+ fail "seeding accepted a remote-helper origin the remote host would execute"
493
+ fi
494
+ assert_grep 'not an accepted clone URL' "$TMP_ROOT/seed-unsafe.out" \
495
+ "the unsafe-origin refusal did not name the reason"
496
+ assert_absent "$TMP_ROOT/seed-unsafe-home" "the unsafe origin still provisioned a remote home"
497
+
498
+ if FM_SECONDMATE_CHARTER='Local-only charter.' FM_SECONDMATE_SCOPE='local only' \
499
+ seed_env "$ROOT/bin/fm-remote-home-seed.sh" seed-localonly remote-mac "$REMOTE_ROOT" \
500
+ "$TMP_ROOT/seed-localonly-home" "delta=$BETA_ORIGIN" \
501
+ > "$TMP_ROOT/seed-localonly.out" 2>&1; then
502
+ fail "a supplied origin bypassed the local-only delivery-mode refusal"
503
+ fi
504
+ assert_grep 'is local-only and cannot be provisioned remotely' "$TMP_ROOT/seed-localonly.out" \
505
+ "the local-only refusal did not name the registered mode"
506
+
507
+ if FM_SECONDMATE_CHARTER='Unregistered charter.' FM_SECONDMATE_SCOPE='unregistered' \
508
+ seed_env "$ROOT/bin/fm-remote-home-seed.sh" seed-unregistered remote-mac "$REMOTE_ROOT" \
509
+ "$TMP_ROOT/seed-unregistered-home" "gamma=$BETA_ORIGIN" \
510
+ > "$TMP_ROOT/seed-unregistered.out" 2>&1; then
511
+ fail "a supplied origin bypassed the project registry requirement"
512
+ fi
513
+ assert_grep 'has no registry record' "$TMP_ROOT/seed-unregistered.out" \
514
+ "the unregistered-project refusal did not name the missing record"
515
+
516
+ out=$(FM_SECONDMATE_CHARTER='Own beta delivery on the build Mac.' \
517
+ FM_SECONDMATE_SCOPE='beta delivery and validation' \
518
+ seed_env "$ROOT/bin/fm-remote-home-seed.sh" seed-noclone remote-mac "$REMOTE_ROOT" \
519
+ "$TMP_ROOT/seed-noclone-home" "beta=$BETA_ORIGIN" 2>&1) \
520
+ || fail "seeding refused a registered project whose origin was supplied"$'\n'"$out"
521
+ assert_contains "$out" "home=remote-mac:$TMP_ROOT/seed-noclone-home" \
522
+ "the no-clone seed did not report the host-qualified home"
523
+ assert_grep '- seed-noclone ' "$TMP_ROOT/seed-parent/data/secondmates.md" \
524
+ "the no-clone seed did not register the remote route"
525
+ assert_present "$TMP_ROOT/seed-noclone-home/projects/beta/README.md" \
526
+ "the remote host did not clone the supplied origin"
527
+ [ "$(git -C "$TMP_ROOT/seed-noclone-home/projects/beta" remote get-url origin)" = "$BETA_ORIGIN" ] \
528
+ || fail "the remote clone did not come from the supplied origin"
529
+ assert_grep '- beta [direct-PR]' "$TMP_ROOT/seed-noclone-home/data/projects.md" \
530
+ "the remote home did not publish the project's registered posture"
531
+ assert_absent "$TMP_ROOT/seed-parent/projects/beta" \
532
+ "seeding cloned the project into the primary project tree"
533
+ [ "$(projects_snapshot "$TMP_ROOT/seed-parent/projects")" = "$PROJECTS_BEFORE" ] \
534
+ || fail "seeding changed the primary project tree"
535
+ pass "remote seeding provisions a supplied origin without touching the primary project tree"
536
+
537
+ # The receiving host validates the origin itself rather than trusting whatever
538
+ # reached it, so a manifest naming an executable transport provisions nothing.
539
+ printf 'schema=fm-remote-home-provision.v1\nid_b64=%s\ncharter_b64=%s\nproject_count=1\nproject=%s|%s|%s|%s\n' \
540
+ "$(printf unsafe-origin | base64 | tr -d '\n')" \
541
+ "$(printf 'Unsafe origin manifest charter.\n' | base64 | tr -d '\n')" \
542
+ "$(printf beta | base64 | tr -d '\n')" \
543
+ "$(printf 'ext::git-upload-pack' | base64 | tr -d '\n')" \
544
+ "$(printf -- '- beta [direct-PR] - beta project (added 2026-08-06)' | base64 | tr -d '\n')" \
545
+ "$(printf direct-PR | base64 | tr -d '\n')" \
546
+ > "$TMP_ROOT/unsafe-origin.manifest"
547
+ if FM_HOME="$TMP_ROOT/unsafe-origin-home" FM_ROOT_OVERRIDE="$REMOTE_ROOT" \
548
+ "$REMOTE_ROOT/bin/fm-remote-home-provision.sh" < "$TMP_ROOT/unsafe-origin.manifest" \
549
+ > "$TMP_ROOT/unsafe-origin.out" 2>&1; then
550
+ fail "remote provisioning accepted an origin the transport had not validated"
551
+ fi
552
+ assert_grep 'not an accepted clone URL' "$TMP_ROOT/unsafe-origin.out" \
553
+ "remote provisioning did not name the rejected origin"
554
+ assert_absent "$TMP_ROOT/unsafe-origin-home" "the rejected manifest left a remote home behind"
555
+ pass "remote provisioning re-validates a supplied origin at the receiving host"
556
+
557
+ # Firstmate is a shared template, so seeding must carry a project origin from any
558
+ # forge or host, not a privileged one. These four URL shapes have to survive the
559
+ # parent's validation, the manifest, the transport, and the receiving host's own
560
+ # validation, and arrive at git unchanged. A fixture resolver records the exact
561
+ # clone source the remote side hands to git and then serves it from a local bare
562
+ # repository, because an offline run cannot reach bitbucket.org itself.
563
+ FORGE_CLONE_LOG="$TMP_ROOT/forge-clone.log"
564
+ FORGE_ORIGIN_MAP="$TMP_ROOT/forge-origin.map"
565
+ : > "$FORGE_CLONE_LOG"
566
+ : > "$FORGE_ORIGIN_MAP"
567
+ forge_project() { # <project> <origin-url>
568
+ local project=$1 origin=$2 tab
569
+ tab=$(printf '\t')
570
+ fm_git_init_commit "$TMP_ROOT/forge-src-$project"
571
+ printf 'served from %s\n' "$origin" > "$TMP_ROOT/forge-src-$project/ORIGIN.txt"
572
+ git -C "$TMP_ROOT/forge-src-$project" add ORIGIN.txt
573
+ git -C "$TMP_ROOT/forge-src-$project" \
574
+ -c user.name='Firstmate Tests' -c user.email='tests@example.invalid' commit -qm origin
575
+ git clone --quiet --bare "$TMP_ROOT/forge-src-$project" "$TMP_ROOT/forge-$project.git"
576
+ rm -rf "$TMP_ROOT/forge-src-$project"
577
+ printf '%s%s%s\n' "$origin" "$tab" "$TMP_ROOT/forge-$project.git" >> "$FORGE_ORIGIN_MAP"
578
+ printf -- '- %s [direct-PR] - %s project (added 2026-08-06)\n' "$project" "$project" \
579
+ >> "$TMP_ROOT/seed-parent/data/projects.md"
580
+ }
581
+ forge_project bitbucket-app 'https://bitbucket.org/team/bitbucket-app.git'
582
+ forge_project ghe-app 'https://git.example.com/org/ghe-app.git'
583
+ forge_project gitlab-app 'ssh://git@gitlab.self.hosted:2222/group/subgroup/gitlab-app.git'
584
+ forge_project scp-app 'git@host.internal:group/scp-app.git'
585
+
586
+ cat > "$REMOTE_ROOT/bin/git" <<SH
587
+ #!/usr/bin/env bash
588
+ # Fixture origin resolver for the remote side: record the clone source exactly as
589
+ # the production code hands it to git, then serve any recorded URL from a local
590
+ # bare repository so the run stays offline. Everything else is real git.
591
+ set -u
592
+ if [ "\${1:-}" = clone ]; then
593
+ printf '%s\n' "\$*" >> '$FORGE_CLONE_LOG'
594
+ args=()
595
+ for arg in "\$@"; do
596
+ replacement=\$(awk -v k="\$arg" -F'\t' '\$1 == k { print \$2; exit }' '$FORGE_ORIGIN_MAP' 2>/dev/null)
597
+ if [ -n "\$replacement" ]; then args+=("\$replacement"); else args+=("\$arg"); fi
598
+ done
599
+ exec '$REAL_GIT' "\${args[@]}"
600
+ fi
601
+ exec '$REAL_GIT' "\$@"
602
+ SH
603
+ chmod +x "$REMOTE_ROOT/bin/git"
604
+
605
+ FORGE_HOME="$TMP_ROOT/seed-forge-home"
606
+ out=$(FM_SECONDMATE_CHARTER='Own delivery for projects hosted anywhere.' \
607
+ FM_SECONDMATE_SCOPE='multi-forge delivery' \
608
+ seed_env "$ROOT/bin/fm-remote-home-seed.sh" seed-forge remote-mac "$REMOTE_ROOT" \
609
+ "$FORGE_HOME" \
610
+ 'bitbucket-app=https://bitbucket.org/team/bitbucket-app.git' \
611
+ 'ghe-app=https://git.example.com/org/ghe-app.git' \
612
+ 'gitlab-app=ssh://git@gitlab.self.hosted:2222/group/subgroup/gitlab-app.git' \
613
+ 'scp-app=git@host.internal:group/scp-app.git' 2>&1) \
614
+ || fail "seeding refused origins hosted outside GitHub"$'\n'"$out"
615
+
616
+ while IFS="$(printf '\t')" read -r forge_origin _; do
617
+ [ -n "$forge_origin" ] || continue
618
+ assert_grep "$forge_origin" "$FORGE_CLONE_LOG" \
619
+ "the remote host did not clone from the supplied origin $forge_origin"
620
+ done < "$FORGE_ORIGIN_MAP"
621
+ for forge_project_name in bitbucket-app ghe-app gitlab-app scp-app; do
622
+ assert_present "$FORGE_HOME/projects/$forge_project_name/.git" \
623
+ "the remote home has no clone for $forge_project_name"
624
+ assert_grep "$forge_project_name" "$FORGE_HOME/data/projects.md" \
625
+ "the remote registry omitted $forge_project_name"
626
+ assert_absent "$TMP_ROOT/seed-parent/projects/$forge_project_name" \
627
+ "seeding $forge_project_name cloned it into the primary project tree"
628
+ done
629
+ # Each clone must carry its own origin's content, so one shared fixture repo
630
+ # cannot make a mismatched route look routed.
631
+ [ "$(cat "$FORGE_HOME/projects/bitbucket-app/ORIGIN.txt")" = \
632
+ 'served from https://bitbucket.org/team/bitbucket-app.git' ] \
633
+ || fail "the bitbucket route did not clone its own origin"
634
+ [ "$(cat "$FORGE_HOME/projects/scp-app/ORIGIN.txt")" = \
635
+ 'served from git@host.internal:group/scp-app.git' ] \
636
+ || fail "the scp-like route did not clone its own origin"
637
+ [ "$(projects_snapshot "$TMP_ROOT/seed-parent/projects")" = "$PROJECTS_BEFORE" ] \
638
+ || fail "seeding non-GitHub projects changed the primary project tree"
639
+ assert_grep '- seed-forge ' "$TMP_ROOT/seed-parent/data/secondmates.md" \
640
+ "the multi-forge route was not registered"
641
+
642
+ rm -f "$REMOTE_ROOT/bin/git"
643
+ [ -z "$(git -C "$REMOTE_ROOT" status --porcelain)" ] \
644
+ || fail "the fixture origin resolver was left behind in the remote code root"
645
+ pass "seeding carries bitbucket, self-hosted, and scp-like origins through to the remote clone"
646
+
647
+ # Provision and register the remote route from the captain-facing primary.
648
+ out=$(FM_SECONDMATE_CHARTER='Own iOS delivery on the build Mac.' \
649
+ FM_SECONDMATE_SCOPE='iOS implementation and Xcode validation' \
650
+ remote_env "$ROOT/bin/fm-remote-home-seed.sh" ios remote-mac "$REMOTE_ROOT" "$REMOTE_HOME" alpha)
651
+ assert_contains "$out" "home=remote-mac:$REMOTE_HOME" "remote seed did not report the host-qualified home"
652
+ assert_grep 'host: remote-mac; root:' "$PARENT/data/secondmates.md" "registry did not record the remote host dimension"
653
+ assert_present "$REMOTE_HOME/.fm-secondmate-home" "remote provisioning did not publish the identity marker"
654
+ assert_present "$REMOTE_HOME/projects/alpha/.git" "remote provisioning did not clone the project on that host"
655
+ assert_grep "$REMOTE_HOME/state/parent-replies.status" "$REMOTE_HOME/data/charter.md" "remote charter did not use its append-only reply log"
656
+ assert_no_grep "$PARENT/state/ios.status" "$REMOTE_HOME/data/charter.md" "remote charter retained the inaccessible local status path"
657
+ if FM_SECONDMATE_CHARTER='Own iOS delivery on the build Mac.' \
658
+ FM_SECONDMATE_SCOPE='iOS implementation and Xcode validation' \
659
+ remote_env "$ROOT/bin/fm-remote-home-seed.sh" ios remote-mac "$REMOTE_ROOT" "$TMP_ROOT/other-home" alpha \
660
+ >/dev/null 2>&1; then
661
+ fail "remote seed allowed an existing id to move to another home"
662
+ fi
663
+ assert_grep "home: $REMOTE_HOME" "$PARENT/data/secondmates.md" "refused remote reassignment changed the durable route"
664
+ pass "remote seed registers the route and provisions the whole home and project clone on that host"
665
+
666
+ PROTOCOL_HOME="$TMP_ROOT/protocol-home"
667
+ mkdir -p "$PROTOCOL_HOME/config" "$PROTOCOL_HOME/data" "$PROTOCOL_HOME/state"
668
+ printf 'complete inherited payload\n' > "$TMP_ROOT/inherit-complete"
669
+ inherit_bytes=$(LC_ALL=C wc -c < "$TMP_ROOT/inherit-complete" | tr -d ' ')
670
+ inherit_hash=$(sha256_file "$TMP_ROOT/inherit-complete")
671
+ if printf 'complete' | FM_HOME="$PROTOCOL_HOME" "$REMOTE_ROOT/bin/fm-remote-inherit.sh" \
672
+ put config/crew-harness "$inherit_bytes" "$inherit_hash" 1 >/dev/null 2>&1; then
673
+ fail "remote inheritance published a truncated payload"
674
+ fi
675
+ assert_absent "$PROTOCOL_HOME/config/crew-harness" "truncated inheritance published a destination"
676
+ FM_HOME="$PROTOCOL_HOME" "$REMOTE_ROOT/bin/fm-remote-inherit.sh" \
677
+ put config/crew-harness "$inherit_bytes" "$inherit_hash" 2 \
678
+ < "$TMP_ROOT/inherit-complete" >/dev/null
679
+ printf 'stale inherited payload\n' > "$TMP_ROOT/inherit-stale"
680
+ inherit_stale_bytes=$(LC_ALL=C wc -c < "$TMP_ROOT/inherit-stale" | tr -d ' ')
681
+ inherit_stale_hash=$(sha256_file "$TMP_ROOT/inherit-stale")
682
+ if FM_HOME="$PROTOCOL_HOME" "$REMOTE_ROOT/bin/fm-remote-inherit.sh" \
683
+ put config/crew-harness "$inherit_stale_bytes" "$inherit_stale_hash" 1 \
684
+ < "$TMP_ROOT/inherit-stale" >/dev/null 2>&1; then
685
+ fail "remote inheritance accepted a superseded payload generation"
686
+ fi
687
+ cmp -s "$TMP_ROOT/inherit-complete" "$PROTOCOL_HOME/config/crew-harness" \
688
+ || fail "superseded inheritance replaced the current payload"
689
+ pass "remote inheritance rejects incomplete and superseded payload generations"
690
+
691
+ # Add one local route to prove mixed fleets remain parseable and projected.
692
+ mkdir -p "$LOCAL_HOME/data" "$LOCAL_HOME/state" "$LOCAL_HOME/config" "$LOCAL_HOME/projects" "$LOCAL_HOME/bin"
693
+ printf 'local\n' > "$LOCAL_HOME/.fm-secondmate-home"
694
+ printf 'fixture\n' > "$LOCAL_HOME/AGENTS.md"
695
+ printf '## In flight\n\n## Queued\n\n## Done\n' > "$LOCAL_HOME/data/backlog.md"
696
+ cat >> "$PARENT/data/secondmates.md" <<EOF
697
+ - local - Local delivery (home: $LOCAL_HOME; scope: local work; projects: alpha; added 2026-08-02)
698
+ EOF
699
+ remote_env "$ROOT/bin/fm-home-seed.sh" validate >/dev/null || fail "mixed local and remote registry validation failed"
700
+ pass "mixed local and remote routes validate without migration"
701
+
702
+ # Launch on the remote home's own configured backend. Parent metadata records
703
+ # host placement separately from that backend and arms the reply source.
704
+ printf 'pi\n' > "$PARENT/config/crew-harness"
705
+ launches_before_inherit=0
706
+ [ ! -f "$HERDR_LOG" ] || launches_before_inherit=$(grep -c '^tab create' "$HERDR_LOG" || true)
707
+ if FM_FAKE_SSH_MODE=inherit-partial remote_env "$ROOT/bin/fm-spawn.sh" ios --secondmate \
708
+ > "$TMP_ROOT/spawn-inherit-partial.out" 2>&1; then
709
+ fail "remote spawn launched after ambiguous partial inheritance"
710
+ fi
711
+ launches_after_inherit=0
712
+ [ ! -f "$HERDR_LOG" ] || launches_after_inherit=$(grep -c '^tab create' "$HERDR_LOG" || true)
713
+ [ "$launches_before_inherit" -eq "$launches_after_inherit" ] \
714
+ || fail "remote spawn reached launch after ambiguous partial inheritance"
715
+ assert_absent "$PARENT/state/ios.meta" "failed remote inheritance published launch metadata"
716
+ out=$(remote_env "$ROOT/bin/fm-spawn.sh" ios --secondmate)
717
+ assert_contains "$out" 'remote=remote-mac backend=herdr' "remote spawn did not report separate host and backend dimensions"
718
+ assert_grep 'remote_host=remote-mac' "$PARENT/state/ios.meta" "parent metadata omitted the remote host"
719
+ assert_grep 'remote_backend=herdr' "$PARENT/state/ios.meta" "parent metadata omitted the remote-local backend"
720
+ assert_grep 'remote_herdr_session=fm-remote' "$PARENT/state/ios.meta" "parent metadata omitted the pinned remote Herdr session"
721
+ assert_grep 'remote_target=fm-remote:' "$PARENT/state/ios.meta" "parent metadata did not record an fm-remote endpoint"
722
+ assert_grep 'herdr_session=fm-remote' "$REMOTE_HOME/state/parent-route/ios.meta" "remote metadata did not record the pinned Herdr session"
723
+ assert_grep '--session fm-remote' "$HERDR_LOG" "remote launch did not target the fm-remote session"
724
+ assert_no_grep '--session default' "$HERDR_LOG" "remote launch targeted the interactive default session"
725
+ assert_grep 'window=remote:ios' "$PARENT/state/ios.meta" "parent metadata pretended the endpoint was local"
726
+ assert_present "$PARENT/state/procevent/remote-reply-ios.source" "remote spawn did not arm its reply source"
727
+ publish_healthy_watcher_identity "$PARENT/state" "$PARENT" "$ROOT/bin/fm-watch.sh"
728
+ [ "$(remote_env "$ROOT/bin/fm-on.sh" ios fm-remote-secondmate-control.sh state ios)" = alive ] \
729
+ || fail "remote endpoint was not projected alive from its own host"
730
+ # Herdr reports a native agent state, so the delivery observation resolves
731
+ # without the rendered-output fallback a tmux endpoint needs.
732
+ [ "$(remote_env "$ROOT/bin/fm-on.sh" ios fm-remote-secondmate-control.sh observe ios)" = idle ] \
733
+ || fail "remote endpoint delivery observation did not execute on its own host"
734
+ pass "remote spawn launches on the remote-local backend and records a host-qualified route"
735
+
736
+ remote_route_meta="$REMOTE_HOME/state/parent-route/ios.meta"
737
+ cp "$remote_route_meta" "$TMP_ROOT/remote-ios-before-default-session.meta"
738
+ legacy_pane=$(sed -n 's/^herdr_pane_id=//p' "$remote_route_meta")
739
+ awk -v pane="$legacy_pane" '
740
+ /^window=/ { print "window=default:" pane; next }
741
+ /^herdr_session=/ { print "herdr_session=default"; next }
742
+ { print }
743
+ ' "$TMP_ROOT/remote-ios-before-default-session.meta" > "$remote_route_meta"
744
+ cp "$HERDR_LOG" "$TMP_ROOT/herdr-before-default-session.log"
745
+ [ "$(remote_env "$ROOT/bin/fm-on.sh" ios fm-remote-secondmate-control.sh state ios 2>/dev/null)" = unverified ] \
746
+ || fail "legacy default-session metadata was not classified unverified"
747
+ if remote_env "$ROOT/bin/fm-on.sh" ios fm-remote-secondmate-control.sh route ios >/dev/null 2>&1 \
748
+ || remote_env "$ROOT/bin/fm-on.sh" ios fm-remote-secondmate-control.sh send ios probe >/dev/null 2>&1 \
749
+ || remote_env "$ROOT/bin/fm-on.sh" ios fm-remote-secondmate-control.sh key ios Enter >/dev/null 2>&1 \
750
+ || remote_env "$ROOT/bin/fm-on.sh" ios fm-remote-secondmate-control.sh capture ios >/dev/null 2>&1 \
751
+ || remote_env "$ROOT/bin/fm-on.sh" ios fm-remote-secondmate-control.sh observe ios >/dev/null 2>&1 \
752
+ || remote_env "$ROOT/bin/fm-on.sh" ios fm-remote-secondmate-control.sh retire ios --force >/dev/null 2>&1 \
753
+ || remote_env "$ROOT/bin/fm-on.sh" ios fm-remote-secondmate-control.sh launch ios codex - - herdr >/dev/null 2>&1; then
754
+ fail "legacy default-session metadata remained operational"
755
+ fi
756
+ cmp -s "$TMP_ROOT/herdr-before-default-session.log" "$HERDR_LOG" \
757
+ || fail "legacy default-session metadata caused a Herdr operation"
758
+ assert_present "$REMOTE_HOME" "refused legacy retirement removed the remote home"
759
+ assert_grep 'herdr_session=default' "$remote_route_meta" "refused legacy retirement rewrote endpoint metadata"
760
+
761
+ awk -v pane="$legacy_pane" '
762
+ /^window=/ { print "window=default:" pane; next }
763
+ { print }
764
+ ' "$TMP_ROOT/remote-ios-before-default-session.meta" > "$remote_route_meta"
765
+ [ "$(remote_env "$ROOT/bin/fm-on.sh" ios fm-remote-secondmate-control.sh state ios 2>/dev/null)" = unverified ] \
766
+ || fail "mismatched fm-remote target was not classified unverified"
767
+ cmp -s "$TMP_ROOT/herdr-before-default-session.log" "$HERDR_LOG" \
768
+ || fail "mismatched fm-remote target caused a Herdr operation"
769
+ mv -f "$TMP_ROOT/remote-ios-before-default-session.meta" "$remote_route_meta"
770
+ pass "legacy and mismatched remote endpoints fail closed before backend access"
771
+
772
+ cp "$PARENT/state/ios.meta" "$TMP_ROOT/parent-ios-before-nonherdr.meta"
773
+ cp "$PARENT/data/secondmates.md" "$TMP_ROOT/registry-before-nonherdr.md"
774
+ set +e
775
+ FM_FAKE_SSH_MODE=launch-nonherdr-route remote_env "$ROOT/bin/fm-spawn.sh" ios --secondmate \
776
+ > "$TMP_ROOT/spawn-nonherdr-route.out" 2>&1
777
+ nonherdr_parent_rc=$?
778
+ set -e
779
+ [ "$nonherdr_parent_rc" -ne 0 ] || fail "parent accepted a non-herdr remote launch route"
780
+ assert_grep "remote launch returned backend 'tmux', expected herdr" "$TMP_ROOT/spawn-nonherdr-route.out" \
781
+ "parent refusal did not name the returned remote backend"
782
+ cmp -s "$TMP_ROOT/parent-ios-before-nonherdr.meta" "$PARENT/state/ios.meta" \
783
+ || fail "parent rewrote its endpoint metadata after a non-herdr route refusal"
784
+ cmp -s "$TMP_ROOT/registry-before-nonherdr.md" "$PARENT/data/secondmates.md" \
785
+ || fail "parent removed or changed the registry route after a non-herdr route refusal"
786
+
787
+ set +e
788
+ FM_FAKE_SSH_MODE=launch-default-session-route remote_env "$ROOT/bin/fm-spawn.sh" ios --secondmate \
789
+ > "$TMP_ROOT/spawn-default-session-route.out" 2>&1
790
+ default_session_parent_rc=$?
791
+ set -e
792
+ [ "$default_session_parent_rc" -ne 0 ] || fail "parent accepted an interactive default-session remote route"
793
+ assert_grep "remote launch returned Herdr session 'default', expected 'fm-remote'" "$TMP_ROOT/spawn-default-session-route.out" \
794
+ "parent refusal did not name the default session"
795
+ cmp -s "$TMP_ROOT/parent-ios-before-nonherdr.meta" "$PARENT/state/ios.meta" \
796
+ || fail "parent rewrote its endpoint metadata after a default-session route refusal"
797
+
798
+ remote_route_meta="$REMOTE_HOME/state/parent-route/ios.meta"
799
+ cp "$remote_route_meta" "$TMP_ROOT/remote-ios-before-legacy.meta"
800
+ cat > "$remote_route_meta" <<EOF
801
+ window=firstmate:fm-ios
802
+ worktree=$REMOTE_HOME
803
+ project=$REMOTE_ROOT
804
+ harness=codex
805
+ kind=secondmate
806
+ backend=tmux
807
+ EOF
808
+ cp "$remote_route_meta" "$TMP_ROOT/remote-ios-legacy-before-refusal.meta"
809
+ printf 'fm-ios|%s\n' "$REMOTE_HOME" > "$TMUX_STATE"
810
+ set +e
811
+ remote_env "$ROOT/bin/fm-on.sh" ios fm-remote-secondmate-control.sh launch ios codex - - herdr \
812
+ > "$TMP_ROOT/legacy-alive-refusal.out" 2>&1
813
+ legacy_alive_rc=$?
814
+ set -e
815
+ [ "$legacy_alive_rc" -ne 0 ] || fail "remote control reused an alive legacy tmux endpoint"
816
+ assert_grep "endpoint is recorded on backend 'tmux', expected 'herdr'" "$TMP_ROOT/legacy-alive-refusal.out" \
817
+ "remote refusal did not name the endpoint's recorded backend"
818
+ cmp -s "$TMP_ROOT/remote-ios-legacy-before-refusal.meta" "$remote_route_meta" \
819
+ || fail "remote refusal changed the legacy endpoint metadata"
820
+ assert_present "$TMUX_STATE" "remote refusal killed the alive legacy endpoint"
821
+ cmp -s "$TMP_ROOT/registry-before-nonherdr.md" "$PARENT/data/secondmates.md" \
822
+ || fail "remote legacy refusal removed or changed the registry route"
823
+ mv -f "$TMP_ROOT/remote-ios-before-legacy.meta" "$remote_route_meta"
824
+ rm -f "$TMUX_STATE"
825
+ pass "non-herdr remote endpoints are refused without changing either route"
826
+
827
+ rm -f "$TMP_ROOT/inherit.entered" "$TMP_ROOT/inherit.release" "$TMP_ROOT/inherit.payload"
828
+ cat > "$PARENT/data/captain-shared.md" <<'EOF'
829
+ # Shared captain preferences
830
+ This file is main-authoritative and maintained by the main firstmate.
831
+ It is read-only in secondmate homes and must not be edited there.
832
+ Changes return through a marked status document pointer.
833
+ stale spawn preference
834
+ EOF
835
+ FM_FAKE_SSH_MODE=inherit-block remote_env "$ROOT/bin/fm-spawn.sh" ios --secondmate \
836
+ > "$TMP_ROOT/spawn-concurrent.out" 2>&1 &
837
+ spawn_concurrent=$!
838
+ spawn_inherit_wait=0
839
+ # Earlier inherited files traverse the worker before captain-shared.md, so give
840
+ # a loaded portable runner 30 seconds to reach this deliberately blocked write.
841
+ while [ ! -f "$TMP_ROOT/inherit.entered" ]; do
842
+ kill -0 "$spawn_concurrent" 2>/dev/null || fail "remote spawn exited before its blocked inheritance write"
843
+ spawn_inherit_wait=$((spawn_inherit_wait + 1))
844
+ [ "$spawn_inherit_wait" -le 1500 ] || fail "remote spawn never reached its blocked inheritance write"
845
+ sleep 0.02
846
+ done
847
+ cat > "$PARENT/data/captain-shared.md" <<'EOF'
848
+ # Shared captain preferences
849
+ This file is main-authoritative and maintained by the main firstmate.
850
+ It is read-only in secondmate homes and must not be edited there.
851
+ Changes return through a marked status document pointer.
852
+ current post-spawn preference
853
+ EOF
854
+ remote_env "$ROOT/bin/fm-config-push.sh" > "$TMP_ROOT/spawn-concurrent-push.out" 2>&1 &
855
+ spawn_config_push=$!
856
+ sleep 0.2
857
+ kill -0 "$spawn_config_push" 2>/dev/null \
858
+ || fail "config push bypassed the active remote spawn inheritance transaction"
859
+ touch "$TMP_ROOT/inherit.release"
860
+ wait "$spawn_concurrent" || fail "serialized remote spawn failed"
861
+ wait "$spawn_config_push" || fail "config push failed after serialized remote spawn"$'\n'"$(cat "$TMP_ROOT/spawn-concurrent-push.out")"
862
+ [ "$(tail -1 "$REMOTE_HOME/data/captain-shared.md")" = 'current post-spawn preference' ] \
863
+ || fail "stale spawn inheritance overwrote later config convergence"
864
+ pass "remote spawn serializes inheritance through launch publication"
865
+
866
+ # A normal marked parent request traverses SSH as a durable remote inbox
867
+ # record plus a rung doorbell - the payload is never typed into the pane. An
868
+ # ambiguous transport (the remote leg executed, then ssh exit 255) is retried
869
+ # identically once, and the idempotent remote write lands both executions on
870
+ # ONE record; the send reports itself unconfirmed with a correlation-preserving
871
+ # resend command, and the expectation resolves only after the correlated remote log
872
+ # delta is ingested.
873
+ ssh_before_send=$(cat "$SSH_COUNT")
874
+ records_before_send=$(find "$REMOTE_HOME/state/parent-route/ios.inbox" -maxdepth 1 -name '*.msg' 2>/dev/null | wc -l | tr -d ' ')
875
+ set +e
876
+ FM_FAKE_SSH_MODE=ambiguous remote_env "$ROOT/bin/fm-send.sh" fm-ios \
877
+ 'report the build result' > "$TMP_ROOT/send.out" 2> "$TMP_ROOT/send.err"
878
+ send_rc=$?
879
+ set -e
880
+ [ "$send_rc" -ne 0 ] || fail "ambiguous remote send claimed definite delivery"
881
+ assert_grep 'Only the correlation-reusing resend below is idempotent' "$TMP_ROOT/send.err" "ambiguous remote send did not state the correlation-preserving resend boundary"
882
+ assert_no_grep 'do not resend' "$TMP_ROOT/send.err" "ambiguous remote send kept the deleted do-not-resend trap"
883
+ ssh_after_send=$(cat "$SSH_COUNT")
884
+ [ "$ssh_after_send" -eq $((ssh_before_send + 2)) ] \
885
+ || fail "ambiguous remote send was not retried exactly once (ssh calls: $((ssh_after_send - ssh_before_send)))"
886
+ records_after_send=$(find "$REMOTE_HOME/state/parent-route/ios.inbox" -maxdepth 1 -name '*.msg' | wc -l | tr -d ' ')
887
+ [ "$records_after_send" -eq $((records_before_send + 1)) ] \
888
+ || fail "the retried remote steer did not dedup onto one new record, went $records_before_send -> $records_after_send"
889
+ assert_no_grep 'report the build result' "$HERDR_LOG" "the steer payload was typed into the remote pane"
890
+ assert_grep 'Firstmate instruction waiting' "$HERDR_LOG" "the remote doorbell never rang"
891
+ CORR=$(newest_remote_inbox_corr)
892
+ [ -n "$CORR" ] || fail "remote send did not carry a correlation token"
893
+ assert_grep "FM_PENDING_REPLY_EXISTING_CORR=$CORR" "$TMP_ROOT/send.err" "ambiguous remote send did not print its correlation-reusing command"
894
+ phase=$(grep '^phase=' "$PARENT/state/pending-replies/$CORR" | cut -d= -f2-)
895
+ [ "$phase" = delivery_unknown ] || fail "ambiguous remote send did not preserve its pending expectation"
896
+ printf 'done [corr=%s]: remote build passed\n' "$CORR" >> "$REMOTE_HOME/state/parent-replies.status"
897
+ SID='remote-reply-ios'
898
+ remote_env "$ROOT/bin/fm-procevent.sh" start "$SID" >/dev/null \
899
+ || fail "remote reply source did not capture the correlated answer"
900
+ RESULT="$PARENT/state/procevent-inbox/$SID.1.result"
901
+ remote_env "$ROOT/bin/fm-procevent-remote-reply.sh" handle ios 1 "$RESULT" >/dev/null \
902
+ || fail "remote reply ingest failed"
903
+ assert_grep "done [corr=$CORR]: remote build passed" "$PARENT/state/ios.status" "correlated remote reply did not reach the parent status channel"
904
+ phase=$(grep '^phase=' "$PARENT/state/pending-replies/$CORR" | cut -d= -f2-)
905
+ [ "$phase" = resolved ] || fail "correlated remote reply did not resolve the parent expectation"
906
+ pass "marked send and routed reply complete through the existing parent correlation owner"
907
+ rm -f "$PARENT/state/.wake-queue"
908
+
909
+ printf '{"revision":2}\n' > "$PARENT/config/crew-dispatch.json"
910
+ printf 'grok\n' > "$PARENT/config/crew-harness"
911
+ set +e
912
+ FM_FAKE_SSH_MODE=inherit-partial remote_env "$ROOT/bin/fm-config-push.sh" \
913
+ > "$TMP_ROOT/config-partial.out" 2>&1
914
+ config_partial_rc=$?
915
+ set -e
916
+ [ "$config_partial_rc" -ne 0 ] || fail "partial remote inheritance claimed complete convergence"
917
+ assert_grep '"revision":2' "$REMOTE_HOME/config/crew-dispatch.json" "partial inheritance did not apply its first file"
918
+ [ "$(cat "$REMOTE_HOME/config/crew-harness")" != grok ] \
919
+ || fail "partial inheritance unexpectedly applied the failed file"
920
+ NUDGE_MARKER="$PARENT/state/.secondmate-nudge-pending/ios.pending"
921
+ assert_grep 'remote=1' "$NUDGE_MARKER" "partial inheritance left no durable remote reread marker"
922
+ publish_healthy_watcher_identity "$PARENT/state" "$PARENT" "$REMOTE_ROOT/bin/fm-watch.sh"
923
+ remote_env "$ROOT/bin/fm-bootstrap.sh" > "$TMP_ROOT/config-partial-retry.out" \
924
+ || fail "bootstrap did not converge partial remote inheritance"
925
+ [ "$(cat "$REMOTE_HOME/config/crew-harness")" = grok ] \
926
+ || fail "bootstrap did not apply the remaining inherited file"
927
+ assert_absent "$NUDGE_MARKER" "bootstrap cleared no remote reread marker after convergence"
928
+ PARTIAL_CONFIG_CORR=$(newest_remote_inbox_corr)
929
+ [ -n "$PARTIAL_CONFIG_CORR" ] || fail "bootstrap config reread did not carry a correlation token"
930
+ printf 'done [corr=%s]: converged inherited config re-read\n' "$PARTIAL_CONFIG_CORR" >> "$REMOTE_HOME/state/parent-replies.status"
931
+ remote_env "$ROOT/bin/fm-procevent.sh" start "$SID" >/dev/null \
932
+ || fail "remote reply source did not capture the converged config acknowledgment"
933
+ PARTIAL_CONFIG_RESULT="$PARENT/state/procevent-inbox/$SID.2.result"
934
+ remote_env "$ROOT/bin/fm-procevent-remote-reply.sh" handle ios 2 "$PARTIAL_CONFIG_RESULT" >/dev/null \
935
+ || fail "converged remote config acknowledgment was not ingested"
936
+ pass "partial remote inheritance retains reread intent through bootstrap convergence"
937
+
938
+ rm -f "$TMP_ROOT/inherit.entered" "$TMP_ROOT/inherit.release" "$TMP_ROOT/inherit.payload"
939
+ cat > "$PARENT/data/captain-shared.md" <<'EOF'
940
+ # Shared captain preferences
941
+ This file is main-authoritative and maintained by the main firstmate.
942
+ It is read-only in secondmate homes and must not be edited there.
943
+ Changes return through a marked status document pointer.
944
+ stale concurrent preference
945
+ EOF
946
+ FM_FAKE_SSH_MODE=inherit-block remote_env "$ROOT/bin/fm-config-push.sh" \
947
+ > "$TMP_ROOT/config-concurrent-first.out" 2>&1 &
948
+ config_first=$!
949
+ inherit_wait=0
950
+ while [ ! -f "$TMP_ROOT/inherit.entered" ]; do
951
+ kill -0 "$config_first" 2>/dev/null || fail "first inheritance transaction exited before its blocked write"
952
+ inherit_wait=$((inherit_wait + 1))
953
+ # Match the earlier spawn/inheritance wait: a loaded portable runner can
954
+ # spend several seconds in the remote entrypoint before reaching this write.
955
+ [ "$inherit_wait" -le 1500 ] || fail "first inheritance transaction never reached its blocked write"
956
+ sleep 0.02
957
+ done
958
+ cat > "$PARENT/data/captain-shared.md" <<'EOF'
959
+ # Shared captain preferences
960
+ This file is main-authoritative and maintained by the main firstmate.
961
+ It is read-only in secondmate homes and must not be edited there.
962
+ Changes return through a marked status document pointer.
963
+ current concurrent preference
964
+ EOF
965
+ remote_env "$ROOT/bin/fm-bootstrap.sh" > "$TMP_ROOT/config-concurrent-second.out" 2>&1 &
966
+ config_second=$!
967
+ sleep 0.2
968
+ kill -0 "$config_second" 2>/dev/null \
969
+ || fail "bootstrap bypassed the active remote inheritance transaction"
970
+ touch "$TMP_ROOT/inherit.release"
971
+ wait "$config_first" || fail "first serialized inheritance transaction failed"
972
+ wait "$config_second" || fail "bootstrap inheritance transaction failed after waiting"
973
+ [ "$(tail -1 "$REMOTE_HOME/data/captain-shared.md")" = 'current concurrent preference' ] \
974
+ || fail "later bootstrap convergence was overwritten by stale inherited bytes"
975
+ pass "config push and bootstrap serialize remote inheritance convergence"
976
+
977
+ printf 'codex\n' > "$PARENT/config/crew-harness"
978
+ # A failed reread nudge now means the durable remote inbox RECORD could not be
979
+ # written (a swallowed doorbell alone no longer fails a recorded steer), so
980
+ # the failure is induced by making the remote steering inbox unwritable.
981
+ chmod 555 "$REMOTE_HOME/state/parent-route/ios.inbox"
982
+ if remote_env "$ROOT/bin/fm-config-push.sh" > "$TMP_ROOT/config-push-fail.out" 2>&1; then
983
+ chmod 755 "$REMOTE_HOME/state/parent-route/ios.inbox"
984
+ fail "remote config push claimed success after its reread record could not be written"
985
+ fi
986
+ if [ ! -f "$NUDGE_MARKER" ]; then
987
+ chmod 755 "$REMOTE_HOME/state/parent-route/ios.inbox"
988
+ printf 'config push failure output:\n%s\n' "$(cat "$TMP_ROOT/config-push-fail.out")" >&2
989
+ fail "failed remote config reread did not retain a retry marker"
990
+ fi
991
+ assert_grep 'remote=1' "$NUDGE_MARKER" "remote config reread marker lost its placement"
992
+ chmod 755 "$REMOTE_HOME/state/parent-route/ios.inbox"
993
+ remote_env "$ROOT/bin/fm-config-push.sh" > "$TMP_ROOT/config-push-retry.out" \
994
+ || fail "unchanged remote config push did not retry its pending reread"
995
+ assert_absent "$NUDGE_MARKER" "successful remote config reread left its retry marker"
996
+ assert_grep 'config-reread: sent' "$TMP_ROOT/config-push-retry.out" "remote config reread retry was not reported"
997
+ CONFIG_CORR=$(newest_remote_inbox_corr)
998
+ [ -n "$CONFIG_CORR" ] || fail "remote config reread did not carry a correlation token"
999
+ printf 'done [corr=%s]: inherited config re-read\n' "$CONFIG_CORR" >> "$REMOTE_HOME/state/parent-replies.status"
1000
+ remote_env "$ROOT/bin/fm-procevent.sh" start "$SID" >/dev/null \
1001
+ || fail "remote reply source did not capture the config reread acknowledgement"
1002
+ CONFIG_RESULT="$PARENT/state/procevent-inbox/$SID.3.result"
1003
+ remote_env "$ROOT/bin/fm-procevent-remote-reply.sh" handle ios 3 "$CONFIG_RESULT" >/dev/null \
1004
+ || fail "remote config reread acknowledgement was not ingested"
1005
+ pass "remote inherited config retains and retries a failed live reread nudge"
1006
+
1007
+ resolve_ios_pending() {
1008
+ local pending_record pending_corr pending_result pending_seq
1009
+ for pending_record in "$PARENT/state/pending-replies"/*; do
1010
+ [ -f "$pending_record" ] || continue
1011
+ [ "$(grep '^task_id=' "$pending_record" | cut -d= -f2-)" = ios ] || continue
1012
+ [ "$(grep '^phase=' "$pending_record" | cut -d= -f2-)" != resolved ] || continue
1013
+ pending_corr=$(basename "$pending_record")
1014
+ printf 'done [corr=%s]: concurrent inherited data re-read\n' "$pending_corr" \
1015
+ >> "$REMOTE_HOME/state/parent-replies.status"
1016
+ remote_env "$ROOT/bin/fm-procevent.sh" start "$SID" >/dev/null \
1017
+ || fail "remote reply source did not capture a concurrent inheritance acknowledgment"
1018
+ pending_result=$(find "$PARENT/state/procevent-inbox" -name "$SID.*.result" -print | sort | tail -1)
1019
+ pending_seq=${pending_result%.result}
1020
+ pending_seq=${pending_seq##*.}
1021
+ remote_env "$ROOT/bin/fm-procevent-remote-reply.sh" handle ios "$pending_seq" "$pending_result" >/dev/null \
1022
+ || fail "concurrent inheritance acknowledgment was not ingested"
1023
+ done
1024
+ }
1025
+ resolve_ios_pending
1026
+
1027
+ # Structured fleet state comes from each home's own snapshot. The remote host is
1028
+ # explicit, and the local route remains alongside it.
1029
+ SNAPSHOT=$(remote_env "$ROOT/bin/fm-fleet-snapshot.sh" --json)
1030
+ if ! printf '%s' "$SNAPSHOT" | jq -e '.secondmate_current.records | any(.id == "ios" and .remote == true and .host == "remote-mac" and .provenance.selected == "structured-home")' >/dev/null; then
1031
+ printf 'secondmate projection:\n%s\n' "$(printf '%s' "$SNAPSHOT" | jq '.secondmate_current')" >&2
1032
+ fail "fleet snapshot did not select the remote structured-home projection"
1033
+ fi
1034
+ printf '%s' "$SNAPSHOT" | jq -e '.tasks[] | select(.id == "ios") | .paths.home.present == true' >/dev/null \
1035
+ || fail "remote structured observation did not prove the remote home present"
1036
+ printf '%s' "$SNAPSHOT" | jq -e '.secondmate_current.records | any(.id == "local" and .remote == false)' >/dev/null \
1037
+ || fail "fleet snapshot lost the existing local secondmate route"
1038
+ pass "fleet snapshot projects mixed local and remote structured state"
1039
+ rm -f "$PARENT/state/.wake-queue"
1040
+
1041
+ # The remote code root updates independently, then the persistent home imports
1042
+ # and fast-forwards to that host-local commit without touching project clones.
1043
+ REMOTE_SEED="$TMP_ROOT/firstmate-seed"
1044
+ git clone -q "file://$REMOTE_ORIGIN" "$REMOTE_SEED"
1045
+ git -C "$REMOTE_SEED" config user.email test@example.com
1046
+ git -C "$REMOTE_SEED" config user.name Test
1047
+ printf 'remote update probe\n' > "$REMOTE_SEED/REMOTE_UPDATE_PROBE"
1048
+ git -C "$REMOTE_SEED" add REMOTE_UPDATE_PROBE
1049
+ git -C "$REMOTE_SEED" commit -qm 'advance remote code root'
1050
+ git -C "$REMOTE_SEED" push -q origin main
1051
+ UPDATE_OUT=$(remote_env "$ROOT/bin/fm-on.sh" ios fm-remote-secondmate-control.sh update ios)
1052
+ assert_contains "$UPDATE_OUT" 'synced:' "remote update did not report a host-local fast-forward"
1053
+ [ "$(git -C "$REMOTE_HOME" rev-parse HEAD)" = "$(git -C "$REMOTE_ROOT" rev-parse HEAD)" ] \
1054
+ || fail "remote persistent home did not fast-forward to its code-root commit"
1055
+ assert_present "$REMOTE_HOME/REMOTE_UPDATE_PROBE" "remote update did not materialize the code-root commit"
1056
+ pass "remote update imports and fast-forwards the persistent home on its configured host"
1057
+
1058
+ rm -f "$TMP_ROOT/doctor.repaired"
1059
+ : > "$DOCTOR_LOG"
1060
+ [ "$(FM_FAKE_SSH_MODE=doctor-fixable remote_env "$ROOT/bin/fm-on.sh" ios fm-remote-secondmate-control.sh state ios)" = unreadable ] \
1061
+ || fail "the stopped-server fixture did not make the pre-repair endpoint probe unreadable"
1062
+ launches_before_repair=$(grep -c '^tab create' "$HERDR_LOG" || true)
1063
+ BOOT_REPAIRED=$(FM_FAKE_SSH_MODE=doctor-fixable remote_env "$ROOT/bin/fm-bootstrap.sh")
1064
+ [ "$(cat "$DOCTOR_LOG")" = 'doctor-fixable -
1065
+ doctor-fixable --fix
1066
+ doctor-fixable -' ] || fail "liveness did not check, repair, and re-check readiness before probing"$'\n'"$(cat "$DOCTOR_LOG")"
1067
+ assert_not_contains "$BOOT_REPAIRED" 'SECONDMATE_LIVENESS: secondmate ios:' \
1068
+ "successful pre-probe readiness repair produced a liveness failure"
1069
+ launches_after_repair=$(grep -c '^tab create' "$HERDR_LOG" || true)
1070
+ [ "$launches_before_repair" -eq "$launches_after_repair" ] \
1071
+ || fail "readiness repair introduced a new remote relaunch point"
1072
+ [ "$(remote_env "$ROOT/bin/fm-on.sh" ios fm-remote-secondmate-control.sh state ios)" = alive ] \
1073
+ || fail "the endpoint was not probed successfully after readiness repair"
1074
+ pass "startup repairs remote readiness before probing without relaunching"
1075
+
1076
+ remote_route_meta="$REMOTE_HOME/state/parent-route/ios.meta"
1077
+ cp "$remote_route_meta" "$TMP_ROOT/remote-ios-before-liveness-legacy.meta"
1078
+ cp "$PARENT/state/ios.meta" "$TMP_ROOT/parent-ios-before-liveness-legacy.meta"
1079
+ cp "$PARENT/data/secondmates.md" "$TMP_ROOT/registry-before-liveness-legacy.md"
1080
+ cat > "$remote_route_meta" <<EOF
1081
+ window=firstmate:fm-ios
1082
+ worktree=$REMOTE_HOME
1083
+ project=$REMOTE_ROOT
1084
+ harness=codex
1085
+ kind=secondmate
1086
+ backend=tmux
1087
+ EOF
1088
+ cp "$remote_route_meta" "$TMP_ROOT/remote-ios-liveness-legacy.meta"
1089
+ printf 'fm-ios|%s\n' "$REMOTE_HOME" > "$TMUX_STATE"
1090
+ tmux_state_before=$(cat "$TMUX_STATE")
1091
+ launches_before_legacy=$(grep -c '^tab create' "$HERDR_LOG" || true)
1092
+ BOOT_LEGACY=$(remote_env "$ROOT/bin/fm-bootstrap.sh")
1093
+ assert_contains "$BOOT_LEGACY" "SECONDMATE_LIVENESS: secondmate ios: skipped: remote endpoint state is unverified on remote-mac" \
1094
+ "liveness accepted an alive legacy remote backend"
1095
+ cmp -s "$TMP_ROOT/remote-ios-liveness-legacy.meta" "$remote_route_meta" \
1096
+ || fail "liveness rewrote the alive legacy endpoint metadata"
1097
+ cmp -s "$TMP_ROOT/parent-ios-before-liveness-legacy.meta" "$PARENT/state/ios.meta" \
1098
+ || fail "liveness rewrote the parent route metadata for an alive legacy endpoint"
1099
+ cmp -s "$TMP_ROOT/registry-before-liveness-legacy.md" "$PARENT/data/secondmates.md" \
1100
+ || fail "liveness changed the registry route for an alive legacy endpoint"
1101
+ [ "$(cat "$TMUX_STATE")" = "$tmux_state_before" ] \
1102
+ || fail "liveness changed or killed the alive legacy endpoint"
1103
+ launches_after_legacy=$(grep -c '^tab create' "$HERDR_LOG" || true)
1104
+ [ "$launches_before_legacy" -eq "$launches_after_legacy" ] \
1105
+ || fail "liveness relaunched an alive legacy endpoint"
1106
+ mv -f "$TMP_ROOT/remote-ios-before-liveness-legacy.meta" "$remote_route_meta"
1107
+ rm -f "$TMUX_STATE"
1108
+ pass "startup reports alive legacy backends without changing their routes"
1109
+
1110
+ # Host loss maps to unknown/unavailable and never creates a local replacement.
1111
+ launches_before=$(grep -c '^tab create' "$HERDR_LOG" || true)
1112
+ rm -rf -- "$PARENT/state/.watch.lock"
1113
+ rm -f -- "$PARENT/state/.last-watcher-beat"
1114
+ BOOT_UNAVAILABLE=$(FM_FAKE_SSH_MODE=unreachable remote_env "$ROOT/bin/fm-bootstrap.sh")
1115
+ assert_contains "$BOOT_UNAVAILABLE" 'SECONDMATE_LIVENESS: secondmate ios: skipped: remote host unavailable or endpoint state unknown' \
1116
+ "bootstrap did not preserve an unreachable remote endpoint as unknown"
1117
+ UNAVAILABLE=$(FM_FAKE_SSH_MODE=unreachable remote_env "$ROOT/bin/fm-fleet-snapshot.sh" --json)
1118
+ printf '%s' "$UNAVAILABLE" | jq -e '.secondmate_current.records | any(.id == "ios" and .current.state == "unknown")' >/dev/null \
1119
+ || fail "unreachable remote host was not projected unknown"
1120
+ printf '%s' "$UNAVAILABLE" | jq -e '.tasks[] | select(.id == "ios") | .paths.home.present == null' >/dev/null \
1121
+ || fail "unreachable remote home presence was not projected unknown"
1122
+ rm -f "$PARENT/state/.wake-queue"
1123
+ launches_after=$(grep -c '^tab create' "$HERDR_LOG" || true)
1124
+ [ "$launches_before" -eq "$launches_after" ] || fail "unreachable projection attempted a replacement launch"
1125
+ assert_present "$PARENT/state/ios.meta" "unreachable readiness removed the parent route metadata"
1126
+ assert_grep '- ios ' "$PARENT/data/secondmates.md" "unreachable readiness removed the registry route"
1127
+ pass "unreachable remote state remains unknown with no local respawn or failover"
1128
+
1129
+ # Retirement delegates its safety check to the remote home. An in-flight child
1130
+ # record refuses cleanup and preserves both machines' durable routes.
1131
+ # A sibling remote secondmate workspace shares fm-remote and must survive every
1132
+ # refusal and the eventual successful retirement of ios.
1133
+ # This fixture overrides FM_ROOT for transport, so teardown's root-owned guard
1134
+ # sees the fixture root rather than the source script path used by fm-send.
1135
+ publish_healthy_watcher_identity "$PARENT/state" "$PARENT" "$REMOTE_ROOT/bin/fm-watch.sh"
1136
+ resolve_ios_pending
1137
+ SIBLING_CREATE=$("$REMOTE_ROOT/bin/herdr" workspace create --cwd "$REMOTE_ROOT" \
1138
+ --label 2ndmate-macos --no-focus --session fm-remote)
1139
+ SIBLING_WORKSPACE=$(printf '%s' "$SIBLING_CREATE" | jq -r '.result.workspace.workspace_id')
1140
+ SIBLING_PANE=$(printf '%s' "$SIBLING_CREATE" | jq -r '.result.root_pane.pane_id')
1141
+ [ -n "$SIBLING_WORKSPACE" ] && [ "$SIBLING_WORKSPACE" != null ] \
1142
+ || fail "the shared-session sibling fixture did not create a workspace"
1143
+ [ -n "$SIBLING_PANE" ] && [ "$SIBLING_PANE" != null ] \
1144
+ || fail "the shared-session sibling fixture did not create a pane"
1145
+ printf 'kind=ship\n' > "$REMOTE_HOME/state/child.meta"
1146
+ rm -rf "$PARENT/state/procevent"
1147
+ : > "$PARENT/state/procevent"
1148
+ if remote_env "$ROOT/bin/fm-teardown.sh" ios >/dev/null 2>&1; then
1149
+ fail "remote retirement ignored in-flight child work"
1150
+ fi
1151
+ assert_present "$REMOTE_HOME" "refused remote retirement removed the home"
1152
+ assert_present "$PARENT/state/ios.meta" "refused remote retirement removed parent metadata"
1153
+ assert_grep '- ios ' "$PARENT/data/secondmates.md" "refused remote retirement removed the route"
1154
+ rm -f "$PARENT/state/procevent"
1155
+ mkdir "$PARENT/state/procevent"
1156
+ remote_env "$ROOT/bin/fm-bootstrap.sh" >/dev/null \
1157
+ || fail "bootstrap failed while repairing a preserved remote reply source"
1158
+ assert_present "$PARENT/state/procevent/remote-reply-ios.source" \
1159
+ "bootstrap did not repair reply registration after retirement rollback"
1160
+ resolve_ios_pending
1161
+ rm -f "$REMOTE_HOME/state/child.meta"
1162
+ mkdir -p "$PARENT/data/handoff"
1163
+ ln -s "$TMP_ROOT/missing-outbox-target" "$PARENT/data/handoff/ios.outbox.md"
1164
+ if remote_env "$ROOT/bin/fm-teardown.sh" ios >/dev/null 2>&1; then
1165
+ fail "remote retirement accepted an unsafe backlog outbox"
1166
+ fi
1167
+ assert_present "$REMOTE_HOME" "unsafe backlog outbox retirement removed the remote home"
1168
+ rm -f "$PARENT/data/handoff/ios.outbox.md"
1169
+ mkdir -p "$TMP_ROOT/external-pending"
1170
+ printf 'task_id=ios\nphase=resolved\n' > "$TMP_ROOT/external-pending/escape"
1171
+ mv "$PARENT/state/pending-replies" "$PARENT/state/pending-replies.safe"
1172
+ ln -s "$TMP_ROOT/external-pending" "$PARENT/state/pending-replies"
1173
+ if remote_env "$ROOT/bin/fm-teardown.sh" ios >/dev/null 2>&1; then
1174
+ fail "remote retirement accepted a symlinked pending-replies directory"
1175
+ fi
1176
+ assert_present "$REMOTE_HOME" "unsafe pending-replies retirement removed the remote home"
1177
+ assert_present "$TMP_ROOT/external-pending/escape" "unsafe retirement removed an external pending reply"
1178
+ rm -f "$PARENT/state/pending-replies"
1179
+ mv "$PARENT/state/pending-replies.safe" "$PARENT/state/pending-replies"
1180
+ retired_wake_corr=$(FM_HOME="$PARENT" bash -c '
1181
+ . "$1"
1182
+ fm_pending_reply_create "$2" "$2/state" ios "New routed work is in your backlog."
1183
+ ' _ "$ROOT/bin/fm-pending-reply-lib.sh" "$PARENT") \
1184
+ || fail "could not seed remote receiver wake retirement state"
1185
+ retired_wake_rec="$PARENT/state/pending-replies/$retired_wake_corr"
1186
+ FM_HOME="$PARENT" bash -c '
1187
+ . "$1"
1188
+ fm_pending_reply_set "$2" phase resolved
1189
+ fm_pending_reply_set "$2" delivered_epoch 1
1190
+ ' _ "$ROOT/bin/fm-pending-reply-lib.sh" "$retired_wake_rec" \
1191
+ || fail "could not settle remote receiver wake retirement state"
1192
+ printf 'confirmed:%s\n' "$retired_wake_corr" > "$PARENT/state/.backlog-handoff-ios.wake-pending"
1193
+ handoff_lock="$PARENT/state/.backlog-handoff-ios.lock"
1194
+ FM_HOME="$PARENT" /bin/bash -c '
1195
+ . "$1"
1196
+ fm_lock_acquire_wait "$2"
1197
+ touch "$3"
1198
+ while [ ! -f "$4" ]; do sleep 0.02; done
1199
+ fm_lock_release "$2"
1200
+ ' _ "$ROOT/bin/fm-wake-lib.sh" "$handoff_lock" "$TMP_ROOT/handoff.entered" \
1201
+ "$TMP_ROOT/handoff.release" &
1202
+ handoff_holder_pid=$!
1203
+ handoff_wait=0
1204
+ while [ ! -f "$TMP_ROOT/handoff.entered" ]; do
1205
+ kill -0 "$handoff_holder_pid" 2>/dev/null || fail "handoff lock holder exited before acquiring the route lock"
1206
+ handoff_wait=$((handoff_wait + 1))
1207
+ [ "$handoff_wait" -le 250 ] || fail "handoff lock holder never acquired the route lock"
1208
+ sleep 0.02
1209
+ done
1210
+ rm -f "$TMUX_STATE" "$TMP_ROOT/launch.entered" "$TMP_ROOT/launch.release"
1211
+ FM_FAKE_SSH_MODE=launch-block remote_env "$ROOT/bin/fm-spawn.sh" ios --secondmate \
1212
+ > "$TMP_ROOT/spawn-retirement.out" 2>&1 &
1213
+ spawn_retirement_pid=$!
1214
+ launch_wait=0
1215
+ # The respawn performs readiness and inheritance jobs before launch, so allow
1216
+ # the same 30-second loaded-runner bound as the earlier blocked worker path.
1217
+ while [ ! -f "$TMP_ROOT/launch.entered" ]; do
1218
+ kill -0 "$spawn_retirement_pid" 2>/dev/null || fail "remote respawn exited before its blocked launch"
1219
+ launch_wait=$((launch_wait + 1))
1220
+ [ "$launch_wait" -le 1500 ] || fail "remote respawn never reached its blocked launch"
1221
+ sleep 0.02
1222
+ done
1223
+ remote_env "$ROOT/bin/fm-teardown.sh" ios > "$TMP_ROOT/teardown-serialized.out" 2>&1 &
1224
+ teardown_pid=$!
1225
+ sleep 0.2
1226
+ kill -0 "$teardown_pid" 2>/dev/null || fail "remote retirement bypassed an active remote respawn"
1227
+ assert_present "$REMOTE_HOME" "remote retirement removed the home during an active remote respawn"
1228
+ touch "$TMP_ROOT/launch.release"
1229
+ if ! wait "$spawn_retirement_pid"; then
1230
+ printf 'serialized respawn output:\n%s\n' "$(cat "$TMP_ROOT/spawn-retirement.out")" >&2
1231
+ fail "serialized remote respawn failed"
1232
+ fi
1233
+ sleep 0.2
1234
+ kill -0 "$teardown_pid" 2>/dev/null || fail "remote retirement bypassed an active backlog handoff"
1235
+ touch "$TMP_ROOT/handoff.release"
1236
+ wait "$handoff_holder_pid" || fail "handoff lock holder failed to release"
1237
+ if ! wait "$teardown_pid"; then
1238
+ printf 'serialized retirement output:\n%s\n' "$(cat "$TMP_ROOT/teardown-serialized.out")" >&2
1239
+ fail "safe remote retirement failed after handoff serialization"
1240
+ fi
1241
+ assert_absent "$REMOTE_HOME" "remote retirement did not remove the remote home"
1242
+ assert_absent "$PARENT/state/ios.meta" "remote retirement did not remove parent metadata"
1243
+ assert_absent "$PARENT/state/.backlog-handoff-ios.wake-pending" \
1244
+ "remote retirement left receiver wake state that could poison a replacement route"
1245
+ assert_absent "$retired_wake_rec" "remote retirement left the retired receiver wake correlation"
1246
+ assert_no_grep '- ios ' "$PARENT/data/secondmates.md" "remote retirement did not remove the registry route"
1247
+ jq -e --arg workspace "$SIBLING_WORKSPACE" --arg pane "$SIBLING_PANE" '
1248
+ any(.workspaces[]; .workspace_id == $workspace and .label == "2ndmate-macos")
1249
+ and any(.tabs[]; .workspace_id == $workspace and .pane_id == $pane)
1250
+ ' "$HERDR_STATE" >/dev/null \
1251
+ || fail "remote retirement removed the sibling secondmate workspace or pane from fm-remote"
1252
+ assert_no_grep 'session stop' "$HERDR_LOG" "remote retirement stopped the shared fm-remote session"
1253
+ assert_no_grep 'server stop' "$HERDR_LOG" "remote retirement stopped the shared fm-remote server"
1254
+ pass "remote retirement refuses child work, then removes only its own endpoint while a shared-session sibling survives"
1255
+
1256
+ echo "ALL TESTS PASSED"