@zerwiz/ymir 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (850) hide show
  1. package/.agents/README.md +291 -0
  2. package/.agents/agents/bragi-marketer.md +111 -0
  3. package/.agents/agents/brokk.md +352 -0
  4. package/.agents/agents/forseti-reviewer.md +93 -0
  5. package/.agents/agents/frigg-consent.md +77 -0
  6. package/.agents/agents/groa-update.md +77 -0
  7. package/.agents/agents/hnoss-designer.md +69 -0
  8. package/.agents/agents/huginn-researcher.md +99 -0
  9. package/.agents/agents/jord-projects.md +78 -0
  10. package/.agents/agents/kvasir-scout.md +91 -0
  11. package/.agents/agents/mimir-planner.md +92 -0
  12. package/.agents/agents/muninn-stow.md +77 -0
  13. package/.agents/agents/saga-bearings.md +78 -0
  14. package/.agents/agents/sindri-developer.md +114 -0
  15. package/.agents/agents/snotra-documenter.md +105 -0
  16. package/.agents/agents/syn-recovery.md +78 -0
  17. package/.agents/agents/tyr-check.md +78 -0
  18. package/.agents/agents/urdh-hold.md +77 -0
  19. package/.agents/agents/volundr-smith.md +80 -0
  20. package/.agents/agents/vor-diagnostics.md +77 -0
  21. package/.agents/assets/agents/naming.md +50 -0
  22. package/.agents/assets/agents/registry.md +93 -0
  23. package/.agents/assets/agents/runtime.md +36 -0
  24. package/.agents/assets/agents/toon-tasks-cli.md +42 -0
  25. package/.agents/assets/templates/company_entity.template.md +37 -0
  26. package/.agents/backend/backends/cmux.sh +649 -0
  27. package/.agents/backend/backends/herdr-eventwait.py +157 -0
  28. package/.agents/backend/backends/herdr-workspace-move.py +114 -0
  29. package/.agents/backend/backends/herdr.sh +3386 -0
  30. package/.agents/backend/backends/orca.sh +290 -0
  31. package/.agents/backend/backends/tmux.sh +357 -0
  32. package/.agents/backend/backends/zellij.sh +700 -0
  33. package/.agents/backend/fm-afk-launch.sh +654 -0
  34. package/.agents/backend/fm-afk-return.sh +241 -0
  35. package/.agents/backend/fm-afk-start.sh +172 -0
  36. package/.agents/backend/fm-arm-command-policy.mjs +960 -0
  37. package/.agents/backend/fm-arm-pretool-check.sh +200 -0
  38. package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
  39. package/.agents/backend/fm-backend.sh +981 -0
  40. package/.agents/backend/fm-backlog-handoff.sh +909 -0
  41. package/.agents/backend/fm-backlog-receive.sh +187 -0
  42. package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
  43. package/.agents/backend/fm-bearings-board.sh +204 -0
  44. package/.agents/backend/fm-bearings-snapshot.sh +546 -0
  45. package/.agents/backend/fm-bootstrap.sh +1568 -0
  46. package/.agents/backend/fm-branch-outcome.sh +203 -0
  47. package/.agents/backend/fm-branch-prompt.sh +101 -0
  48. package/.agents/backend/fm-brief.sh +452 -0
  49. package/.agents/backend/fm-busy-event.sh +234 -0
  50. package/.agents/backend/fm-busy-lib.sh +986 -0
  51. package/.agents/backend/fm-captain-hold.sh +1001 -0
  52. package/.agents/backend/fm-cd-command-policy.mjs +153 -0
  53. package/.agents/backend/fm-cd-pretool-check.sh +190 -0
  54. package/.agents/backend/fm-check-lib.sh +74 -0
  55. package/.agents/backend/fm-check-register.sh +42 -0
  56. package/.agents/backend/fm-check-unregister.sh +52 -0
  57. package/.agents/backend/fm-classify-lib.sh +1770 -0
  58. package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
  59. package/.agents/backend/fm-composer-lib.sh +1416 -0
  60. package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
  61. package/.agents/backend/fm-config-push.sh +254 -0
  62. package/.agents/backend/fm-control-lib.sh +251 -0
  63. package/.agents/backend/fm-control.sh +878 -0
  64. package/.agents/backend/fm-crew-state.sh +625 -0
  65. package/.agents/backend/fm-cursor-lib.sh +243 -0
  66. package/.agents/backend/fm-decision-hold.sh +232 -0
  67. package/.agents/backend/fm-doc-audience-check.sh +269 -0
  68. package/.agents/backend/fm-dod-lib.sh +67 -0
  69. package/.agents/backend/fm-ensure-agents-md.sh +253 -0
  70. package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
  71. package/.agents/backend/fm-extension.mjs +2577 -0
  72. package/.agents/backend/fm-extension.sh +16 -0
  73. package/.agents/backend/fm-ff-lib.sh +420 -0
  74. package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
  75. package/.agents/backend/fm-fleet-sync.sh +458 -0
  76. package/.agents/backend/fm-fleet-view.sh +96 -0
  77. package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
  78. package/.agents/backend/fm-guard.sh +242 -0
  79. package/.agents/backend/fm-harness.sh +195 -0
  80. package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
  81. package/.agents/backend/fm-herdr-lab.sh +346 -0
  82. package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
  83. package/.agents/backend/fm-home-seed.sh +984 -0
  84. package/.agents/backend/fm-home-summary-refresh.sh +255 -0
  85. package/.agents/backend/fm-hook-host-lib.sh +36 -0
  86. package/.agents/backend/fm-inactive-reconcile.sh +531 -0
  87. package/.agents/backend/fm-inbox.sh +399 -0
  88. package/.agents/backend/fm-install-actionlint.sh +84 -0
  89. package/.agents/backend/fm-install-herdr.sh +96 -0
  90. package/.agents/backend/fm-install-shellcheck.sh +84 -0
  91. package/.agents/backend/fm-install-treehouse.sh +95 -0
  92. package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
  93. package/.agents/backend/fm-lease-lib.sh +218 -0
  94. package/.agents/backend/fm-lease.sh +189 -0
  95. package/.agents/backend/fm-line-cap-lib.sh +51 -0
  96. package/.agents/backend/fm-lint-workflows.sh +137 -0
  97. package/.agents/backend/fm-lint.sh +591 -0
  98. package/.agents/backend/fm-lock-lib.sh +104 -0
  99. package/.agents/backend/fm-lock.sh +107 -0
  100. package/.agents/backend/fm-marker-lib.sh +12 -0
  101. package/.agents/backend/fm-merge-local.sh +74 -0
  102. package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
  103. package/.agents/backend/fm-nm-run-lib.sh +124 -0
  104. package/.agents/backend/fm-on.sh +125 -0
  105. package/.agents/backend/fm-operational-input.sh +253 -0
  106. package/.agents/backend/fm-peek.sh +46 -0
  107. package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
  108. package/.agents/backend/fm-pr-check.sh +135 -0
  109. package/.agents/backend/fm-pr-lib.sh +1016 -0
  110. package/.agents/backend/fm-pr-merge.sh +707 -0
  111. package/.agents/backend/fm-pr-poll.sh +110 -0
  112. package/.agents/backend/fm-primary-scope-lib.sh +33 -0
  113. package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
  114. package/.agents/backend/fm-procevent-lavish.sh +628 -0
  115. package/.agents/backend/fm-procevent-lib.sh +892 -0
  116. package/.agents/backend/fm-procevent-quota.sh +290 -0
  117. package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
  118. package/.agents/backend/fm-procevent-when.sh +504 -0
  119. package/.agents/backend/fm-procevent.sh +1466 -0
  120. package/.agents/backend/fm-project-mode.sh +92 -0
  121. package/.agents/backend/fm-project-origin-lib.sh +180 -0
  122. package/.agents/backend/fm-promote.sh +282 -0
  123. package/.agents/backend/fm-public-followup-emit.sh +260 -0
  124. package/.agents/backend/fm-public-followup-lib.sh +398 -0
  125. package/.agents/backend/fm-public-followup.sh +1330 -0
  126. package/.agents/backend/fm-push-transition-lib.sh +168 -0
  127. package/.agents/backend/fm-quota-axi-lib.sh +93 -0
  128. package/.agents/backend/fm-quota-choose.sh +384 -0
  129. package/.agents/backend/fm-remote-delta-read.sh +189 -0
  130. package/.agents/backend/fm-remote-doctor.sh +800 -0
  131. package/.agents/backend/fm-remote-entrypoint.sh +193 -0
  132. package/.agents/backend/fm-remote-file.sh +234 -0
  133. package/.agents/backend/fm-remote-home-provision.sh +259 -0
  134. package/.agents/backend/fm-remote-home-seed.sh +259 -0
  135. package/.agents/backend/fm-remote-inherit-push.sh +90 -0
  136. package/.agents/backend/fm-remote-inherit.sh +179 -0
  137. package/.agents/backend/fm-remote-job-lib.sh +1241 -0
  138. package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
  139. package/.agents/backend/fm-remote-job-worker.sh +1124 -0
  140. package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
  141. package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
  142. package/.agents/backend/fm-review-diff.sh +158 -0
  143. package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
  144. package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
  145. package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
  146. package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
  147. package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
  148. package/.agents/backend/fm-secondmate-report.sh +88 -0
  149. package/.agents/backend/fm-send.sh +1059 -0
  150. package/.agents/backend/fm-session-lock-lib.sh +176 -0
  151. package/.agents/backend/fm-session-start.sh +970 -0
  152. package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
  153. package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
  154. package/.agents/backend/fm-sessionstart-run.sh +145 -0
  155. package/.agents/backend/fm-spawn.sh +3153 -0
  156. package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
  157. package/.agents/backend/fm-startup-memory-budget.sh +94 -0
  158. package/.agents/backend/fm-startup-network.sh +642 -0
  159. package/.agents/backend/fm-stow-cascade.sh +251 -0
  160. package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
  161. package/.agents/backend/fm-supervise-daemon.sh +1732 -0
  162. package/.agents/backend/fm-supervision-instructions.sh +215 -0
  163. package/.agents/backend/fm-supervision-lib.sh +90 -0
  164. package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
  165. package/.agents/backend/fm-tangle-lib.sh +53 -0
  166. package/.agents/backend/fm-task-inbox-lib.sh +402 -0
  167. package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
  168. package/.agents/backend/fm-teardown.sh +2922 -0
  169. package/.agents/backend/fm-test-isolation-proof.sh +588 -0
  170. package/.agents/backend/fm-test-run.sh +2191 -0
  171. package/.agents/backend/fm-timeout-lib.sh +141 -0
  172. package/.agents/backend/fm-timing-lib.sh +179 -0
  173. package/.agents/backend/fm-tmux-lib.sh +291 -0
  174. package/.agents/backend/fm-tool-update-check.sh +898 -0
  175. package/.agents/backend/fm-trace-context-lib.sh +227 -0
  176. package/.agents/backend/fm-transition-lib.sh +103 -0
  177. package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
  178. package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
  179. package/.agents/backend/fm-turnend-guard.sh +432 -0
  180. package/.agents/backend/fm-update.sh +110 -0
  181. package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
  182. package/.agents/backend/fm-voice-client.py +1373 -0
  183. package/.agents/backend/fm-voice-relay.py +1256 -0
  184. package/.agents/backend/fm-wake-drain.sh +592 -0
  185. package/.agents/backend/fm-wake-grant.sh +127 -0
  186. package/.agents/backend/fm-wake-lib.sh +1849 -0
  187. package/.agents/backend/fm-watch-arm.sh +602 -0
  188. package/.agents/backend/fm-watch-checkpoint.sh +109 -0
  189. package/.agents/backend/fm-watch.sh +1962 -0
  190. package/.agents/backend/fm-x-dismiss.sh +116 -0
  191. package/.agents/backend/fm-x-followup.sh +286 -0
  192. package/.agents/backend/fm-x-lib.sh +1000 -0
  193. package/.agents/backend/fm-x-link.sh +234 -0
  194. package/.agents/backend/fm-x-poll.sh +185 -0
  195. package/.agents/backend/fm-x-reply.sh +385 -0
  196. package/.agents/backend/fm_voice_frame.py +166 -0
  197. package/.agents/backend/fm_voice_records.py +574 -0
  198. package/.agents/backend/model-bridge.py +290 -0
  199. package/.agents/backend/opencode-go-bridge.py +30 -0
  200. package/.agents/bus/README.md +12 -0
  201. package/.agents/bus/messages.json +1 -0
  202. package/.agents/config/agents.machine.example.yaml +16 -0
  203. package/.agents/config/agents.yaml +50 -0
  204. package/.agents/config/agents.yaml.example +72 -0
  205. package/.agents/config/crew-dispatch.json +22 -0
  206. package/.agents/config/cron.yaml +9 -0
  207. package/.agents/config/eindri-dispatch.json +67 -0
  208. package/.agents/config/eindri-harness +1 -0
  209. package/.agents/config/startup-memory-budget +1 -0
  210. package/.agents/config/tailscale-sync.yaml +27 -0
  211. package/.agents/filebrowser/.filebrowser.json +6 -0
  212. package/.agents/filebrowser/README.md +11 -0
  213. package/.agents/gateway/README.md +12 -0
  214. package/.agents/github/README.md +15 -0
  215. package/.agents/github/webhooks/.gitkeep +0 -0
  216. package/.agents/github/workflows/.gitkeep +0 -0
  217. package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
  218. package/.agents/harness/opencode/plugins/package.json +4 -0
  219. package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
  220. package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
  221. package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
  222. package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
  223. package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
  224. package/.agents/memory/README.md +46 -0
  225. package/.agents/memory/kaia.engram +0 -0
  226. package/.agents/memory/kaia.engram-shm +0 -0
  227. package/.agents/memory/kaia.engram-wal +0 -0
  228. package/.agents/memory/runes_audit.md +5 -0
  229. package/.agents/memory/well/episodes.jsonl +367 -0
  230. package/.agents/memory/well/workspace.jsonl +27 -0
  231. package/.agents/migrations/0001-hodd-layout.sh +38 -0
  232. package/.agents/migrations/0002-a2a-mcp.sh +27 -0
  233. package/.agents/migrations/0003-private-data-separation.sh +111 -0
  234. package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
  235. package/.agents/sandbox/Dockerfile.utgard +19 -0
  236. package/.agents/sandbox/README.md +14 -0
  237. package/.agents/sandbox/utgard.config.json +14 -0
  238. package/.agents/skills/README.md +59 -0
  239. package/.agents/skills/bragi-marketing/README.md +3 -0
  240. package/.agents/skills/bragi-marketing/SKILL.md +82 -0
  241. package/.agents/skills/eindri-homes/README.md +3 -0
  242. package/.agents/skills/eindri-homes/SKILL.md +291 -0
  243. package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
  244. package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
  245. package/.agents/skills/frigg-consent/README.md +3 -0
  246. package/.agents/skills/frigg-consent/SKILL.md +56 -0
  247. package/.agents/skills/galdr-ymirsystem/README.md +3 -0
  248. package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
  249. package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
  250. package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
  251. package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
  252. package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
  253. package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
  254. package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
  255. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
  256. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
  257. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
  258. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
  259. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
  260. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
  261. package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
  262. package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
  263. package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
  264. package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
  265. package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
  266. package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
  267. package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
  268. package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
  269. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
  270. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
  271. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
  272. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
  273. package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
  274. package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
  275. package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
  276. package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
  277. package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
  278. package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
  279. package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
  280. package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
  281. package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
  282. package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
  283. package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
  284. package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
  285. package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
  286. package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
  287. package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
  288. package/.agents/skills/gjallarhorn-relay/README.md +3 -0
  289. package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
  290. package/.agents/skills/groa-update/SKILL.md +70 -0
  291. package/.agents/skills/hamr-adapters/README.md +3 -0
  292. package/.agents/skills/hamr-adapters/SKILL.md +123 -0
  293. package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
  294. package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
  295. package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
  296. package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
  297. package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
  298. package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
  299. package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
  300. package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
  301. package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
  302. package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
  303. package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
  304. package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
  305. package/.agents/skills/herdr-panes/README.md +3 -0
  306. package/.agents/skills/herdr-panes/SKILL.md +92 -0
  307. package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
  308. package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
  309. package/.agents/skills/hnoss-design/DESIGN.md +19 -0
  310. package/.agents/skills/hnoss-design/README.md +3 -0
  311. package/.agents/skills/hnoss-design/SKILL.md +83 -0
  312. package/.agents/skills/hvild-afk/README.md +3 -0
  313. package/.agents/skills/hvild-afk/SKILL.md +241 -0
  314. package/.agents/skills/jord-projects/README.md +3 -0
  315. package/.agents/skills/jord-projects/SKILL.md +92 -0
  316. package/.agents/skills/lifecycle/SKILL.md +56 -0
  317. package/.agents/skills/lifecycle/smoke_test.sh +52 -0
  318. package/.agents/skills/lifecycle/start.sh +21 -0
  319. package/.agents/skills/lifecycle/status.sh +35 -0
  320. package/.agents/skills/lifecycle/stop.sh +16 -0
  321. package/.agents/skills/muninn-stow/README.md +3 -0
  322. package/.agents/skills/muninn-stow/SKILL.md +309 -0
  323. package/.agents/skills/no-mistakes/SKILL.md +379 -0
  324. package/.agents/skills/nornir-schedule/README.md +3 -0
  325. package/.agents/skills/nornir-schedule/SKILL.md +25 -0
  326. package/.agents/skills/nornir-schedule/assets/events.md +137 -0
  327. package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
  328. package/.agents/skills/nsr-compliance/README.md +3 -0
  329. package/.agents/skills/nsr-compliance/SKILL.md +29 -0
  330. package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
  331. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
  332. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
  333. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
  334. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
  335. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
  336. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
  337. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
  338. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
  339. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
  340. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
  341. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
  342. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
  343. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
  344. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
  345. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
  346. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
  347. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
  348. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
  349. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
  350. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
  351. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
  352. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
  353. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  354. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
  355. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
  356. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
  357. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
  358. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
  359. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
  360. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
  361. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
  362. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
  363. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
  364. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
  365. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
  366. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
  367. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
  368. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
  369. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
  370. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
  371. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
  372. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
  373. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
  374. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
  375. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
  376. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
  377. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
  378. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
  379. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
  380. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
  381. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
  382. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
  383. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
  384. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
  385. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
  386. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
  387. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
  388. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
  389. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
  390. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
  391. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
  392. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
  393. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
  394. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
  395. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
  396. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
  397. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
  398. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
  399. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
  400. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
  401. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
  402. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
  403. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
  404. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
  405. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
  406. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
  407. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
  408. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
  409. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
  410. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
  411. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
  412. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
  413. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
  414. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
  415. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
  416. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
  417. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
  418. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
  419. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
  420. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
  421. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
  422. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
  423. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
  424. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
  425. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
  426. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
  427. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
  428. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
  429. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
  430. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
  431. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
  432. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
  433. package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
  434. package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
  435. package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
  436. package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
  437. package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
  438. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
  439. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
  440. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
  441. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
  442. package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
  443. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
  444. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
  445. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
  446. package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
  447. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
  448. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
  449. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
  450. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
  451. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
  452. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
  453. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
  454. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  455. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
  456. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
  457. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
  458. package/.agents/skills/pr-ops/README.md +3 -0
  459. package/.agents/skills/pr-ops/SKILL.md +109 -0
  460. package/.agents/skills/ratatoskr-a2a/README.md +3 -0
  461. package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
  462. package/.agents/skills/rules-check-drift/SKILL.md +81 -0
  463. package/.agents/skills/saga-bearings/README.md +3 -0
  464. package/.agents/skills/saga-bearings/SKILL.md +26 -0
  465. package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
  466. package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
  467. package/.agents/skills/saga-bearings/assets/recap.md +56 -0
  468. package/.agents/skills/syn-recovery/README.md +3 -0
  469. package/.agents/skills/syn-recovery/SKILL.md +54 -0
  470. package/.agents/skills/tyr-check/README.md +3 -0
  471. package/.agents/skills/tyr-check/SKILL.md +218 -0
  472. package/.agents/skills/urdh-hold/README.md +3 -0
  473. package/.agents/skills/urdh-hold/SKILL.md +25 -0
  474. package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
  475. package/.agents/skills/urdh-hold/assets/hold.md +54 -0
  476. package/.agents/skills/vor-diagnostics/README.md +3 -0
  477. package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
  478. package/.agents/skills/ymir-host/README.md +3 -0
  479. package/.agents/skills/ymir-host/SKILL.md +34 -0
  480. package/.agents/skills/ymir-host/assets/desktop.md +36 -0
  481. package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
  482. package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
  483. package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
  484. package/.agents/skills/ymir-host/assets/update.md +64 -0
  485. package/.agents/tests/assets/board-render-harness.mjs +117 -0
  486. package/.agents/tests/cmux-test-safety.sh +51 -0
  487. package/.agents/tests/fixtures.sh +291 -0
  488. package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
  489. package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
  490. package/.agents/tests/fm-afk-launch.test.sh +961 -0
  491. package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
  492. package/.agents/tests/fm-afk-return.test.sh +283 -0
  493. package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
  494. package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
  495. package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
  496. package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
  497. package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
  498. package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
  499. package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
  500. package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
  501. package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
  502. package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
  503. package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
  504. package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
  505. package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
  506. package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
  507. package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
  508. package/.agents/tests/fm-backend-orca.test.sh +1357 -0
  509. package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
  510. package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
  511. package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
  512. package/.agents/tests/fm-backend.test.sh +1142 -0
  513. package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
  514. package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
  515. package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
  516. package/.agents/tests/fm-bearings-board.test.sh +414 -0
  517. package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
  518. package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
  519. package/.agents/tests/fm-bootstrap.test.sh +1178 -0
  520. package/.agents/tests/fm-branch-supervision.test.sh +552 -0
  521. package/.agents/tests/fm-brief.test.sh +785 -0
  522. package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
  523. package/.agents/tests/fm-busy-state.test.sh +465 -0
  524. package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
  525. package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
  526. package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
  527. package/.agents/tests/fm-check-unregister.test.sh +198 -0
  528. package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
  529. package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
  530. package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
  531. package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
  532. package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
  533. package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
  534. package/.agents/tests/fm-composer-ghost.test.sh +716 -0
  535. package/.agents/tests/fm-composer-lib.test.sh +666 -0
  536. package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
  537. package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
  538. package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
  539. package/.agents/tests/fm-control.test.sh +910 -0
  540. package/.agents/tests/fm-crew-state.test.sh +1610 -0
  541. package/.agents/tests/fm-cursor-harness.test.sh +404 -0
  542. package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
  543. package/.agents/tests/fm-cursor-primary.test.sh +708 -0
  544. package/.agents/tests/fm-daemon.test.sh +2740 -0
  545. package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
  546. package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
  547. package/.agents/tests/fm-extension-binding.test.sh +2187 -0
  548. package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
  549. package/.agents/tests/fm-fleet-sync.test.sh +721 -0
  550. package/.agents/tests/fm-gate-refuse.test.sh +356 -0
  551. package/.agents/tests/fm-gitignore-config.test.sh +89 -0
  552. package/.agents/tests/fm-gotmp.test.sh +230 -0
  553. package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
  554. package/.agents/tests/fm-grok-harness.test.sh +117 -0
  555. package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
  556. package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
  557. package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
  558. package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
  559. package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
  560. package/.agents/tests/fm-herdr-lab.test.sh +243 -0
  561. package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
  562. package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
  563. package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
  564. package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
  565. package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
  566. package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
  567. package/.agents/tests/fm-kimi-harness.test.sh +687 -0
  568. package/.agents/tests/fm-lint-workflows.test.sh +532 -0
  569. package/.agents/tests/fm-lint.test.sh +1023 -0
  570. package/.agents/tests/fm-muse-harness.test.sh +932 -0
  571. package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
  572. package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
  573. package/.agents/tests/fm-on.test.sh +507 -0
  574. package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
  575. package/.agents/tests/fm-operational-input.test.sh +160 -0
  576. package/.agents/tests/fm-peek-remote.test.sh +110 -0
  577. package/.agents/tests/fm-pending-reply.test.sh +1288 -0
  578. package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
  579. package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
  580. package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
  581. package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
  582. package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
  583. package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
  584. package/.agents/tests/fm-pr-merge.test.sh +2135 -0
  585. package/.agents/tests/fm-procevent-quota.test.sh +225 -0
  586. package/.agents/tests/fm-procevent-when.test.sh +406 -0
  587. package/.agents/tests/fm-procevent.test.sh +1745 -0
  588. package/.agents/tests/fm-project-origin.test.sh +110 -0
  589. package/.agents/tests/fm-public-followup.test.sh +2272 -0
  590. package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
  591. package/.agents/tests/fm-quota-choose.test.sh +626 -0
  592. package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
  593. package/.agents/tests/fm-remote-doctor.test.sh +625 -0
  594. package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
  595. package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
  596. package/.agents/tests/fm-remote-job.test.sh +768 -0
  597. package/.agents/tests/fm-remote-reply.test.sh +520 -0
  598. package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
  599. package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
  600. package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
  601. package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
  602. package/.agents/tests/fm-review-diff.test.sh +176 -0
  603. package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
  604. package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
  605. package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
  606. package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
  607. package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
  608. package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
  609. package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
  610. package/.agents/tests/fm-send-inbox.test.sh +352 -0
  611. package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
  612. package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
  613. package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
  614. package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
  615. package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
  616. package/.agents/tests/fm-send-settle.test.sh +147 -0
  617. package/.agents/tests/fm-send-strict.test.sh +241 -0
  618. package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
  619. package/.agents/tests/fm-session-start.test.sh +2515 -0
  620. package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
  621. package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
  622. package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
  623. package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
  624. package/.agents/tests/fm-spawn-batch.test.sh +148 -0
  625. package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
  626. package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
  627. package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
  628. package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
  629. package/.agents/tests/fm-startup-network.test.sh +712 -0
  630. package/.agents/tests/fm-stow-cascade.test.sh +370 -0
  631. package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
  632. package/.agents/tests/fm-supervision-events.test.sh +156 -0
  633. package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
  634. package/.agents/tests/fm-tangle-guard.test.sh +277 -0
  635. package/.agents/tests/fm-task-delivery.test.sh +420 -0
  636. package/.agents/tests/fm-task-inbox.test.sh +513 -0
  637. package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
  638. package/.agents/tests/fm-teardown.test.sh +2665 -0
  639. package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
  640. package/.agents/tests/fm-test-fixtures.test.sh +132 -0
  641. package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
  642. package/.agents/tests/fm-test-run.test.sh +1196 -0
  643. package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
  644. package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
  645. package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
  646. package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
  647. package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
  648. package/.agents/tests/fm-transition-lib.test.sh +54 -0
  649. package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
  650. package/.agents/tests/fm-update.test.sh +304 -0
  651. package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
  652. package/.agents/tests/fm-voice-relay.test.sh +4741 -0
  653. package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
  654. package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
  655. package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
  656. package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
  657. package/.agents/tests/fm-wake-queue.test.sh +1224 -0
  658. package/.agents/tests/fm-watch-arm.test.sh +821 -0
  659. package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
  660. package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
  661. package/.agents/tests/fm-watch-triage.test.sh +3889 -0
  662. package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
  663. package/.agents/tests/fm-x-mode.test.sh +3042 -0
  664. package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
  665. package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
  666. package/.agents/tests/herdr-test-safety.sh +42 -0
  667. package/.agents/tests/lib.sh +315 -0
  668. package/.agents/tests/remote-herdr-fixture.sh +125 -0
  669. package/.agents/tests/secondmate-helpers.sh +193 -0
  670. package/.agents/tests/smoke.test.sh +93 -0
  671. package/.agents/tests/wake-helpers.sh +338 -0
  672. package/.agents/tests/zellij-test-safety.sh +60 -0
  673. package/.agents/tools/README.md +10 -0
  674. package/.agents/tools/bin/tasks-cli +267 -0
  675. package/.agents/tools/tasks-cli.ts +216 -0
  676. package/AGENTS.md +454 -0
  677. package/CHANGELOG.md +881 -0
  678. package/LICENSE +1 -1
  679. package/NOTICE +63 -0
  680. package/README.md +113 -39
  681. package/RULES/01-domains.md +51 -0
  682. package/RULES/02-agents.md +78 -0
  683. package/RULES/03-houses.md +61 -0
  684. package/RULES/04-hoard.md +65 -0
  685. package/RULES/05-platforms.md +67 -0
  686. package/RULES/06-append-only.md +58 -0
  687. package/RULES/07-config.md +45 -0
  688. package/RULES/08-delivery-gate.md +75 -0
  689. package/RULES/README.md +20 -0
  690. package/bin/README.md +107 -0
  691. package/bin/a2a-mcp.sh +98 -0
  692. package/bin/a2a-serve.py +103 -0
  693. package/bin/a2a-serve.sh +8 -0
  694. package/bin/a2a-talk.sh +50 -0
  695. package/bin/a2abridge-ensure.sh +108 -0
  696. package/bin/agent-run.sh +43 -0
  697. package/bin/agents-config.sh +233 -0
  698. package/bin/apodex-smoke-test.sh +92 -0
  699. package/bin/bifrost-bridge.sh +112 -0
  700. package/bin/bifrost-ingress.sh +77 -0
  701. package/bin/bootstrap-macos.sh +107 -0
  702. package/bin/bootstrap-windows.ps1 +112 -0
  703. package/bin/branch-guard.sh +64 -0
  704. package/bin/brokk +73 -0
  705. package/bin/brokk-classify-lib.sh +1770 -0
  706. package/bin/brokk-lease-lib.sh +222 -0
  707. package/bin/brokk-lease.sh +193 -0
  708. package/bin/brokk-lint.sh +65 -0
  709. package/bin/brokk-send.sh +38 -0
  710. package/bin/brokk-update.sh +9 -0
  711. package/bin/brokk-wake-grant.sh +127 -0
  712. package/bin/brokk-wake-lib.sh +1849 -0
  713. package/bin/changelog-guard.sh +165 -0
  714. package/bin/crash-sense.sh +160 -0
  715. package/bin/desktop-place.sh +224 -0
  716. package/bin/docs-guard.sh +39 -0
  717. package/bin/editor-place.sh +207 -0
  718. package/bin/eindri-acclaim.sh +34 -0
  719. package/bin/eindri-control.sh +28 -0
  720. package/bin/eindri-role.sh +83 -0
  721. package/bin/eindri-seen.sh +41 -0
  722. package/bin/eindri-send.sh +15 -0
  723. package/bin/eindri-start.sh +85 -0
  724. package/bin/eindri-watch.sh +57 -0
  725. package/bin/einherjar-spawn.sh +576 -0
  726. package/bin/eir-doctor.sh +112 -0
  727. package/bin/erindi-brief.sh +271 -0
  728. package/bin/fleet-apply.sh +68 -0
  729. package/bin/github-deploy.sh +81 -0
  730. package/bin/gjallarhorn-expose.sh +102 -0
  731. package/bin/gjallarhorn-notify.sh +58 -0
  732. package/bin/gjallarhorn-purge.sh +54 -0
  733. package/bin/gjallarhorn-tunnel.sh +72 -0
  734. package/bin/gleipnir-lock-lib.sh +284 -0
  735. package/bin/groa-update.sh +122 -0
  736. package/bin/hall-snapshot.sh +92 -0
  737. package/bin/hamr-harness.sh +246 -0
  738. package/bin/heimdall-ssh-keys.sh +374 -0
  739. package/bin/herdr-agents.py +44 -0
  740. package/bin/herdr-ensure.sh +104 -0
  741. package/bin/herdr-run.sh +431 -0
  742. package/bin/hermes-ensure.sh +88 -0
  743. package/bin/hoard-lib.sh +25 -0
  744. package/bin/hodd.sh +78 -0
  745. package/bin/host-sense.sh +199 -0
  746. package/bin/huginn-research-worker.sh +108 -0
  747. package/bin/local-model-lock.sh +51 -0
  748. package/bin/mimir-bridge.py +226 -0
  749. package/bin/mimir-bridge.sh +87 -0
  750. package/bin/mimir-ingest.sh +131 -0
  751. package/bin/mimir.sh +113 -0
  752. package/bin/mjollnir-webhook.sh +59 -0
  753. package/bin/mjollnir.sh +85 -0
  754. package/bin/model-hardware.sh +82 -0
  755. package/bin/model-resolve.sh +147 -0
  756. package/bin/models-detect.sh +178 -0
  757. package/bin/nornir-cron-start.sh +162 -0
  758. package/bin/nornir-job-daily-briefing.sh +191 -0
  759. package/bin/nornir-job-git-sync.sh +152 -0
  760. package/bin/nornir-job-memory-housekeeping.sh +114 -0
  761. package/bin/nornir-job-observer.sh +166 -0
  762. package/bin/nornir-job-skillopt-sleep.sh +76 -0
  763. package/bin/omarchy-hook-install.sh +60 -0
  764. package/bin/omarchy-install.sh +148 -0
  765. package/bin/omarchy-plugins.sh +174 -0
  766. package/bin/omarchy-sense.sh +149 -0
  767. package/bin/perm-guard.sh +30 -0
  768. package/bin/pi-agent.sh +68 -0
  769. package/bin/pi-ensure.sh +77 -0
  770. package/bin/pi-local.sh +37 -0
  771. package/bin/pi-seat.sh +63 -0
  772. package/bin/prereq-ensure.sh +166 -0
  773. package/bin/private-guard.sh +38 -0
  774. package/bin/project-git.sh +55 -0
  775. package/bin/public-guard.sh +70 -0
  776. package/bin/ratatoskr.sh +49 -0
  777. package/bin/realm-lib.sh +32 -0
  778. package/bin/repo-scrub.sh +62 -0
  779. package/bin/rodd-operational-input.sh +250 -0
  780. package/bin/runes-append.sh +198 -0
  781. package/bin/saga-session-start.sh +172 -0
  782. package/bin/saga-sessionstart-run.sh +79 -0
  783. package/bin/saga-wake-drain.sh +41 -0
  784. package/bin/secret-guard.sh +63 -0
  785. package/bin/sessrumnir-ensure.sh +100 -0
  786. package/bin/sessrumnir-sync.sh +237 -0
  787. package/bin/sessrumnir.sh +91 -0
  788. package/bin/skillopt-setup.sh +76 -0
  789. package/bin/skuld-branch-outcome.sh +203 -0
  790. package/bin/skuld-branch-prompt.sh +101 -0
  791. package/bin/smidja-bootstrap.sh +90 -0
  792. package/bin/smidja-observe.sh +86 -0
  793. package/bin/syn-arm-pretool-check.sh +57 -0
  794. package/bin/syn-asset-pretool-check.sh +76 -0
  795. package/bin/syn-cd-pretool-check.sh +28 -0
  796. package/bin/syn-guard-pretool-check.sh +83 -0
  797. package/bin/syn-turnend-guard.sh +36 -0
  798. package/bin/syn-watch-arm.sh +99 -0
  799. package/bin/tailscale-sync.sh +190 -0
  800. package/bin/telegram-bot.sh +69 -0
  801. package/bin/toolchain.sh +74 -0
  802. package/bin/utgard.sh +111 -0
  803. package/bin/valhalla.sh +115 -0
  804. package/bin/valknut-load.sh +260 -0
  805. package/bin/vor-crew-state.sh +124 -0
  806. package/bin/wedge-notify.sh +47 -0
  807. package/bin/workspace-provision.sh +80 -0
  808. package/bin/workspace-rag.sh +150 -0
  809. package/bin/wyrd-db.sh +47 -0
  810. package/bin/yggdrasil.sh +152 -0
  811. package/bin/ymir-edit.sh +145 -0
  812. package/bin/ymir-install.sh +641 -0
  813. package/bin/ymir-invite.sh +93 -0
  814. package/bin/ymir-isolation.sh +88 -0
  815. package/bin/ymir-migrate.sh +61 -0
  816. package/bin/ymir-platform.sh +348 -0
  817. package/bin/ymir-say.sh +97 -0
  818. package/bin/ymir-setup-auth.sh +108 -0
  819. package/bin/ymir-validate.sh +173 -0
  820. package/bin/ymir.js +74 -16
  821. package/docs/Architecture.md +339 -0
  822. package/docs/design.md +351 -0
  823. package/docs/installations/README.md +63 -0
  824. package/docs/installations/ymir-home.md +93 -0
  825. package/docs/integration.md +105 -0
  826. package/docs/lore.md +1056 -0
  827. package/docs/research/agent-harnesses.md +290 -0
  828. package/docs/runbooks/README.md +19 -0
  829. package/docs/runbooks/a2a.md +91 -0
  830. package/docs/runbooks/agent-permissions.md +99 -0
  831. package/docs/runbooks/agents.md +57 -0
  832. package/docs/runbooks/models.md +69 -0
  833. package/docs/runbooks/realm-onboarding.md +60 -0
  834. package/docs/runbooks/secrets-and-hoard.md +60 -0
  835. package/docs/runbooks/updates-and-migrations.md +110 -0
  836. package/docs/runbooks/worktrees-isolation.md +72 -0
  837. package/docs/session-start.md +105 -0
  838. package/docs/skillopt-integration.md +321 -0
  839. package/docs/sync-system.md +66 -0
  840. package/docs/workspaces.md +104 -0
  841. package/docs/ymir-rut.md +260 -0
  842. package/package.json +44 -14
  843. package/packaging/build.sh +74 -0
  844. package/packaging/macos/Ymir Installer.command +43 -0
  845. package/packaging/windows/ymir-setup.nsi +74 -0
  846. package/scripts/README.md +22 -0
  847. package/scripts/electron.sh +210 -0
  848. package/scripts/start.sh +167 -0
  849. package/scripts/stop.sh +113 -0
  850. package/install.sh +0 -35
@@ -0,0 +1,2972 @@
1
+ #!/usr/bin/env bash
2
+ # tests/fm-secondmate-safety.test.sh - secondmate home safety invariants:
3
+ # the path-boundary matrices (seed/spawn/teardown), registry/charter/origin
4
+ # validation, treehouse lease handling, no-mistakes initialization of new
5
+ # clones, child-worktree protection, and backlog-handoff safety. The happy-path
6
+ # operator flow lives in fm-secondmate-lifecycle-e2e.test.sh; this file keeps the
7
+ # destructive-invariant coverage that an e2e run cannot deterministically reach.
8
+ set -u
9
+
10
+ # shellcheck source=tests/secondmate-helpers.sh disable=SC1091
11
+ . "$(dirname "${BASH_SOURCE[0]}")/secondmate-helpers.sh"
12
+
13
+ TMP_ROOT=$(fm_test_tmproot fm-secondmate-safety)
14
+ export FM_BACKEND=tmux
15
+
16
+ file_mode() {
17
+ if [ "$(uname)" = Darwin ]; then
18
+ stat -f %Lp "$1"
19
+ else
20
+ stat -c %a "$1"
21
+ fi
22
+ }
23
+
24
+ install_fake_process_event_sweep() {
25
+ local home=$1 log=$2
26
+ mkdir -p "$home/bin"
27
+ cat > "$home/bin/fm-procevent.sh" <<'SH'
28
+ #!/usr/bin/env bash
29
+ set -eu
30
+ case "${1:-}" in
31
+ sweep-home)
32
+ if [ "${2:-}" = --preflight ]; then
33
+ exit 0
34
+ fi
35
+ [ "$#" -eq 1 ] || exit 2
36
+ printf '%s\n' "$FM_HOME" >> "$FM_FAKE_PROCEVENT_SWEEP_LOG"
37
+ rm -f -- "$FM_HOME"/state/procevent/*.source "$FM_HOME"/state/procevent/*.runner
38
+ ;;
39
+ reconcile)
40
+ printf '%s\n' "$FM_HOME" >> "$FM_FAKE_PROCEVENT_REARM_LOG"
41
+ [ -z "${FM_FAKE_PROCEVENT_REARM_FAIL:-}" ] || exit 1
42
+ ;;
43
+ *) exit 2 ;;
44
+ esac
45
+ SH
46
+ chmod +x "$home/bin/fm-procevent.sh"
47
+ : > "$log"
48
+ }
49
+
50
+ test_fm_home_parameterization() {
51
+ local brief home_one home_two out
52
+ home_one="$TMP_ROOT/home one"
53
+ home_two="$TMP_ROOT/home-two"
54
+ mkdir -p "$home_one/data" "$home_one/state" "$home_two/data" "$home_two/state"
55
+ printf '%s\n' '- app [local-only +yolo] - test app (added 2026-06-22)' > "$home_one/data/projects.md"
56
+
57
+ out=$(FM_HOME="$home_one" "$ROOT/bin/fm-project-mode.sh" app)
58
+ [ "$out" = "local-only on" ] || fail "fm-project-mode did not read projects.md from FM_HOME"
59
+ out=$(FM_HOME="$home_two" "$ROOT/bin/fm-project-mode.sh" app 2>/dev/null)
60
+ [ "$out" = "no-mistakes off" ] || fail "fm-project-mode did not isolate missing registry by home"
61
+
62
+ FM_HOME="$home_one" "$ROOT/bin/fm-brief.sh" task-a app --mode no-mistakes >/dev/null || fail "brief scaffold failed under FM_HOME"
63
+ brief="$home_one/data/task-a/brief.md"
64
+ [ -f "$brief" ] || fail "brief was not written under FM_HOME/data"
65
+ grep -F ">> '$home_one/state/task-a.status'" "$brief" >/dev/null || fail "brief did not shell-quote FM_HOME state path"
66
+
67
+ FM_HOME="$home_one" "$ROOT/bin/fm-brief.sh" task-b app --scout >/dev/null || fail "scout brief scaffold failed under FM_HOME"
68
+ brief="$home_one/data/task-b/brief.md"
69
+ grep -F ">> '$home_one/state/task-b.status'" "$brief" >/dev/null || fail "scout brief did not shell-quote FM_HOME state path"
70
+
71
+ FM_HOME="$home_one" FM_SECONDMATE_CHARTER='ops domain' "$ROOT/bin/fm-brief.sh" task-c --secondmate app >/dev/null \
72
+ || fail "secondmate brief scaffold failed under FM_HOME"
73
+ brief="$home_one/data/task-c/brief.md"
74
+ grep -F ">> '$home_one/state/task-c.status'" "$brief" >/dev/null || fail "secondmate brief did not shell-quote FM_HOME state path"
75
+
76
+ printf 'project=x\n' > "$home_one/state/task-a.meta"
77
+ FM_HOME="$home_one" FM_GUARD_GRACE=999999 "$ROOT/bin/fm-pr-check.sh" task-a https://github.com/example/repo/pull/1 >/dev/null 2>/dev/null \
78
+ || fail "fm-pr-check failed under FM_HOME"
79
+ [ -f "$home_one/state/task-a.check.sh" ] || fail "pr check was not written under FM_HOME/state"
80
+ [ ! -e "$home_two/state/task-a.check.sh" ] || fail "pr check leaked into another home"
81
+ pass "FM_HOME parameterizes data and state paths"
82
+ }
83
+
84
+ test_lock_status_is_per_home() {
85
+ local home_one home_two out
86
+ home_one="$TMP_ROOT/lock-one"
87
+ home_two="$TMP_ROOT/lock-two"
88
+ mkdir -p "$home_one/state" "$home_two/state"
89
+ printf '999999\n' > "$home_one/state/.lock"
90
+ out=$(FM_HOME="$home_one" "$ROOT/bin/fm-lock.sh" status)
91
+ printf '%s\n' "$out" | grep -F 'lock: stale' >/dev/null || fail "home one lock status did not read its own lock"
92
+ out=$(FM_HOME="$home_two" "$ROOT/bin/fm-lock.sh" status)
93
+ [ "$out" = "lock: free" ] || fail "home two lock status was affected by home one"
94
+ pass "fm-lock status is scoped per home"
95
+ }
96
+
97
+ test_seed_allows_overlapping_clones_and_drops_owner() {
98
+ # A project may appear in several secondmates' (non-exclusive) clone lists; the
99
+ # registry never uses the legacy owns: field, and the removed `owner` subcommand
100
+ # stays gone. The full happy seed - charter copied, clones+origins, no-mistakes
101
+ # init, modes preserved - is asserted by fm-secondmate-lifecycle-e2e.
102
+ local home design other
103
+ home="$TMP_ROOT/overlap-main"
104
+ design="$TMP_ROOT/overlap-design"
105
+ other="$TMP_ROOT/overlap-other"
106
+ mkdir -p "$home/projects" "$home/data" "$home/state"
107
+ fm_git_init_commit "$home/projects/alpha"
108
+ fm_git_init_commit "$home/projects/beta"
109
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/seed-overlap-alpha.git"
110
+ fm_git_add_origin "$home/projects/beta" "$TMP_ROOT/remotes/seed-overlap-beta.git"
111
+ cat > "$home/data/projects.md" <<EOF
112
+ - alpha [direct-PR] - alpha project (added 2026-06-22)
113
+ - beta [direct-PR] - beta project (added 2026-06-22)
114
+ EOF
115
+
116
+ FM_HOME="$home" FM_SECONDMATE_CHARTER='feature design for alpha beta' \
117
+ FM_SECONDMATE_SCOPE='feature design for alpha beta' \
118
+ "$ROOT/bin/fm-home-seed.sh" design "$design" alpha beta >/dev/null \
119
+ || fail "initial seed failed"
120
+ assert_grep '- design - feature design for alpha beta' "$home/data/secondmates.md" "design registry line missing"
121
+ assert_grep 'projects: alpha, beta' "$home/data/secondmates.md" "design project clone list missing"
122
+ assert_no_grep 'owns:' "$home/data/secondmates.md" "registry used the legacy owns field"
123
+
124
+ # beta is shared with a second secondmate of a different scope (overlap allowed).
125
+ FM_HOME="$home" FM_SECONDMATE_CHARTER='issue triage for beta' \
126
+ FM_SECONDMATE_SCOPE='issue triage for beta' \
127
+ "$ROOT/bin/fm-home-seed.sh" other "$other" beta >/dev/null 2>&1 \
128
+ || fail "seed refused overlapping project clones across different scopes"
129
+ assert_grep '- other - issue triage for beta' "$home/data/secondmates.md" "overlapping registry line missing"
130
+ FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null || fail "registry validation rejected overlapping clones"
131
+
132
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" owner alpha >/dev/null 2>&1; then
133
+ fail "owner subcommand still succeeded after routing moved to scopes"
134
+ fi
135
+ pass "seed allows overlapping project clone lists and drops the owns/owner routing"
136
+ }
137
+
138
+ test_home_seed_validate_rejects_unparseable_registry_entry() {
139
+ local home err
140
+ home="$TMP_ROOT/unparseable-registry-home"
141
+ err="$TMP_ROOT/unparseable-registry.err"
142
+ mkdir -p "$home/data"
143
+ printf '%s\n' '- broken - prose (home: /tmp/child; scope: missing projects and date)' > "$home/data/secondmates.md"
144
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null 2>"$err"; then
145
+ fail "home-seed validation accepted an operationally unparseable registry record"
146
+ fi
147
+ grep -F 'malformed secondmate registry entry' "$err" >/dev/null \
148
+ || fail "home-seed validation did not explain the malformed registry record"
149
+ pass "home-seed validation rejects registry records no operational parser can consume"
150
+ }
151
+
152
+ test_home_seed_refuses_broken_registry_symlink() {
153
+ local home sub err target
154
+ home="$TMP_ROOT/broken-registry-symlink-home"
155
+ sub="$TMP_ROOT/broken-registry-symlink-subhome"
156
+ err="$TMP_ROOT/broken-registry-symlink.err"
157
+ target="$home/data/missing-secondmates.md"
158
+ mkdir -p "$home/data" "$home/state" "$home/projects"
159
+ ln -s "$target" "$home/data/secondmates.md"
160
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null 2>"$err"; then
161
+ fail "home-seed validation accepted a broken registry symlink"
162
+ fi
163
+ grep -F 'secondmate registry is unavailable or unsafe' "$err" >/dev/null \
164
+ || fail "home-seed validation did not explain the broken registry symlink"
165
+ if FM_HOME="$home" FM_SECONDMATE_CHARTER='design domain' \
166
+ "$ROOT/bin/fm-home-seed.sh" design "$sub" alpha >/dev/null 2>"$err"; then
167
+ fail "home seeding accepted a broken registry symlink"
168
+ fi
169
+ [ -L "$home/data/secondmates.md" ] || fail "home seeding replaced the broken registry symlink"
170
+ [ ! -e "$target" ] || fail "home seeding wrote through the broken registry symlink"
171
+ [ ! -e "$sub" ] || fail "home seeding provisioned a home before broken registry refusal"
172
+ [ ! -e "$home/data/design" ] || fail "home seeding created a brief before broken registry refusal"
173
+ pass "home seeding refuses broken registry symlinks before provisioning"
174
+ }
175
+
176
+ test_home_seed_refuses_unreadable_registry() {
177
+ local home sub err registry
178
+ home="$TMP_ROOT/unreadable-registry-home"
179
+ sub="$TMP_ROOT/unreadable-registry-subhome"
180
+ err="$TMP_ROOT/unreadable-registry.err"
181
+ registry="$home/data/secondmates.md"
182
+ mkdir -p "$home/data" "$home/state" "$home/projects"
183
+ printf '%s\n' '- design - design domain (home: /tmp/design; scope: design; projects: alpha; added 2026-07-30)' > "$registry"
184
+ chmod 000 "$registry"
185
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null 2>"$err"; then
186
+ chmod 600 "$registry"
187
+ fail "home-seed validation accepted an unreadable registry"
188
+ fi
189
+ grep -F 'secondmate registry is unavailable or unsafe' "$err" >/dev/null || {
190
+ chmod 600 "$registry"
191
+ fail "home-seed validation did not explain the unreadable registry"
192
+ }
193
+ if FM_HOME="$home" FM_SECONDMATE_CHARTER='design domain' \
194
+ "$ROOT/bin/fm-home-seed.sh" design "$sub" alpha >/dev/null 2>"$err"; then
195
+ chmod 600 "$registry"
196
+ fail "home seeding accepted an unreadable registry"
197
+ fi
198
+ chmod 600 "$registry"
199
+ [ ! -e "$sub" ] || fail "home seeding provisioned a home before unreadable registry refusal"
200
+ [ ! -e "$home/data/design" ] || fail "home seeding created a brief before unreadable registry refusal"
201
+ pass "home seeding refuses unreadable registries before provisioning"
202
+ }
203
+
204
+ test_home_seed_validate_rejects_duplicate_homes() {
205
+ local home subhome subhome_abs err
206
+ home="$TMP_ROOT/duplicate-home"
207
+ subhome="$TMP_ROOT/duplicate-subhome"
208
+ err="$TMP_ROOT/duplicate-home.err"
209
+ mkdir -p "$home/data" "$subhome"
210
+ subhome_abs=$(cd "$subhome" && pwd -P)
211
+ cat > "$home/data/secondmates.md" <<EOF
212
+ - design - design domain mentions home: $TMP_ROOT/ignored-summary-home (home: $subhome_abs; scope: design work mentions home: $TMP_ROOT/ignored-scope-home; projects: alpha; added 2026-06-22)
213
+ - triage - triage domain (home: $subhome_abs; scope: issue triage; projects: beta; added 2026-06-22)
214
+ EOF
215
+
216
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null 2>"$err"; then
217
+ fail "registry validation accepted two secondmates with the same home"
218
+ fi
219
+ grep -F 'duplicate secondmate home assignment' "$err" >/dev/null \
220
+ || fail "registry validation did not explain duplicate home assignment"
221
+ pass "home seed validation rejects duplicate home routes"
222
+ }
223
+
224
+ test_home_seed_validate_rejects_duplicate_ids() {
225
+ local home first second first_abs second_abs err
226
+ home="$TMP_ROOT/duplicate-id-home"
227
+ first="$TMP_ROOT/duplicate-id-first"
228
+ second="$TMP_ROOT/duplicate-id-second"
229
+ err="$TMP_ROOT/duplicate-id.err"
230
+ mkdir -p "$home/data" "$first" "$second"
231
+ first_abs=$(cd "$first" && pwd -P)
232
+ second_abs=$(cd "$second" && pwd -P)
233
+ cat > "$home/data/secondmates.md" <<EOF
234
+ - design - design domain (home: $first_abs; scope: design work; projects: alpha; added 2026-06-22)
235
+ - design - design domain (home: $second_abs; scope: design work; projects: beta; added 2026-06-22)
236
+ EOF
237
+
238
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null 2>"$err"; then
239
+ fail "registry validation accepted two homes for the same secondmate id"
240
+ fi
241
+ grep -F 'duplicate secondmate id assignment' "$err" >/dev/null \
242
+ || fail "registry validation did not explain duplicate id assignment"
243
+ pass "home seed validation rejects duplicate id routes"
244
+ }
245
+
246
+ test_home_seed_validate_rejects_nested_homes() {
247
+ local home ancestor descendant ancestor_abs descendant_abs err
248
+ home="$TMP_ROOT/nested-home"
249
+ ancestor="$TMP_ROOT/nested-domain-a"
250
+ descendant="$ancestor/domain-b"
251
+ err="$TMP_ROOT/nested-home.err"
252
+ mkdir -p "$home/data" "$ancestor" "$descendant"
253
+ ancestor_abs=$(cd "$ancestor" && pwd -P)
254
+ descendant_abs=$(cd "$descendant" && pwd -P)
255
+ cat > "$home/data/secondmates.md" <<EOF
256
+ - design - design domain (home: $ancestor_abs; scope: design work; projects: alpha; added 2026-06-22)
257
+ - triage - triage domain (home: $descendant_abs; scope: issue triage; projects: beta; added 2026-06-22)
258
+ EOF
259
+
260
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null 2>"$err"; then
261
+ fail "registry validation accepted nested secondmate homes"
262
+ fi
263
+ grep -F 'overlapping secondmate home assignment' "$err" >/dev/null \
264
+ || fail "registry validation did not explain nested home assignment"
265
+ pass "home seed validation rejects nested home routes"
266
+ }
267
+
268
+ test_home_seed_uses_treehouse_acquired_home() {
269
+ local home acquired acquired_abs fakebin log lease out
270
+ home="$TMP_ROOT/dash-home"
271
+ acquired="$TMP_ROOT/dash-acquired-home"
272
+ mkdir -p "$home/projects" "$home/data" "$home/state"
273
+ fm_git_init_commit "$home/projects/alpha"
274
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/dash-alpha.git"
275
+ printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
276
+ git clone --quiet "$ROOT" "$acquired"
277
+ fakebin=$(make_fake_tmux "$TMP_ROOT/dash-fake")
278
+ log="$TMP_ROOT/dash-fake/tmux.log"
279
+ lease="$TMP_ROOT/dash-fake/lease"
280
+
281
+ out=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TREEHOUSE_HOME="$acquired" FM_FAKE_TMUX_LOG="$log" \
282
+ FM_FAKE_TREEHOUSE_LEASE_FILE="$lease" \
283
+ FM_SECONDMATE_CHARTER='dash acquired scope' FM_SECONDMATE_SCOPE='dash acquired scope' \
284
+ "$ROOT/bin/fm-home-seed.sh" dash - alpha) \
285
+ || fail "seed failed for a treehouse-acquired home"
286
+ acquired_abs=$(cd "$acquired" && pwd -P)
287
+ printf '%s\n' "$out" | grep -F "home=$acquired_abs" >/dev/null || fail "seed did not report acquired home"
288
+ grep -F 'treehouse get --lease --lease-holder dash' "$log" >/dev/null || fail "seed did not durably lease a home under the secondmate id"
289
+ [ -f "$lease" ] || fail "seed did not record a treehouse lease"
290
+ [ "$(cat "$lease")" = dash ] || fail "seed did not set the lease holder to the secondmate id"
291
+ [ -f "$acquired/.fm-secondmate-home" ] || fail "seed did not mark acquired home"
292
+ [ "$(cat "$acquired/.fm-secondmate-home")" = dash ] || fail "seed wrote wrong acquired-home marker"
293
+ [ -d "$acquired/projects/alpha/.git" ] || fail "seed did not clone project into acquired home"
294
+ grep -F "home: $acquired_abs" "$home/data/secondmates.md" >/dev/null || fail "registry did not record acquired home"
295
+ pass "home seeding durably leases treehouse-acquired dash homes under the secondmate id"
296
+ }
297
+
298
+ test_home_seed_returns_treehouse_acquired_home_on_assignment_failure() {
299
+ local home acquired acquired_abs fakebin log err
300
+ home="$TMP_ROOT/dash-fail-home"
301
+ acquired="$TMP_ROOT/dash-fail-acquired-home"
302
+ err="$TMP_ROOT/dash-fail.err"
303
+ mkdir -p "$home/projects" "$home/data" "$home/state"
304
+ fm_git_init_commit "$home/projects/alpha"
305
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/dash-fail-alpha.git"
306
+ printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
307
+ git clone --quiet "$ROOT" "$acquired"
308
+ acquired_abs=$(cd "$acquired" && pwd -P)
309
+ printf 'other\n' > "$acquired/.fm-secondmate-home"
310
+ fakebin=$(make_fake_tmux "$TMP_ROOT/dash-fail-fake")
311
+ log="$TMP_ROOT/dash-fail-fake/tmux.log"
312
+
313
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TREEHOUSE_HOME="$acquired" FM_FAKE_TMUX_LOG="$log" \
314
+ FM_SECONDMATE_CHARTER='dash acquired scope' FM_SECONDMATE_SCOPE='dash acquired scope' \
315
+ "$ROOT/bin/fm-home-seed.sh" dash - alpha >/dev/null 2>"$err"; then
316
+ fail "seed reused an acquired home marked for another secondmate"
317
+ fi
318
+ grep -F 'already marked for other' "$err" >/dev/null || fail "seed did not explain acquired marked-home rejection"
319
+ grep -F "treehouse return --force $acquired_abs" "$log" >/dev/null \
320
+ || fail "failed acquired seed did not return the home through treehouse"
321
+ if [ -f "$home/data/secondmates.md" ] && grep -F -- '- dash ' "$home/data/secondmates.md" >/dev/null; then
322
+ fail "failed acquired seed left a registry route"
323
+ fi
324
+ pass "home seeding returns rejected acquired homes through treehouse"
325
+ }
326
+
327
+ test_home_seed_warns_when_acquired_home_return_fails() {
328
+ local home acquired acquired_abs fakebin log err lease
329
+ home="$TMP_ROOT/dash-return-fail-home"
330
+ acquired="$TMP_ROOT/dash-return-fail-acquired-home"
331
+ err="$TMP_ROOT/dash-return-fail.err"
332
+ mkdir -p "$home/projects" "$home/data" "$home/state"
333
+ fm_git_init_commit "$home/projects/alpha"
334
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/dash-return-fail-alpha.git"
335
+ printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
336
+ git clone --quiet "$ROOT" "$acquired"
337
+ acquired_abs=$(cd "$acquired" && pwd -P)
338
+ printf 'other\n' > "$acquired/.fm-secondmate-home"
339
+ fakebin=$(make_fake_tmux "$TMP_ROOT/dash-return-fail-fake")
340
+ log="$TMP_ROOT/dash-return-fail-fake/tmux.log"
341
+ lease="$TMP_ROOT/dash-return-fail-fake/lease"
342
+
343
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TREEHOUSE_HOME="$acquired" FM_FAKE_TMUX_LOG="$log" \
344
+ FM_FAKE_TREEHOUSE_LEASE_FILE="$lease" FM_FAKE_TREEHOUSE_RETURN_FAIL=1 \
345
+ FM_SECONDMATE_CHARTER='dash acquired scope' FM_SECONDMATE_SCOPE='dash acquired scope' \
346
+ "$ROOT/bin/fm-home-seed.sh" dash - alpha >/dev/null 2>"$err"; then
347
+ fail "seed reused an acquired home after return failure setup"
348
+ fi
349
+ grep -F 'already marked for other' "$err" >/dev/null || fail "seed did not report original acquired-home rejection"
350
+ grep -F "warning: failed to return treehouse-acquired home $acquired_abs during seed rollback" "$err" >/dev/null \
351
+ || fail "seed rollback did not warn when treehouse return failed"
352
+ [ -f "$lease" ] || fail "failed rollback return did not preserve lease evidence"
353
+ grep -F "treehouse return --force $acquired_abs" "$log" >/dev/null \
354
+ || fail "failed rollback did not attempt to return the acquired home"
355
+ pass "home seed rollback warns when treehouse-acquired return fails"
356
+ }
357
+
358
+ test_home_seed_does_not_return_unsafe_acquired_home() {
359
+ local home descendant fakebin log err
360
+ home="$TMP_ROOT/dash-active-home"
361
+ descendant="$home/data/dash-descendant-home"
362
+ err="$TMP_ROOT/dash-active.err"
363
+ mkdir -p "$home/projects" "$home/data" "$home/state"
364
+ fm_git_init_commit "$home/projects/alpha"
365
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/dash-active-alpha.git"
366
+ printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
367
+ fakebin=$(make_fake_tmux "$TMP_ROOT/dash-active-fake")
368
+ log="$TMP_ROOT/dash-active-fake/tmux.log"
369
+
370
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TREEHOUSE_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
371
+ "$ROOT/bin/fm-home-seed.sh" dash - alpha >/dev/null 2>"$err"; then
372
+ fail "seed accepted an acquired home matching the active firstmate home"
373
+ fi
374
+ grep -F 'secondmate home cannot be the active firstmate home' "$err" >/dev/null \
375
+ || fail "seed did not explain active acquired-home rejection"
376
+ grep -F "treehouse return --force" "$log" >/dev/null \
377
+ && fail "seed returned an unsafe acquired active home through treehouse"
378
+ [ -d "$home/projects/alpha" ] || fail "unsafe acquired-home rollback removed the active home"
379
+
380
+ : > "$log"
381
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TREEHOUSE_HOME="$descendant" FM_FAKE_TMUX_LOG="$log" \
382
+ "$ROOT/bin/fm-home-seed.sh" dash - alpha >/dev/null 2>"$err"; then
383
+ fail "seed accepted an acquired home inside the active firstmate home"
384
+ fi
385
+ grep -F 'secondmate home cannot be inside the active firstmate home' "$err" >/dev/null \
386
+ || fail "seed did not explain active descendant acquired-home rejection"
387
+ grep -F "treehouse return --force" "$log" >/dev/null \
388
+ && fail "seed returned an unsafe acquired active descendant through treehouse"
389
+ [ -d "$descendant" ] || fail "unsafe acquired-home rollback removed the active descendant"
390
+ pass "home seeding leaves unsafe acquired active homes untouched"
391
+ }
392
+
393
+ test_home_seed_rolls_back_failed_clone() {
394
+ local home subhome err missing_remote
395
+ home="$TMP_ROOT/rollback-home"
396
+ subhome="$TMP_ROOT/rollback-subhome"
397
+ err="$TMP_ROOT/rollback-home.err"
398
+ missing_remote="$TMP_ROOT/remotes/missing-beta.git"
399
+ mkdir -p "$home/projects" "$home/data" "$home/state"
400
+ fm_git_init_commit "$home/projects/alpha"
401
+ fm_git_init_commit "$home/projects/beta"
402
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/rollback-alpha.git"
403
+ git -C "$home/projects/beta" remote add origin "file://$missing_remote"
404
+ cat > "$home/data/projects.md" <<EOF
405
+ - alpha [direct-PR] - alpha project (added 2026-06-22)
406
+ - beta [direct-PR] - beta project (added 2026-06-22)
407
+ EOF
408
+
409
+ if FM_HOME="$home" FM_SECONDMATE_CHARTER='rollback scope' FM_SECONDMATE_SCOPE='rollback scope' \
410
+ "$ROOT/bin/fm-home-seed.sh" rollback "$subhome" alpha beta >/dev/null 2>"$err"; then
411
+ fail "seed succeeded even though the second project clone failed"
412
+ fi
413
+ grep -F 'does not appear to be a git repository' "$err" >/dev/null \
414
+ || grep -F 'repository' "$err" >/dev/null \
415
+ || fail "seed failure did not include the clone error"
416
+ [ ! -e "$subhome" ] || fail "failed seed left the newly created secondmate home behind"
417
+ [ ! -e "$subhome/.fm-secondmate-home" ] || fail "failed seed left a subhome marker"
418
+ [ ! -e "$subhome/projects/alpha" ] || fail "failed seed left a previously cloned project"
419
+ [ ! -e "$home/data/rollback/brief.md" ] || fail "failed seed left a generated charter brief"
420
+ if [ -f "$home/data/secondmates.md" ] && grep -F -- '- rollback ' "$home/data/secondmates.md" >/dev/null; then
421
+ fail "failed seed left a registry route"
422
+ fi
423
+ pass "home seeding rolls back failed clone attempts without residue"
424
+ }
425
+
426
+ test_home_seed_refuses_missing_filled_charter() {
427
+ local home subhome err
428
+ home="$TMP_ROOT/missing-charter-home"
429
+ subhome="$TMP_ROOT/missing-charter-subhome"
430
+ err="$TMP_ROOT/missing-charter.err"
431
+ mkdir -p "$home/projects" "$home/data" "$home/state"
432
+ fm_git_init_commit "$home/projects/alpha"
433
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/missing-charter-alpha.git"
434
+ printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
435
+
436
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
437
+ fail "seed accepted a direct seed without a filled charter"
438
+ fi
439
+ grep -F 'no filled secondmate charter brief' "$err" >/dev/null \
440
+ || fail "seed did not explain missing filled charter refusal"
441
+ [ ! -e "$subhome" ] || fail "missing charter seed left a generated subhome"
442
+ [ ! -e "$home/data/design/brief.md" ] || fail "missing charter seed generated a placeholder charter"
443
+ pass "home seeding refuses direct seed without filled charter text"
444
+ }
445
+
446
+ test_home_seed_refuses_placeholder_charter() {
447
+ local home subhome err
448
+ home="$TMP_ROOT/placeholder-charter-home"
449
+ subhome="$TMP_ROOT/placeholder-charter-subhome"
450
+ err="$TMP_ROOT/placeholder-charter.err"
451
+ mkdir -p "$home/projects" "$home/data" "$home/state"
452
+ fm_git_init_commit "$home/projects/alpha"
453
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/placeholder-charter-alpha.git"
454
+ printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
455
+ FM_HOME="$home" "$ROOT/bin/fm-brief.sh" design --secondmate alpha >/dev/null \
456
+ || fail "placeholder charter scaffold failed"
457
+
458
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
459
+ fail "seed accepted an unfilled placeholder charter"
460
+ fi
461
+ grep -F 'still contains {TASK}' "$err" >/dev/null \
462
+ || fail "seed did not explain placeholder charter refusal"
463
+ [ ! -e "$subhome" ] || fail "placeholder charter seed left a generated subhome"
464
+ [ ! -e "$subhome/projects/alpha" ] || fail "placeholder charter seed cloned before refusing"
465
+ pass "home seeding refuses unfilled placeholder charters"
466
+ }
467
+
468
+ test_home_seed_refuses_empty_charter_fields() {
469
+ local home subhome err
470
+ home="$TMP_ROOT/empty-charter-home"
471
+ subhome="$TMP_ROOT/empty-charter-subhome"
472
+ err="$TMP_ROOT/empty-charter.err"
473
+ mkdir -p "$home/projects" "$home/data" "$home/state"
474
+ fm_git_init_commit "$home/projects/alpha"
475
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/empty-charter-alpha.git"
476
+ printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
477
+
478
+ if FM_HOME="$home" FM_SECONDMATE_CHARTER=' ' "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
479
+ fail "seed accepted a whitespace-only charter"
480
+ fi
481
+ grep -F 'empty Charter section' "$err" >/dev/null \
482
+ || fail "seed did not explain empty charter refusal"
483
+ [ ! -e "$subhome" ] || fail "empty charter seed left a generated subhome"
484
+
485
+ rm -rf "$home/data/design" "$subhome" "$err"
486
+ FM_SECONDMATE_SCOPE=' ' scaffold_secondmate_charter "$home" design 'filled charter' alpha \
487
+ || fail "empty scope fixture scaffold failed"
488
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
489
+ fail "seed accepted an empty routing scope"
490
+ fi
491
+ grep -F 'empty Routing scope section' "$err" >/dev/null \
492
+ || fail "seed did not explain empty routing scope refusal"
493
+ [ ! -e "$subhome" ] || fail "empty routing scope seed left a generated subhome"
494
+ pass "home seeding refuses empty normalized charter fields"
495
+ }
496
+
497
+ test_home_seed_no_projects_end_to_end() {
498
+ # A domain whose subject is the firstmate repo itself needs no project clones:
499
+ # the deliberate --no-projects signal scaffolds, seeds, registers, and spawns a
500
+ # project-less home end to end with no placeholder clone.
501
+ local home sub sub_abs fakebin log meta proj_val out
502
+ home="$TMP_ROOT/no-projects-seed-home"
503
+ sub="$TMP_ROOT/no-projects-seed-subhome"
504
+ mkdir -p "$home/projects" "$home/data" "$home/state"
505
+ fakebin=$(make_fake_tmux "$TMP_ROOT/no-projects-fake")
506
+ log="$TMP_ROOT/no-projects-fake/tmux.log"
507
+
508
+ out=$(FM_HOME="$home" FM_SECONDMATE_CHARTER='firstmate self-development' \
509
+ FM_SECONDMATE_SCOPE='firstmate repo work' \
510
+ "$ROOT/bin/fm-home-seed.sh" fdev "$sub" --no-projects) \
511
+ || fail "project-less seed failed"
512
+ sub_abs=$(cd "$sub" && pwd -P)
513
+ printf '%s\n' "$out" | grep -F "home=$sub_abs" >/dev/null || fail "seed did not report the project-less subhome"
514
+
515
+ # Registered with an empty projects field, marked, charter copied, no clones.
516
+ assert_grep '- fdev - firstmate self-development' "$home/data/secondmates.md" "project-less registry line missing"
517
+ assert_grep 'scope: firstmate repo work' "$home/data/secondmates.md" "project-less registry scope missing"
518
+ assert_grep 'projects: ;' "$home/data/secondmates.md" "project-less registry did not render an empty projects field"
519
+ [ "$(cat "$sub/.fm-secondmate-home")" = fdev ] || fail "project-less seed did not mark the subhome"
520
+ assert_present "$sub/data/charter.md" "project-less seed did not copy the charter"
521
+ [ -z "$(ls -A "$sub/projects" 2>/dev/null)" ] || fail "project-less seed cloned a project"
522
+ FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null || fail "registry validation failed after project-less seed"
523
+
524
+ # Spawn tolerates the empty projects field: the home resolves from the registry
525
+ # and the projects meta is recorded empty rather than breaking the launch.
526
+ : > "$log"
527
+ PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
528
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/no-projects-fake/pane.txt" \
529
+ "$ROOT/bin/fm-spawn.sh" fdev "$sub" codex --secondmate >/dev/null 2>&1 \
530
+ || fail "project-less secondmate spawn failed"
531
+ meta="$home/state/fdev.meta"
532
+ assert_grep 'kind=secondmate' "$meta" "project-less spawn meta lost kind=secondmate"
533
+ assert_grep "home=$sub_abs" "$meta" "project-less spawn meta lost the subhome"
534
+ proj_val=$(grep '^projects=' "$meta" | head -1 | cut -d= -f2-)
535
+ [ -z "$proj_val" ] || fail "project-less spawn recorded a non-empty projects meta: '$proj_val'"
536
+ pass "home seeding scaffolds, registers, and spawns a project-less home end to end"
537
+ }
538
+
539
+ test_secondmate_spawn_resolves_punctuated_registry_projects() {
540
+ local home sub sub_abs fakebin log meta projects
541
+ home="$TMP_ROOT/punctuated-spawn-home"
542
+ sub="$TMP_ROOT/punctuated-spawn-subhome"
543
+ mkdir -p "$home/data" "$home/state" "$home/config" "$home/projects"
544
+ mkdir -p "$sub/data" "$sub/state" "$sub/config" "$sub/projects"
545
+ mark_firstmate_home "$sub"
546
+ printf 'punctuated\n' > "$sub/.fm-secondmate-home"
547
+ printf '# Charter\n\nHandled work.\n' > "$sub/data/charter.md"
548
+ sub_abs=$(cd "$sub" && pwd -P)
549
+ printf -- '- punctuated - launch notes (parenthetical) (home: %s; scope: launch (child); semicolon is valid; projects: alpha, beta; added 2026-07-30)' \
550
+ "$sub_abs" > "$home/data/secondmates.md"
551
+ FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null \
552
+ || fail "home-seed validation rejected punctuated registry fields before spawn"
553
+ fakebin=$(make_fake_tmux "$TMP_ROOT/punctuated-spawn-fake")
554
+ log="$TMP_ROOT/punctuated-spawn-fake/tmux.log"
555
+ PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
556
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/punctuated-spawn-fake/pane.txt" \
557
+ "$ROOT/bin/fm-spawn.sh" punctuated codex --secondmate >/dev/null 2>&1 \
558
+ || fail "secondmate spawn failed for punctuated registry fields"
559
+ meta="$home/state/punctuated.meta"
560
+ projects=$(grep '^projects=' "$meta" | cut -d= -f2-)
561
+ [ "$projects" = 'alpha, beta' ] \
562
+ || fail "secondmate spawn resolved the wrong projects field: '$projects'"
563
+ pass "secondmate spawn resolves home validation and projects from punctuated registry fields"
564
+ }
565
+
566
+ test_secondmate_spawn_refuses_ambiguous_and_mismatched_registry_bindings() {
567
+ local row case_name home sub other fakebin log err meta_before
568
+ for row in duplicate-id unterminated-duplicate-id duplicate-home supplied-mismatch metadata-mismatch; do
569
+ case_name=${row%%|*}
570
+ home="$TMP_ROOT/spawn-binding-$case_name-home"
571
+ sub="$TMP_ROOT/spawn-binding-$case_name-sub"
572
+ other="$TMP_ROOT/spawn-binding-$case_name-other"
573
+ mkdir -p "$home/data" "$home/state" "$home/config" "$home/projects"
574
+ mark_firstmate_home "$sub"
575
+ mark_firstmate_home "$other"
576
+ printf 'domain\n' > "$sub/.fm-secondmate-home"
577
+ printf 'domain\n' > "$other/.fm-secondmate-home"
578
+ case "$case_name" in
579
+ duplicate-id)
580
+ cat > "$home/data/secondmates.md" <<EOF
581
+ - domain - primary route (home: $sub; scope: valid (scope); punctuation; projects: alpha; added 2026-07-30)
582
+ - domain - duplicate route (home: $other; scope: duplicate; projects: beta; added 2026-07-30)
583
+ EOF
584
+ ;;
585
+ unterminated-duplicate-id)
586
+ printf -- '- domain - primary route (home: %s; scope: valid (scope); punctuation; projects: alpha; added 2026-07-30)\n- domain - duplicate route (home: %s; scope: duplicate; projects: beta; added 2026-07-30)' \
587
+ "$sub" "$other" > "$home/data/secondmates.md"
588
+ ;;
589
+ duplicate-home)
590
+ cat > "$home/data/secondmates.md" <<EOF
591
+ - domain - primary route (home: $sub; scope: valid (scope); punctuation; projects: alpha; added 2026-07-30)
592
+ - other - duplicate home route (home: $sub; scope: duplicate; projects: beta; added 2026-07-30)
593
+ EOF
594
+ ;;
595
+ supplied-mismatch|metadata-mismatch)
596
+ printf -- '- domain - mismatched route (home: %s; scope: valid (scope); punctuation; projects: alpha; added 2026-07-30)\n' \
597
+ "$other" > "$home/data/secondmates.md"
598
+ ;;
599
+ esac
600
+ fakebin=$(make_fake_tmux "$TMP_ROOT/spawn-binding-$case_name-fake")
601
+ log="$TMP_ROOT/spawn-binding-$case_name-fake/tmux.log"
602
+ err="$TMP_ROOT/spawn-binding-$case_name.err"
603
+ if [ "$case_name" = metadata-mismatch ]; then
604
+ fm_write_secondmate_meta "$home/state/domain.meta" "$sub"
605
+ meta_before="$TMP_ROOT/spawn-binding-$case_name.meta.before"
606
+ cp "$home/state/domain.meta" "$meta_before"
607
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
608
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-binding-$case_name-fake/pane.txt" \
609
+ "$ROOT/bin/fm-spawn.sh" domain codex --secondmate >/dev/null 2>"$err"; then
610
+ fail "secondmate spawn accepted $case_name registry binding"
611
+ fi
612
+ cmp -s "$meta_before" "$home/state/domain.meta" || fail "secondmate spawn changed metadata after $case_name refusal"
613
+ else
614
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
615
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-binding-$case_name-fake/pane.txt" \
616
+ "$ROOT/bin/fm-spawn.sh" domain "$sub" codex --secondmate >/dev/null 2>"$err"; then
617
+ fail "secondmate spawn accepted $case_name registry binding"
618
+ fi
619
+ [ ! -e "$home/state/domain.meta" ] || fail "secondmate spawn wrote metadata after $case_name refusal"
620
+ fi
621
+ [ ! -e "$home/state/.spawn-domain.lock" ] || fail "secondmate spawn left a lock after $case_name refusal"
622
+ grep -F 'new-window' "$log" >/dev/null && fail "secondmate spawn created an endpoint before $case_name refusal"
623
+ done
624
+ pass "secondmate spawn refuses ambiguous, supplied-home, and metadata-home registry bindings"
625
+ }
626
+
627
+ test_home_seed_refuses_projectful_reused_charter_for_projectless_home() {
628
+ local home reusable_sub stale_sub stale_brief stale_brief_before err
629
+ home="$TMP_ROOT/no-projects-reused-charter-home"
630
+ reusable_sub="$TMP_ROOT/no-projects-reused-charter-valid-subhome"
631
+ stale_sub="$TMP_ROOT/no-projects-reused-charter-stale-subhome"
632
+ stale_brief="$home/data/stale/brief.md"
633
+ stale_brief_before="$TMP_ROOT/no-projects-reused-charter.before"
634
+ err="$TMP_ROOT/no-projects-reused-charter.err"
635
+ mkdir -p "$home/data" "$home/state" "$reusable_sub/data" "$stale_sub/data"
636
+ mark_firstmate_home "$reusable_sub"
637
+ mark_firstmate_home "$stale_sub"
638
+
639
+ scaffold_secondmate_charter "$home" reusable 'firstmate self-development' --no-projects \
640
+ || fail "project-less charter scaffold failed"
641
+ printf '\n# Custom note\nThe projects above are local clones for work you supervise.\n' >> "$home/data/reusable/brief.md"
642
+ FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" reusable "$reusable_sub" --no-projects >/dev/null \
643
+ || fail "project-less seed rejected a reused project-less charter"
644
+ assert_grep 'None. This is a project-less domain' "$reusable_sub/data/charter.md" \
645
+ "reused project-less charter was not copied"
646
+
647
+ scaffold_secondmate_charter "$home" stale 'firstmate self-development. None. This is a project-less domain.' alpha \
648
+ || fail "projectful charter scaffold failed"
649
+ sed 's/The projects above are local clones for work you supervise; they are not an exclusive ownership claim./Project clone details are customized for this domain./' \
650
+ "$stale_brief" > "$stale_brief_before"
651
+ mv "$stale_brief_before" "$stale_brief"
652
+ cp "$stale_brief" "$stale_brief_before"
653
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" stale "$stale_sub" --no-projects >/dev/null 2>"$err"; then
654
+ fail "project-less seed accepted a reused charter with project clones"
655
+ fi
656
+ grep -F 'existing charter brief' "$err" >/dev/null \
657
+ || fail "project-less charter refusal did not name the stale charter conflict"
658
+ grep -F 'fm-brief.sh stale --secondmate --no-projects' "$err" >/dev/null \
659
+ || fail "project-less charter refusal did not explain how to re-scaffold"
660
+ cmp -s "$stale_brief_before" "$stale_brief" \
661
+ || fail "project-less charter refusal changed the reused charter"
662
+ assert_absent "$stale_sub/.fm-secondmate-home" "project-less charter refusal wrote a home marker"
663
+ assert_absent "$stale_sub/data/charter.md" "project-less charter refusal copied a charter"
664
+ assert_absent "$stale_sub/projects" "project-less charter refusal created a projects directory"
665
+ if grep -F -- '- stale ' "$home/data/secondmates.md" >/dev/null; then
666
+ fail "project-less charter refusal wrote a parent registry route"
667
+ fi
668
+ pass "home seeding validates reused project-less charters before mutation"
669
+ }
670
+
671
+ test_home_seed_refuses_projectless_conversion_of_populated_home() {
672
+ local home sub err registry_before
673
+ home="$TMP_ROOT/no-projects-conversion-home"
674
+ sub="$TMP_ROOT/no-projects-conversion-subhome"
675
+ err="$TMP_ROOT/no-projects-conversion.err"
676
+ mkdir -p "$home/data" "$home/state" "$sub/data" "$sub/projects/existing-clone"
677
+ mark_firstmate_home "$sub"
678
+ fm_git_init_commit "$sub/projects/existing-clone"
679
+ cat > "$sub/data/projects.md" <<EOF
680
+ - registry-only [direct-PR] - retained project entry (added 2026-06-22)
681
+ EOF
682
+ registry_before=$(cat "$sub/data/projects.md")
683
+
684
+ if FM_HOME="$home" FM_SECONDMATE_CHARTER='firstmate self-development' \
685
+ FM_SECONDMATE_SCOPE='firstmate repo work' \
686
+ "$ROOT/bin/fm-home-seed.sh" fdev "$sub" --no-projects >/dev/null 2>"$err"; then
687
+ fail "project-less seed converted a populated secondmate home"
688
+ fi
689
+ grep -F 'existing-clone' "$err" >/dev/null \
690
+ || fail "project-less conversion refusal did not name the existing clone"
691
+ grep -F 'registry-only' "$err" >/dev/null \
692
+ || fail "project-less conversion refusal did not name the registry entry"
693
+ grep -F 'retire or clean this home first' "$err" >/dev/null \
694
+ || fail "project-less conversion refusal did not explain the required cleanup"
695
+ assert_present "$sub/projects/existing-clone/.git" "project-less conversion refusal removed the existing clone"
696
+ [ "$registry_before" = "$(cat "$sub/data/projects.md")" ] \
697
+ || fail "project-less conversion refusal changed the project registry"
698
+ assert_absent "$sub/.fm-secondmate-home" "project-less conversion refusal wrote a home marker"
699
+ assert_absent "$sub/data/charter.md" "project-less conversion refusal copied a charter"
700
+ assert_absent "$sub/state" "project-less conversion refusal left an operational directory"
701
+ if [ -f "$home/data/secondmates.md" ] && grep -F -- '- fdev ' "$home/data/secondmates.md" >/dev/null; then
702
+ fail "project-less conversion refusal wrote a parent registry route"
703
+ fi
704
+ pass "home seeding refuses project-less conversion of a populated home"
705
+ }
706
+
707
+ test_home_seed_refuses_projectless_home_with_uninspectable_projects() {
708
+ local home sub err
709
+ home="$TMP_ROOT/no-projects-uninspectable-home"
710
+ sub="$TMP_ROOT/no-projects-uninspectable-subhome"
711
+ err="$TMP_ROOT/no-projects-uninspectable.err"
712
+ mkdir -p "$home/data" "$home/state" "$sub/data" "$sub/projects/hidden-clone"
713
+ mark_firstmate_home "$sub"
714
+ fm_git_init_commit "$sub/projects/hidden-clone"
715
+ chmod 311 "$sub/projects"
716
+
717
+ if FM_HOME="$home" FM_SECONDMATE_CHARTER='firstmate self-development' \
718
+ FM_SECONDMATE_SCOPE='firstmate repo work' \
719
+ "$ROOT/bin/fm-home-seed.sh" fdev "$sub" --no-projects >/dev/null 2>"$err"; then
720
+ chmod 700 "$sub/projects"
721
+ fail "project-less seed accepted a home whose projects directory could not be inspected"
722
+ fi
723
+ chmod 700 "$sub/projects"
724
+ grep -F 'cannot inspect existing projects directory' "$err" >/dev/null \
725
+ || fail "project-less seed did not explain the projects inspection failure"
726
+ grep -F 'resolve its access permissions or retire or clean this home' "$err" >/dev/null \
727
+ || fail "project-less seed did not explain how to resolve the inspection failure"
728
+ assert_present "$sub/projects/hidden-clone/.git" "project-less inspection refusal removed the existing clone"
729
+ assert_absent "$sub/.fm-secondmate-home" "project-less inspection refusal wrote a home marker"
730
+ assert_absent "$sub/data/charter.md" "project-less inspection refusal copied a charter"
731
+ assert_absent "$sub/state" "project-less inspection refusal left an operational directory"
732
+ if [ -f "$home/data/secondmates.md" ] && grep -F -- '- fdev ' "$home/data/secondmates.md" >/dev/null; then
733
+ fail "project-less inspection refusal wrote a parent registry route"
734
+ fi
735
+ pass "home seeding refuses project-less homes whose projects directory cannot be inspected"
736
+ }
737
+
738
+ test_home_seed_refuses_projectless_home_with_symlinked_projects() {
739
+ local home sub target err
740
+ home="$TMP_ROOT/no-projects-symlinked-projects-home"
741
+ sub="$TMP_ROOT/no-projects-symlinked-projects-subhome"
742
+ target="$sub/retained-projects"
743
+ err="$TMP_ROOT/no-projects-symlinked-projects.err"
744
+ mkdir -p "$home/data" "$home/state" "$sub/data" "$target/hidden-clone"
745
+ mark_firstmate_home "$sub"
746
+ fm_git_init_commit "$target/hidden-clone"
747
+ ln -s "$target" "$sub/projects"
748
+ chmod 311 "$target"
749
+
750
+ if FM_HOME="$home" FM_SECONDMATE_CHARTER='firstmate self-development' \
751
+ FM_SECONDMATE_SCOPE='firstmate repo work' \
752
+ "$ROOT/bin/fm-home-seed.sh" fdev "$sub" --no-projects >/dev/null 2>"$err"; then
753
+ chmod 700 "$target"
754
+ fail "project-less seed accepted a home whose projects directory is a symlink"
755
+ fi
756
+ chmod 700 "$target"
757
+ grep -F 'projects directory' "$err" >/dev/null \
758
+ || fail "project-less seed did not identify the symlinked projects directory"
759
+ grep -F 'it is a symlink' "$err" >/dev/null \
760
+ || fail "project-less seed did not explain the symlinked projects directory refusal"
761
+ assert_present "$target/hidden-clone/.git" "project-less symlink refusal removed the target clone"
762
+ [ -L "$sub/projects" ] || fail "project-less symlink refusal changed the projects symlink"
763
+ [ "$(readlink "$sub/projects")" = "$target" ] \
764
+ || fail "project-less symlink refusal retargeted the projects symlink"
765
+ assert_absent "$sub/.fm-secondmate-home" "project-less symlink refusal wrote a home marker"
766
+ assert_absent "$sub/data/charter.md" "project-less symlink refusal copied a charter"
767
+ assert_absent "$sub/state" "project-less symlink refusal left an operational directory"
768
+ if [ -f "$home/data/secondmates.md" ] && grep -F -- '- fdev ' "$home/data/secondmates.md" >/dev/null; then
769
+ fail "project-less symlink refusal wrote a parent registry route"
770
+ fi
771
+ pass "home seeding refuses project-less homes with symlinked projects directories"
772
+ }
773
+
774
+ test_home_seed_refuses_projectless_home_with_non_directory_projects() {
775
+ local home sub err projects_before
776
+ home="$TMP_ROOT/no-projects-nondirectory-projects-home"
777
+ sub="$TMP_ROOT/no-projects-nondirectory-projects-subhome"
778
+ err="$TMP_ROOT/no-projects-nondirectory-projects.err"
779
+ mkdir -p "$home/data" "$home/state" "$sub/data"
780
+ mark_firstmate_home "$sub"
781
+ printf '%s\n' 'retained project path' > "$sub/projects"
782
+ projects_before=$(cat "$sub/projects")
783
+
784
+ if FM_HOME="$home" FM_SECONDMATE_CHARTER='firstmate self-development' \
785
+ FM_SECONDMATE_SCOPE='firstmate repo work' \
786
+ "$ROOT/bin/fm-home-seed.sh" fdev "$sub" --no-projects >/dev/null 2>"$err"; then
787
+ fail "project-less seed accepted a home whose projects path is not a directory"
788
+ fi
789
+ grep -F 'projects directory' "$err" >/dev/null \
790
+ || fail "project-less seed did not identify the non-directory projects path"
791
+ grep -F 'it is not a directory' "$err" >/dev/null \
792
+ || fail "project-less seed did not explain the non-directory projects path refusal"
793
+ [ "$projects_before" = "$(cat "$sub/projects")" ] \
794
+ || fail "project-less non-directory refusal changed the projects path"
795
+ assert_absent "$sub/.fm-secondmate-home" "project-less non-directory refusal wrote a home marker"
796
+ assert_absent "$sub/data/charter.md" "project-less non-directory refusal copied a charter"
797
+ assert_absent "$sub/state" "project-less non-directory refusal left an operational directory"
798
+ if [ -f "$home/data/secondmates.md" ] && grep -F -- '- fdev ' "$home/data/secondmates.md" >/dev/null; then
799
+ fail "project-less non-directory refusal wrote a parent registry route"
800
+ fi
801
+ pass "home seeding refuses project-less homes with non-directory projects paths"
802
+ }
803
+
804
+ test_home_seed_refuses_projectless_home_with_uninspectable_registry() {
805
+ local home sub err registry_before
806
+ home="$TMP_ROOT/no-projects-uninspectable-registry-home"
807
+ sub="$TMP_ROOT/no-projects-uninspectable-registry-subhome"
808
+ err="$TMP_ROOT/no-projects-uninspectable-registry.err"
809
+ mkdir -p "$home/data" "$home/state" "$sub/data"
810
+ mark_firstmate_home "$sub"
811
+ printf '%s\n' '- hidden-registry [direct-PR] - retained project entry (added 2026-06-22)' > "$sub/data/projects.md"
812
+ registry_before=$(cat "$sub/data/projects.md")
813
+ chmod 000 "$sub/data/projects.md"
814
+
815
+ if FM_HOME="$home" FM_SECONDMATE_CHARTER='firstmate self-development' \
816
+ FM_SECONDMATE_SCOPE='firstmate repo work' \
817
+ "$ROOT/bin/fm-home-seed.sh" fdev "$sub" --no-projects >/dev/null 2>"$err"; then
818
+ chmod 600 "$sub/data/projects.md"
819
+ fail "project-less seed accepted a home whose project registry could not be inspected"
820
+ fi
821
+ chmod 600 "$sub/data/projects.md"
822
+ grep -F 'cannot inspect existing project registry' "$err" >/dev/null \
823
+ || fail "project-less seed did not explain the project registry inspection failure"
824
+ grep -F 'resolve its access permissions or retire or clean this home' "$err" >/dev/null \
825
+ || fail "project-less seed did not explain how to resolve the project registry inspection failure"
826
+ [ "$registry_before" = "$(cat "$sub/data/projects.md")" ] \
827
+ || fail "project-less inspection refusal changed the project registry"
828
+ assert_absent "$sub/.fm-secondmate-home" "project-less registry inspection refusal wrote a home marker"
829
+ assert_absent "$sub/data/charter.md" "project-less registry inspection refusal copied a charter"
830
+ assert_absent "$sub/state" "project-less registry inspection refusal left an operational directory"
831
+ assert_absent "$sub/projects" "project-less registry inspection refusal created a projects directory"
832
+ if [ -f "$home/data/secondmates.md" ] && grep -F -- '- fdev ' "$home/data/secondmates.md" >/dev/null; then
833
+ fail "project-less registry inspection refusal wrote a parent registry route"
834
+ fi
835
+ pass "home seeding refuses project-less homes whose project registry cannot be inspected"
836
+ }
837
+
838
+ test_home_seed_refuses_missing_projects_without_signal() {
839
+ # Accidental omission of the project list, with no deliberate --no-projects
840
+ # signal, must fail loudly and leave nothing behind, so a forgotten argument is
841
+ # never mistaken for an intentional project-less seed.
842
+ local home sub err
843
+ home="$TMP_ROOT/missing-projects-home"
844
+ sub="$TMP_ROOT/missing-projects-subhome"
845
+ err="$TMP_ROOT/missing-projects.err"
846
+ mkdir -p "$home/projects" "$home/data" "$home/state"
847
+
848
+ if FM_HOME="$home" FM_SECONDMATE_CHARTER='some scope' \
849
+ "$ROOT/bin/fm-home-seed.sh" fdev "$sub" >/dev/null 2>"$err"; then
850
+ fail "seed accepted a project-less home without the deliberate --no-projects signal"
851
+ fi
852
+ assert_absent "$sub" "loud-failure seed created a subhome"
853
+ if [ -f "$home/data/secondmates.md" ] && grep -F -- '- fdev ' "$home/data/secondmates.md" >/dev/null; then
854
+ fail "loud-failure seed left a registry route"
855
+ fi
856
+
857
+ # The deliberate signal is mutually exclusive with a project list.
858
+ if FM_HOME="$home" FM_SECONDMATE_CHARTER='some scope' \
859
+ "$ROOT/bin/fm-home-seed.sh" fdev "$sub" --no-projects alpha >/dev/null 2>"$err"; then
860
+ fail "seed accepted --no-projects combined with a project list"
861
+ fi
862
+ grep -F 'cannot be combined with a project list' "$err" >/dev/null \
863
+ || fail "seed did not explain the --no-projects mutual-exclusion rejection"
864
+ pass "home seeding fails loudly on accidental project omission and rejects mixed --no-projects"
865
+ }
866
+
867
+ test_home_seed_refuses_local_only_project() {
868
+ local home subhome err
869
+ home="$TMP_ROOT/local-only-seed-home"
870
+ subhome="$TMP_ROOT/local-only-seed-subhome"
871
+ err="$TMP_ROOT/local-only-seed.err"
872
+ mkdir -p "$home/projects" "$home/data" "$home/state"
873
+ fm_git_init_commit "$home/projects/alpha"
874
+ printf '%s\n' '- alpha [local-only] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
875
+
876
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
877
+ fail "seed allowed a local-only project into a secondmate home"
878
+ fi
879
+ grep -F 'project alpha is local-only; secondmate routes support only no-mistakes and direct-PR projects' "$err" >/dev/null \
880
+ || fail "seed did not explain local-only project rejection"
881
+ [ ! -e "$subhome" ] || fail "seed created a subhome before rejecting a local-only project"
882
+ pass "home seeding refuses local-only projects"
883
+ }
884
+
885
+ test_home_seed_refuses_registry_delimiter_home() {
886
+ local home subhome err
887
+ home="$TMP_ROOT/delimiter-home"
888
+ subhome="$TMP_ROOT/delimiter)subhome"
889
+ err="$TMP_ROOT/delimiter-home.err"
890
+ mkdir -p "$home/projects" "$home/data" "$home/state"
891
+ fm_git_init_commit "$home/projects/alpha"
892
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/delimiter-alpha.git"
893
+ printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
894
+
895
+ if FM_HOME="$home" FM_SECONDMATE_CHARTER='delimiter charter' "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
896
+ fail "seed accepted a home path with registry delimiters"
897
+ fi
898
+ grep -F 'secondmate home path contains registry delimiters' "$err" >/dev/null \
899
+ || fail "seed did not explain delimiter home refusal"
900
+ [ ! -e "$subhome/.fm-secondmate-home" ] || fail "delimiter home seed wrote a marker"
901
+ if [ -f "$home/data/secondmates.md" ] && grep -F -- '- design ' "$home/data/secondmates.md" >/dev/null; then
902
+ fail "delimiter home seed wrote a registry route"
903
+ fi
904
+ pass "home seeding refuses registry delimiter home paths"
905
+ }
906
+
907
+ test_home_seed_refuses_active_home_and_root() {
908
+ local home err active_ancestor active_descendant root_clone root_descendant root_ancestor root_inside
909
+ active_ancestor="$TMP_ROOT/active-seed-ancestor"
910
+ home="$active_ancestor/main-home"
911
+ err="$TMP_ROOT/active-seed.err"
912
+ active_descendant="$home/nested/design-home"
913
+ root_clone="$TMP_ROOT/active-seed-root"
914
+ root_descendant="$root_clone/tmp/design-home"
915
+ root_ancestor="$TMP_ROOT/active-seed-root-ancestor"
916
+ root_inside="$root_ancestor/nested-root"
917
+ git clone --quiet "$ROOT" "$active_ancestor"
918
+ mkdir -p "$home/projects" "$home/data" "$home/state"
919
+ fm_git_init_commit "$home/projects/alpha"
920
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/active-alpha.git"
921
+ printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
922
+ scaffold_secondmate_charter "$home" design 'design domain' alpha || fail "charter scaffold failed for active-home seed test"
923
+
924
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$home" alpha >/dev/null 2>"$err"; then
925
+ fail "seed allowed secondmate home to reuse active FM_HOME"
926
+ fi
927
+ grep -F 'secondmate home cannot be the active firstmate home' "$err" >/dev/null \
928
+ || fail "seed did not explain active FM_HOME rejection"
929
+
930
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$active_descendant" alpha >/dev/null 2>"$err"; then
931
+ fail "seed allowed secondmate home inside active FM_HOME"
932
+ fi
933
+ grep -F 'secondmate home cannot be inside the active firstmate home' "$err" >/dev/null \
934
+ || fail "seed did not explain active FM_HOME descendant rejection"
935
+ [ ! -e "$home/nested" ] || fail "seed created a directory inside active FM_HOME before descendant rejection"
936
+
937
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$active_ancestor" alpha >/dev/null 2>"$err"; then
938
+ fail "seed allowed secondmate home to contain active FM_HOME"
939
+ fi
940
+ grep -F 'secondmate home cannot be an ancestor of the active firstmate home' "$err" >/dev/null \
941
+ || fail "seed did not explain active FM_HOME ancestor rejection"
942
+ [ ! -f "$active_ancestor/.fm-secondmate-home" ] || fail "seed marked an ancestor of active FM_HOME"
943
+
944
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$ROOT" alpha >/dev/null 2>"$err"; then
945
+ fail "seed allowed secondmate home to reuse FM_ROOT"
946
+ fi
947
+ grep -F 'secondmate home cannot be the firstmate repo' "$err" >/dev/null \
948
+ || fail "seed did not explain FM_ROOT rejection"
949
+
950
+ git clone --quiet "$ROOT" "$root_clone"
951
+ if FM_HOME="$home" FM_ROOT_OVERRIDE="$root_clone" "$ROOT/bin/fm-home-seed.sh" design "$root_descendant" alpha >/dev/null 2>"$err"; then
952
+ fail "seed allowed secondmate home inside FM_ROOT"
953
+ fi
954
+ grep -F 'secondmate home cannot be inside the firstmate repo' "$err" >/dev/null \
955
+ || fail "seed did not explain FM_ROOT descendant rejection"
956
+ [ ! -e "$root_clone/tmp" ] || fail "seed created a directory inside FM_ROOT before descendant rejection"
957
+
958
+ git clone --quiet "$ROOT" "$root_ancestor"
959
+ git clone --quiet "$ROOT" "$root_inside"
960
+ if FM_HOME="$home" FM_ROOT_OVERRIDE="$root_inside" "$ROOT/bin/fm-home-seed.sh" design "$root_ancestor" alpha >/dev/null 2>"$err"; then
961
+ fail "seed allowed secondmate home to contain FM_ROOT"
962
+ fi
963
+ grep -F 'secondmate home cannot be an ancestor of the firstmate repo' "$err" >/dev/null \
964
+ || fail "seed did not explain FM_ROOT ancestor rejection"
965
+ [ ! -f "$root_ancestor/.fm-secondmate-home" ] || fail "seed marked an ancestor of FM_ROOT"
966
+ pass "home seeding refuses active home and repo root"
967
+ }
968
+
969
+ test_home_seed_refuses_home_marked_for_another_id() {
970
+ local home subhome err
971
+ home="$TMP_ROOT/marked-seed-home"
972
+ subhome="$TMP_ROOT/marked-seed-subhome"
973
+ err="$TMP_ROOT/marked-seed.err"
974
+ mkdir -p "$home/projects" "$home/data" "$home/state"
975
+ fm_git_init_commit "$home/projects/alpha"
976
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/marked-alpha.git"
977
+ git clone --quiet "$ROOT" "$subhome"
978
+ printf 'other\n' > "$subhome/.fm-secondmate-home"
979
+ printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
980
+ scaffold_secondmate_charter "$home" design 'design domain' alpha || fail "charter scaffold failed for marked-home seed test"
981
+
982
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
983
+ fail "seed reused a home marked for another secondmate"
984
+ fi
985
+ grep -F 'already marked for other' "$err" >/dev/null || fail "seed did not explain marked-home rejection"
986
+ [ "$(cat "$subhome/.fm-secondmate-home")" = "other" ] || fail "seed overwrote another secondmate marker"
987
+ pass "home seeding refuses homes marked for another id"
988
+ }
989
+
990
+ test_home_seed_refuses_home_registered_to_another_id() {
991
+ local home subhome subhome_abs err
992
+ home="$TMP_ROOT/registered-seed-home"
993
+ subhome="$TMP_ROOT/registered-seed-subhome"
994
+ err="$TMP_ROOT/registered-seed.err"
995
+ mkdir -p "$home/projects" "$home/data" "$home/state"
996
+ fm_git_init_commit "$home/projects/alpha"
997
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/registered-alpha.git"
998
+ git clone --quiet "$ROOT" "$subhome"
999
+ subhome_abs=$(cd "$subhome" && pwd -P)
1000
+ printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
1001
+ printf '%s\n' '- other - other domain (home: '"$subhome_abs"'; scope: other domain; projects: beta; added 2026-06-22)' > "$home/data/secondmates.md"
1002
+ scaffold_secondmate_charter "$home" design 'design domain' alpha || fail "charter scaffold failed for registered-home seed test"
1003
+
1004
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
1005
+ fail "seed reused a home registered to another secondmate"
1006
+ fi
1007
+ grep -F 'already registered to other' "$err" >/dev/null || fail "seed did not explain registered-home rejection"
1008
+ [ ! -e "$subhome/.fm-secondmate-home" ] || fail "seed wrote a marker before rejecting a registered home"
1009
+ pass "home seeding refuses homes registered to another id"
1010
+ }
1011
+
1012
+ test_home_seed_refuses_reassigning_existing_id_to_different_home() {
1013
+ local home first second first_abs second_abs err
1014
+ home="$TMP_ROOT/reassign-id-home"
1015
+ first="$TMP_ROOT/reassign-id-first"
1016
+ second="$TMP_ROOT/reassign-id-second"
1017
+ err="$TMP_ROOT/reassign-id.err"
1018
+ mkdir -p "$home/projects" "$home/data" "$home/state"
1019
+ fm_git_init_commit "$home/projects/alpha"
1020
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/reassign-alpha.git"
1021
+ printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
1022
+
1023
+ FM_HOME="$home" FM_SECONDMATE_CHARTER='design domain' FM_SECONDMATE_SCOPE='design domain' \
1024
+ "$ROOT/bin/fm-home-seed.sh" design "$first" alpha >/dev/null \
1025
+ || fail "initial seed failed for reassigning-id test"
1026
+ first_abs=$(cd "$first" && pwd -P)
1027
+
1028
+ if FM_HOME="$home" FM_SECONDMATE_CHARTER='design domain' FM_SECONDMATE_SCOPE='design domain' \
1029
+ "$ROOT/bin/fm-home-seed.sh" design "$second" alpha >/dev/null 2>"$err"; then
1030
+ fail "seed reassigned an existing secondmate id to a different home"
1031
+ fi
1032
+ grep -F "secondmate id design is already registered to home $first_abs" "$err" >/dev/null \
1033
+ || fail "seed did not explain same-id different-home rejection"
1034
+ [ ! -e "$second" ] || fail "failed id reassignment created the new subhome"
1035
+ [ "$(cat "$first/.fm-secondmate-home")" = design ] || fail "failed id reassignment changed the original marker"
1036
+ grep -F "home: $first_abs" "$home/data/secondmates.md" >/dev/null \
1037
+ || fail "failed id reassignment did not preserve the original registry route"
1038
+ second_abs=$(cd "$(dirname "$second")" && printf '%s/%s\n' "$(pwd -P)" "$(basename "$second")")
1039
+ grep -F "home: $second_abs" "$home/data/secondmates.md" >/dev/null \
1040
+ && fail "failed id reassignment recorded the rejected home"
1041
+ pass "home seeding refuses same-id reassignment to a different home"
1042
+ }
1043
+
1044
+ test_home_seed_refuses_home_overlapping_registered_home() {
1045
+ local home registered_parent registered_child nested parent err
1046
+ home="$TMP_ROOT/overlap-seed-home"
1047
+ registered_parent="$TMP_ROOT/overlap-registered-parent"
1048
+ registered_child="$TMP_ROOT/overlap-registered-child-parent/child"
1049
+ nested="$registered_parent/nested"
1050
+ parent="$TMP_ROOT/overlap-registered-child-parent"
1051
+ err="$TMP_ROOT/overlap-seed.err"
1052
+ mkdir -p "$home/projects" "$home/data" "$home/state"
1053
+ fm_git_init_commit "$home/projects/alpha"
1054
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/overlap-alpha.git"
1055
+ git clone --quiet "$ROOT" "$registered_parent"
1056
+ git clone --quiet "$ROOT" "$registered_child"
1057
+ printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
1058
+ cat > "$home/data/secondmates.md" <<EOF
1059
+ - parent - parent domain (home: $registered_parent; scope: parent domain; projects: beta; added 2026-06-22)
1060
+ - child - child domain (home: $registered_child; scope: child domain; projects: gamma; added 2026-06-22)
1061
+ EOF
1062
+
1063
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$nested" alpha >/dev/null 2>"$err"; then
1064
+ fail "seed accepted a home inside a registered secondmate home"
1065
+ fi
1066
+ grep -F 'overlaps registered secondmate home' "$err" >/dev/null \
1067
+ || fail "seed did not explain registered ancestor overlap"
1068
+ [ ! -e "$nested" ] || fail "seed created a nested home inside a registered home"
1069
+
1070
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$parent" alpha >/dev/null 2>"$err"; then
1071
+ fail "seed accepted a home containing a registered secondmate home"
1072
+ fi
1073
+ grep -F 'overlaps registered secondmate home' "$err" >/dev/null \
1074
+ || fail "seed did not explain registered descendant overlap"
1075
+ [ ! -f "$parent/.fm-secondmate-home" ] || fail "seed marked a home containing a registered home"
1076
+ pass "home seeding refuses registered home overlaps"
1077
+ }
1078
+
1079
+ test_home_seed_refuses_remote_backed_project_without_origin() {
1080
+ local home subhome err
1081
+ home="$TMP_ROOT/no-origin-home"
1082
+ subhome="$TMP_ROOT/no-origin-subhome"
1083
+ err="$TMP_ROOT/no-origin.err"
1084
+ mkdir -p "$home/projects" "$home/data" "$home/state"
1085
+ fm_git_init_commit "$home/projects/alpha"
1086
+ printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
1087
+ scaffold_secondmate_charter "$home" design 'design domain' alpha || fail "charter scaffold failed for no-origin seed test"
1088
+
1089
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
1090
+ fail "seed allowed remote-backed project without origin"
1091
+ fi
1092
+ grep -F 'project alpha is direct-PR but has no origin remote' "$err" >/dev/null || fail "seed did not explain missing origin for remote-backed project"
1093
+ pass "remote-backed subhome seeding requires a source origin"
1094
+ }
1095
+
1096
+ test_home_seed_refuses_existing_remote_backed_project_with_wrong_origin() {
1097
+ local home subhome subhome_abs err expected
1098
+ home="$TMP_ROOT/wrong-origin-home"
1099
+ subhome="$TMP_ROOT/wrong-origin-subhome"
1100
+ err="$TMP_ROOT/wrong-origin.err"
1101
+ mkdir -p "$home/projects" "$home/data" "$home/state"
1102
+ fm_git_init_commit "$home/projects/alpha"
1103
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/wrong-alpha.git"
1104
+ git clone --quiet "$ROOT" "$subhome"
1105
+ subhome_abs=$(cd "$subhome" && pwd -P)
1106
+ mkdir -p "$subhome/projects"
1107
+ git clone --quiet "$home/projects/alpha" "$subhome/projects/alpha"
1108
+ printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
1109
+ scaffold_secondmate_charter "$home" design 'design domain' alpha || fail "charter scaffold failed for wrong-origin seed test"
1110
+
1111
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
1112
+ fail "seed accepted existing remote-backed project with wrong origin"
1113
+ fi
1114
+ expected=$(git -C "$home/projects/alpha" remote get-url origin)
1115
+ grep -F "seeded project alpha at $subhome_abs/projects/alpha has origin" "$err" >/dev/null \
1116
+ || fail "seed did not identify wrong origin for existing remote-backed project"
1117
+ grep -F "expected $expected" "$err" >/dev/null \
1118
+ || fail "seed did not report expected origin for existing remote-backed project"
1119
+ pass "remote-backed subhome seeding validates existing destination origins"
1120
+ }
1121
+
1122
+ test_home_seed_resolves_relative_source_origins() {
1123
+ local home subhome subhome_abs expected out actual
1124
+ home="$TMP_ROOT/relative-origin-home"
1125
+ subhome="$TMP_ROOT/relative-origin-subhome"
1126
+ mkdir -p "$home/projects" "$home/data" "$home/state" "$home/remotes"
1127
+ fm_git_init_commit "$home/projects/alpha"
1128
+ git clone --quiet --bare "$home/projects/alpha" "$home/remotes/relative-alpha.git"
1129
+ git -C "$home/projects/alpha" remote add origin ../../remotes/relative-alpha.git
1130
+ printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
1131
+ scaffold_secondmate_charter "$home" design 'design domain' alpha || fail "charter scaffold failed for relative origin seed test"
1132
+
1133
+ out=$(FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha)
1134
+ subhome_abs=$(cd "$subhome" && pwd -P)
1135
+ expected=$(cd "$home/remotes/relative-alpha.git" && pwd -P)
1136
+ printf '%s\n' "$out" | grep -F "home=$subhome_abs" >/dev/null || fail "seed did not report relative-origin subhome"
1137
+ [ -d "$subhome/projects/alpha/.git" ] || fail "relative source origin was not cloned"
1138
+ actual=$(git -C "$subhome/projects/alpha" remote get-url origin)
1139
+ [ "$actual" = "$expected" ] || fail "relative source origin was not cloned through the resolved path"
1140
+ FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null \
1141
+ || fail "relative source origin did not compare equal on reseed"
1142
+ pass "home seeding resolves relative source origins against the source project"
1143
+ }
1144
+
1145
+ test_home_seed_skips_initialized_existing_no_mistakes_projects() {
1146
+ local home subhome err fakebin log origin
1147
+ home="$TMP_ROOT/existing-initialized-home"
1148
+ subhome="$TMP_ROOT/existing-initialized-subhome"
1149
+ err="$TMP_ROOT/existing-initialized.err"
1150
+ log="$TMP_ROOT/existing-initialized-no-mistakes.log"
1151
+ mkdir -p "$home/projects" "$home/data" "$home/state"
1152
+ fm_git_init_commit "$home/projects/alpha"
1153
+ fm_git_init_commit "$home/projects/beta"
1154
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/existing-alpha.git"
1155
+ fm_git_add_origin "$home/projects/beta" "$TMP_ROOT/remotes/existing-beta.git"
1156
+ git clone --quiet "$ROOT" "$subhome"
1157
+ mkdir -p "$subhome/projects"
1158
+ origin=$(git -C "$home/projects/alpha" remote get-url origin)
1159
+ git clone --quiet "$origin" "$subhome/projects/alpha"
1160
+ git -C "$subhome/projects/alpha" remote add no-mistakes "$TMP_ROOT/no-mistakes-alpha.git"
1161
+ printf '%s\n' '- alpha - alpha project (added 2026-06-22)' '- beta - beta project (added 2026-06-22)' > "$home/data/projects.md"
1162
+ fakebin=$(make_recording_no_mistakes "$TMP_ROOT/existing-initialized-fake")
1163
+ : > "$log"
1164
+
1165
+ if PATH="$fakebin:$PATH" FM_FAKE_NO_MISTAKES_LOG="$log" FM_FAKE_NO_MISTAKES_FAIL_PROJECT=beta \
1166
+ FM_HOME="$home" FM_SECONDMATE_CHARTER='existing init rollback scope' FM_SECONDMATE_SCOPE='existing init rollback scope' \
1167
+ "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha beta >/dev/null 2>"$err"; then
1168
+ fail "seed succeeded even though later no-mistakes initialization failed"
1169
+ fi
1170
+ grep -F 'failed to initialize no-mistakes for beta' "$err" >/dev/null \
1171
+ || fail "seed did not explain later no-mistakes initialization failure"
1172
+ grep -F "$subhome/projects/alpha" "$log" >/dev/null \
1173
+ && fail "seed ran no-mistakes against an initialized existing clone"
1174
+ [ ! -f "$subhome/projects/alpha/.no-mistakes-init" ] || fail "seed mutated initialized existing clone with no-mistakes init"
1175
+ [ ! -f "$subhome/projects/alpha/.no-mistakes-doctor" ] || fail "seed mutated initialized existing clone with no-mistakes doctor"
1176
+ [ ! -e "$subhome/projects/beta" ] || fail "failed seed left a newly cloned project after no-mistakes failure"
1177
+ pass "home seeding skips initialized existing no-mistakes clones"
1178
+ }
1179
+
1180
+ test_home_seed_refuses_uninitialized_existing_no_mistakes_project() {
1181
+ local home subhome err fakebin log origin
1182
+ home="$TMP_ROOT/existing-uninitialized-home"
1183
+ subhome="$TMP_ROOT/existing-uninitialized-subhome"
1184
+ err="$TMP_ROOT/existing-uninitialized.err"
1185
+ log="$TMP_ROOT/existing-uninitialized-no-mistakes.log"
1186
+ mkdir -p "$home/projects" "$home/data" "$home/state"
1187
+ fm_git_init_commit "$home/projects/alpha"
1188
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/uninitialized-alpha.git"
1189
+ git clone --quiet "$ROOT" "$subhome"
1190
+ mkdir -p "$subhome/projects"
1191
+ origin=$(git -C "$home/projects/alpha" remote get-url origin)
1192
+ git clone --quiet "$origin" "$subhome/projects/alpha"
1193
+ printf '%s\n' '- alpha - alpha project (added 2026-06-22)' > "$home/data/projects.md"
1194
+ fakebin=$(make_recording_no_mistakes "$TMP_ROOT/existing-uninitialized-fake")
1195
+ : > "$log"
1196
+
1197
+ if PATH="$fakebin:$PATH" FM_FAKE_NO_MISTAKES_LOG="$log" \
1198
+ FM_HOME="$home" FM_SECONDMATE_CHARTER='existing uninitialized scope' \
1199
+ "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
1200
+ fail "seed initialized a preexisting no-mistakes clone"
1201
+ fi
1202
+ grep -F 'refusing to mutate preexisting clone' "$err" >/dev/null \
1203
+ || fail "seed did not explain uninitialized existing no-mistakes clone refusal"
1204
+ [ ! -s "$log" ] || fail "seed ran no-mistakes before refusing an uninitialized existing clone"
1205
+ [ ! -f "$subhome/projects/alpha/.no-mistakes-init" ] || fail "seed mutated uninitialized existing clone"
1206
+ pass "home seeding refuses uninitialized existing no-mistakes clones"
1207
+ }
1208
+
1209
+ test_home_seed_refuses_project_destinations_outside_subhome() {
1210
+ local home subhome sink err
1211
+ home="$TMP_ROOT/symlink-project-home"
1212
+ subhome="$TMP_ROOT/symlink-project-subhome"
1213
+ sink="$home/data/symlink-projects"
1214
+ err="$TMP_ROOT/symlink-project.err"
1215
+ mkdir -p "$home/projects" "$home/data" "$home/state" "$sink"
1216
+ fm_git_init_commit "$home/projects/alpha"
1217
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/symlink-alpha.git"
1218
+ git clone --quiet "$ROOT" "$subhome"
1219
+ rm -rf "$subhome/projects"
1220
+ ln -s "$sink" "$subhome/projects"
1221
+ printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
1222
+ scaffold_secondmate_charter "$home" design 'design domain' alpha || fail "charter scaffold failed for symlink destination seed test"
1223
+
1224
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
1225
+ fail "seed followed a subhome projects symlink outside the subhome"
1226
+ fi
1227
+ grep -F 'secondmate projects directory must resolve inside the secondmate home' "$err" >/dev/null \
1228
+ || fail "seed did not explain unsafe project destination rejection"
1229
+ [ ! -e "$sink/alpha" ] || fail "seed cloned a project through an unsafe projects symlink"
1230
+ [ ! -f "$subhome/.fm-secondmate-home" ] || fail "seed marked subhome after unsafe project destination rejection"
1231
+ pass "home seeding refuses project destinations outside the subhome"
1232
+ }
1233
+
1234
+ test_home_seed_refuses_operational_dirs_outside_subhome() {
1235
+ local home subhome sink err opdir
1236
+ home="$TMP_ROOT/symlink-opdir-home"
1237
+ err="$TMP_ROOT/symlink-opdir.err"
1238
+ mkdir -p "$home/projects" "$home/data" "$home/state"
1239
+ fm_git_init_commit "$home/projects/alpha"
1240
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/symlink-opdir-alpha.git"
1241
+ printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
1242
+ scaffold_secondmate_charter "$home" design 'design domain' alpha || fail "charter scaffold failed for symlink operational dir seed test"
1243
+
1244
+ for opdir in data state config; do
1245
+ subhome="$TMP_ROOT/symlink-opdir-subhome-$opdir"
1246
+ sink="$home/data/symlink-opdir-$opdir"
1247
+ rm -rf "$subhome" "$sink"
1248
+ git clone --quiet "$ROOT" "$subhome"
1249
+ mkdir -p "$sink"
1250
+ rm -rf "${subhome:?}/${opdir:?}"
1251
+ ln -s "$sink" "$subhome/$opdir"
1252
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
1253
+ fail "seed accepted a subhome with $opdir symlinked outside the subhome"
1254
+ fi
1255
+ grep -F "secondmate $opdir directory must resolve inside the secondmate home" "$err" >/dev/null \
1256
+ || fail "seed did not explain unsafe $opdir directory rejection"
1257
+ [ ! -f "$subhome/.fm-secondmate-home" ] || fail "seed marked subhome after unsafe $opdir directory rejection"
1258
+ done
1259
+ pass "home seeding refuses operational directories outside the subhome"
1260
+ }
1261
+
1262
+ test_home_seed_refuses_unsafe_leaf_files() {
1263
+ local home subhome sink err leaf target expected
1264
+ home="$TMP_ROOT/symlink-leaf-home"
1265
+ err="$TMP_ROOT/symlink-leaf.err"
1266
+ mkdir -p "$home/projects" "$home/data" "$home/state"
1267
+ fm_git_init_commit "$home/projects/alpha"
1268
+ fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/symlink-leaf-alpha.git"
1269
+ printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
1270
+ scaffold_secondmate_charter "$home" design 'design domain' alpha || fail "charter scaffold failed for symlink leaf seed test"
1271
+
1272
+ for leaf in data/projects.md data/charter.md .fm-secondmate-home .fm-secondmate-parent; do
1273
+ subhome="$TMP_ROOT/symlink-leaf-subhome-${leaf//\//-}"
1274
+ sink="$home/data/symlink-leaf-${leaf//\//-}"
1275
+ rm -rf "$subhome" "$sink"
1276
+ git clone --quiet "$ROOT" "$subhome"
1277
+ mkdir -p "$(dirname "$subhome/$leaf")" "$(dirname "$sink")"
1278
+ expected=outside
1279
+ if [ "$leaf" = ".fm-secondmate-home" ]; then
1280
+ expected=design
1281
+ fi
1282
+ printf '%s\n' "$expected" > "$sink"
1283
+ ln -s "$sink" "$subhome/$leaf"
1284
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
1285
+ fail "seed accepted symlinked leaf file $leaf"
1286
+ fi
1287
+ grep -F 'secondmate leaf file must not be a symlink:' "$err" >/dev/null \
1288
+ || fail "seed did not explain symlinked leaf refusal for $leaf"
1289
+ target=$(cat "$sink")
1290
+ [ "$target" = "$expected" ] || fail "seed overwrote outside symlink target for $leaf"
1291
+ [ ! -f "$subhome/.fm-secondmate-home" ] || [ "$leaf" = ".fm-secondmate-home" ] || fail "seed marked subhome after symlinked leaf refusal"
1292
+ done
1293
+ for leaf in data/projects.md data/charter.md .fm-secondmate-home .fm-secondmate-parent; do
1294
+ subhome="$TMP_ROOT/directory-leaf-subhome-${leaf//\//-}"
1295
+ rm -rf "$subhome"
1296
+ git clone --quiet "$ROOT" "$subhome"
1297
+ mkdir -p "$subhome/$leaf"
1298
+ if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
1299
+ fail "seed accepted directory leaf $leaf"
1300
+ fi
1301
+ grep -F 'secondmate leaf file must be a regular file:' "$err" >/dev/null \
1302
+ || fail "seed did not explain directory leaf refusal for $leaf"
1303
+ [ -d "$subhome/$leaf" ] || fail "seed changed directory leaf $leaf"
1304
+ [ ! -f "$subhome/.fm-secondmate-home" ] \
1305
+ || fail "seed published an identity marker after directory leaf refusal for $leaf"
1306
+ done
1307
+ pass "home seeding refuses symlinked and non-regular leaf files"
1308
+ }
1309
+
1310
+ test_home_seed_preserves_existing_parent_binding() {
1311
+ local parent_a parent_b child child_abs before err out parent_a_abs parent_b_abs leaf
1312
+ parent_a="$TMP_ROOT/reseed-parent-a"
1313
+ parent_b="$TMP_ROOT/reseed-parent-b"
1314
+ child="$TMP_ROOT/reseed-parent-child"
1315
+ before="$TMP_ROOT/reseed-parent-before"
1316
+ err="$TMP_ROOT/reseed-parent.err"
1317
+ mkdir -p "$parent_a/data" "$parent_a/state" "$parent_a/projects" \
1318
+ "$parent_b/data" "$parent_b/state" "$parent_b/projects" "$before/data"
1319
+
1320
+ FM_HOME="$parent_a" FM_SECONDMATE_CHARTER='Durable parent reseed charter.' \
1321
+ FM_SECONDMATE_SCOPE='durable parent reseed scope' \
1322
+ "$ROOT/bin/fm-home-seed.sh" mate "$child" --no-projects >/dev/null \
1323
+ || fail "initial durable-parent seed failed"
1324
+ parent_a_abs=$(cd "$parent_a" && pwd -P)
1325
+ parent_b_abs=$(cd "$parent_b" && pwd -P)
1326
+ child_abs=$(cd "$child" && pwd -P)
1327
+ for leaf in data/projects.md data/charter.md .fm-secondmate-home .fm-secondmate-parent; do
1328
+ mkdir -p "$before/$(dirname "$leaf")"
1329
+ cp "$child/$leaf" "$before/$leaf"
1330
+ done
1331
+
1332
+ if FM_HOME="$parent_b" FM_SECONDMATE_CHARTER='Replacement parent charter.' \
1333
+ FM_SECONDMATE_SCOPE='replacement parent scope' \
1334
+ "$ROOT/bin/fm-home-seed.sh" mate "$child" --no-projects > /dev/null 2>"$err"; then
1335
+ fail "reseed replaced a valid durable parent binding"
1336
+ fi
1337
+ grep -F "bound to parent $parent_a_abs, not requested parent $parent_b_abs" "$err" >/dev/null \
1338
+ || fail "mismatched-parent reseed did not name both parent identities"
1339
+ for leaf in data/projects.md data/charter.md .fm-secondmate-home .fm-secondmate-parent; do
1340
+ cmp -s "$before/$leaf" "$child/$leaf" \
1341
+ || fail "mismatched-parent reseed changed $leaf"
1342
+ done
1343
+ [ ! -e "$parent_b/data/mate/brief.md" ] \
1344
+ || fail "mismatched-parent reseed created a replacement parent brief"
1345
+ [ ! -e "$parent_b/data/secondmates.md" ] \
1346
+ || fail "mismatched-parent reseed registered the child to the replacement parent"
1347
+
1348
+ out=$(FM_HOME="$parent_a" "$ROOT/bin/fm-home-seed.sh" mate "$child" --no-projects) \
1349
+ || fail "matching-parent reseed failed"
1350
+ printf '%s\n' "$out" | grep -F "home=$child_abs" >/dev/null \
1351
+ || fail "matching-parent reseed did not report success"
1352
+ cmp -s "$before/.fm-secondmate-parent" "$child/.fm-secondmate-parent" \
1353
+ || fail "matching-parent reseed changed the durable parent binding"
1354
+ pass "home reseeding preserves and enforces the durable parent binding"
1355
+ }
1356
+
1357
+ test_secondmate_spawn_requires_seeded_matching_home() {
1358
+ local home subhome wronghome marker_only active_descendant active_ancestor ancestor_active_home fakeroot root_descendant root_ancestor root_inside fakebin log err
1359
+ home="$TMP_ROOT/spawn-validate-home"
1360
+ subhome="$TMP_ROOT/spawn-validate-subhome"
1361
+ wronghome="$TMP_ROOT/spawn-validate-wronghome"
1362
+ marker_only="$TMP_ROOT/spawn-validate-marker-only"
1363
+ active_descendant="$home/data/spawn-descendant-home"
1364
+ active_ancestor="$TMP_ROOT/spawn-active-ancestor"
1365
+ ancestor_active_home="$active_ancestor/main-home"
1366
+ fakeroot="$TMP_ROOT/spawn-validate-root"
1367
+ root_descendant="$fakeroot/tmp/spawn-descendant-home"
1368
+ root_ancestor="$TMP_ROOT/spawn-root-ancestor"
1369
+ root_inside="$root_ancestor/repo"
1370
+ mkdir -p "$home/data" "$home/state" "$subhome/data" "$wronghome/data" "$marker_only/data" "$active_descendant/data" "$root_descendant/data" "$fakeroot/bin"
1371
+ cat > "$fakeroot/bin/fm-guard.sh" <<'SH'
1372
+ #!/usr/bin/env bash
1373
+ exit 0
1374
+ SH
1375
+ chmod +x "$fakeroot/bin/fm-guard.sh"
1376
+ mkdir -p "$ancestor_active_home/data" "$ancestor_active_home/state" "$active_ancestor/data" "$root_ancestor/data" "$root_inside/bin"
1377
+ cat > "$root_inside/bin/fm-guard.sh" <<'SH'
1378
+ #!/usr/bin/env bash
1379
+ exit 0
1380
+ SH
1381
+ chmod +x "$root_inside/bin/fm-guard.sh"
1382
+ fakebin=$(make_fake_tmux "$TMP_ROOT/spawn-validate-fake")
1383
+ log="$TMP_ROOT/spawn-validate-fake/tmux.log"
1384
+ err="$TMP_ROOT/spawn-validate.err"
1385
+
1386
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
1387
+ "$ROOT/bin/fm-spawn.sh" domain "$subhome" codex --secondmate >/dev/null 2>"$err"; then
1388
+ fail "secondmate spawn accepted an unseeded home"
1389
+ fi
1390
+ grep -F 'not a seeded secondmate home' "$err" >/dev/null || fail "spawn did not explain missing seed marker"
1391
+ # Canonical ordering proof: validation runs before any tmux side-effect. Every rejection
1392
+ # reason below shares this one linear pre-launch path, so they each assert only their own
1393
+ # refusal message rather than re-proving "no window created before validation" each time.
1394
+ grep -F 'new-window' "$log" >/dev/null && fail "spawn created a window before validation"
1395
+
1396
+ printf 'other\n' > "$wronghome/.fm-secondmate-home"
1397
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
1398
+ "$ROOT/bin/fm-spawn.sh" domain "$wronghome" codex --secondmate >/dev/null 2>"$err"; then
1399
+ fail "secondmate spawn accepted a home marked for another secondmate"
1400
+ fi
1401
+ grep -F 'marked for secondmate other, expected domain' "$err" >/dev/null || fail "spawn did not explain marker mismatch"
1402
+
1403
+ printf 'domain\n' > "$marker_only/.fm-secondmate-home"
1404
+ printf 'charter\n' > "$marker_only/data/charter.md"
1405
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
1406
+ "$ROOT/bin/fm-spawn.sh" domain "$marker_only" codex --secondmate >/dev/null 2>"$err"; then
1407
+ fail "secondmate spawn accepted a marked home missing AGENTS.md"
1408
+ fi
1409
+ grep -F 'not a firstmate home (missing AGENTS.md)' "$err" >/dev/null || fail "spawn did not explain missing AGENTS.md"
1410
+
1411
+ printf '# Firstmate\n' > "$marker_only/AGENTS.md"
1412
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
1413
+ "$ROOT/bin/fm-spawn.sh" domain "$marker_only" codex --secondmate >/dev/null 2>"$err"; then
1414
+ fail "secondmate spawn accepted a marked home missing bin"
1415
+ fi
1416
+ grep -F 'not a firstmate home (missing bin/)' "$err" >/dev/null || fail "spawn did not explain missing bin"
1417
+
1418
+ printf 'domain\n' > "$home/.fm-secondmate-home"
1419
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
1420
+ "$ROOT/bin/fm-spawn.sh" domain "$home" codex --secondmate >/dev/null 2>"$err"; then
1421
+ fail "secondmate spawn accepted the active home"
1422
+ fi
1423
+ grep -F 'secondmate home cannot be the active firstmate home' "$err" >/dev/null || fail "spawn did not reject active home"
1424
+
1425
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
1426
+ "$ROOT/bin/fm-spawn.sh" domain "$ROOT" codex --secondmate >/dev/null 2>"$err"; then
1427
+ fail "secondmate spawn accepted the firstmate repo root"
1428
+ fi
1429
+ grep -F 'secondmate home cannot be the firstmate repo' "$err" >/dev/null || fail "spawn did not reject firstmate repo root"
1430
+
1431
+ printf 'domain\n' > "$active_descendant/.fm-secondmate-home"
1432
+ printf 'charter\n' > "$active_descendant/data/charter.md"
1433
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
1434
+ "$ROOT/bin/fm-spawn.sh" domain "$active_descendant" codex --secondmate >/dev/null 2>"$err"; then
1435
+ fail "secondmate spawn accepted a home inside the active firstmate home"
1436
+ fi
1437
+ grep -F 'secondmate home cannot be inside the active firstmate home' "$err" >/dev/null || fail "spawn did not reject active home descendant"
1438
+
1439
+ printf 'domain\n' > "$active_ancestor/.fm-secondmate-home"
1440
+ printf 'charter\n' > "$active_ancestor/data/charter.md"
1441
+ if PATH="$fakebin:$PATH" FM_HOME="$ancestor_active_home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
1442
+ "$ROOT/bin/fm-spawn.sh" domain "$active_ancestor" codex --secondmate >/dev/null 2>"$err"; then
1443
+ fail "secondmate spawn accepted a home containing the active firstmate home"
1444
+ fi
1445
+ grep -F 'secondmate home cannot be an ancestor of the active firstmate home' "$err" >/dev/null || fail "spawn did not reject active home ancestor"
1446
+
1447
+ printf 'domain\n' > "$root_descendant/.fm-secondmate-home"
1448
+ printf 'charter\n' > "$root_descendant/data/charter.md"
1449
+ if PATH="$fakebin:$PATH" FM_ROOT_OVERRIDE="$fakeroot" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
1450
+ "$ROOT/bin/fm-spawn.sh" domain "$root_descendant" codex --secondmate >/dev/null 2>"$err"; then
1451
+ fail "secondmate spawn accepted a home inside the firstmate repo"
1452
+ fi
1453
+ grep -F 'secondmate home cannot be inside the firstmate repo' "$err" >/dev/null || fail "spawn did not reject repo root descendant"
1454
+
1455
+ printf 'domain\n' > "$root_ancestor/.fm-secondmate-home"
1456
+ printf 'charter\n' > "$root_ancestor/data/charter.md"
1457
+ if PATH="$fakebin:$PATH" FM_ROOT_OVERRIDE="$root_inside" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
1458
+ "$ROOT/bin/fm-spawn.sh" domain "$root_ancestor" codex --secondmate >/dev/null 2>"$err"; then
1459
+ fail "secondmate spawn accepted a home containing the firstmate repo"
1460
+ fi
1461
+ grep -F 'secondmate home cannot be an ancestor of the firstmate repo' "$err" >/dev/null || fail "spawn did not reject repo ancestor"
1462
+
1463
+ pass "secondmate spawn validates homes before launch"
1464
+ }
1465
+
1466
+ test_secondmate_spawn_refuses_operational_dirs_outside_subhome() {
1467
+ local home subhome sink fakebin log err opdir
1468
+ home="$TMP_ROOT/spawn-opdir-home"
1469
+ fakebin=$(make_fake_tmux "$TMP_ROOT/spawn-opdir-fake")
1470
+ log="$TMP_ROOT/spawn-opdir-fake/tmux.log"
1471
+ err="$TMP_ROOT/spawn-opdir.err"
1472
+ mkdir -p "$home/data" "$home/state"
1473
+
1474
+ for opdir in data state config projects; do
1475
+ subhome="$TMP_ROOT/spawn-opdir-subhome-$opdir"
1476
+ sink="$home/data/spawn-opdir-$opdir"
1477
+ rm -rf "$subhome" "$sink"
1478
+ mkdir -p "$subhome/data" "$subhome/state" "$subhome/config" "$subhome/projects" "$sink"
1479
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
1480
+ printf 'charter\n' > "$subhome/data/charter.md"
1481
+ rm -rf "${subhome:?}/${opdir:?}"
1482
+ ln -s "$sink" "$subhome/$opdir"
1483
+ if [ "$opdir" = data ]; then
1484
+ printf 'charter\n' > "$sink/charter.md"
1485
+ fi
1486
+ : > "$log"
1487
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-opdir-fake/pane.txt" \
1488
+ "$ROOT/bin/fm-spawn.sh" domain "$subhome" codex --secondmate >/dev/null 2>"$err"; then
1489
+ fail "secondmate spawn accepted a subhome with $opdir symlinked outside the subhome"
1490
+ fi
1491
+ grep -F "secondmate $opdir directory must resolve inside the secondmate home" "$err" >/dev/null \
1492
+ || fail "spawn did not explain unsafe $opdir directory rejection"
1493
+ grep -F 'new-window' "$log" >/dev/null && fail "spawn created a window before unsafe $opdir directory validation"
1494
+ done
1495
+ pass "secondmate spawn refuses operational directories outside the subhome"
1496
+ }
1497
+
1498
+ test_fm_send_refuses_bare_window_without_home_meta() {
1499
+ # The happy path (a bare fm-<id> resolves the window recorded in THIS home's
1500
+ # meta and never a foreign same-named window) is asserted in the lifecycle e2e.
1501
+ # Here: with NO meta for the id, send must refuse rather than fall back to a
1502
+ # foreign same-named window that list-windows happens to return.
1503
+ local home fakebin log err
1504
+ home="$TMP_ROOT/send-home"
1505
+ mkdir -p "$home/state"
1506
+ touch "$home/state/.last-watcher-beat"
1507
+ fakebin=$(make_fake_tmux "$TMP_ROOT/send-fake")
1508
+ log="$TMP_ROOT/send-fake/tmux.log"
1509
+ err="$TMP_ROOT/send-fake/send.err"
1510
+
1511
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_WINDOW="other-session:fm-missing" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/send-fake/pane.txt" \
1512
+ "$ROOT/bin/fm-send.sh" fm-missing 'wrong home' >/dev/null 2>"$err"; then
1513
+ fail "fm-send sent to a bare firstmate window without home metadata"
1514
+ fi
1515
+ grep -F "no metadata for fm-missing in $home/state" "$err" >/dev/null \
1516
+ || fail "fm-send did not explain missing home metadata"
1517
+ grep -F 'send-keys -t other-session:fm-missing' "$log" >/dev/null \
1518
+ && fail "fm-send fell back to a foreign same-name window"
1519
+ pass "fm-send refuses a bare firstmate window with no metadata in this home"
1520
+ }
1521
+
1522
+ test_secondmate_teardown_retires_empty_home() {
1523
+ local home subhome subhome_abs fakebin log lease fmroot
1524
+ home="$TMP_ROOT/teardown-home"
1525
+ subhome="$TMP_ROOT/teardown-subhome"
1526
+ fmroot="$TMP_ROOT/teardown-fmroot"
1527
+ make_firstmate_git_root "$fmroot"
1528
+ git -C "$fmroot" worktree add --quiet --detach "$subhome" HEAD
1529
+ mkdir -p "$home/state" "$home/data" "$subhome/state"
1530
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
1531
+ subhome_abs=$(cd "$subhome" && pwd -P)
1532
+ cat > "$home/state/domain.meta" <<EOF
1533
+ window=firstmate:fm-domain
1534
+ worktree=$subhome
1535
+ project=$subhome
1536
+ harness=echo
1537
+ kind=secondmate
1538
+ mode=secondmate
1539
+ yolo=off
1540
+ home=$subhome
1541
+ projects=alpha
1542
+ EOF
1543
+ printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
1544
+ fakebin=$(make_fake_tmux "$TMP_ROOT/teardown-fake")
1545
+ log="$TMP_ROOT/teardown-fake/tmux.log"
1546
+ lease="$TMP_ROOT/teardown-fake/lease"
1547
+ printf 'domain\n' > "$lease"
1548
+ PATH="$fakebin:$PATH" FM_ROOT_OVERRIDE="$fmroot" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/teardown-fake/pane.txt" \
1549
+ FM_FAKE_TREEHOUSE_LEASE_FILE="$lease" \
1550
+ "$ROOT/bin/fm-teardown.sh" domain >/dev/null 2>/dev/null \
1551
+ || fail "teardown failed for empty secondmate home"
1552
+ grep -F "treehouse return --force $subhome_abs" "$log" >/dev/null || fail "teardown did not release the secondmate home lease via treehouse return"
1553
+ [ ! -e "$lease" ] || fail "teardown left the secondmate home lease held after retirement"
1554
+ [ ! -d "$subhome" ] || fail "teardown did not remove the retired secondmate home"
1555
+ [ ! -e "$home/state/domain.meta" ] || fail "teardown did not clear parent meta"
1556
+ grep -F -- '- domain ' "$home/data/secondmates.md" >/dev/null && fail "teardown did not remove secondmate registry route"
1557
+ pass "secondmate teardown retires empty homes and releases routing"
1558
+ }
1559
+
1560
+ test_secondmate_teardown_refuses_ambiguous_and_mismatched_registry_bindings() {
1561
+ local case_name home sub other fakebin log err meta_before registry_before
1562
+ for case_name in duplicate-id duplicate-home home-mismatch; do
1563
+ home="$TMP_ROOT/teardown-binding-$case_name-home"
1564
+ sub="$TMP_ROOT/teardown-binding-$case_name-sub"
1565
+ other="$TMP_ROOT/teardown-binding-$case_name-other"
1566
+ mkdir -p "$home/state" "$home/data" "$sub/state" "$sub/data" "$sub/config" "$sub/projects" "$other"
1567
+ printf 'domain\n' > "$sub/.fm-secondmate-home"
1568
+ fm_write_secondmate_meta "$home/state/domain.meta" "$sub"
1569
+ case "$case_name" in
1570
+ duplicate-id)
1571
+ cat > "$home/data/secondmates.md" <<EOF
1572
+ - domain - primary route (home: $sub; scope: valid (scope); punctuation; projects: alpha; added 2026-07-30)
1573
+ - domain - duplicate route (home: $other; scope: duplicate; projects: beta; added 2026-07-30)
1574
+ EOF
1575
+ ;;
1576
+ duplicate-home)
1577
+ cat > "$home/data/secondmates.md" <<EOF
1578
+ - domain - primary route (home: $sub; scope: valid (scope); punctuation; projects: alpha; added 2026-07-30)
1579
+ - other - duplicate home route (home: $sub; scope: duplicate; projects: beta; added 2026-07-30)
1580
+ EOF
1581
+ ;;
1582
+ home-mismatch)
1583
+ printf -- '- domain - mismatched route (home: %s; scope: valid (scope); punctuation; projects: alpha; added 2026-07-30)\n' \
1584
+ "$other" > "$home/data/secondmates.md"
1585
+ ;;
1586
+ esac
1587
+ meta_before="$TMP_ROOT/teardown-binding-$case_name.meta.before"
1588
+ registry_before="$TMP_ROOT/teardown-binding-$case_name.registry.before"
1589
+ cp "$home/state/domain.meta" "$meta_before"
1590
+ cp "$home/data/secondmates.md" "$registry_before"
1591
+ fakebin=$(make_fake_tmux "$TMP_ROOT/teardown-binding-$case_name-fake")
1592
+ log="$TMP_ROOT/teardown-binding-$case_name-fake/tmux.log"
1593
+ err="$TMP_ROOT/teardown-binding-$case_name.err"
1594
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
1595
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/teardown-binding-$case_name-fake/pane.txt" \
1596
+ "$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
1597
+ fail "secondmate teardown accepted $case_name registry binding"
1598
+ fi
1599
+ [ -d "$sub" ] || fail "secondmate teardown removed the home after $case_name refusal"
1600
+ cmp -s "$meta_before" "$home/state/domain.meta" || fail "secondmate teardown changed metadata after $case_name refusal"
1601
+ cmp -s "$registry_before" "$home/data/secondmates.md" || fail "secondmate teardown changed registry after $case_name refusal"
1602
+ grep -F 'kill-window' "$log" >/dev/null && fail "secondmate teardown killed an endpoint before $case_name refusal"
1603
+ done
1604
+ pass "secondmate teardown refuses ambiguous and identity-mismatched registry bindings"
1605
+ }
1606
+
1607
+ test_secondmate_teardown_sweeps_process_events_before_removal() {
1608
+ local home subhome subhome_abs fakebin log sweep_log
1609
+ home="$TMP_ROOT/procevent-teardown-home"
1610
+ subhome="$TMP_ROOT/procevent-teardown-subhome"
1611
+ sweep_log="$TMP_ROOT/procevent-teardown-sweep.log"
1612
+ mkdir -p "$home/state" "$home/data" "$subhome/state/procevent"
1613
+ mark_firstmate_home "$subhome"
1614
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
1615
+ printf 'adapter=lavish\n' > "$subhome/state/procevent/source.source"
1616
+ printf 'runner\n' > "$subhome/state/procevent/source.runner"
1617
+ install_fake_process_event_sweep "$subhome" "$sweep_log"
1618
+ subhome_abs=$(cd "$subhome" && pwd -P)
1619
+ fm_write_secondmate_meta "$home/state/domain.meta" "$subhome"
1620
+ printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
1621
+ fakebin=$(make_fake_tmux "$TMP_ROOT/procevent-teardown-fake")
1622
+ log="$TMP_ROOT/procevent-teardown-fake/tmux.log"
1623
+
1624
+ PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
1625
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/procevent-teardown-fake/pane.txt" \
1626
+ FM_FAKE_PROCEVENT_SWEEP_LOG="$sweep_log" \
1627
+ "$ROOT/bin/fm-teardown.sh" domain >/dev/null 2>/dev/null \
1628
+ || fail "normal secondmate teardown failed after process-event sweep"
1629
+ grep -Fx "$subhome_abs" "$sweep_log" >/dev/null || fail "normal secondmate teardown did not invoke the child home's sweep"
1630
+ [ ! -d "$subhome" ] || fail "normal secondmate teardown retained a successfully swept home"
1631
+ [ ! -e "$home/state/domain.meta" ] || fail "normal swept teardown retained parent evidence"
1632
+ pass "normal secondmate teardown sweeps process events before removal"
1633
+ }
1634
+
1635
+ test_secondmate_teardown_refuses_process_events_without_sweep_script() {
1636
+ local home subhome fakebin log err claim_root
1637
+ home="$TMP_ROOT/procevent-refusal-home"
1638
+ subhome="$TMP_ROOT/procevent-refusal-subhome"
1639
+ err="$TMP_ROOT/procevent-refusal.err"
1640
+ claim_root="$TMP_ROOT/procevent-refusal-claims"
1641
+ mkdir -p "$home/state" "$home/data" "$subhome/state/procevent" "$claim_root"
1642
+ mark_firstmate_home "$subhome"
1643
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
1644
+ printf 'adapter=lavish\n' > "$subhome/state/procevent/source.source"
1645
+ printf '%s\n999999\ntoken\nidentity\n' "$subhome" > "$claim_root/source.claim"
1646
+ fm_write_secondmate_meta "$home/state/domain.meta" "$subhome"
1647
+ printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
1648
+ fakebin=$(make_fake_tmux "$TMP_ROOT/procevent-refusal-fake")
1649
+ log="$TMP_ROOT/procevent-refusal-fake/tmux.log"
1650
+
1651
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_PROCEVENT_CLAIM_ROOT="$claim_root" \
1652
+ FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/procevent-refusal-fake/pane.txt" \
1653
+ "$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
1654
+ fail "force teardown removed process-event state without a sweep-capable child script"
1655
+ fi
1656
+ grep -F 'no sweep-capable bin/fm-procevent.sh' "$err" >/dev/null || fail "missing sweep capability refusal was not explained"
1657
+ [ -d "$subhome" ] || fail "missing sweep capability refusal removed the home"
1658
+ [ -e "$home/state/domain.meta" ] || fail "missing sweep capability refusal removed parent evidence"
1659
+ grep -F -- '- domain ' "$home/data/secondmates.md" >/dev/null || fail "missing sweep capability refusal removed the route"
1660
+ [ -e "$subhome/state/procevent/source.source" ] || fail "missing sweep capability refusal removed the registration"
1661
+ [ -e "$claim_root/source.claim" ] || fail "missing sweep capability refusal removed the claim"
1662
+ grep -F 'kill-window' "$log" >/dev/null && fail "missing sweep capability refusal killed a runtime endpoint"
1663
+ pass "secondmate teardown preserves state when process-event sweeping is unavailable"
1664
+ }
1665
+
1666
+ test_secondmate_teardown_preserves_process_events_on_later_refusal() {
1667
+ local home subhome fakebin log sweep_log err
1668
+ home="$TMP_ROOT/procevent-later-refusal-home"
1669
+ subhome="$TMP_ROOT/procevent-later-refusal-subhome"
1670
+ sweep_log="$TMP_ROOT/procevent-later-refusal-sweep.log"
1671
+ err="$TMP_ROOT/procevent-later-refusal.err"
1672
+ mkdir -p "$home/state/public-followup/registry" "$home/data" "$subhome/state/procevent"
1673
+ mark_firstmate_home "$subhome"
1674
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
1675
+ printf 'adapter=lavish\n' > "$subhome/state/procevent/source.source"
1676
+ install_fake_process_event_sweep "$subhome" "$sweep_log"
1677
+ printf 'FMX_PAIRING_TOKEN=test-token\n' > "$home/.env"
1678
+ printf 'work_home=secondmate:domain\nwork_id=domain\n' > "$home/state/public-followup/registry/obligation"
1679
+ fm_write_secondmate_meta "$home/state/domain.meta" "$subhome"
1680
+ printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
1681
+ fakebin=$(make_fake_tmux "$TMP_ROOT/procevent-later-refusal-fake")
1682
+ log="$TMP_ROOT/procevent-later-refusal-fake/tmux.log"
1683
+ cat > "$fakebin/tasks-axi" <<'SH'
1684
+ #!/usr/bin/env bash
1685
+ exit 1
1686
+ SH
1687
+ chmod +x "$fakebin/tasks-axi"
1688
+
1689
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
1690
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/procevent-later-refusal-fake/pane.txt" \
1691
+ FM_FAKE_PROCEVENT_SWEEP_LOG="$sweep_log" \
1692
+ "$ROOT/bin/fm-teardown.sh" domain >/dev/null 2>"$err"; then
1693
+ fail "teardown bypassed a later public-followup refusal"
1694
+ fi
1695
+ grep -F 'still owes a public reply' "$err" >/dev/null || fail "later public-followup refusal was not reached"
1696
+ [ ! -s "$sweep_log" ] || fail "later refusal retired process-event sources before teardown was authorized"
1697
+ [ -e "$subhome/state/procevent/source.source" ] || fail "later refusal removed the process-event registration"
1698
+ [ -d "$subhome" ] || fail "later refusal removed the secondmate home"
1699
+ [ -e "$home/state/domain.meta" ] || fail "later refusal removed parent evidence"
1700
+ pass "later teardown refusals preserve active process-event sources"
1701
+ }
1702
+
1703
+ test_secondmate_force_teardown_sweeps_nested_homes() {
1704
+ local home subhome childhome subhome_abs childhome_abs fakebin log sweep_log
1705
+ home="$TMP_ROOT/procevent-force-home"
1706
+ subhome="$TMP_ROOT/procevent-force-subhome"
1707
+ childhome="$TMP_ROOT/procevent-force-childhome"
1708
+ sweep_log="$TMP_ROOT/procevent-force-sweep.log"
1709
+ mkdir -p "$home/state" "$home/data" "$subhome/state/procevent" "$childhome/state/procevent"
1710
+ mark_firstmate_home "$subhome"
1711
+ mark_firstmate_home "$childhome"
1712
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
1713
+ printf 'nested\n' > "$childhome/.fm-secondmate-home"
1714
+ printf 'adapter=lavish\n' > "$subhome/state/procevent/parent-source.source"
1715
+ printf 'adapter=lavish\n' > "$childhome/state/procevent/child-source.source"
1716
+ install_fake_process_event_sweep "$subhome" "$sweep_log"
1717
+ install_fake_process_event_sweep "$childhome" "$sweep_log"
1718
+ subhome_abs=$(cd "$subhome" && pwd -P)
1719
+ childhome_abs=$(cd "$childhome" && pwd -P)
1720
+ fm_write_secondmate_meta "$home/state/domain.meta" "$subhome"
1721
+ fm_write_secondmate_meta "$subhome/state/nested.meta" "$childhome"
1722
+ cat > "$home/data/secondmates.md" <<EOF
1723
+ - domain - design domain (home: $subhome; scope: design domain; projects: alpha; added 2026-06-22)
1724
+ - nested - nested domain (home: $childhome; scope: nested domain; projects: beta; added 2026-06-22)
1725
+ EOF
1726
+ fakebin=$(make_fake_tmux "$TMP_ROOT/procevent-force-fake")
1727
+ log="$TMP_ROOT/procevent-force-fake/tmux.log"
1728
+
1729
+ PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
1730
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/procevent-force-fake/pane.txt" \
1731
+ FM_FAKE_PROCEVENT_SWEEP_LOG="$sweep_log" \
1732
+ "$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>/dev/null \
1733
+ || fail "force teardown failed after recursively sweeping process events"
1734
+ grep -Fx "$subhome_abs" "$sweep_log" >/dev/null || fail "force teardown did not sweep the parent secondmate home"
1735
+ grep -Fx "$childhome_abs" "$sweep_log" >/dev/null || fail "force teardown did not sweep the nested secondmate home"
1736
+ [ ! -d "$subhome" ] || fail "force teardown retained the swept parent home"
1737
+ [ ! -d "$childhome" ] || fail "force teardown retained the swept nested home"
1738
+ pass "force teardown sweeps nested secondmate homes before deletion"
1739
+ }
1740
+
1741
+ test_secondmate_force_teardown_preserves_nested_restore_status() {
1742
+ local home subhome childhome grandchildhome fmroot fakebin log sweep_log rearm_log err rc backup
1743
+ home="$TMP_ROOT/procevent-nested-fail-home"
1744
+ subhome="$TMP_ROOT/procevent-nested-fail-subhome"
1745
+ childhome="$TMP_ROOT/procevent-nested-fail-childhome"
1746
+ grandchildhome="$TMP_ROOT/procevent-nested-fail-grandchildhome"
1747
+ fmroot="$TMP_ROOT/procevent-nested-fail-fmroot"
1748
+ sweep_log="$TMP_ROOT/procevent-nested-fail-sweep.log"
1749
+ rearm_log="$TMP_ROOT/procevent-nested-fail-rearm.log"
1750
+ err="$TMP_ROOT/procevent-nested-fail.err"
1751
+ make_firstmate_git_root "$fmroot"
1752
+ git -C "$fmroot" worktree add --quiet --detach "$grandchildhome" HEAD
1753
+ mkdir -p "$home/state" "$home/data" "$subhome/state" "$childhome/state" "$grandchildhome/state/procevent"
1754
+ mark_firstmate_home "$subhome"
1755
+ mark_firstmate_home "$childhome"
1756
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
1757
+ printf 'nested\n' > "$childhome/.fm-secondmate-home"
1758
+ printf 'leaf\n' > "$grandchildhome/.fm-secondmate-home"
1759
+ printf 'adapter=lavish\n' > "$grandchildhome/state/procevent/leaf-source.source"
1760
+ install_fake_process_event_sweep "$grandchildhome" "$sweep_log"
1761
+ : > "$rearm_log"
1762
+ fm_write_secondmate_meta "$home/state/domain.meta" "$subhome"
1763
+ fm_write_secondmate_meta "$subhome/state/nested.meta" "$childhome"
1764
+ fm_write_secondmate_meta "$childhome/state/leaf.meta" "$grandchildhome"
1765
+ cat > "$home/data/secondmates.md" <<EOF
1766
+ - domain - design domain (home: $subhome; scope: design domain; projects: alpha; added 2026-06-22)
1767
+ - nested - nested domain (home: $childhome; scope: nested domain; projects: beta; added 2026-06-22)
1768
+ - leaf - leaf domain (home: $grandchildhome; scope: leaf domain; projects: gamma; added 2026-06-22)
1769
+ EOF
1770
+ fakebin=$(make_fake_tmux "$TMP_ROOT/procevent-nested-fail-fake")
1771
+ log="$TMP_ROOT/procevent-nested-fail-fake/tmux.log"
1772
+
1773
+ set +e
1774
+ PATH="$fakebin:$PATH" FM_ROOT_OVERRIDE="$fmroot" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
1775
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/procevent-nested-fail-fake/pane.txt" \
1776
+ FM_FAKE_PROCEVENT_SWEEP_LOG="$sweep_log" FM_FAKE_PROCEVENT_REARM_LOG="$rearm_log" \
1777
+ FM_FAKE_TREEHOUSE_RETURN_FAIL=1 FM_FAKE_PROCEVENT_REARM_FAIL=1 \
1778
+ "$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"
1779
+ rc=$?
1780
+ set -e
1781
+
1782
+ [ "$rc" -eq 4 ] || fail "nested process-event restoration failure was collapsed at a recursive teardown boundary"
1783
+ grep -F 'active waits may remain retired; recover registrations from ' "$err" >/dev/null || fail "nested restoration failure did not report its recovery backup"
1784
+ backup=$(find "$TMP_ROOT" -maxdepth 1 -type d -name '.fm-procevent-restore.*' \
1785
+ -exec test -e '{}/leaf-source.source' \; -print -quit)
1786
+ [ -n "$backup" ] && [ -e "$backup/leaf-source.source" ] || fail "nested restoration failure did not retain its registration backup"
1787
+ [ -e "$childhome/state/leaf.meta" ] || fail "nested restoration failure removed its parent identity record"
1788
+ [ -e "$subhome/state/nested.meta" ] || fail "nested restoration failure removed its ancestor identity record"
1789
+ [ -e "$home/state/domain.meta" ] || fail "nested restoration failure removed its top-level identity record"
1790
+ pass "force teardown preserves nested process-event restoration status and recovery state"
1791
+ }
1792
+
1793
+ test_secondmate_teardown_refuses_failed_leased_home_return() {
1794
+ local home subhome subhome_abs fakebin log fmroot err rc sweep_log rearm_log backup
1795
+ home="$TMP_ROOT/teardown-return-fail-home"
1796
+ subhome="$TMP_ROOT/teardown-return-fail-subhome"
1797
+ fmroot="$TMP_ROOT/teardown-return-fail-fmroot"
1798
+ err="$TMP_ROOT/teardown-return-fail.err"
1799
+ sweep_log="$TMP_ROOT/teardown-return-fail-sweep.log"
1800
+ rearm_log="$TMP_ROOT/teardown-return-fail-rearm.log"
1801
+ make_firstmate_git_root "$fmroot"
1802
+ git -C "$fmroot" worktree add --quiet --detach "$subhome" HEAD
1803
+ mkdir -p "$home/state" "$home/data" "$subhome/state/procevent"
1804
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
1805
+ printf 'adapter=lavish\nargc=1\nargv:\n/bin/true\n' > "$subhome/state/procevent/source.source"
1806
+ install_fake_process_event_sweep "$subhome" "$sweep_log"
1807
+ : > "$rearm_log"
1808
+ subhome_abs=$(cd "$subhome" && pwd -P)
1809
+ cat > "$home/state/domain.meta" <<EOF
1810
+ window=firstmate:fm-domain
1811
+ worktree=$subhome
1812
+ project=$subhome
1813
+ harness=echo
1814
+ kind=secondmate
1815
+ mode=secondmate
1816
+ yolo=off
1817
+ home=$subhome
1818
+ projects=alpha
1819
+ EOF
1820
+ printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
1821
+ fakebin=$(make_fake_tmux "$TMP_ROOT/teardown-return-fail-fake")
1822
+ log="$TMP_ROOT/teardown-return-fail-fake/tmux.log"
1823
+
1824
+ set +e
1825
+ PATH="$fakebin:$PATH" FM_ROOT_OVERRIDE="$fmroot" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/teardown-return-fail-fake/pane.txt" \
1826
+ FM_FAKE_PROCEVENT_SWEEP_LOG="$sweep_log" FM_FAKE_PROCEVENT_REARM_LOG="$rearm_log" \
1827
+ FM_FAKE_TREEHOUSE_RETURN_FAIL=1 \
1828
+ "$ROOT/bin/fm-teardown.sh" domain >/dev/null 2>"$err"
1829
+ rc=$?
1830
+ set -e
1831
+
1832
+ [ "$rc" -ne 0 ] || fail "teardown succeeded despite failed treehouse return"
1833
+ grep -F "treehouse return --force $subhome_abs" "$log" >/dev/null || fail "teardown did not try to return the leased home"
1834
+ grep -F 'treehouse return failed for secondmate home' "$err" >/dev/null || fail "teardown did not report failed leased home return"
1835
+ [ -d "$subhome" ] || fail "teardown removed a leased home after return failed"
1836
+ [ -e "$subhome/state/procevent/source.source" ] || fail "failed leased-home return did not restore the source registration"
1837
+ grep -Fx "$subhome_abs" "$rearm_log" >/dev/null || fail "failed leased-home return did not rearm restored process-event sources"
1838
+ [ -e "$home/state/domain.meta" ] || fail "teardown cleared meta after leased home return failed"
1839
+ grep -F -- '- domain ' "$home/data/secondmates.md" >/dev/null || fail "teardown removed registry route after leased home return failed"
1840
+
1841
+ set +e
1842
+ PATH="$fakebin:$PATH" FM_ROOT_OVERRIDE="$fmroot" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/teardown-return-fail-fake/pane.txt" \
1843
+ FM_FAKE_PROCEVENT_SWEEP_LOG="$sweep_log" FM_FAKE_PROCEVENT_REARM_LOG="$rearm_log" \
1844
+ FM_FAKE_TREEHOUSE_RETURN_FAIL=1 FM_FAKE_PROCEVENT_REARM_FAIL=1 \
1845
+ "$ROOT/bin/fm-teardown.sh" domain >/dev/null 2>"$err"
1846
+ rc=$?
1847
+ set -e
1848
+
1849
+ [ "$rc" -eq 4 ] || fail "failed process-event restoration did not return its distinct recoverable status"
1850
+ grep -F 'active waits may remain retired; recover registrations from ' "$err" >/dev/null || fail "failed process-event restoration did not report its recovery backup"
1851
+ backup=$(find "$TMP_ROOT" -maxdepth 1 -type d -name '.fm-procevent-restore.*' \
1852
+ -exec test -e '{}/source.source' \; -print -quit)
1853
+ [ -n "$backup" ] && [ -e "$backup/source.source" ] || fail "failed process-event restoration did not retain its registration backup"
1854
+ pass "secondmate teardown refuses to hide failed leased-home return"
1855
+ }
1856
+
1857
+ test_secondmate_teardown_removes_plain_clone_home_without_treehouse_return() {
1858
+ local home subhome subhome_abs fakebin log
1859
+ home="$TMP_ROOT/plain-clone-teardown-home"
1860
+ subhome="$TMP_ROOT/plain-clone-teardown-subhome"
1861
+ mkdir -p "$home/state" "$home/data" "$subhome/state"
1862
+ mark_firstmate_home "$subhome"
1863
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
1864
+ subhome_abs=$(cd "$subhome" && pwd -P)
1865
+ cat > "$home/state/domain.meta" <<EOF
1866
+ window=firstmate:fm-domain
1867
+ worktree=$subhome
1868
+ project=$subhome
1869
+ harness=echo
1870
+ kind=secondmate
1871
+ mode=secondmate
1872
+ yolo=off
1873
+ home=$subhome
1874
+ projects=alpha
1875
+ EOF
1876
+ printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
1877
+ fakebin=$(make_fake_tmux "$TMP_ROOT/plain-clone-teardown-fake")
1878
+ log="$TMP_ROOT/plain-clone-teardown-fake/tmux.log"
1879
+
1880
+ PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/plain-clone-teardown-fake/pane.txt" \
1881
+ FM_FAKE_TREEHOUSE_RETURN_FAIL=1 \
1882
+ "$ROOT/bin/fm-teardown.sh" domain >/dev/null 2>/dev/null \
1883
+ || fail "teardown failed for plain-clone secondmate home"
1884
+ grep -F "treehouse return --force $subhome_abs" "$log" >/dev/null && fail "teardown tried to return a plain-clone home through treehouse"
1885
+ [ ! -d "$subhome" ] || fail "teardown did not remove the plain-clone secondmate home"
1886
+ [ ! -e "$home/state/domain.meta" ] || fail "teardown did not clear parent meta for plain-clone home"
1887
+ grep -F -- '- domain ' "$home/data/secondmates.md" >/dev/null && fail "teardown did not remove plain-clone registry route"
1888
+ pass "secondmate teardown raw-removes plain-clone homes"
1889
+ }
1890
+
1891
+ test_secondmate_force_teardown_discards_child_work() {
1892
+ local home subhome childproj childwt fakebin log
1893
+ home="$TMP_ROOT/force-teardown-home"
1894
+ subhome="$TMP_ROOT/force-teardown-subhome"
1895
+ childproj="$subhome/projects/alpha"
1896
+ childwt="$TMP_ROOT/force-child-worktree"
1897
+ mkdir -p "$home/state" "$home/data" "$subhome/state"
1898
+ fm_git_worktree "$childproj" "$childwt" force-child
1899
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
1900
+ cat > "$home/state/domain.meta" <<EOF
1901
+ window=firstmate:fm-domain
1902
+ worktree=$subhome
1903
+ project=$subhome
1904
+ harness=echo
1905
+ kind=secondmate
1906
+ mode=secondmate
1907
+ yolo=off
1908
+ home=$subhome
1909
+ projects=alpha
1910
+ EOF
1911
+ printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
1912
+ cat > "$subhome/state/child.meta" <<EOF
1913
+ window=firstmate:fm-child
1914
+ worktree=$childwt
1915
+ project=$childproj
1916
+ harness=echo
1917
+ kind=ship
1918
+ mode=no-mistakes
1919
+ yolo=off
1920
+ EOF
1921
+ fakebin=$(make_fake_tmux "$TMP_ROOT/force-teardown-fake")
1922
+ log="$TMP_ROOT/force-teardown-fake/tmux.log"
1923
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/force-teardown-fake/pane.txt" \
1924
+ "$ROOT/bin/fm-teardown.sh" domain >/dev/null 2>&1; then
1925
+ fail "teardown allowed a secondmate with in-flight child work"
1926
+ fi
1927
+ PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/force-teardown-fake/pane.txt" \
1928
+ "$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>/dev/null \
1929
+ || fail "force teardown failed to discard child work"
1930
+ [ ! -d "$subhome" ] || fail "force teardown did not remove the retired secondmate home"
1931
+ [ ! -d "$childwt" ] || fail "force teardown did not remove child worktree"
1932
+ [ ! -e "$home/state/domain.meta" ] || fail "teardown did not clear parent meta"
1933
+ grep -F -- '- domain ' "$home/data/secondmates.md" >/dev/null && fail "force teardown did not remove secondmate registry route"
1934
+ grep -F 'kill-window -t =firstmate:=fm-child' "$log" >/dev/null || fail "force teardown did not kill child window"
1935
+ grep -F 'kill-window -t =firstmate:=fm-domain' "$log" >/dev/null || fail "force teardown did not kill parent window"
1936
+ pass "secondmate force teardown discards child work"
1937
+ }
1938
+
1939
+ test_secondmate_force_teardown_preserves_child_on_unproven_lock() {
1940
+ local home subhome childproj childwt fakebin log err rc lock
1941
+ home="$TMP_ROOT/force-lock-home"
1942
+ subhome="$TMP_ROOT/force-lock-subhome"
1943
+ childproj="$subhome/projects/alpha"
1944
+ childwt="$TMP_ROOT/force-lock-child-worktree"
1945
+ err="$TMP_ROOT/force-lock-child.err"
1946
+ mkdir -p "$home/state" "$home/data" "$subhome/state"
1947
+ fm_git_worktree "$childproj" "$childwt" force-child-lock
1948
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
1949
+ cat > "$home/state/domain.meta" <<EOF
1950
+ window=firstmate:fm-domain
1951
+ worktree=$subhome
1952
+ project=$subhome
1953
+ harness=echo
1954
+ kind=secondmate
1955
+ mode=secondmate
1956
+ yolo=off
1957
+ home=$subhome
1958
+ projects=alpha
1959
+ EOF
1960
+ printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
1961
+ cat > "$subhome/state/child.meta" <<EOF
1962
+ window=firstmate:fm-child
1963
+ worktree=$childwt
1964
+ project=$childproj
1965
+ harness=echo
1966
+ kind=ship
1967
+ mode=no-mistakes
1968
+ yolo=off
1969
+ EOF
1970
+ fakebin=$(make_fake_tmux "$TMP_ROOT/force-lock-child-fake")
1971
+ log="$TMP_ROOT/force-lock-child-fake/tmux.log"
1972
+ cat > "$fakebin/treehouse" <<'SH'
1973
+ #!/usr/bin/env bash
1974
+ set -u
1975
+ printf 'treehouse %s\n' "$*" >> "${FM_FAKE_TMUX_LOG:-/dev/null}"
1976
+ case "${1:-}" in
1977
+ return)
1978
+ shift
1979
+ target=
1980
+ while [ $# -gt 0 ]; do
1981
+ case "$1" in
1982
+ --force) ;;
1983
+ *) target=$1 ;;
1984
+ esac
1985
+ shift
1986
+ done
1987
+ lock=$(git -C "$target" rev-parse --git-path index.lock 2>/dev/null || true)
1988
+ if [ -n "$lock" ] && [ -e "$lock" ]; then
1989
+ echo "fatal: Unable to create '$lock': File exists." >&2
1990
+ exit 128
1991
+ fi
1992
+ [ -n "$target" ] && rm -rf -- "$target"
1993
+ exit 0
1994
+ ;;
1995
+ esac
1996
+ exit 0
1997
+ SH
1998
+ cat > "$fakebin/lsof" <<'SH'
1999
+ #!/usr/bin/env bash
2000
+ exit 0
2001
+ SH
2002
+ chmod +x "$fakebin/treehouse" "$fakebin/lsof"
2003
+ lock=$(git -C "$childwt" rev-parse --git-path index.lock)
2004
+ mkdir -p "$(dirname "$lock")"
2005
+ : > "$lock"
2006
+ touch -t 200001010000 "$lock"
2007
+
2008
+ set +e
2009
+ PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/force-lock-child-fake/pane.txt" \
2010
+ FM_STALE_WORKTREE_LOCK_RETRY_WAIT_SECS=0 FM_STALE_WORKTREE_LOCK_AGE_SECS=1 \
2011
+ "$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"
2012
+ rc=$?
2013
+ set -e
2014
+
2015
+ [ "$rc" -ne 0 ] || fail "force teardown succeeded after child treehouse refused an unproven lock"
2016
+ [ -d "$childwt" ] || fail "force teardown raw-removed child worktree after unproven lock refusal"
2017
+ [ -e "$lock" ] || fail "force teardown removed unproven child index.lock"
2018
+ [ -d "$subhome" ] || fail "force teardown removed subhome after child lock refusal"
2019
+ [ -e "$subhome/state/child.meta" ] || fail "force teardown cleared child meta after child lock refusal"
2020
+ grep -F 'not provably stale' "$err" >/dev/null || fail "force teardown did not explain unproven child lock refusal"
2021
+ pass "secondmate force teardown preserves child worktree after unproven lock refusal"
2022
+ }
2023
+
2024
+ test_secondmate_force_teardown_allows_non_state_operational_dir_symlinks_inside_home() {
2025
+ local opdir home subhome target fakebin err log
2026
+ for opdir in data config projects; do
2027
+ home="$TMP_ROOT/symlink-inside-teardown-home-$opdir"
2028
+ subhome="$TMP_ROOT/symlink-inside-teardown-subhome-$opdir"
2029
+ target="$subhome/internal-$opdir"
2030
+ err="$TMP_ROOT/symlink-inside-teardown-$opdir.err"
2031
+ rm -rf "$home" "$subhome"
2032
+ mkdir -p "$home/state" "$home/data" "$subhome" "$target"
2033
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
2034
+ ln -s "$target" "$subhome/$opdir"
2035
+ cat > "$home/state/domain.meta" <<EOF
2036
+ window=firstmate:fm-domain
2037
+ worktree=$subhome
2038
+ project=$subhome
2039
+ harness=echo
2040
+ kind=secondmate
2041
+ mode=secondmate
2042
+ yolo=off
2043
+ home=$subhome
2044
+ projects=alpha
2045
+ EOF
2046
+ printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
2047
+ fakebin=$(make_fake_tmux "$TMP_ROOT/symlink-inside-teardown-fake-$opdir")
2048
+ log="$TMP_ROOT/symlink-inside-teardown-fake-$opdir/tmux.log"
2049
+ PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/symlink-inside-teardown-fake-$opdir/pane.txt" \
2050
+ "$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err" \
2051
+ || fail "force teardown refused $opdir symlinked inside the secondmate home"
2052
+ [ ! -e "$subhome" ] || fail "force teardown did not remove subhome with inside $opdir symlink"
2053
+ [ ! -e "$home/state/domain.meta" ] || fail "force teardown did not clear parent meta for inside $opdir symlink"
2054
+ grep -F 'kill-window -t =firstmate:=fm-domain' "$log" >/dev/null || fail "force teardown did not kill parent window for inside $opdir symlink"
2055
+ done
2056
+ pass "force teardown allows non-state operational directory symlinks inside the subhome"
2057
+ }
2058
+
2059
+ test_secondmate_force_teardown_refuses_operational_dir_symlink_outside_home() {
2060
+ local home subhome external_state fakebin err log
2061
+ home="$TMP_ROOT/symlink-state-teardown-home"
2062
+ subhome="$TMP_ROOT/symlink-state-teardown-subhome"
2063
+ external_state="$home/data/external-state"
2064
+ err="$TMP_ROOT/symlink-state-teardown.err"
2065
+ mkdir -p "$home/state" "$home/data" "$subhome" "$external_state"
2066
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
2067
+ ln -s "$external_state" "$subhome/state"
2068
+ cat > "$home/state/domain.meta" <<EOF
2069
+ window=firstmate:fm-domain
2070
+ worktree=$subhome
2071
+ project=$subhome
2072
+ harness=echo
2073
+ kind=secondmate
2074
+ mode=secondmate
2075
+ yolo=off
2076
+ home=$subhome
2077
+ projects=alpha
2078
+ EOF
2079
+ printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
2080
+ fakebin=$(make_fake_tmux "$TMP_ROOT/symlink-state-teardown-fake")
2081
+ log="$TMP_ROOT/symlink-state-teardown-fake/tmux.log"
2082
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/symlink-state-teardown-fake/pane.txt" \
2083
+ "$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
2084
+ fail "force teardown accepted a symlinked secondmate state directory"
2085
+ fi
2086
+ [ -d "$subhome" ] || fail "force teardown removed subhome after symlinked state refusal"
2087
+ [ -d "$external_state" ] || fail "force teardown removed external symlink target"
2088
+ grep -F 'state directory' "$err" >/dev/null || fail "teardown did not explain symlinked state refusal"
2089
+ grep -F 'resolves outside the secondmate home' "$err" >/dev/null || fail "teardown did not identify unsafe state symlink"
2090
+ grep -F 'kill-window' "$log" >/dev/null && fail "teardown killed a window before symlinked state refusal"
2091
+ pass "force teardown refuses operational directory symlinks outside the subhome"
2092
+ }
2093
+
2094
+ test_secondmate_teardown_path_boundary_matrix() {
2095
+ # The teardown path-boundary matrix: a secondmate home is refused (and left
2096
+ # fully intact, with no window killed before validation) when it is unmarked,
2097
+ # an ancestor of the active firstmate home, inside the active firstmate home,
2098
+ # or inside the firstmate repo. One row per hazard, one shared assertion block.
2099
+ local row base home subhome fmroot fakebin log err expect tid
2100
+ while IFS='|' read -r row expect; do
2101
+ [ -n "$row" ] || continue
2102
+ base="$TMP_ROOT/td-pb-$row"
2103
+ fmroot="$ROOT" # real firstmate repo unless a row overrides it
2104
+ tid=domain
2105
+ case "$row" in
2106
+ unmarked)
2107
+ home="$base/main"; subhome="$base/sub"
2108
+ mkdir -p "$home/state" "$home/data" "$subhome/state"
2109
+ # No .fm-secondmate-home marker on purpose.
2110
+ ;;
2111
+ ancestor)
2112
+ # The home being torn down is an ANCESTOR of the active firstmate home.
2113
+ subhome="$base/anc"; home="$subhome/main-home"
2114
+ mkdir -p "$home/state" "$home/data" "$subhome/state"
2115
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
2116
+ ;;
2117
+ active-descendant)
2118
+ home="$base/desc"; subhome="$home/data/domain-home"
2119
+ mkdir -p "$home/state" "$home/data" "$subhome/state"
2120
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
2121
+ ;;
2122
+ repo-descendant)
2123
+ home="$base/home"; fmroot="$base/root"; subhome="$fmroot/tmp/domain-home"; tid='repo-domain'
2124
+ mkdir -p "$home/state" "$home/data" "$subhome/state" "$fmroot/bin"
2125
+ cat > "$fmroot/bin/fm-guard.sh" <<'SH'
2126
+ #!/usr/bin/env bash
2127
+ exit 0
2128
+ SH
2129
+ chmod +x "$fmroot/bin/fm-guard.sh"
2130
+ printf 'repo-domain\n' > "$subhome/.fm-secondmate-home"
2131
+ ;;
2132
+ esac
2133
+ fm_write_secondmate_meta "$home/state/$tid.meta" "$subhome"
2134
+ printf -- '- %s - design domain (home: %s; scope: design domain; projects: alpha; added 2026-06-22)\n' \
2135
+ "$tid" "$subhome" > "$home/data/secondmates.md"
2136
+ fakebin=$(make_fake_tmux "$base/fake")
2137
+ log="$base/fake/tmux.log"
2138
+ err="$base/teardown.err"
2139
+ if PATH="$fakebin:$PATH" FM_ROOT_OVERRIDE="$fmroot" FM_HOME="$home" \
2140
+ FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$base/fake/pane.txt" \
2141
+ "$ROOT/bin/fm-teardown.sh" "$tid" >/dev/null 2>"$err"; then
2142
+ fail "teardown ($row) accepted a hazardous secondmate home"
2143
+ fi
2144
+ grep -F "$expect" "$err" >/dev/null || fail "teardown ($row) did not explain the refusal (expected '$expect'): $(cat "$err")"
2145
+ [ -d "$subhome" ] || fail "teardown ($row) removed the protected home after refusal"
2146
+ [ -e "$home/state/$tid.meta" ] || fail "teardown ($row) cleared the parent meta after refusal"
2147
+ grep -F -- "- $tid " "$home/data/secondmates.md" >/dev/null || fail "teardown ($row) removed the registry route after refusal"
2148
+ grep -F 'kill-window' "$log" >/dev/null && fail "teardown ($row) killed a window before validation"
2149
+ done <<'ROWS'
2150
+ unmarked|not a seeded secondmate home
2151
+ ancestor|ancestor of the active firstmate home
2152
+ active-descendant|inside the active firstmate home
2153
+ repo-descendant|inside the firstmate repo
2154
+ ROWS
2155
+ pass "secondmate teardown path-boundary matrix refuses unmarked/ancestor/active-descendant/repo-descendant homes"
2156
+ }
2157
+
2158
+ test_secondmate_teardown_refuses_registered_nested_home() {
2159
+ local home subhome nested fakebin err log
2160
+ home="$TMP_ROOT/nested-teardown-home"
2161
+ subhome="$TMP_ROOT/nested-teardown-subhome"
2162
+ nested="$subhome/nested-domain"
2163
+ err="$TMP_ROOT/nested-teardown.err"
2164
+ mkdir -p "$home/state" "$home/data" "$subhome/state" "$nested/state"
2165
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
2166
+ printf 'nested\n' > "$nested/.fm-secondmate-home"
2167
+ cat > "$home/state/domain.meta" <<EOF
2168
+ window=firstmate:fm-domain
2169
+ worktree=$subhome
2170
+ project=$subhome
2171
+ harness=echo
2172
+ kind=secondmate
2173
+ mode=secondmate
2174
+ yolo=off
2175
+ home=$subhome
2176
+ projects=alpha
2177
+ EOF
2178
+ cat > "$home/state/nested.meta" <<EOF
2179
+ window=firstmate:fm-nested
2180
+ worktree=$nested
2181
+ project=$nested
2182
+ harness=echo
2183
+ kind=secondmate
2184
+ mode=secondmate
2185
+ yolo=off
2186
+ home=$nested
2187
+ projects=beta
2188
+ EOF
2189
+ cat > "$home/data/secondmates.md" <<EOF
2190
+ - domain - design domain (home: $subhome; scope: design domain; projects: alpha; added 2026-06-22)
2191
+ - nested - nested domain mentions home: $TMP_ROOT/ignored-summary-home (home: $nested; scope: nested domain mentions home: $TMP_ROOT/ignored-scope-home; projects: beta; added 2026-06-22)
2192
+ EOF
2193
+ fakebin=$(make_fake_tmux "$TMP_ROOT/nested-teardown-fake")
2194
+ log="$TMP_ROOT/nested-teardown-fake/tmux.log"
2195
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/nested-teardown-fake/pane.txt" \
2196
+ "$ROOT/bin/fm-teardown.sh" domain >/dev/null 2>"$err"; then
2197
+ fail "teardown removed a home containing another registered secondmate home"
2198
+ fi
2199
+ [ -d "$subhome" ] || fail "teardown removed registered ancestor home after refusal"
2200
+ [ -d "$nested" ] || fail "teardown removed registered nested home after refusal"
2201
+ [ -e "$home/state/domain.meta" ] || fail "teardown cleared ancestor meta after nested-home refusal"
2202
+ [ -e "$home/state/nested.meta" ] || fail "teardown cleared nested meta after nested-home refusal"
2203
+ grep -F 'kill-window' "$log" >/dev/null && fail "teardown killed a window before nested-home refusal"
2204
+ grep -F 'contains registered secondmate home' "$err" >/dev/null || fail "teardown did not explain registered nested-home refusal"
2205
+ pass "secondmate teardown refuses homes containing registered nested homes"
2206
+ }
2207
+
2208
+ test_secondmate_teardown_refuses_child_registry_nested_home() {
2209
+ local home subhome nested fakebin err log
2210
+ home="$TMP_ROOT/child-registry-teardown-home"
2211
+ subhome="$TMP_ROOT/child-registry-teardown-subhome"
2212
+ nested="$subhome/nested-domain"
2213
+ err="$TMP_ROOT/child-registry-teardown.err"
2214
+ mkdir -p "$home/state" "$home/data" "$subhome/state" "$subhome/data" "$nested/state"
2215
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
2216
+ printf 'nested\n' > "$nested/.fm-secondmate-home"
2217
+ cat > "$home/state/domain.meta" <<EOF
2218
+ window=firstmate:fm-domain
2219
+ worktree=$subhome
2220
+ project=$subhome
2221
+ harness=echo
2222
+ kind=secondmate
2223
+ mode=secondmate
2224
+ yolo=off
2225
+ home=$subhome
2226
+ projects=alpha
2227
+ EOF
2228
+ printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
2229
+ printf '%s\n' '- nested - nested domain (home: '"$nested"'; scope: nested domain; projects: beta; added 2026-06-22)' > "$subhome/data/secondmates.md"
2230
+ fakebin=$(make_fake_tmux "$TMP_ROOT/child-registry-teardown-fake")
2231
+ log="$TMP_ROOT/child-registry-teardown-fake/tmux.log"
2232
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/child-registry-teardown-fake/pane.txt" \
2233
+ "$ROOT/bin/fm-teardown.sh" domain >/dev/null 2>"$err"; then
2234
+ fail "teardown removed a home containing a child-registry secondmate home"
2235
+ fi
2236
+ [ -d "$subhome" ] || fail "teardown removed ancestor home after child-registry refusal"
2237
+ [ -d "$nested" ] || fail "teardown removed child-registry nested home after refusal"
2238
+ [ -e "$home/state/domain.meta" ] || fail "teardown cleared parent meta after child-registry refusal"
2239
+ grep -F 'kill-window' "$log" >/dev/null && fail "teardown killed a window before child-registry refusal"
2240
+ grep -F 'contains registered secondmate home' "$err" >/dev/null || fail "teardown did not explain child-registry nested-home refusal"
2241
+ pass "secondmate teardown refuses nested homes from the child registry"
2242
+ }
2243
+
2244
+ test_secondmate_force_teardown_prevalidates_before_child_cleanup() {
2245
+ local home subhome childproj childwt fakebin err log
2246
+ home="$TMP_ROOT/prevalidate-teardown-home"
2247
+ subhome="$TMP_ROOT/prevalidate-teardown-subhome"
2248
+ childproj="$subhome/projects/alpha"
2249
+ childwt="$TMP_ROOT/prevalidate-child-worktree"
2250
+ err="$TMP_ROOT/prevalidate-teardown.err"
2251
+ mkdir -p "$home/state" "$home/data" "$subhome/state" "$childproj" "$childwt"
2252
+ cat > "$home/state/domain.meta" <<EOF
2253
+ window=firstmate:fm-domain
2254
+ worktree=$subhome
2255
+ project=$subhome
2256
+ harness=echo
2257
+ kind=secondmate
2258
+ mode=secondmate
2259
+ yolo=off
2260
+ home=$subhome
2261
+ projects=alpha
2262
+ EOF
2263
+ printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
2264
+ cat > "$subhome/state/child.meta" <<EOF
2265
+ window=firstmate:fm-child
2266
+ worktree=$childwt
2267
+ project=$childproj
2268
+ harness=echo
2269
+ kind=ship
2270
+ mode=no-mistakes
2271
+ yolo=off
2272
+ EOF
2273
+ fakebin=$(make_fake_tmux "$TMP_ROOT/prevalidate-teardown-fake")
2274
+ log="$TMP_ROOT/prevalidate-teardown-fake/tmux.log"
2275
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/prevalidate-teardown-fake/pane.txt" \
2276
+ "$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
2277
+ fail "force teardown discarded child work before validating subhome"
2278
+ fi
2279
+ [ -d "$subhome" ] || fail "force teardown removed unmarked subhome after refusal"
2280
+ [ -d "$childwt" ] || fail "force teardown removed child worktree before validation"
2281
+ [ -e "$home/state/domain.meta" ] || fail "force teardown cleared parent meta before validation"
2282
+ [ -e "$subhome/state/child.meta" ] || fail "force teardown cleared child meta before validation"
2283
+ grep -F 'kill-window' "$log" >/dev/null && fail "force teardown killed windows before subhome validation"
2284
+ grep -F 'not a seeded secondmate home' "$err" >/dev/null || fail "force teardown did not explain missing seed marker"
2285
+ pass "force teardown validates subhome before child cleanup"
2286
+ }
2287
+
2288
+ # A per-task lock cannot protect a task that does not exist yet. Forced teardown
2289
+ # enumerates a home's task set, locks what it found, then re-enumerates while
2290
+ # removing - so a fresh spawn publishing inside that window was destructively
2291
+ # processed while never lifecycle-locked (reproduced with real agents). The
2292
+ # per-home task-set lock serializes the two. Both directions are asserted here,
2293
+ # by HOLDING the lock rather than racing on timing, so neither case can go
2294
+ # quietly vacuous.
2295
+ seed_task_set_lock_home() { # <tag> -> echoes "<home>|<subhome>"
2296
+ local tag=$1 home subhome childproj childwt
2297
+ home="$TMP_ROOT/$tag-home"
2298
+ subhome="$TMP_ROOT/$tag-subhome"
2299
+ childproj="$subhome/projects/alpha"
2300
+ childwt="$TMP_ROOT/$tag-child-worktree"
2301
+ mkdir -p "$home/state" "$home/data" "$subhome/state" "$subhome/data"
2302
+ # A real worktree pair: child-removal validation runs before the task-set
2303
+ # preflight and refuses a child whose worktree is not a genuine worktree of
2304
+ # its project, which would mask the refusal under test.
2305
+ fm_git_worktree "$childproj" "$childwt" "child-$tag"
2306
+ printf '%s\n' domain > "$subhome/.fm-secondmate-home"
2307
+ cat > "$home/state/domain.meta" <<EOF
2308
+ window=firstmate:fm-domain
2309
+ worktree=$subhome
2310
+ project=$subhome
2311
+ harness=echo
2312
+ kind=secondmate
2313
+ mode=secondmate
2314
+ yolo=off
2315
+ home=$subhome
2316
+ projects=alpha
2317
+ EOF
2318
+ printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
2319
+ cat > "$subhome/state/child.meta" <<EOF
2320
+ window=firstmate:fm-child
2321
+ worktree=$childwt
2322
+ project=$childproj
2323
+ harness=echo
2324
+ kind=ship
2325
+ mode=no-mistakes
2326
+ yolo=off
2327
+ EOF
2328
+ printf '%s|%s\n' "$home" "$subhome"
2329
+ }
2330
+
2331
+ task_set_lock_path() { # <state-dir>
2332
+ local state=$1
2333
+ ( . "$ROOT/bin/fm-wake-lib.sh"; fm_task_set_lock_path "$state" )
2334
+ }
2335
+
2336
+ # The holder must stay ALIVE: fm_lock_try_acquire reclaims a lock whose owning
2337
+ # pid is gone, so a lock taken in a subshell that then exits would be stolen and
2338
+ # the contention under test would never happen.
2339
+ hold_task_set_lock() { # <state-dir> -> echoes "<holder-pid> <lock-path>"
2340
+ local state=$1 lock holder i=0
2341
+ lock=$(task_set_lock_path "$state") || return 1
2342
+ [ -n "$lock" ] || return 1
2343
+ # stdout/stderr are redirected so the long-lived holder does not inherit this
2344
+ # function's command-substitution pipe; leaving it open would block the caller
2345
+ # until the holder exited, by which point the lock would be stale and the
2346
+ # contention under test could not happen.
2347
+ (
2348
+ # shellcheck source=/dev/null
2349
+ . "$ROOT/bin/fm-wake-lib.sh"
2350
+ fm_lock_try_acquire "$lock" || exit 1
2351
+ sleep 30
2352
+ ) >/dev/null 2>&1 &
2353
+ holder=$!
2354
+ while [ ! -e "$lock" ] && [ "$i" -lt 100 ]; do
2355
+ sleep 0.1
2356
+ i=$((i + 1))
2357
+ done
2358
+ [ -e "$lock" ] || {
2359
+ kill "$holder" 2>/dev/null || true
2360
+ wait "$holder" 2>/dev/null || true
2361
+ return 1
2362
+ }
2363
+ printf '%s %s\n' "$holder" "$lock"
2364
+ }
2365
+
2366
+ seed_empty_task_set_home() { # <tag> -> echoes "<home>|<subhome>"
2367
+ local tag=$1 home subhome
2368
+ home="$TMP_ROOT/$tag-home"
2369
+ subhome="$TMP_ROOT/$tag-subhome"
2370
+ mkdir -p "$home/state" "$home/data" "$subhome/data"
2371
+ printf '%s\n' domain > "$subhome/.fm-secondmate-home"
2372
+ cat > "$home/state/domain.meta" <<EOF
2373
+ window=firstmate:fm-domain
2374
+ worktree=$subhome
2375
+ project=$subhome
2376
+ harness=echo
2377
+ kind=secondmate
2378
+ mode=secondmate
2379
+ yolo=off
2380
+ home=$subhome
2381
+ projects=alpha
2382
+ EOF
2383
+ printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
2384
+ printf '%s|%s\n' "$home" "$subhome"
2385
+ }
2386
+
2387
+ test_force_teardown_refuses_non_directory_descendant_state() {
2388
+ local home subhome fakebin err log rec
2389
+ rec=$(seed_empty_task_set_home taskset-state-file)
2390
+ IFS='|' read -r home subhome <<EOF
2391
+ $rec
2392
+ EOF
2393
+ printf '%s\n' 'not a state directory' > "$subhome/state"
2394
+ err="$TMP_ROOT/taskset-state-file.err"
2395
+ fakebin=$(make_fake_tmux "$TMP_ROOT/taskset-state-file-fake")
2396
+ log="$TMP_ROOT/taskset-state-file-fake/tmux.log"
2397
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
2398
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/taskset-state-file-fake/pane.txt" \
2399
+ "$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
2400
+ fail "forced teardown accepted a non-directory descendant state path"
2401
+ fi
2402
+ [ -d "$subhome" ] || fail "state-path refusal removed the descendant home"
2403
+ [ -f "$subhome/state" ] || fail "state-path refusal changed the non-directory state path"
2404
+ [ -e "$home/state/domain.meta" ] || fail "state-path refusal removed parent metadata"
2405
+ grep -F 'kill-window' "$log" >/dev/null && fail "state-path refusal killed a window"
2406
+ grep -F "$(basename "$subhome")" "$err" >/dev/null || fail "state-path refusal did not name the descendant home: $(cat "$err")"
2407
+ grep -F 'not a directory' "$err" >/dev/null || fail "state-path refusal did not explain the concrete problem: $(cat "$err")"
2408
+ pass "forced teardown refuses a non-directory descendant state path"
2409
+ }
2410
+
2411
+ test_force_teardown_refuses_symlinked_descendant_state() {
2412
+ local home subhome fakebin err log rec
2413
+ rec=$(seed_empty_task_set_home taskset-state-symlink)
2414
+ IFS='|' read -r home subhome <<EOF
2415
+ $rec
2416
+ EOF
2417
+ mkdir -p "$subhome/state-target"
2418
+ ln -s state-target "$subhome/state"
2419
+ err="$TMP_ROOT/taskset-state-symlink.err"
2420
+ fakebin=$(make_fake_tmux "$TMP_ROOT/taskset-state-symlink-fake")
2421
+ log="$TMP_ROOT/taskset-state-symlink-fake/tmux.log"
2422
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
2423
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/taskset-state-symlink-fake/pane.txt" \
2424
+ "$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
2425
+ fail "forced teardown accepted a symlinked descendant state path"
2426
+ fi
2427
+ [ -d "$subhome" ] || fail "symlinked state-path refusal removed the descendant home"
2428
+ [ -L "$subhome/state" ] || fail "symlinked state-path refusal changed the state symlink"
2429
+ [ -d "$subhome/state-target" ] || fail "symlinked state-path refusal changed the state target"
2430
+ [ -e "$home/state/domain.meta" ] || fail "symlinked state-path refusal removed parent metadata"
2431
+ grep -F 'kill-window' "$log" >/dev/null && fail "symlinked state-path refusal killed a window"
2432
+ grep -F "$(basename "$subhome")" "$err" >/dev/null || fail "symlinked state-path refusal did not name the descendant home: $(cat "$err")"
2433
+ grep -F 'symbolic-link state path' "$err" >/dev/null || fail "symlinked state-path refusal did not explain the concrete problem: $(cat "$err")"
2434
+ pass "forced teardown refuses a symlinked descendant state path"
2435
+ }
2436
+
2437
+ test_force_teardown_locks_descendant_with_absent_state() {
2438
+ local home subhome fakebin err log rec claim_root ready release lock pid i=0
2439
+ rec=$(seed_empty_task_set_home taskset-state-absent)
2440
+ IFS='|' read -r home subhome <<EOF
2441
+ $rec
2442
+ EOF
2443
+ claim_root="$TMP_ROOT/taskset-state-absent-xdg/firstmate/procevent-claims"
2444
+ ready="$TMP_ROOT/taskset-state-absent.ready"
2445
+ release="$TMP_ROOT/taskset-state-absent.release"
2446
+ mkdir -p "$claim_root" "$subhome/bin"
2447
+ printf '%s\n' "$subhome" > "$claim_root/held.claim"
2448
+ cat > "$subhome/bin/fm-procevent.sh" <<'SH'
2449
+ #!/usr/bin/env bash
2450
+ set -u
2451
+ if [ "${1:-}" = sweep-home ] && [ "${2:-}" = --preflight ]; then
2452
+ : > "$FM_TASK_SET_TEST_READY"
2453
+ while [ ! -e "$FM_TASK_SET_TEST_RELEASE" ]; do sleep 0.05; done
2454
+ exit 1
2455
+ fi
2456
+ exit 1
2457
+ SH
2458
+ chmod +x "$subhome/bin/fm-procevent.sh"
2459
+ err="$TMP_ROOT/taskset-state-absent.err"
2460
+ fakebin=$(make_fake_tmux "$TMP_ROOT/taskset-state-absent-fake")
2461
+ log="$TMP_ROOT/taskset-state-absent-fake/tmux.log"
2462
+ PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
2463
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/taskset-state-absent-fake/pane.txt" \
2464
+ XDG_STATE_HOME="$TMP_ROOT/taskset-state-absent-xdg" \
2465
+ FM_TASK_SET_TEST_READY="$ready" FM_TASK_SET_TEST_RELEASE="$release" \
2466
+ "$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err" &
2467
+ pid=$!
2468
+ while [ ! -e "$ready" ] && kill -0 "$pid" 2>/dev/null && [ "$i" -lt 200 ]; do
2469
+ sleep 0.05
2470
+ i=$((i + 1))
2471
+ done
2472
+ [ -e "$ready" ] || {
2473
+ : > "$release"
2474
+ wait "$pid" 2>/dev/null || true
2475
+ fail "forced teardown did not reach the post-lock preflight: $(cat "$err")"
2476
+ }
2477
+ lock=$(task_set_lock_path "$subhome/state") \
2478
+ || fail "could not resolve the established descendant task-set lock"
2479
+ [ -d "$subhome/state" ] || fail "forced teardown did not establish the absent state directory"
2480
+ [ -e "$lock" ] || fail "forced teardown did not own the descendant task-set lock during preflight"
2481
+ kill -0 "$pid" 2>/dev/null || fail "forced teardown exited before task-set ownership was observed"
2482
+ : > "$release"
2483
+ if wait "$pid"; then
2484
+ fail "forced teardown ignored the staged process-event preflight refusal"
2485
+ fi
2486
+ [ -d "$subhome" ] || fail "post-lock refusal removed the descendant home"
2487
+ [ -e "$home/state/domain.meta" ] || fail "post-lock refusal removed parent metadata"
2488
+ [ ! -e "$lock" ] || fail "refused teardown left the descendant task-set lock behind"
2489
+ grep -F 'kill-window' "$log" >/dev/null && fail "post-lock refusal killed a window"
2490
+ pass "forced teardown locks a descendant whose state directory was absent"
2491
+ }
2492
+
2493
+ test_force_teardown_refuses_while_a_task_is_being_published() {
2494
+ local home subhome fakebin err log lock rec held holder
2495
+ rec=$(seed_task_set_lock_home taskset-teardown)
2496
+ IFS='|' read -r home subhome <<EOF
2497
+ $rec
2498
+ EOF
2499
+ err="$TMP_ROOT/taskset-teardown.err"
2500
+ # Stand in for a fresh spawn that is mid-publication in the secondmate's home.
2501
+ held=$(hold_task_set_lock "$subhome/state") \
2502
+ || fail "could not stage a held task-set lock"
2503
+ holder=${held%% *}
2504
+ lock=${held#* }
2505
+ fakebin=$(make_fake_tmux "$TMP_ROOT/taskset-teardown-fake")
2506
+ log="$TMP_ROOT/taskset-teardown-fake/tmux.log"
2507
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
2508
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/taskset-teardown-fake/pane.txt" \
2509
+ "$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
2510
+ fail "forced teardown proceeded while a task was being published"
2511
+ fi
2512
+ [ -d "$subhome" ] || fail "forced teardown removed the home despite refusing"
2513
+ [ -e "$subhome/state/child.meta" ] || fail "forced teardown removed child metadata despite refusing"
2514
+ [ -e "$home/state/domain.meta" ] || fail "forced teardown cleared parent metadata despite refusing"
2515
+ grep -F 'kill-window' "$log" >/dev/null && fail "forced teardown killed a window despite refusing"
2516
+ [ -e "$lock" ] || fail "forced teardown removed the publisher's task-set lock"
2517
+ grep -F 'task-set lock is held' "$err" >/dev/null \
2518
+ || fail "the refusal did not name the task-set contention: $(cat "$err")"
2519
+ kill "$holder" 2>/dev/null || true
2520
+ wait "$holder" 2>/dev/null || true
2521
+ pass "forced teardown refuses while a fresh task is being published in the home"
2522
+ }
2523
+
2524
+ test_fresh_spawn_refuses_while_a_forced_teardown_owns_the_task_set() {
2525
+ local home subhome err rec held holder lock
2526
+ rec=$(seed_task_set_lock_home taskset-spawn)
2527
+ IFS='|' read -r home subhome <<EOF
2528
+ $rec
2529
+ EOF
2530
+ err="$TMP_ROOT/taskset-spawn.err"
2531
+ # Stand in for a forced teardown that already enumerated this home's task set.
2532
+ held=$(hold_task_set_lock "$subhome/state") \
2533
+ || fail "could not stage a held task-set lock"
2534
+ holder=${held%% *}
2535
+ lock=${held#* }
2536
+ if FM_HOME="$subhome" FM_SPAWN_NO_GUARD=1 \
2537
+ "$ROOT/bin/fm-spawn.sh" newtask "$subhome/projects/alpha" --scout >/dev/null 2>"$err"; then
2538
+ kill "$holder" 2>/dev/null || true
2539
+ fail "a fresh spawn published a task while a forced teardown owned the set"
2540
+ fi
2541
+ [ ! -e "$subhome/state/newtask.meta" ] \
2542
+ || fail "a refused spawn still published a durable record"
2543
+ [ -e "$lock" ] || fail "a refused spawn removed the teardown's task-set lock"
2544
+ grep -F "task set is locked" "$err" >/dev/null \
2545
+ || fail "the spawn refusal did not name the task-set contention: $(cat "$err")"
2546
+ [ ! -e "$subhome/state/.spawn-newtask.lock" ] \
2547
+ || fail "a refused spawn left its own task lock behind"
2548
+ kill "$holder" 2>/dev/null || true
2549
+ wait "$holder" 2>/dev/null || true
2550
+ pass "a fresh spawn refuses to publish while a forced teardown owns the task set"
2551
+ }
2552
+
2553
+ test_fresh_remote_secondmate_spawn_refuses_while_task_set_is_owned() {
2554
+ local home err held holder lock
2555
+ home="$TMP_ROOT/taskset-remote-spawn-home"
2556
+ err="$TMP_ROOT/taskset-remote-spawn.err"
2557
+ mkdir -p "$home/state" "$home/data"
2558
+ printf '%s\n' '- remote-new - remote domain (host: remote-mac; root: /remote/root; home: /remote/home; scope: remote work; projects: alpha; added 2026-08-02)' \
2559
+ > "$home/data/secondmates.md"
2560
+ held=$(hold_task_set_lock "$home/state") \
2561
+ || fail "could not stage a held remote-spawn task-set lock"
2562
+ holder=${held%% *}
2563
+ lock=${held#* }
2564
+ if FM_HOME="$home" FM_SPAWN_NO_GUARD=1 \
2565
+ "$ROOT/bin/fm-spawn.sh" remote-new --secondmate >/dev/null 2>"$err"; then
2566
+ kill "$holder" 2>/dev/null || true
2567
+ fail "a fresh remote secondmate spawn published while the task set was owned"
2568
+ fi
2569
+ [ ! -e "$home/state/remote-new.meta" ] \
2570
+ || fail "a refused remote secondmate spawn still published a durable record"
2571
+ [ -e "$lock" ] || fail "a refused remote secondmate spawn removed the owner's task-set lock"
2572
+ grep -F "task set is locked" "$err" >/dev/null \
2573
+ || fail "the remote spawn refusal did not name task-set contention: $(cat "$err")"
2574
+ [ ! -e "$home/state/.spawn-remote-new.lock" ] \
2575
+ || fail "a refused remote secondmate spawn left its own task lock behind"
2576
+ kill "$holder" 2>/dev/null || true
2577
+ wait "$holder" 2>/dev/null || true
2578
+ pass "a fresh remote secondmate spawn refuses while the task set is owned"
2579
+ }
2580
+
2581
+ test_secondmate_force_teardown_refuses_child_active_home_descendant() {
2582
+ local home subhome childproj childwt fakebin err log
2583
+ home="$TMP_ROOT/child-active-descendant-home"
2584
+ subhome="$TMP_ROOT/child-active-descendant-subhome"
2585
+ childproj="$subhome/projects/alpha"
2586
+ childwt="$home/data"
2587
+ err="$TMP_ROOT/child-active-descendant.err"
2588
+ mkdir -p "$home/state" "$home/data" "$subhome/state" "$childproj"
2589
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
2590
+ cat > "$home/state/domain.meta" <<EOF
2591
+ window=firstmate:fm-domain
2592
+ worktree=$subhome
2593
+ project=$subhome
2594
+ harness=echo
2595
+ kind=secondmate
2596
+ mode=secondmate
2597
+ yolo=off
2598
+ home=$subhome
2599
+ projects=alpha
2600
+ EOF
2601
+ printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
2602
+ cat > "$subhome/state/child.meta" <<EOF
2603
+ window=firstmate:fm-child
2604
+ worktree=$childwt
2605
+ project=$childproj
2606
+ harness=echo
2607
+ kind=ship
2608
+ mode=no-mistakes
2609
+ yolo=off
2610
+ EOF
2611
+ fakebin=$(make_fake_tmux "$TMP_ROOT/child-active-descendant-fake")
2612
+ log="$TMP_ROOT/child-active-descendant-fake/tmux.log"
2613
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/child-active-descendant-fake/pane.txt" \
2614
+ "$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
2615
+ fail "force teardown removed a child worktree inside active FM_HOME"
2616
+ fi
2617
+ [ -d "$home/data" ] || fail "force teardown removed active home data"
2618
+ [ -d "$subhome" ] || fail "force teardown removed subhome after child validation refusal"
2619
+ [ -e "$home/state/domain.meta" ] || fail "force teardown cleared parent meta after child validation refusal"
2620
+ [ -e "$subhome/state/child.meta" ] || fail "force teardown cleared child meta after child validation refusal"
2621
+ grep -F 'kill-window' "$log" >/dev/null && fail "force teardown killed windows before child validation refusal"
2622
+ grep -F 'inside the active firstmate home' "$err" >/dev/null || fail "force teardown did not explain active home descendant rejection"
2623
+ pass "force teardown refuses child worktrees inside the active home"
2624
+ }
2625
+
2626
+ test_secondmate_force_teardown_refuses_child_repo_descendant() {
2627
+ local home subhome childproj childwt fakeroot fakebin err log
2628
+ home="$TMP_ROOT/child-repo-descendant-home"
2629
+ subhome="$TMP_ROOT/child-repo-descendant-subhome"
2630
+ childproj="$subhome/projects/alpha"
2631
+ fakeroot="$TMP_ROOT/child-repo-descendant-root"
2632
+ childwt="$fakeroot/data"
2633
+ err="$TMP_ROOT/child-repo-descendant.err"
2634
+ mkdir -p "$home/state" "$home/data" "$subhome/state" "$childproj" "$childwt" "$fakeroot/bin"
2635
+ cat > "$fakeroot/bin/fm-guard.sh" <<'SH'
2636
+ #!/usr/bin/env bash
2637
+ exit 0
2638
+ SH
2639
+ chmod +x "$fakeroot/bin/fm-guard.sh"
2640
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
2641
+ cat > "$home/state/domain.meta" <<EOF
2642
+ window=firstmate:fm-domain
2643
+ worktree=$subhome
2644
+ project=$subhome
2645
+ harness=echo
2646
+ kind=secondmate
2647
+ mode=secondmate
2648
+ yolo=off
2649
+ home=$subhome
2650
+ projects=alpha
2651
+ EOF
2652
+ printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
2653
+ cat > "$subhome/state/child.meta" <<EOF
2654
+ window=firstmate:fm-child
2655
+ worktree=$childwt
2656
+ project=$childproj
2657
+ harness=echo
2658
+ kind=ship
2659
+ mode=no-mistakes
2660
+ yolo=off
2661
+ EOF
2662
+ fakebin=$(make_fake_tmux "$TMP_ROOT/child-repo-descendant-fake")
2663
+ log="$TMP_ROOT/child-repo-descendant-fake/tmux.log"
2664
+ if PATH="$fakebin:$PATH" FM_ROOT_OVERRIDE="$fakeroot" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/child-repo-descendant-fake/pane.txt" \
2665
+ "$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
2666
+ fail "force teardown removed a child worktree inside FM_ROOT"
2667
+ fi
2668
+ [ -d "$childwt" ] || fail "force teardown removed repo descendant worktree"
2669
+ [ -d "$subhome" ] || fail "force teardown removed subhome after repo child validation refusal"
2670
+ [ -e "$home/state/domain.meta" ] || fail "force teardown cleared parent meta after repo child validation refusal"
2671
+ [ -e "$subhome/state/child.meta" ] || fail "force teardown cleared child meta after repo child validation refusal"
2672
+ grep -F 'kill-window' "$log" >/dev/null && fail "force teardown killed windows before repo child validation refusal"
2673
+ grep -F 'inside the firstmate repo' "$err" >/dev/null || fail "force teardown did not explain repo descendant rejection"
2674
+ pass "force teardown refuses child worktrees inside the firstmate repo"
2675
+ }
2676
+
2677
+ test_secondmate_force_teardown_refuses_unregistered_child_worktree() {
2678
+ local home subhome childproj childwt fakebin err log
2679
+ home="$TMP_ROOT/unregistered-child-home"
2680
+ subhome="$TMP_ROOT/unregistered-child-subhome"
2681
+ childproj="$subhome/projects/alpha"
2682
+ childwt="$TMP_ROOT/unregistered-child-worktree"
2683
+ err="$TMP_ROOT/unregistered-child.err"
2684
+ mkdir -p "$home/state" "$home/data" "$subhome/state" "$childproj" "$childwt"
2685
+ printf 'domain\n' > "$subhome/.fm-secondmate-home"
2686
+ cat > "$home/state/domain.meta" <<EOF
2687
+ window=firstmate:fm-domain
2688
+ worktree=$subhome
2689
+ project=$subhome
2690
+ harness=echo
2691
+ kind=secondmate
2692
+ mode=secondmate
2693
+ yolo=off
2694
+ home=$subhome
2695
+ projects=alpha
2696
+ EOF
2697
+ printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
2698
+ cat > "$subhome/state/child.meta" <<EOF
2699
+ window=firstmate:fm-child
2700
+ worktree=$childwt
2701
+ project=$childproj
2702
+ harness=echo
2703
+ kind=ship
2704
+ mode=no-mistakes
2705
+ yolo=off
2706
+ EOF
2707
+ fakebin=$(make_fake_tmux "$TMP_ROOT/unregistered-child-fake")
2708
+ log="$TMP_ROOT/unregistered-child-fake/tmux.log"
2709
+ if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/unregistered-child-fake/pane.txt" \
2710
+ "$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
2711
+ fail "force teardown removed an unregistered child worktree"
2712
+ fi
2713
+ [ -d "$childwt" ] || fail "force teardown removed unregistered child worktree"
2714
+ [ -d "$subhome" ] || fail "force teardown removed subhome after unregistered child refusal"
2715
+ [ -e "$home/state/domain.meta" ] || fail "force teardown cleared parent meta after unregistered child refusal"
2716
+ [ -e "$subhome/state/child.meta" ] || fail "force teardown cleared child meta after unregistered child refusal"
2717
+ grep -F 'kill-window' "$log" >/dev/null && fail "force teardown killed windows before unregistered child refusal"
2718
+ grep -F 'is not a git worktree for' "$err" >/dev/null || fail "force teardown did not explain unregistered child rejection"
2719
+ pass "force teardown refuses unregistered child worktree paths"
2720
+ }
2721
+
2722
+ test_secondmate_idle_pane_is_not_stale() {
2723
+ local home fakebin out pid window
2724
+ home="$TMP_ROOT/watch-home"
2725
+ mkdir -p "$home/state"
2726
+ window="firstmate:fm-domain"
2727
+ cat > "$home/state/domain.meta" <<EOF
2728
+ window=$window
2729
+ worktree=$TMP_ROOT/watch-subhome
2730
+ project=$TMP_ROOT/watch-subhome
2731
+ harness=echo
2732
+ kind=secondmate
2733
+ home=$TMP_ROOT/watch-subhome
2734
+ projects=alpha
2735
+ EOF
2736
+ fakebin=$(make_fake_tmux "$TMP_ROOT/watch-fake")
2737
+ out="$TMP_ROOT/watch-fake/watch.out"
2738
+ PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_WINDOW="$window" FM_FAKE_TMUX_LOG="$TMP_ROOT/watch-fake/tmux.log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/watch-fake/pane.txt" \
2739
+ FM_POLL=1 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$ROOT/bin/fm-watch.sh" > "$out" &
2740
+ pid=$!
2741
+ if ! wait_live "$pid" 25; then
2742
+ wait "$pid" || true
2743
+ grep -F "stale: $window" "$out" >/dev/null && fail "idle secondmate pane triggered stale wake"
2744
+ fail "watcher exited unexpectedly while supervising idle secondmate"
2745
+ fi
2746
+ kill "$pid" 2>/dev/null || true
2747
+ wait "$pid" 2>/dev/null || true
2748
+ grep -F "stale: $window" "$out" >/dev/null && fail "idle secondmate pane triggered stale wake"
2749
+ pass "idle kind=secondmate pane is healthy and not stale"
2750
+ }
2751
+
2752
+ test_secondmate_charter_brief_is_idle_by_default() {
2753
+ local home brief
2754
+ home="$TMP_ROOT/idle-charter-home"
2755
+ mkdir -p "$home/data" "$home/state"
2756
+ scaffold_secondmate_charter "$home" idle-sm 'feature work for alpha' alpha
2757
+ brief="$home/data/idle-sm/brief.md"
2758
+ [ -f "$brief" ] || fail "secondmate charter brief was not scaffolded"
2759
+ # Idle contract: waits for routed work, never self-initiates.
2760
+ grep -F 'go idle and wait silently for the main firstmate' "$brief" >/dev/null \
2761
+ || fail "charter brief does not tell the secondmate to go idle and wait for routed work"
2762
+ grep -F 'Act only on tasks the main firstmate routes to you' "$brief" >/dev/null \
2763
+ || fail "charter brief does not restrict work to routed tasks"
2764
+ grep -F 'never spawn a survey, audit, or any self-directed' "$brief" >/dev/null \
2765
+ || fail "charter brief does not forbid self-initiated survey/audit work"
2766
+ # Reconcile-on-startup must remain: bootstrap and recovery still run, scoped to own work.
2767
+ grep -F 'run normal firstmate bootstrap and recovery' "$brief" >/dev/null \
2768
+ || fail "charter brief dropped the bootstrap/recovery reconciliation step"
2769
+ grep -F 'only to RECONCILE work that is already yours' "$brief" >/dev/null \
2770
+ || fail "charter brief does not scope startup work to reconciling existing work"
2771
+ # Regression guard: the over-broad phrasing that got misread as "go find work" is gone.
2772
+ if grep -F 'then supervise work that matches your scope' "$brief" >/dev/null; then
2773
+ fail "charter brief still uses the over-broad 'supervise work that matches your scope' phrasing"
2774
+ fi
2775
+ pass "secondmate charter brief is idle by default and does not self-initiate work"
2776
+ }
2777
+
2778
+ test_backlog_handoff_aborts_safely() {
2779
+ # The happy move (verbatim into the Queued section, out-of-scope left alone,
2780
+ # idempotent re-run) is asserted in the lifecycle e2e. Here: every refusal path
2781
+ # aborts atomically and mutates neither backlog.
2782
+ local home subhome subhome_abs before
2783
+ home="$TMP_ROOT/handoff-main"
2784
+ subhome="$TMP_ROOT/handoff-sub"
2785
+ mkdir -p "$home/data" "$home/state"
2786
+ seed_secondmate_home_marker "$subhome" design
2787
+ subhome_abs=$(cd "$subhome" && pwd -P)
2788
+ printf -- '- design - feature work (home: %s; scope: feature work; projects: alpha; added 2026-06-22)\n' "$subhome_abs" > "$home/data/secondmates.md"
2789
+ cat > "$home/data/backlog.md" <<'EOF'
2790
+ ## In flight
2791
+ - [ ] live-task - active work (repo: alpha, since 2026-06-20)
2792
+
2793
+ ## Queued
2794
+ - [ ] bug-z - fix bug z (repo: gamma)
2795
+
2796
+ ## Done
2797
+ - [x] old-task - shipped thing - local main (merged 2026-06-19)
2798
+ EOF
2799
+
2800
+ # A key matching neither backlog aborts atomically: nothing moves.
2801
+ before=$(cat "$home/data/backlog.md")
2802
+ if FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design bug-z no-such-key >/dev/null 2>&1; then
2803
+ fail "handoff succeeded despite an unmatched key"
2804
+ fi
2805
+ [ "$before" = "$(cat "$home/data/backlog.md")" ] || fail "handoff with an unmatched key still mutated the main backlog"
2806
+ grep -F 'bug-z' "$home/data/backlog.md" >/dev/null || fail "atomic abort lost the valid bug-z item"
2807
+
2808
+ # An in-flight item is refused (active ownership lives in tmux + state too).
2809
+ before=$(cat "$home/data/backlog.md")
2810
+ if FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design live-task >/dev/null 2>&1; then
2811
+ fail "handoff accepted an in-flight backlog item"
2812
+ fi
2813
+ [ "$before" = "$(cat "$home/data/backlog.md")" ] || fail "handoff with an in-flight key mutated the main backlog"
2814
+ grep -F 'live-task' "$home/data/backlog.md" >/dev/null || fail "in-flight refusal lost the live task"
2815
+ [ ! -e "$subhome/data/backlog.md" ] || ! grep -F 'live-task' "$subhome/data/backlog.md" >/dev/null || fail "in-flight refusal copied the live task into the secondmate backlog"
2816
+
2817
+ # An unregistered secondmate id is refused.
2818
+ if FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" ghost bug-z >/dev/null 2>&1; then
2819
+ fail "handoff accepted an unregistered secondmate id"
2820
+ fi
2821
+ pass "fm-backlog-handoff aborts atomically on unmatched, in-flight, and unregistered targets"
2822
+ }
2823
+
2824
+ test_backlog_handoff_refuses_done_items_and_non_secondmate_homes() {
2825
+ local home subhome subhome_abs projhome projhome_abs markerhome markerhome_abs symlinkhome symlinkhome_abs outside before_main before_sub out
2826
+ home="$TMP_ROOT/handoff-safety-main"
2827
+ subhome="$TMP_ROOT/handoff-safety-sub"
2828
+ projhome="$TMP_ROOT/handoff-safety-proj"
2829
+ markerhome="$TMP_ROOT/handoff-safety-marker"
2830
+ symlinkhome="$TMP_ROOT/handoff-safety-symlink"
2831
+ outside="$TMP_ROOT/handoff-safety-outside"
2832
+ mkdir -p "$home/data" "$home/state"
2833
+
2834
+ seed_secondmate_home_marker "$subhome" archive
2835
+ subhome_abs=$(cd "$subhome" && pwd -P)
2836
+ printf '## Queued\n- [ ] keep-me - stays (repo: alpha)\n' > "$subhome/data/backlog.md"
2837
+ printf -- '- archive - archival (home: %s; scope: archival; projects: alpha; added 2026-06-22)\n' "$subhome_abs" > "$home/data/secondmates.md"
2838
+ printf '##\tDone\n- [x] shipped-task - shipped thing - local main (merged 2026-06-19)\n' > "$home/data/backlog.md"
2839
+ before_main="$TMP_ROOT/handoff-safety-main.before"
2840
+ before_sub="$TMP_ROOT/handoff-safety-sub.before"
2841
+ cp "$home/data/backlog.md" "$before_main"
2842
+ cp "$subhome/data/backlog.md" "$before_sub"
2843
+ if out=$(FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" archive shipped-task 2>&1); then
2844
+ fail "handoff accepted a Done backlog item"
2845
+ fi
2846
+ printf '%s\n' "$out" | grep -F 'shipped-task' >/dev/null \
2847
+ || fail "Done-item refusal did not name the selected item"
2848
+ printf '%s\n' "$out" | grep -F 'queued work only' >/dev/null \
2849
+ || fail "Done-item refusal did not state the queued-only contract"
2850
+ cmp -s "$before_main" "$home/data/backlog.md" \
2851
+ || fail "Done-item refusal mutated the main backlog"
2852
+ cmp -s "$before_sub" "$subhome/data/backlog.md" \
2853
+ || fail "Done-item refusal mutated the secondmate backlog"
2854
+
2855
+ # A registered home that is not a seeded secondmate home (e.g. a project clone)
2856
+ # is refused, and nothing is written into it.
2857
+ fm_git_init_commit "$projhome"
2858
+ projhome_abs=$(cd "$projhome" && pwd -P)
2859
+ printf -- '- proj-sm - bogus (home: %s; scope: bogus; projects: alpha; added 2026-06-22)\n' "$projhome_abs" >> "$home/data/secondmates.md"
2860
+ if FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" proj-sm shipped-task >/dev/null 2>&1; then
2861
+ fail "handoff wrote into a destination that is not a seeded secondmate home"
2862
+ fi
2863
+ [ ! -e "$projhome/data/backlog.md" ] || fail "handoff created a backlog inside a non-secondmate home"
2864
+
2865
+ mkdir -p "$markerhome/data"
2866
+ markerhome_abs=$(cd "$markerhome" && pwd -P)
2867
+ printf 'marker-sm\n' > "$markerhome/.fm-secondmate-home"
2868
+ printf -- '- marker-sm - bogus (home: %s; scope: bogus; projects: alpha; added 2026-06-22)\n' "$markerhome_abs" >> "$home/data/secondmates.md"
2869
+ cat > "$home/data/backlog.md" <<'EOF'
2870
+ ## Queued
2871
+ - [ ] marker-task - should not move (repo: alpha)
2872
+ EOF
2873
+ if FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" marker-sm marker-task >/dev/null 2>&1; then
2874
+ fail "handoff accepted a marker-only directory as a secondmate home"
2875
+ fi
2876
+ [ ! -e "$markerhome/data/backlog.md" ] || fail "handoff wrote into a marker-only directory"
2877
+ grep -F 'marker-task' "$home/data/backlog.md" >/dev/null || fail "marker-only refusal lost the main backlog item"
2878
+
2879
+ seed_secondmate_home_marker "$symlinkhome" symlink-sm
2880
+ symlinkhome_abs=$(cd "$symlinkhome" && pwd -P)
2881
+ mkdir -p "$outside"
2882
+ rm -rf "$symlinkhome/data"
2883
+ ln -s "$outside" "$symlinkhome/data"
2884
+ printf -- '- symlink-sm - bogus (home: %s; scope: bogus; projects: alpha; added 2026-06-22)\n' "$symlinkhome_abs" >> "$home/data/secondmates.md"
2885
+ cat > "$home/data/backlog.md" <<'EOF'
2886
+ ## Queued
2887
+ - [ ] symlink-task - should not move (repo: alpha)
2888
+ EOF
2889
+ if FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" symlink-sm symlink-task >/dev/null 2>&1; then
2890
+ fail "handoff accepted a secondmate home with data outside the home"
2891
+ fi
2892
+ [ ! -e "$outside/backlog.md" ] || fail "handoff wrote through a symlinked secondmate data directory"
2893
+ grep -F 'symlink-task' "$home/data/backlog.md" >/dev/null || fail "symlink refusal lost the main backlog item"
2894
+ pass "fm-backlog-handoff refuses Done items under whitespace section headings and unsafe homes"
2895
+ }
2896
+
2897
+ test_fm_home_parameterization
2898
+ test_lock_status_is_per_home
2899
+ test_seed_allows_overlapping_clones_and_drops_owner
2900
+ test_home_seed_validate_rejects_unparseable_registry_entry
2901
+ test_home_seed_refuses_broken_registry_symlink
2902
+ test_home_seed_refuses_unreadable_registry
2903
+ test_home_seed_validate_rejects_duplicate_homes
2904
+ test_home_seed_validate_rejects_duplicate_ids
2905
+ test_home_seed_validate_rejects_nested_homes
2906
+ test_home_seed_uses_treehouse_acquired_home
2907
+ test_home_seed_returns_treehouse_acquired_home_on_assignment_failure
2908
+ test_home_seed_warns_when_acquired_home_return_fails
2909
+ test_home_seed_does_not_return_unsafe_acquired_home
2910
+ test_home_seed_rolls_back_failed_clone
2911
+ test_home_seed_refuses_missing_filled_charter
2912
+ test_home_seed_refuses_placeholder_charter
2913
+ test_home_seed_refuses_empty_charter_fields
2914
+ test_home_seed_no_projects_end_to_end
2915
+ test_secondmate_spawn_resolves_punctuated_registry_projects
2916
+ test_secondmate_spawn_refuses_ambiguous_and_mismatched_registry_bindings
2917
+ test_home_seed_refuses_projectful_reused_charter_for_projectless_home
2918
+ test_home_seed_refuses_projectless_conversion_of_populated_home
2919
+ test_home_seed_refuses_projectless_home_with_uninspectable_projects
2920
+ test_home_seed_refuses_projectless_home_with_symlinked_projects
2921
+ test_home_seed_refuses_projectless_home_with_non_directory_projects
2922
+ test_home_seed_refuses_projectless_home_with_uninspectable_registry
2923
+ test_home_seed_refuses_missing_projects_without_signal
2924
+ test_home_seed_refuses_local_only_project
2925
+ test_home_seed_refuses_registry_delimiter_home
2926
+ test_home_seed_refuses_active_home_and_root
2927
+ test_home_seed_refuses_home_marked_for_another_id
2928
+ test_home_seed_refuses_home_registered_to_another_id
2929
+ test_home_seed_refuses_reassigning_existing_id_to_different_home
2930
+ test_home_seed_refuses_home_overlapping_registered_home
2931
+ test_home_seed_refuses_remote_backed_project_without_origin
2932
+ test_home_seed_refuses_existing_remote_backed_project_with_wrong_origin
2933
+ test_home_seed_resolves_relative_source_origins
2934
+ test_home_seed_skips_initialized_existing_no_mistakes_projects
2935
+ test_home_seed_refuses_uninitialized_existing_no_mistakes_project
2936
+ test_home_seed_refuses_project_destinations_outside_subhome
2937
+ test_home_seed_refuses_operational_dirs_outside_subhome
2938
+ test_home_seed_refuses_unsafe_leaf_files
2939
+ test_home_seed_preserves_existing_parent_binding
2940
+ test_secondmate_spawn_requires_seeded_matching_home
2941
+ test_secondmate_spawn_refuses_operational_dirs_outside_subhome
2942
+ test_fm_send_refuses_bare_window_without_home_meta
2943
+ test_secondmate_teardown_retires_empty_home
2944
+ test_secondmate_teardown_refuses_ambiguous_and_mismatched_registry_bindings
2945
+ test_secondmate_teardown_sweeps_process_events_before_removal
2946
+ test_secondmate_teardown_refuses_process_events_without_sweep_script
2947
+ test_secondmate_teardown_preserves_process_events_on_later_refusal
2948
+ test_secondmate_force_teardown_sweeps_nested_homes
2949
+ test_secondmate_force_teardown_preserves_nested_restore_status
2950
+ test_secondmate_teardown_refuses_failed_leased_home_return
2951
+ test_secondmate_teardown_removes_plain_clone_home_without_treehouse_return
2952
+ test_secondmate_force_teardown_discards_child_work
2953
+ test_secondmate_force_teardown_preserves_child_on_unproven_lock
2954
+ test_secondmate_force_teardown_allows_non_state_operational_dir_symlinks_inside_home
2955
+ test_secondmate_force_teardown_refuses_operational_dir_symlink_outside_home
2956
+ test_secondmate_teardown_refuses_registered_nested_home
2957
+ test_secondmate_teardown_refuses_child_registry_nested_home
2958
+ test_secondmate_force_teardown_prevalidates_before_child_cleanup
2959
+ test_force_teardown_refuses_non_directory_descendant_state
2960
+ test_force_teardown_refuses_symlinked_descendant_state
2961
+ test_force_teardown_locks_descendant_with_absent_state
2962
+ test_force_teardown_refuses_while_a_task_is_being_published
2963
+ test_fresh_spawn_refuses_while_a_forced_teardown_owns_the_task_set
2964
+ test_fresh_remote_secondmate_spawn_refuses_while_task_set_is_owned
2965
+ test_secondmate_force_teardown_refuses_child_active_home_descendant
2966
+ test_secondmate_force_teardown_refuses_child_repo_descendant
2967
+ test_secondmate_force_teardown_refuses_unregistered_child_worktree
2968
+ test_secondmate_teardown_path_boundary_matrix
2969
+ test_secondmate_idle_pane_is_not_stale
2970
+ test_secondmate_charter_brief_is_idle_by_default
2971
+ test_backlog_handoff_aborts_safely
2972
+ test_backlog_handoff_refuses_done_items_and_non_secondmate_homes