@zerwiz/ymir 0.1.4 → 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 -59
  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,2922 @@
1
+ #!/usr/bin/env bash
2
+ # Tear down a finished task: return the treehouse worktree, release the Orca
3
+ # worktree, or retire a secondmate home; kill the recorded runtime endpoint,
4
+ # clear volatile state, and CLOSE this home's backlog item for ship and scout
5
+ # tasks before reporting success (a secondmate teardown closes none, since
6
+ # secondmates are not backlog items), then refresh/prune the project's clone for
7
+ # PR-based ship tasks.
8
+ # Removing state/<id>.meta and closing the backlog item are one step, not two:
9
+ # bin/fm-backlog-transition-lib.sh owns that invariant, and both halves run under
10
+ # the task's own meta lock before this script reports success. Because the
11
+ # completion links (the PR, the report path, a local-main note) live only in the
12
+ # record being removed, the intended close is recorded in
13
+ # state/<id>.backlog-close first, so a process killed between the halves leaves
14
+ # the next session start enough to finish it; a landed close removes that record.
15
+ # A close that fails is fatal and loud, preserves its pending-close record, and
16
+ # is retried by the next session start. The transition is skipped on a
17
+ # config/backlog-backend=manual home and in a home that keeps no
18
+ # data/backlog.md; those cases print the manual follow-up. An automatic-backend
19
+ # home with a backlog but no compatible tasks-axi refuses before cleanup.
20
+ # None of this loosens the landed-work gates below: the transition runs only on
21
+ # the paths that already proceed to remove the record.
22
+ # REFUSES if the worktree holds work that has not LANDED, because cleanup
23
+ # hard-resets/removes the worktree and kills its processes. Work has landed when it is
24
+ # reachable from any remote-tracking branch (a fork counts as a remote, so
25
+ # upstream-contribution PRs pushed to a fork satisfy this in any mode), OR - for a
26
+ # normal ship task whose commits are not so reachable - when its PR is merged and
27
+ # GitHub reports a PR head that contains the current local work, or its content is
28
+ # already present in the up-to-date default branch. This recognizes the common
29
+ # squash-merge-then-delete-branch flow, where the branch's own commits live nowhere
30
+ # on a remote yet the change is fully in main.
31
+ # The PR itself is resolved from the task's recorded pr= when present, or - when
32
+ # no pr= was ever recorded (e.g. a yolo-authorized merge on a repo with no PR CI,
33
+ # where the usual "checks green" fm-pr-check.sh trigger never fires) - by looking
34
+ # up a merged PR whose head branch matches the worktree's branch, fetching its head
35
+ # via refs/pull/<n>/head when the branch itself was deleted. So a missing pr= never
36
+ # by itself causes a false refusal of landed work.
37
+ # A gh lookup error falls back to the content check; if that is also inconclusive,
38
+ # teardown refuses rather than risk discarding unlanded work.
39
+ # Uncommitted changes are never landed.
40
+ # local-only projects additionally accept work merged into the local default
41
+ # branch (firstmate performs that merge after configured approval) as a fallback
42
+ # for the common case where there is no remote at all.
43
+ # Scout tasks (kind=scout in meta) carve out of that check: their worktree is
44
+ # declared scratch and the report at data/<task-id>/report.md is the work
45
+ # product. Teardown proceeds only once the report exists and the shared
46
+ # unresolved-decision completion gate verifies its captain-held inventory.
47
+ # Before destructive cleanup, teardown validates task check artifacts as
48
+ # ordinary single-link files on the state device. It refuses and preserves
49
+ # task state when that proof fails; otherwise it removes the task's check,
50
+ # trust record, PR sidecar, and publication record with the rest of the
51
+ # volatile state.
52
+ # Orca tasks use the same safety checks, then close the recorded terminal and
53
+ # remove the recorded worktree through `orca worktree rm`; teardown never guesses
54
+ # an Orca target from ambient CLI state.
55
+ # A Herdr presentation journal never authorizes cleanup. Teardown still closes
56
+ # only the exact task pane from ordinary endpoint metadata and never calls
57
+ # `workspace close`. It retires the non-authoritative journal only when a
58
+ # read-only token correlation agrees with that endpoint and pane closure is
59
+ # confirmed. Otherwise the journal stays quarantined for manual inspection.
60
+ # Projected closes share the presentation-order lock, refuse to close the
61
+ # captain's active tab, and restore the exact response-derived pre-close tab
62
+ # if Herdr's last-pane cleanup focuses an unrelated neighboring workspace.
63
+ # Secondmates (kind=secondmate in meta) are retired explicitly. Normal
64
+ # teardown refuses while their home has in-flight crewmate meta files; --force
65
+ # is the approved discard path that prevalidates child removal targets, locks each
66
+ # descendant home's task set before enumeration, and holds those locks through
67
+ # child cleanup. Contention refuses the complete forced teardown before child
68
+ # mutation. Local and remote retirement serialize their destructive phase with
69
+ # that mate's backlog-handoff lock under the registry lock. Pending handoff wake
70
+ # state is retired with the home, and local removal failure restores that state
71
+ # before preserving the route for retry. Teardown then discards child work, kills
72
+ # child runtime endpoints, and removes the retired home. Removing a leased home
73
+ # releases its durable treehouse lease so the pool slot is freed,
74
+ # never left leased forever. If the treehouse return fails, teardown leaves the
75
+ # leased home and state in place instead of hiding a still-held lease.
76
+ # Usage: fm-teardown.sh <task-id> [--force]
77
+ # --force skips ordinary-task dirty and landed-work checks, skips scout report
78
+ # checks, and discards secondmate child work for kind=secondmate. Only use it
79
+ # when the captain has explicitly said to discard the work.
80
+ #
81
+ # Transient / stale worktree git lock recovery (teardown-lock-race): a crew process
82
+ # killed mid-git-operation can leave a .git/worktrees/<wt>/index.lock (or, for a
83
+ # non-linked worktree, .git/index.lock) that makes `treehouse return --force` fail
84
+ # with Unable to create '...index.lock': File exists. That lock is usually transient
85
+ # (the dying process finishes or exits within seconds) and must never be force-deleted
86
+ # while a live git process might still own it - the fix is patience, not rm.
87
+ #
88
+ # On that failure signature only, teardown_treehouse_return:
89
+ # 1. Retries up to FM_TREEHOUSE_RETURN_LOCK_RETRIES times (default 3), waiting
90
+ # FM_TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS (default 1s; falls back to the older
91
+ # FM_STALE_WORKTREE_LOCK_RETRY_WAIT_SECS name when the new one is unset) between
92
+ # attempts. Retries key off the error text, not whether the lock file still
93
+ # exists after the failed attempt - a lock that self-clears mid-check still
94
+ # deserves a retry of the return.
95
+ # 2. Other treehouse return failures still abort immediately and loudly (no retry).
96
+ # 3. If every retry still hits the lock signature and the lock remains, it is removed
97
+ # and the return tried once more ONLY when the lock is provably stale per
98
+ # bin/fm-lock-lib.sh's fm_lock_is_provably_stale, passing the worktree dir as the
99
+ # companion directory and FM_STALE_WORKTREE_LOCK_AGE_SECS (default 30s) as the age
100
+ # threshold. That shared proof owns the exact lsof-holder, mtime-age, and fail-safe
101
+ # rules.
102
+ # 4. If retries exhaust and the lock is not provably stale, teardown fails as loudly
103
+ # as a normal return failure and notes that the lock persisted across the retry
104
+ # window. A missing `lsof`, or a lock that fails any stale check, is treated as
105
+ # NOT provably stale (fail safe): the lock is left untouched.
106
+ # The same proof is used when non-force safety inspection cannot run because the lock
107
+ # is present; teardown clears only a provably stale lock, then re-runs the safety
108
+ # checks before any destructive return. Teardown output notes every wait, retry, and
109
+ # removal so the operator can see what happened.
110
+ #
111
+ # Pre-teardown cleanup sequence (runs once every landed/discard-work safety
112
+ # refusal above has already passed, and BEFORE any worktree return, branch
113
+ # delete, or backend kill below - a still-active run or a leaked process may
114
+ # own live work in that worktree):
115
+ # Fix 1 - conclude the task's own no-mistakes run. A ship task's worktree can
116
+ # be torn down while its no-mistakes pipeline run is still PARKED at a gate
117
+ # (awaiting_approval/fix_review/any awaiting_agent field), with no worker
118
+ # left to ever answer it - the run then sits there holding a fleet slot
119
+ # indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a
120
+ # post-CI approval gate after the worker was already cleaned up). A run
121
+ # with an autonomous step still under way (running/fixing/ci) is left
122
+ # alone: no-mistakes drives those against its own gate-repo clone, not the
123
+ # crew's worktree, so they are not orphaned by removing the worktree.
124
+ # conclude_task_no_mistakes_run attributes the active-or-most-recent run to
125
+ # THIS task only when its branch AND code identity (bin/fm-nm-run-lib.sh's
126
+ # strict fm_nm_head_matches_worktree rule) both match this worktree, then
127
+ # runs `no-mistakes axi abort --run <id>` for
128
+ # that verified run instance. A run already terminal
129
+ # (an outcome is set) or not parked at a gate is left untouched. Idempotent:
130
+ # an already-aborted run reads back terminal and is skipped on retry.
131
+ # Fix 2 - reap leaked descendant processes. A backgrounded/disowned process
132
+ # started under the worktree (or its per-task tasktmp) does not receive the
133
+ # SIGHUP/SIGTERM that closing the backend pane sends to its own foreground
134
+ # process group, so it survives reparented to init (observed 2026-08-03:
135
+ # two `go test` binaries, deadlines blown past by ~100x, pinning CPU for
136
+ # hours with no live task meta to attribute them to once teardown had
137
+ # already removed it). reap_task_worktree_processes finds every process
138
+ # whose CURRENT WORKING DIRECTORY is this task's own worktree or tasktmp
139
+ # root via `lsof -a -d cwd` (cheap: bounded by process count, not by
140
+ # walking the worktree's file tree) and sends TERM, then KILL after a short
141
+ # grace period to any survivor whose process identity still matches. Both
142
+ # roots are unique per task and never
143
+ # shared, so this can never reach another task's or the primary's
144
+ # processes. Idempotent: nothing left to find is a silent no-op.
145
+ # Fix 3 - sweep abandoned remote job workers. A remote job worker started
146
+ # from a worktree's own bin/ outlives that worktree's removal without
147
+ # being reachable by Fix 2, because its working directory is wherever it
148
+ # was launched rather than the task worktree (observed 2026-08-07: 29
149
+ # workers at ppid 1, 1-2 days old, each still polling and appending to a
150
+ # log in a pruned no-mistakes gate worktree). bin/fm-remote-job-reap-orphans.sh
151
+ # owns that sweep and its safety rule; it never touches a worker whose code
152
+ # root still exists, so the account's healthy LaunchAgent worker and every
153
+ # live remote secondmate worker are out of scope. Best effort: a sweep
154
+ # failure never blocks this teardown.
155
+ set -eu
156
+
157
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
158
+ FM_ROOT="${FM_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}"
159
+ FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$FM_ROOT}}"
160
+ STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}"
161
+ DATA="${FM_DATA_OVERRIDE:-$FM_HOME/data}"
162
+ CONFIG="${FM_CONFIG_OVERRIDE:-$FM_HOME/config}"
163
+ SECONDMATE_REG="$DATA/secondmates.md"
164
+ SUB_HOME_MARKER=".fm-secondmate-home"
165
+ SUB_HOME_PARENT_MARKER=".fm-secondmate-parent"
166
+ # shellcheck source=bin/fm-tasks-axi-lib.sh
167
+ . "$SCRIPT_DIR/fm-tasks-axi-lib.sh"
168
+ # shellcheck source=bin/fm-backlog-transition-lib.sh
169
+ . "$SCRIPT_DIR/fm-backlog-transition-lib.sh"
170
+ # shellcheck source=bin/fm-backend.sh
171
+ . "$SCRIPT_DIR/fm-backend.sh"
172
+ # shellcheck source=bin/fm-control-lib.sh
173
+ . "$SCRIPT_DIR/fm-control-lib.sh"
174
+ # shellcheck source=bin/fm-lock-lib.sh
175
+ . "$SCRIPT_DIR/fm-lock-lib.sh"
176
+ # shellcheck source=bin/fm-classify-lib.sh
177
+ . "$SCRIPT_DIR/fm-classify-lib.sh"
178
+ # shellcheck source=bin/fm-gate-refuse-lib.sh
179
+ . "$SCRIPT_DIR/fm-gate-refuse-lib.sh"
180
+ # shellcheck source=bin/fm-pr-lib.sh
181
+ . "$SCRIPT_DIR/fm-pr-lib.sh"
182
+ # shellcheck source=bin/fm-public-followup-lib.sh
183
+ . "$SCRIPT_DIR/fm-public-followup-lib.sh"
184
+ # shellcheck source=bin/fm-secondmate-registry-lib.sh
185
+ . "$SCRIPT_DIR/fm-secondmate-registry-lib.sh"
186
+ # shellcheck source=bin/fm-secondmate-parent-lib.sh
187
+ . "$SCRIPT_DIR/fm-secondmate-parent-lib.sh"
188
+ # shellcheck source=bin/fm-pending-reply-lib.sh
189
+ . "$SCRIPT_DIR/fm-pending-reply-lib.sh"
190
+ # shellcheck source=bin/fm-nm-run-lib.sh
191
+ . "$SCRIPT_DIR/fm-nm-run-lib.sh"
192
+ if [ "$#" -lt 1 ] || ! fm_task_id_path_safe "$1"; then
193
+ echo "error: invalid teardown request" >&2
194
+ exit 2
195
+ fi
196
+ ID=$1
197
+ FORCE=${2:-}
198
+ fm_backlog_directory_present "$STATE" "state directory" || {
199
+ echo "error: teardown refused: $FM_BACKLOG_TRANSITION_ERROR" >&2
200
+ exit 1
201
+ }
202
+ # shellcheck source=bin/fm-wake-lib.sh
203
+ . "$SCRIPT_DIR/fm-wake-lib.sh"
204
+ # Supervision lease guard: post-landing cleanup is overlap territory between
205
+ # the two Pi supervision actors; refuse while the OTHER actor holds this
206
+ # task's live lease (contract: bin/fm-lease-lib.sh; no-op in homes without
207
+ # leases).
208
+ # shellcheck source=bin/fm-lease-lib.sh
209
+ . "$SCRIPT_DIR/fm-lease-lib.sh"
210
+ # Role partition: forced teardown discards work, and the supervision branch
211
+ # never discards anything - only an ordinary landed-work teardown is branch
212
+ # territory (contract: bin/fm-lease-lib.sh).
213
+ if [ "$FORCE" = --force ] && [ "$(fm_lease_actor)" = branch ]; then
214
+ echo "error: forced teardown refused - the supervision branch cannot discard work" >&2
215
+ exit "$FM_LEASE_REFUSE_EXIT"
216
+ fi
217
+ fm_lease_guard "$ID" "teardown (fm-teardown)"
218
+ CONTROL_LOCK="$STATE/.control-$ID.lock"
219
+ CONTROL_LOCK_HELD=0
220
+ META_LOCK=
221
+ META_LOCK_HELD=0
222
+ DESCENDANT_LOCK_PATHS=()
223
+ DESCENDANT_TASK_STATES=()
224
+ DESCENDANT_TASK_IDS=()
225
+ DESCENDANT_TASK_KINDS=()
226
+ DESCENDANT_TASK_HOMES=()
227
+ teardown_release_locks() {
228
+ local status=$? i
229
+ if declare -F teardown_release_herdr_locks >/dev/null 2>&1; then
230
+ teardown_release_herdr_locks || true
231
+ fi
232
+ for ((i=${#DESCENDANT_LOCK_PATHS[@]} - 1; i >= 0; i--)); do
233
+ fm_lock_release "${DESCENDANT_LOCK_PATHS[$i]}" || true
234
+ done
235
+ DESCENDANT_LOCK_PATHS=()
236
+ if [ -n "${HANDOFF_WAKE_RETIRE_LOCK:-}" ]; then
237
+ fm_lock_release "$HANDOFF_WAKE_RETIRE_LOCK" || true
238
+ HANDOFF_WAKE_RETIRE_LOCK=
239
+ fi
240
+ if [ -n "${LOCAL_HANDOFF_LOCK:-}" ]; then
241
+ fm_lock_release "$LOCAL_HANDOFF_LOCK" || true
242
+ LOCAL_HANDOFF_LOCK=
243
+ fi
244
+ if [ -n "${LOCAL_REGISTRY_LOCK:-}" ]; then
245
+ fm_lock_release "$LOCAL_REGISTRY_LOCK" || true
246
+ LOCAL_REGISTRY_LOCK=
247
+ fi
248
+ if [ "$META_LOCK_HELD" = 1 ]; then
249
+ fm_lock_release "$META_LOCK" || true
250
+ META_LOCK_HELD=0
251
+ fi
252
+ if [ "$CONTROL_LOCK_HELD" = 1 ]; then
253
+ fm_lock_release "$CONTROL_LOCK" || true
254
+ CONTROL_LOCK_HELD=0
255
+ fi
256
+ fm_lease_guard_release || true
257
+ return "$status"
258
+ }
259
+ trap teardown_release_locks EXIT
260
+ fm_lock_try_acquire "$CONTROL_LOCK" || {
261
+ echo "error: another lifecycle action is already running for task $ID; nothing was changed" >&2
262
+ exit 1
263
+ }
264
+ CONTROL_LOCK_HELD=1
265
+ # Fail closed before any fleet mutation: a no-mistakes gate agent must never tear
266
+ # down a worktree (see bin/fm-gate-refuse-lib.sh).
267
+ fm_refuse_if_gate_agent
268
+ FM_LOCK_LOG_PREFIX=teardown
269
+
270
+ META="$STATE/$ID.meta"
271
+ fm_backlog_record_present "$META" "task record" "$STATE" || {
272
+ echo "error: teardown refused: $FM_BACKLOG_TRANSITION_ERROR" >&2
273
+ exit 1
274
+ }
275
+ META_LOCK=$(fm_meta_lock_path "$META") || exit 1
276
+ fm_lock_acquire_wait "$META_LOCK"
277
+ META_LOCK_HELD=1
278
+ fm_backlog_record_present "$META" "task record" "$STATE" || {
279
+ echo "error: teardown refused after locking: $FM_BACKLOG_TRANSITION_ERROR" >&2
280
+ exit 1
281
+ }
282
+ TEARDOWN_META_KIND=$(fm_meta_get "$META" kind)
283
+ [ -n "$TEARDOWN_META_KIND" ] || TEARDOWN_META_KIND=ship
284
+ TEARDOWN_CLEANUP_RECOVERY=$(fm_meta_get "$META" cleanup_recovery)
285
+ TEARDOWN_META_SPAWN_GEN=
286
+ TEARDOWN_BACKLOG_APPLIES=0
287
+ TEARDOWN_BACKLOG_SKIP_REASON=
288
+ if [ "$TEARDOWN_CLEANUP_RECOVERY" != orca ]; then
289
+ if fm_backlog_transition_applies "$CONFIG" "$DATA" "$TEARDOWN_META_KIND"; then
290
+ TEARDOWN_BACKLOG_APPLIES=1
291
+ else
292
+ TEARDOWN_BACKLOG_GATE_STATUS=$?
293
+ if [ "$TEARDOWN_BACKLOG_GATE_STATUS" -eq 2 ]; then
294
+ echo "error: task $ID cannot be torn down because its backlog data directory is inaccessible: $DATA ($FM_BACKLOG_TRANSITION_ERROR)" >&2
295
+ exit 1
296
+ fi
297
+ TEARDOWN_BACKLOG_SKIP_REASON=$FM_BACKLOG_TRANSITION_SKIP
298
+ fi
299
+ fi
300
+ if [ "$TEARDOWN_BACKLOG_APPLIES" = 1 ]; then
301
+ if ! fm_backlog_meta_spawn_gen "$META" "$STATE"; then
302
+ echo "error: task $ID's record has no spawn_gen that identifies one exact incarnation ($FM_BACKLOG_TRANSITION_ERROR); refusing automatic teardown - relaunch the task to publish an unambiguous incarnation, then retry teardown" >&2
303
+ exit 1
304
+ fi
305
+ TEARDOWN_META_SPAWN_GEN=$FM_BACKLOG_META_SPAWN_GEN
306
+ fi
307
+
308
+ REMOTE_HANDOFF_DIR_PRESENT=0
309
+ REMOTE_HANDOFF_DIR_REAL=
310
+ REMOTE_OUTBOX_PRESENT=0
311
+ REMOTE_PENDING_DIR_PRESENT=0
312
+ REMOTE_PENDING_DIR_REAL=
313
+ REMOTE_HANDOFF_LOCK=
314
+ REMOTE_REGISTRY_LOCK=
315
+ REMOTE_REPLY_LIFECYCLE_LOCK=
316
+ LOCAL_HANDOFF_LOCK=
317
+ LOCAL_REGISTRY_LOCK=
318
+ HANDOFF_WAKE_RETIRE_MARKER=
319
+ HANDOFF_WAKE_RETIRE_VALUE=
320
+ HANDOFF_WAKE_RETIRE_CORR=
321
+ HANDOFF_WAKE_RETIRE_LOCK=
322
+ HANDOFF_WAKE_RETIRE_STAGE=
323
+
324
+ handoff_wake_retire_validate() {
325
+ local marker="$STATE/.backlog-handoff-$ID.wake-pending" value corr rec confirmation
326
+ HANDOFF_WAKE_RETIRE_MARKER=
327
+ HANDOFF_WAKE_RETIRE_VALUE=
328
+ HANDOFF_WAKE_RETIRE_CORR=
329
+ [ -e "$marker" ] || [ -L "$marker" ] || return 0
330
+ [ -f "$marker" ] && [ ! -L "$marker" ] || {
331
+ echo "REFUSED: receiver wake state for secondmate $ID is unsafe" >&2
332
+ return 1
333
+ }
334
+ value=$(cat "$marker" 2>/dev/null || true)
335
+ case "$value" in
336
+ pending|confirmed) ;;
337
+ prepared:*)
338
+ corr=${value#prepared:}
339
+ corr=${corr%%:*}
340
+ printf '%s' "$value" | grep -Eq '^prepared:[a-f0-9]{16}:[a-f0-9]{16}$' || {
341
+ echo "REFUSED: receiver wake state for secondmate $ID is invalid" >&2
342
+ return 1
343
+ }
344
+ ;;
345
+ pending:*|confirmed:*)
346
+ corr=${value#*:}
347
+ printf '%s' "$corr" | grep -Eq '^[a-f0-9]{16}$' || {
348
+ echo "REFUSED: receiver wake state for secondmate $ID is invalid" >&2
349
+ return 1
350
+ }
351
+ ;;
352
+ *)
353
+ echo "REFUSED: receiver wake state for secondmate $ID is invalid" >&2
354
+ return 1
355
+ ;;
356
+ esac
357
+ if [ -n "$corr" ]; then
358
+ rec=$(fm_pending_reply_path "$STATE" "$corr")
359
+ if [ -e "$rec" ] || [ -L "$rec" ]; then
360
+ [ -f "$rec" ] && [ ! -L "$rec" ] \
361
+ && [ "$(fm_pending_reply_get "$rec" task_id)" = "$ID" ] || {
362
+ echo "REFUSED: receiver wake correlation for secondmate $ID is unsafe or belongs to another task" >&2
363
+ return 1
364
+ }
365
+ fi
366
+ confirmation=$(fm_pending_reply_delivery_confirmation_path "$STATE" "$corr")
367
+ if [ -e "$confirmation" ] || [ -L "$confirmation" ]; then
368
+ [ -f "$confirmation" ] && [ ! -L "$confirmation" ] || {
369
+ echo "REFUSED: receiver wake delivery state for secondmate $ID is unsafe" >&2
370
+ return 1
371
+ }
372
+ fi
373
+ HANDOFF_WAKE_RETIRE_CORR=$corr
374
+ fi
375
+ HANDOFF_WAKE_RETIRE_MARKER=$marker
376
+ HANDOFF_WAKE_RETIRE_VALUE=$value
377
+ }
378
+
379
+ handoff_wake_retire() {
380
+ local marker=$HANDOFF_WAKE_RETIRE_MARKER corr=$HANDOFF_WAKE_RETIRE_CORR lock rec confirmation rc=0
381
+ [ -n "$marker" ] || return 0
382
+ [ -f "$marker" ] && [ ! -L "$marker" ] \
383
+ && [ "$(cat "$marker" 2>/dev/null || true)" = "$HANDOFF_WAKE_RETIRE_VALUE" ] || return 1
384
+ if [ -n "$corr" ]; then
385
+ lock="$STATE/.pending-reply-$corr.lock"
386
+ fm_lock_acquire_wait "$lock" || return 1
387
+ rec=$(fm_pending_reply_path "$STATE" "$corr")
388
+ confirmation=$(fm_pending_reply_delivery_confirmation_path "$STATE" "$corr")
389
+ if { [ ! -e "$rec" ] && [ ! -L "$rec" ]; } \
390
+ || { [ -f "$rec" ] && [ ! -L "$rec" ] \
391
+ && [ "$(fm_pending_reply_get "$rec" task_id)" = "$ID" ]; }; then
392
+ rm -f -- "$confirmation" "$rec" "$marker" || rc=$?
393
+ else
394
+ rc=1
395
+ fi
396
+ fm_lock_release "$lock"
397
+ return "$rc"
398
+ fi
399
+ rm -f -- "$marker"
400
+ }
401
+
402
+ handoff_wake_retire_stage_restore() {
403
+ local stage=$HANDOFF_WAKE_RETIRE_STAGE marker rec confirmation name destination
404
+ [ -n "$stage" ] || return 0
405
+ marker="$STATE/.backlog-handoff-$ID.wake-pending"
406
+ rec=
407
+ confirmation=
408
+ if [ -n "$HANDOFF_WAKE_RETIRE_CORR" ]; then
409
+ rec=$(fm_pending_reply_path "$STATE" "$HANDOFF_WAKE_RETIRE_CORR")
410
+ confirmation=$(fm_pending_reply_delivery_confirmation_path "$STATE" "$HANDOFF_WAKE_RETIRE_CORR")
411
+ fi
412
+ for name in record confirmation marker; do
413
+ [ -e "$stage/$name" ] || continue
414
+ case "$name" in
415
+ record) destination=$rec ;;
416
+ confirmation) destination=$confirmation ;;
417
+ marker) destination=$marker ;;
418
+ esac
419
+ [ -n "$destination" ] && [ ! -e "$destination" ] && [ ! -L "$destination" ] \
420
+ && mv -- "$stage/$name" "$destination" || return 1
421
+ done
422
+ rm -f -- "$stage/corr" || return 1
423
+ rmdir -- "$stage" || return 1
424
+ if [ -n "$HANDOFF_WAKE_RETIRE_LOCK" ]; then
425
+ fm_lock_release "$HANDOFF_WAKE_RETIRE_LOCK" || return 1
426
+ HANDOFF_WAKE_RETIRE_LOCK=
427
+ fi
428
+ HANDOFF_WAKE_RETIRE_STAGE=
429
+ }
430
+
431
+ handoff_wake_retire_stage_commit() {
432
+ local stage=$HANDOFF_WAKE_RETIRE_STAGE retired
433
+ [ -n "$stage" ] || return 0
434
+ retired="$stage.retired.$$"
435
+ [ ! -e "$retired" ] && [ ! -L "$retired" ] || return 1
436
+ mv -- "$stage" "$retired" || return 1
437
+ HANDOFF_WAKE_RETIRE_STAGE=
438
+ if [ -n "$HANDOFF_WAKE_RETIRE_LOCK" ]; then
439
+ fm_lock_release "$HANDOFF_WAKE_RETIRE_LOCK" || return 1
440
+ HANDOFF_WAKE_RETIRE_LOCK=
441
+ fi
442
+ rm -rf -- "$retired" || echo "warning: retired receiver wake state remains at $retired" >&2
443
+ }
444
+
445
+ handoff_wake_retire_stage_recover() {
446
+ local home=$1 stage="$STATE/.backlog-handoff-$ID.wake-retiring" corr
447
+ [ -e "$stage" ] || [ -L "$stage" ] || return 0
448
+ [ -d "$stage" ] && [ ! -L "$stage" ] || {
449
+ echo "REFUSED: receiver wake retirement state for secondmate $ID is unsafe" >&2
450
+ return 1
451
+ }
452
+ if [ ! -e "$stage/corr" ] && [ ! -L "$stage/corr" ]; then
453
+ rmdir -- "$stage" 2>/dev/null && return 0
454
+ echo "REFUSED: receiver wake retirement state for secondmate $ID is incomplete" >&2
455
+ return 1
456
+ fi
457
+ [ -f "$stage/corr" ] && [ ! -L "$stage/corr" ] || {
458
+ echo "REFUSED: receiver wake retirement state for secondmate $ID is unsafe" >&2
459
+ return 1
460
+ }
461
+ corr=$(cat "$stage/corr" 2>/dev/null || true)
462
+ [ -z "$corr" ] || printf '%s' "$corr" | grep -Eq '^[a-f0-9]{16}$' || {
463
+ echo "REFUSED: receiver wake retirement correlation for secondmate $ID is invalid" >&2
464
+ return 1
465
+ }
466
+ local staged
467
+ for staged in "$stage/marker" "$stage/record" "$stage/confirmation"; do
468
+ [ ! -e "$staged" ] && [ ! -L "$staged" ] && continue
469
+ [ -f "$staged" ] && [ ! -L "$staged" ] || {
470
+ echo "REFUSED: receiver wake retirement state for secondmate $ID is unsafe" >&2
471
+ return 1
472
+ }
473
+ done
474
+ HANDOFF_WAKE_RETIRE_CORR=$corr
475
+ HANDOFF_WAKE_RETIRE_STAGE=$stage
476
+ if [ -n "$corr" ]; then
477
+ HANDOFF_WAKE_RETIRE_LOCK="$STATE/.pending-reply-$corr.lock"
478
+ fm_lock_acquire_wait "$HANDOFF_WAKE_RETIRE_LOCK" || return 1
479
+ fi
480
+ if [ -e "$home" ] || [ -L "$home" ]; then
481
+ handoff_wake_retire_stage_restore
482
+ else
483
+ handoff_wake_retire_stage_commit
484
+ fi
485
+ }
486
+
487
+ handoff_wake_retire_stage() {
488
+ local stage="$STATE/.backlog-handoff-$ID.wake-retiring" marker=$HANDOFF_WAKE_RETIRE_MARKER
489
+ local corr=$HANDOFF_WAKE_RETIRE_CORR rec confirmation
490
+ [ -n "$marker" ] || return 0
491
+ [ ! -e "$stage" ] && [ ! -L "$stage" ] || return 1
492
+ (umask 077; mkdir -- "$stage") || return 1
493
+ HANDOFF_WAKE_RETIRE_STAGE=$stage
494
+ printf '%s\n' "$corr" > "$stage/corr" || { handoff_wake_retire_stage_restore || true; return 1; }
495
+ if [ -n "$corr" ]; then
496
+ HANDOFF_WAKE_RETIRE_LOCK="$STATE/.pending-reply-$corr.lock"
497
+ fm_lock_acquire_wait "$HANDOFF_WAKE_RETIRE_LOCK" || {
498
+ HANDOFF_WAKE_RETIRE_LOCK=
499
+ handoff_wake_retire_stage_restore || true
500
+ return 1
501
+ }
502
+ rec=$(fm_pending_reply_path "$STATE" "$corr")
503
+ confirmation=$(fm_pending_reply_delivery_confirmation_path "$STATE" "$corr")
504
+ if [ -e "$rec" ] && ! mv -- "$rec" "$stage/record"; then
505
+ handoff_wake_retire_stage_restore || true
506
+ return 1
507
+ fi
508
+ if [ -e "$confirmation" ] && ! mv -- "$confirmation" "$stage/confirmation"; then
509
+ handoff_wake_retire_stage_restore || true
510
+ return 1
511
+ fi
512
+ fi
513
+ if ! mv -- "$marker" "$stage/marker"; then
514
+ handoff_wake_retire_stage_restore || true
515
+ return 1
516
+ fi
517
+ }
518
+
519
+ remote_teardown_locks_release() {
520
+ if [ -n "$REMOTE_REPLY_LIFECYCLE_LOCK" ]; then
521
+ fm_lock_release "$REMOTE_REPLY_LIFECYCLE_LOCK"
522
+ REMOTE_REPLY_LIFECYCLE_LOCK=
523
+ fi
524
+ if [ -n "$REMOTE_HANDOFF_LOCK" ]; then
525
+ fm_lock_release "$REMOTE_HANDOFF_LOCK"
526
+ REMOTE_HANDOFF_LOCK=
527
+ fi
528
+ if [ -n "$REMOTE_REGISTRY_LOCK" ]; then
529
+ fm_lock_release "$REMOTE_REGISTRY_LOCK"
530
+ REMOTE_REGISTRY_LOCK=
531
+ fi
532
+ }
533
+
534
+ remote_recovery_paths_validate() {
535
+ local mode=${1:-initial} handoff_dir outbox pending_dir real rec
536
+ handoff_dir="$DATA/handoff"
537
+ outbox="$handoff_dir/$ID.outbox.md"
538
+ pending_dir="$STATE/pending-replies"
539
+ if [ -e "$handoff_dir" ] || [ -L "$handoff_dir" ]; then
540
+ [ -d "$handoff_dir" ] && [ ! -L "$handoff_dir" ] \
541
+ || { echo "REFUSED: remote handoff recovery directory is unsafe" >&2; return 1; }
542
+ real=$(CDPATH='' cd -- "$handoff_dir" 2>/dev/null && pwd -P) || return 1
543
+ if [ "$mode" = initial ]; then
544
+ REMOTE_HANDOFF_DIR_PRESENT=1
545
+ REMOTE_HANDOFF_DIR_REAL=$real
546
+ elif [ "$REMOTE_HANDOFF_DIR_PRESENT" -ne 1 ] || [ "$REMOTE_HANDOFF_DIR_REAL" != "$real" ]; then
547
+ echo "REFUSED: remote handoff recovery directory changed during retirement" >&2
548
+ return 1
549
+ fi
550
+ elif [ "$mode" != initial ] && [ "$REMOTE_HANDOFF_DIR_PRESENT" -ne 0 ]; then
551
+ echo "REFUSED: remote handoff recovery directory changed during retirement" >&2
552
+ return 1
553
+ fi
554
+ if [ -e "$outbox" ] || [ -L "$outbox" ]; then
555
+ [ -f "$outbox" ] && [ ! -L "$outbox" ] \
556
+ || { echo "REFUSED: remote backlog outbox is unsafe" >&2; return 1; }
557
+ if [ "$mode" = initial ]; then
558
+ REMOTE_OUTBOX_PRESENT=1
559
+ elif [ "$REMOTE_OUTBOX_PRESENT" -ne 1 ]; then
560
+ echo "REFUSED: remote backlog outbox changed during retirement" >&2
561
+ return 1
562
+ fi
563
+ elif [ "$mode" != initial ] && [ "$REMOTE_OUTBOX_PRESENT" -ne 0 ]; then
564
+ echo "REFUSED: remote backlog outbox changed during retirement" >&2
565
+ return 1
566
+ fi
567
+ if [ -e "$pending_dir" ] || [ -L "$pending_dir" ]; then
568
+ [ -d "$pending_dir" ] && [ ! -L "$pending_dir" ] \
569
+ || { echo "REFUSED: pending-replies recovery directory is unsafe" >&2; return 1; }
570
+ real=$(CDPATH='' cd -- "$pending_dir" 2>/dev/null && pwd -P) || return 1
571
+ if [ "$mode" = initial ]; then
572
+ REMOTE_PENDING_DIR_PRESENT=1
573
+ REMOTE_PENDING_DIR_REAL=$real
574
+ elif [ "$REMOTE_PENDING_DIR_PRESENT" -ne 1 ] || [ "$REMOTE_PENDING_DIR_REAL" != "$real" ]; then
575
+ echo "REFUSED: pending-replies recovery directory changed during retirement" >&2
576
+ return 1
577
+ fi
578
+ for rec in "$pending_dir"/*; do
579
+ [ -e "$rec" ] || [ -L "$rec" ] || continue
580
+ [ -f "$rec" ] && [ ! -L "$rec" ] \
581
+ || { echo "REFUSED: pending-replies contains an unsafe recovery entry" >&2; return 1; }
582
+ done
583
+ elif [ "$mode" != initial ] && [ "$REMOTE_PENDING_DIR_PRESENT" -ne 0 ]; then
584
+ echo "REFUSED: pending-replies recovery directory changed during retirement" >&2
585
+ return 1
586
+ fi
587
+ }
588
+
589
+ remote_pending_replies_cleanup() {
590
+ local rec
591
+ [ "$REMOTE_PENDING_DIR_PRESENT" -eq 1 ] || return 0
592
+ (
593
+ CDPATH='' cd -- "$STATE/pending-replies" 2>/dev/null || exit 1
594
+ [ "$(pwd -P)" = "$REMOTE_PENDING_DIR_REAL" ] || exit 1
595
+ for rec in ./*; do
596
+ [ -e "$rec" ] || [ -L "$rec" ] || continue
597
+ [ -f "$rec" ] && [ ! -L "$rec" ] || exit 1
598
+ [ "$(fm_meta_get "$rec" task_id)" = "$ID" ] && rm -f -- "$rec"
599
+ done
600
+ )
601
+ }
602
+
603
+ remote_outbox_cleanup() {
604
+ [ "$REMOTE_OUTBOX_PRESENT" -eq 1 ] || return 0
605
+ (
606
+ CDPATH='' cd -- "$DATA/handoff" 2>/dev/null || exit 1
607
+ [ "$(pwd -P)" = "$REMOTE_HANDOFF_DIR_REAL" ] || exit 1
608
+ [ -f "$ID.outbox.md" ] && [ ! -L "$ID.outbox.md" ] || exit 1
609
+ rm -f -- "$ID.outbox.md"
610
+ )
611
+ }
612
+
613
+ remote_secondmate_teardown() {
614
+ local remote_host remote_root remote_home kind route_host route_root route_home out rc tmp rec phase task_id
615
+ remote_host=$(fm_meta_get "$META" remote_host)
616
+ [ -n "$remote_host" ] || return 3
617
+ kind=$(fm_meta_get "$META" kind)
618
+ [ "$kind" = secondmate ] || { echo "REFUSED: remote placement metadata is valid only for a secondmate" >&2; return 1; }
619
+ remote_root=$(fm_meta_get "$META" remote_root)
620
+ remote_home=$(fm_meta_get "$META" home)
621
+ [ -n "$remote_root" ] && [ -n "$remote_home" ] || { echo "REFUSED: remote secondmate metadata is incomplete" >&2; return 1; }
622
+ secondmate_registry_line_for_id "$SECONDMATE_REG" "$ID" || { echo "REFUSED: remote secondmate route is missing or ambiguous" >&2; return 1; }
623
+ [ "$SECONDMATE_REGISTRY_REMOTE" -eq 1 ] || { echo "REFUSED: secondmate registry route is not remote" >&2; return 1; }
624
+ route_host=$SECONDMATE_REGISTRY_HOST
625
+ route_root=$SECONDMATE_REGISTRY_ROOT
626
+ route_home=$SECONDMATE_REGISTRY_HOME
627
+ [ "$route_host" = "$remote_host" ] && [ "$route_root" = "$remote_root" ] && [ "$route_home" = "$remote_home" ] \
628
+ || { echo "REFUSED: remote secondmate metadata does not match its registry route" >&2; return 1; }
629
+ [ -z "$FORCE" ] || [ "$FORCE" = --force ] || { echo "error: invalid teardown option: $FORCE" >&2; return 2; }
630
+ handoff_wake_retire_validate || return 1
631
+ remote_recovery_paths_validate initial || return 1
632
+ if [ "$FORCE" != --force ] && [ "$REMOTE_OUTBOX_PRESENT" -eq 1 ]; then
633
+ echo "REFUSED: remote secondmate $ID still has a pending backlog outbox; deliver it or explicitly discard with --force" >&2
634
+ return 1
635
+ fi
636
+ if [ "$FORCE" != --force ] && [ -d "$STATE/pending-replies" ]; then
637
+ for rec in "$STATE/pending-replies"/*; do
638
+ [ -f "$rec" ] || continue
639
+ task_id=$(fm_meta_get "$rec" task_id)
640
+ [ "$task_id" = "$ID" ] || continue
641
+ phase=$(fm_meta_get "$rec" phase)
642
+ [ "$phase" = resolved ] || {
643
+ echo "REFUSED: remote secondmate $ID still has an unresolved routed reply" >&2
644
+ return 1
645
+ }
646
+ done
647
+ fi
648
+ "$SCRIPT_DIR/fm-procevent-remote-reply.sh" retire-quiesce-locked "$ID" "$FORCE" >/dev/null 2>&1 || {
649
+ echo "REFUSED: remote secondmate $ID still has an unhandled captured reply" >&2
650
+ return 1
651
+ }
652
+ "$FM_ROOT/bin/fm-guard.sh" || true
653
+ if [ "$FORCE" = --force ]; then
654
+ if out=$("$SCRIPT_DIR/fm-on.sh" "$ID" fm-remote-secondmate-control.sh retire "$ID" --force < /dev/null 2>&1); then rc=0; else rc=$?; fi
655
+ else
656
+ if out=$("$SCRIPT_DIR/fm-on.sh" "$ID" fm-remote-secondmate-control.sh retire "$ID" < /dev/null 2>&1); then rc=0; else rc=$?; fi
657
+ fi
658
+ if [ "$rc" -ne 0 ]; then
659
+ [ -z "$out" ] || printf '%s\n' "$out" >&2
660
+ if [ "$rc" -eq 255 ]; then
661
+ echo "error: remote retirement completion is unknown; preserving the route and local records for same-host reconciliation" >&2
662
+ elif ! "$SCRIPT_DIR/fm-procevent-remote-reply.sh" arm-locked "$ID" >/dev/null 2>&1; then
663
+ echo "error: remote retirement failed and the reply source could not be re-armed" >&2
664
+ fi
665
+ return "$rc"
666
+ fi
667
+ remote_recovery_paths_validate recheck || {
668
+ echo "error: remote home retired but local recovery paths changed; preserving the local route for retry" >&2
669
+ return 1
670
+ }
671
+ "$SCRIPT_DIR/fm-procevent-remote-reply.sh" retire-finalize-locked "$ID" "$FORCE" >/dev/null 2>&1 || {
672
+ echo "error: remote home retired but reply-source cleanup is incomplete; preserving the local route for retry" >&2
673
+ return 1
674
+ }
675
+ if [ "$FORCE" = --force ]; then
676
+ remote_outbox_cleanup || { echo "error: remote outbox cleanup failed; preserving the local route for retry" >&2; return 1; }
677
+ fi
678
+ remote_pending_replies_cleanup \
679
+ || { echo "error: remote pending-reply cleanup failed; preserving the local route for retry" >&2; return 1; }
680
+ handoff_wake_retire \
681
+ || { echo "error: remote receiver wake cleanup failed; preserving the local route for retry" >&2; return 1; }
682
+ tmp="$SECONDMATE_REG.tmp.$$"
683
+ grep -vE "^- $ID( |$)" "$SECONDMATE_REG" > "$tmp" || true
684
+ mv -f -- "$tmp" "$SECONDMATE_REG"
685
+ status_retire_presentation_task "$STATE" "$ID" || return 1
686
+ fm_backlog_atomic_transition remove "$STATE/$ID.meta" "task record" "$STATE" || return 1
687
+ rm -f -- "$STATE/$ID.turn-ended"
688
+ printf 'teardown %s complete (remote %s:%s)\n' "$ID" "$remote_host" "$remote_home"
689
+ return 0
690
+ }
691
+
692
+ remote_secondmate_teardown_locked() {
693
+ local rc
694
+ [ -n "$(fm_meta_get "$META" remote_host)" ] || return 3
695
+ REMOTE_REGISTRY_LOCK=$(secondmate_registry_lock_path "$STATE")
696
+ fm_lock_acquire_wait "$REMOTE_REGISTRY_LOCK" || return 1
697
+ REMOTE_HANDOFF_LOCK="$STATE/.backlog-handoff-$ID.lock"
698
+ fm_lock_acquire_wait "$REMOTE_HANDOFF_LOCK" || {
699
+ remote_teardown_locks_release
700
+ return 1
701
+ }
702
+ REMOTE_REPLY_LIFECYCLE_LOCK=$(secondmate_reply_lifecycle_lock_path "$STATE" "$ID")
703
+ fm_lock_acquire_wait "$REMOTE_REPLY_LIFECYCLE_LOCK" || {
704
+ remote_teardown_locks_release
705
+ return 1
706
+ }
707
+ if remote_secondmate_teardown; then rc=0; else rc=$?; fi
708
+ remote_teardown_locks_release
709
+ return "$rc"
710
+ }
711
+
712
+ if remote_secondmate_teardown_locked; then
713
+ "$SCRIPT_DIR/fm-home-summary-refresh.sh" --best-effort || true
714
+ exit 0
715
+ else
716
+ remote_teardown_rc=$?
717
+ fi
718
+ [ "$remote_teardown_rc" -eq 3 ] || exit "$remote_teardown_rc"
719
+
720
+ # This is the first cleanup authorization check. It is metadata-only and must
721
+ # complete before fm-guard, a backend command, file removal, branch deletion,
722
+ # worktree return, registry change, or process termination can run.
723
+ fm_backend_validate_task_endpoint "$META" "$ID" || exit 1
724
+ BACKEND=$FM_BACKEND_VALIDATED_BACKEND
725
+ T=$FM_BACKEND_VALIDATED_TARGET
726
+ WT=$(fm_meta_get "$META" worktree)
727
+ PROJ=$(fm_meta_get "$META" project)
728
+ T_ORCA=
729
+ [ "$BACKEND" != orca ] || T_ORCA=$T
730
+ if [ "${FM_TEARDOWN_GUARD_DONE:-0}" != 1 ]; then
731
+ "$FM_ROOT/bin/fm-guard.sh" || true
732
+ fi
733
+ HOME_PATH=$(grep '^home=' "$META" | cut -d= -f2- || true)
734
+ PR_URL=$(grep '^pr=' "$META" | tail -1 | cut -d= -f2- || true)
735
+ # tasktmp is recorded by fm-spawn for tasks that set up a per-task temp root
736
+ # (/tmp/fm-<id>/); absent for tasks spawned before that change, so tolerate empty.
737
+ TASK_TMP=$(grep '^tasktmp=' "$META" | cut -d= -f2- || true)
738
+ BUSY_GEN=$(fm_meta_get "$META" busy_gen)
739
+ if [ -z "$BUSY_GEN" ]; then
740
+ BUSY_GEN=$(cat "$STATE/$ID.busy-gen" 2>/dev/null || true)
741
+ fi
742
+ ORCA_WORKTREE_ID=$(fm_meta_get "$META" orca_worktree_id)
743
+ ORCA_PATH_MATCH_VERIFIED=0
744
+ CLEANUP_RECOVERY=$TEARDOWN_CLEANUP_RECOVERY
745
+
746
+ KIND=$TEARDOWN_META_KIND
747
+ MODE=$(grep '^mode=' "$META" | cut -d= -f2- || true)
748
+ [ -n "$MODE" ] || MODE=no-mistakes
749
+ PUBLIC_FOLLOWUP_HOME=$FM_HOME
750
+ PUBLIC_FOLLOWUP_STATE=$STATE
751
+ PUBLIC_FOLLOWUP_WORK_HOME=main
752
+ PUBLIC_FOLLOWUP_PARENT_UNRESOLVED=0
753
+ PUBLIC_FOLLOWUP_PARENT_RELAY_ACTIVE=0
754
+ PUBLIC_FOLLOWUP_RELAY_ACTIVE=0
755
+ public_followup_canonical_home() {
756
+ local home=$1
757
+ case "$home" in /*) ;; *) return 1 ;; esac
758
+ CDPATH='' cd -- "$home" 2>/dev/null && pwd -P
759
+ }
760
+ public_followup_resolve_primary_home() {
761
+ local parent=$1 child=$2 id=$3 parent_meta registry meta_home
762
+ fm_pf_home_id_valid "secondmate:$id" || return 1
763
+ parent=$(public_followup_canonical_home "$parent") || return 1
764
+ child=$(public_followup_canonical_home "$child") || return 1
765
+ [ "$parent" != "$child" ] || return 1
766
+ parent_meta="$parent/state/$id.meta"
767
+ [ -f "$parent_meta" ] && [ ! -L "$parent_meta" ] || return 1
768
+ [ "$(fm_meta_get "$parent_meta" kind)" = secondmate ] || return 1
769
+ meta_home=$(fm_meta_get "$parent_meta" home)
770
+ meta_home=$(CDPATH='' cd -- "$meta_home" 2>/dev/null && pwd -P) || return 1
771
+ [ "$meta_home" = "$child" ] || return 1
772
+ registry="$parent/data/secondmates.md"
773
+ secondmate_registry_validate_bindings "$registry" secondmate_registry_path_key "$id" "$child" || return 1
774
+ printf '%s\n' "$parent"
775
+ }
776
+ if [ -f "$FM_HOME/$SUB_HOME_MARKER" ]; then
777
+ SECOND_MATE_ID=$(sed -n '1p' "$FM_HOME/$SUB_HOME_MARKER")
778
+ # The durable parent record (written once at seeding, next to the identity
779
+ # marker) names this home's route to its parent: "local" when they share a
780
+ # filesystem, "remote" when the parent lives on another machine. Absent for
781
+ # a home seeded before this record existed, which preserves today's exact
782
+ # env-var-only behavior for that legacy home rather than guessing its route.
783
+ PARENT_ROUTE_FILE="$FM_HOME/$SUB_HOME_PARENT_MARKER"
784
+ PARENT_ROUTE_RECORD=absent
785
+ PARENT_ROUTE=
786
+ PARENT_ROUTE_HOME=
787
+ if [ -e "$PARENT_ROUTE_FILE" ] || [ -L "$PARENT_ROUTE_FILE" ]; then
788
+ PARENT_ROUTE_RECORD=invalid
789
+ if fm_secondmate_parent_record_parse "$PARENT_ROUTE_FILE"; then
790
+ PARENT_ROUTE=$FM_SECONDMATE_PARENT_ROUTE
791
+ PARENT_ROUTE_HOME=$FM_SECONDMATE_PARENT_HOME
792
+ PARENT_ROUTE_RECORD=valid
793
+ fi
794
+ fi
795
+ if [ "$PARENT_ROUTE_RECORD" = invalid ]; then
796
+ PUBLIC_FOLLOWUP_PARENT_UNRESOLVED=1
797
+ elif [ "$PARENT_ROUTE" = remote ]; then
798
+ # The entire promised-public-reply subsystem is same-filesystem by
799
+ # construction (bin/fm-public-followup-emit.sh header): a parent recorded
800
+ # on another machine can never hold a delegated promise for this child, so
801
+ # the delegated-parent path is out of scope and never refuses cleanup on
802
+ # its own. A token committed directly to THIS home's own .env is still a
803
+ # real, same-filesystem signal, so it is still checked - but read only
804
+ # from the file, never from the process environment, so an unrelated
805
+ # export in the remote host's own login shell cannot trigger it the way
806
+ # fm_pf_relay_active's environment-wins rule would.
807
+ if [ -f "$FM_HOME/.env" ]; then
808
+ HOME_ENV_TOKEN=$(fmx_env_get FMX_PAIRING_TOKEN "$FM_HOME/.env")
809
+ [ -z "$HOME_ENV_TOKEN" ] || PUBLIC_FOLLOWUP_PARENT_RELAY_ACTIVE=1
810
+ fi
811
+ if [ "$PUBLIC_FOLLOWUP_PARENT_RELAY_ACTIVE" = 1 ]; then
812
+ PUBLIC_FOLLOWUP_PARENT_UNRESOLVED=1
813
+ else
814
+ PUBLIC_FOLLOWUP_HOME=
815
+ PUBLIC_FOLLOWUP_STATE=
816
+ fi
817
+ elif [ "$PARENT_ROUTE" = local ]; then
818
+ PUBLIC_FOLLOWUP_PARENT_UNRESOLVED=1
819
+ PRIMARY_HOME_CANDIDATE=${FM_PUBLIC_FOLLOWUP_PRIMARY_HOME:-$PARENT_ROUTE_HOME}
820
+ PARENT_BINDINGS_MATCH=1
821
+ if [ -n "${FM_PUBLIC_FOLLOWUP_PRIMARY_HOME:-}" ]; then
822
+ LIVE_PARENT_HOME=$(public_followup_canonical_home \
823
+ "$FM_PUBLIC_FOLLOWUP_PRIMARY_HOME") || PARENT_BINDINGS_MATCH=0
824
+ DURABLE_PARENT_HOME=$(public_followup_canonical_home \
825
+ "$PARENT_ROUTE_HOME") || PARENT_BINDINGS_MATCH=0
826
+ if [ "$PARENT_BINDINGS_MATCH" = 1 ] \
827
+ && [ "$LIVE_PARENT_HOME" != "$DURABLE_PARENT_HOME" ]; then
828
+ PARENT_BINDINGS_MATCH=0
829
+ fi
830
+ fi
831
+ if [ "$PARENT_BINDINGS_MATCH" = 1 ] \
832
+ && fm_pf_home_id_valid "secondmate:$SECOND_MATE_ID"; then
833
+ PUBLIC_FOLLOWUP_WORK_HOME="secondmate:$SECOND_MATE_ID"
834
+ if PUBLIC_FOLLOWUP_HOME=$(public_followup_resolve_primary_home \
835
+ "$PRIMARY_HOME_CANDIDATE" "$FM_HOME" "$SECOND_MATE_ID"); then
836
+ PUBLIC_FOLLOWUP_STATE="$PUBLIC_FOLLOWUP_HOME/state"
837
+ PUBLIC_FOLLOWUP_PARENT_UNRESOLVED=0
838
+ if [ "$FORCE" != "--force" ] \
839
+ && fm_pf_relay_active "$PUBLIC_FOLLOWUP_HOME"; then
840
+ PUBLIC_FOLLOWUP_RELAY_ACTIVE=1
841
+ fi
842
+ else
843
+ PUBLIC_FOLLOWUP_HOME=
844
+ PUBLIC_FOLLOWUP_STATE=
845
+ fi
846
+ fi
847
+ else
848
+ # A home seeded before the durable record existed retains the legacy
849
+ # launch-time binding behavior unchanged.
850
+ PRIMARY_HOME_CANDIDATE=${FM_PUBLIC_FOLLOWUP_PRIMARY_HOME:-}
851
+ if [ -n "$PRIMARY_HOME_CANDIDATE" ]; then
852
+ if fm_pf_relay_active "$PRIMARY_HOME_CANDIDATE"; then
853
+ PUBLIC_FOLLOWUP_PARENT_RELAY_ACTIVE=1
854
+ fi
855
+ elif fm_pf_relay_active "$FM_HOME"; then
856
+ PUBLIC_FOLLOWUP_PARENT_RELAY_ACTIVE=1
857
+ fi
858
+ if [ "$PUBLIC_FOLLOWUP_PARENT_RELAY_ACTIVE" = 1 ]; then
859
+ PUBLIC_FOLLOWUP_PARENT_UNRESOLVED=1
860
+ if fm_pf_home_id_valid "secondmate:$SECOND_MATE_ID"; then
861
+ PUBLIC_FOLLOWUP_WORK_HOME="secondmate:$SECOND_MATE_ID"
862
+ if PUBLIC_FOLLOWUP_HOME=$(public_followup_resolve_primary_home \
863
+ "$PRIMARY_HOME_CANDIDATE" "$FM_HOME" "$SECOND_MATE_ID"); then
864
+ PUBLIC_FOLLOWUP_STATE="$PUBLIC_FOLLOWUP_HOME/state"
865
+ PUBLIC_FOLLOWUP_PARENT_UNRESOLVED=0
866
+ if [ "$FORCE" != "--force" ] \
867
+ && fm_pf_relay_active "$PUBLIC_FOLLOWUP_HOME"; then
868
+ PUBLIC_FOLLOWUP_RELAY_ACTIVE=1
869
+ fi
870
+ else
871
+ PUBLIC_FOLLOWUP_HOME=
872
+ PUBLIC_FOLLOWUP_STATE=
873
+ fi
874
+ fi
875
+ else
876
+ PUBLIC_FOLLOWUP_HOME=
877
+ PUBLIC_FOLLOWUP_STATE=
878
+ fi
879
+ fi
880
+ elif [ "$KIND" = secondmate ]; then
881
+ PUBLIC_FOLLOWUP_WORK_HOME="secondmate:$ID"
882
+ if [ "$FORCE" != "--force" ] && fm_pf_relay_active "$FM_HOME"; then
883
+ PUBLIC_FOLLOWUP_RELAY_ACTIVE=1
884
+ fi
885
+ elif [ "$FORCE" != "--force" ] && fm_pf_relay_active "$FM_HOME"; then
886
+ PUBLIC_FOLLOWUP_RELAY_ACTIVE=1
887
+ fi
888
+
889
+ default_branch() {
890
+ local ref branch
891
+ ref=$(git -C "$PROJ" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || true)
892
+ if [ -n "$ref" ]; then
893
+ echo "${ref#origin/}"
894
+ return 0
895
+ fi
896
+ for branch in main master; do
897
+ if git -C "$PROJ" show-ref --verify --quiet "refs/heads/$branch"; then
898
+ echo "$branch"
899
+ return 0
900
+ fi
901
+ done
902
+ return 1
903
+ }
904
+
905
+ meta_value() {
906
+ local meta=$1 key=$2
907
+ fm_meta_get "$meta" "$key"
908
+ }
909
+
910
+ require_orca_worktree_id() {
911
+ local meta=$1 id
912
+ id=$(meta_value "$meta" orca_worktree_id)
913
+ if [ -z "$id" ]; then
914
+ echo "error: missing orca_worktree_id in $meta; cannot remove Orca worktree" >&2
915
+ return 1
916
+ fi
917
+ printf '%s\n' "$id"
918
+ }
919
+
920
+ require_orca_terminal() {
921
+ local meta=$1 terminal
922
+ terminal=$(meta_value "$meta" terminal)
923
+ if [ -z "$terminal" ]; then
924
+ echo "error: missing terminal in $meta; cannot close Orca terminal" >&2
925
+ return 1
926
+ fi
927
+ printf '%s\n' "$terminal"
928
+ }
929
+
930
+ if [ "$BACKEND" = orca ] && [ "$KIND" != secondmate ]; then
931
+ ORCA_WORKTREE_ID=$(require_orca_worktree_id "$META") || exit 1
932
+ T_ORCA=$(meta_value "$META" terminal)
933
+ [ -z "$T_ORCA" ] || T=$T_ORCA
934
+ fi
935
+
936
+ # Where a harness's firstmate-owned global turn-end registry entry lives is
937
+ # owned by bin/fm-control-lib.sh, so teardown and the control plane's relaunch
938
+ # retire the same artifact rather than each carrying its own copy of the path.
939
+ remove_grok_turnend_auth() {
940
+ local state_dir=$1 id=$2 token_path token='' path
941
+ token_path=$(fm_control_harness_turnend_token_path grok "$state_dir" "$id") || return 1
942
+ if [ -n "$token_path" ] && [ -f "$token_path" ]; then
943
+ IFS= read -r token < "$token_path" || [ -n "$token" ] || return 1
944
+ fi
945
+ path=$(fm_control_harness_turnend_auth_path grok "$token") || return 1
946
+ [ -n "$path" ] || return 0
947
+ rm -f -- "$path"
948
+ }
949
+
950
+ remove_kimi_turnend_auth() {
951
+ local state_dir=$1 id=$2 token_path token='' path
952
+ token_path=$(fm_control_harness_turnend_token_path kimi "$state_dir" "$id") || return 1
953
+ if [ -n "$token_path" ] && [ -f "$token_path" ]; then
954
+ IFS= read -r token < "$token_path" || [ -n "$token" ] || return 1
955
+ fi
956
+ path=$(fm_control_harness_turnend_auth_path kimi "$token") || return 1
957
+ [ -n "$path" ] || return 0
958
+ rm -f -- "$path"
959
+ }
960
+
961
+ retire_busy_state() {
962
+ local state_dir=$1 id=$2 gen=${3:-}
963
+ if [ -n "$gen" ]; then
964
+ "$SCRIPT_DIR/fm-busy-event.sh" retire "$state_dir" "$id" --gen "$gen"
965
+ elif [ -f "$state_dir/$id.busy-gen" ]; then
966
+ "$SCRIPT_DIR/fm-busy-event.sh" retire "$state_dir" "$id" --current-gen
967
+ fi
968
+ }
969
+
970
+ validate_pr_poll_cleanup() {
971
+ local state_dir=$1 id=$2 state_device artifact has_artifact=0
972
+ fm_task_id_path_safe "$id" || return 0
973
+ for artifact in "$state_dir/$id.check.sh" "$state_dir/$id.pr-poll" \
974
+ "$state_dir/$id.pr-poll-registration" "$state_dir/$id.pr-poll-retirement" \
975
+ "$state_dir/$id.check-trust"; do
976
+ [ -e "$artifact" ] || [ -L "$artifact" ] || continue
977
+ has_artifact=1
978
+ done
979
+ [ "$has_artifact" -eq 1 ] || return 0
980
+ [ -d "$state_dir" ] && [ ! -L "$state_dir" ] || return 1
981
+ state_device=$(fm_pr_file_device "$state_dir") || return 1
982
+ for artifact in "$state_dir/$id.check.sh" "$state_dir/$id.pr-poll" \
983
+ "$state_dir/$id.pr-poll-registration" "$state_dir/$id.pr-poll-retirement" \
984
+ "$state_dir/$id.check-trust"; do
985
+ [ -e "$artifact" ] || [ -L "$artifact" ] || continue
986
+ if [ ! -f "$artifact" ] || [ -L "$artifact" ] \
987
+ || [ "$(fm_pr_file_device "$artifact")" != "$state_device" ] \
988
+ || [ "$(fm_pr_file_link_count "$artifact")" != 1 ]; then
989
+ echo "REFUSED: unsafe task PR-check artifact; preserving task state." >&2
990
+ return 1
991
+ fi
992
+ done
993
+ if [ -e "$state_dir/$id.pr-poll-retirement" ] \
994
+ || [ -L "$state_dir/$id.pr-poll-retirement" ]; then
995
+ fm_pr_poll_retirement_state_valid "$state_dir" "$id" || {
996
+ echo "REFUSED: invalid PR-poll retirement receipt; preserving task state." >&2
997
+ return 1
998
+ }
999
+ fi
1000
+ }
1001
+
1002
+ remove_pr_poll_artifacts() {
1003
+ local state_dir=$1 id=$2
1004
+ validate_pr_poll_cleanup "$state_dir" "$id" || return 1
1005
+ fm_pr_poll_retirement_recover_one "$state_dir" "$id" "$SCRIPT_DIR/fm-pr-poll.sh" || return 1
1006
+ fm_pr_poll_merge_notified_remove "$state_dir" "$id" || return 1
1007
+ rm -f "$state_dir/$id.check.sh" "$state_dir/$id.pr-poll" \
1008
+ "$state_dir/$id.pr-poll-registration" "$state_dir/$id.pr-poll-retirement" \
1009
+ "$state_dir/$id.check-trust" || return 1
1010
+ }
1011
+
1012
+ # Resolve the PR number for a worktree branch via gh-axi. Echoes the number on a
1013
+ # single match and returns 0; returns non-zero on no match or any lookup failure,
1014
+ # so the caller treats it as "no PR found" (fail-safe).
1015
+ pr_number_from_branch() {
1016
+ local branch=$1 out n
1017
+ [ -n "$branch" ] && [ "$branch" != HEAD ] || return 1
1018
+ out=$( cd "$WT" && gh-axi pr list --state all --head "$branch" --limit 1 2>/dev/null ) || return 1
1019
+ n=$(printf '%s\n' "$out" | sed -n 's/^[[:space:]]*\([0-9][0-9]*\),.*/\1/p' | head -1)
1020
+ [ -n "$n" ] || return 1
1021
+ printf '%s' "$n"
1022
+ }
1023
+
1024
+ pr_number_from_target() {
1025
+ local target=$1 n
1026
+ case "$target" in
1027
+ '' ) return 1 ;;
1028
+ *"/pull/"*)
1029
+ n=${target##*/pull/}
1030
+ n=${n%%[!0-9]*}
1031
+ ;;
1032
+ [0-9]*)
1033
+ n=${target%%[!0-9]*}
1034
+ ;;
1035
+ *) return 1 ;;
1036
+ esac
1037
+ [ -n "$n" ] || return 1
1038
+ printf '%s' "$n"
1039
+ }
1040
+
1041
+ ensure_commit_object() {
1042
+ local target=$1 commit=$2 n
1043
+ git -C "$WT" cat-file -e "$commit^{commit}" 2>/dev/null && return 0
1044
+ n=$(pr_number_from_target "$target") || return 1
1045
+ git -C "$WT" remote get-url origin >/dev/null 2>&1 || return 1
1046
+ git -C "$WT" fetch --quiet origin "refs/pull/$n/head" >/dev/null 2>&1 || return 1
1047
+ git -C "$WT" cat-file -e "$commit^{commit}" 2>/dev/null
1048
+ }
1049
+
1050
+ patch_id_for_commit() {
1051
+ local commit=$1
1052
+ git -C "$WT" show --pretty=medium --no-ext-diff "$commit" 2>/dev/null \
1053
+ | git patch-id --stable 2>/dev/null \
1054
+ | awk 'NR == 1 { print $1 }'
1055
+ }
1056
+
1057
+ unpushed_patches_are_in_pr_head() {
1058
+ local pr_head=$1 current base pr_patch_ids commit patch_id unpushed
1059
+ current=$(git -C "$WT" rev-parse --verify HEAD 2>/dev/null) || return 1
1060
+ base=$(git -C "$WT" merge-base "$current" "$pr_head" 2>/dev/null) || return 1
1061
+ pr_patch_ids=$(
1062
+ git -C "$WT" log --format=%H "$base..$pr_head" -- 2>/dev/null \
1063
+ | while IFS= read -r commit; do
1064
+ patch_id_for_commit "$commit"
1065
+ done \
1066
+ | sed '/^$/d' \
1067
+ | sort -u
1068
+ ) || return 1
1069
+ [ -n "$pr_patch_ids" ] || return 1
1070
+ unpushed=$(git -C "$WT" log --format=%H HEAD --not --remotes -- 2>/dev/null) || return 1
1071
+ [ -n "$unpushed" ] || return 1
1072
+ while IFS= read -r commit; do
1073
+ [ -n "$commit" ] || continue
1074
+ patch_id=$(patch_id_for_commit "$commit") || return 1
1075
+ [ -n "$patch_id" ] || return 1
1076
+ printf '%s\n' "$pr_patch_ids" | grep -qxF "$patch_id" || return 1
1077
+ done <<EOF
1078
+ $unpushed
1079
+ EOF
1080
+ }
1081
+
1082
+ # Is the worktree's PR merged for local work contained in that PR? Resolves the
1083
+ # PR from the recorded pr= URL first, then from the branch name, and asks GitHub
1084
+ # for both the PR state and head. Returns non-zero when the PR is not merged, the
1085
+ # current work is not contained in the PR head, no PR is found, or any gh error
1086
+ # occurs - the caller then falls back to the content check.
1087
+ pr_is_merged() {
1088
+ local branch=$1 target view state remainder head resolved_url current landed=0
1089
+ if [ -n "$PR_URL" ]; then
1090
+ target=$PR_URL
1091
+ else
1092
+ target=$(pr_number_from_branch "$branch") || return 1
1093
+ fi
1094
+ [ -n "$target" ] || return 1
1095
+ view=$(cd "$WT" && gh pr view "$target" --json state,headRefOid,url -q '.state + "\t" + .headRefOid + "\t" + .url' 2>/dev/null) || return 1
1096
+ state=${view%%$'\t'*}
1097
+ remainder=${view#*$'\t'}
1098
+ [ "$state" != "$view" ] || return 1
1099
+ head=${remainder%%$'\t'*}
1100
+ resolved_url=${remainder#*$'\t'}
1101
+ [ "$head" != "$remainder" ] || return 1
1102
+ case "$state" in
1103
+ MERGED|merged) ;;
1104
+ *) return 1 ;;
1105
+ esac
1106
+ [ -n "$head" ] || return 1
1107
+ ensure_commit_object "$target" "$head" || return 1
1108
+ current=$(git -C "$WT" rev-parse --verify HEAD 2>/dev/null) || return 1
1109
+ if git -C "$WT" merge-base --is-ancestor "$current" "$head" 2>/dev/null; then
1110
+ landed=1
1111
+ elif unpushed_patches_are_in_pr_head "$head"; then
1112
+ landed=1
1113
+ fi
1114
+ [ "$landed" = 1 ] || return 1
1115
+ if [ -z "$PR_URL" ]; then
1116
+ [ -n "$resolved_url" ] || return 1
1117
+ PR_URL=$resolved_url
1118
+ fi
1119
+ return 0
1120
+ }
1121
+
1122
+ # Is the branch's content already present in the up-to-date default branch? Fetches
1123
+ # first, then 3-way merges the default branch with HEAD: when HEAD introduces nothing
1124
+ # the default branch does not already contain (e.g. its change landed via squash) the
1125
+ # merged tree equals the default branch's tree. This isolates branch-only changes, so
1126
+ # unrelated commits the default branch gained past the merge-base do not count as
1127
+ # "added". Returns non-zero when inconclusive (no default ref, or a merge conflict),
1128
+ # so the caller refuses rather than guesses.
1129
+ content_in_default() {
1130
+ local name ref default_tree merged_tree
1131
+ name=$(default_branch) || return 1
1132
+ if git -C "$WT" remote get-url origin >/dev/null 2>&1; then
1133
+ git -C "$WT" fetch --quiet origin "+refs/heads/$name:refs/remotes/origin/$name" >/dev/null 2>&1 || return 1
1134
+ ref="refs/remotes/origin/$name"
1135
+ elif git -C "$WT" rev-parse --quiet --verify "refs/heads/$name" >/dev/null 2>&1; then
1136
+ ref="refs/heads/$name"
1137
+ else
1138
+ return 1
1139
+ fi
1140
+ default_tree=$(git -C "$WT" rev-parse --quiet --verify "$ref^{tree}" 2>/dev/null) || return 1
1141
+ [ -n "$default_tree" ] || return 1
1142
+ merged_tree=$(git -C "$WT" merge-tree --write-tree "$ref" HEAD 2>/dev/null) || return 1
1143
+ merged_tree=$(printf '%s\n' "$merged_tree" | head -1)
1144
+ [ "$merged_tree" = "$default_tree" ]
1145
+ }
1146
+
1147
+ # Has the worktree's committed work actually LANDED, though its commits are not
1148
+ # reachable from any remote-tracking branch? True when a merged PR proves the
1149
+ # current local work is contained in the PR head, OR the content is already in the
1150
+ # default branch (fallback, which also covers the no-PR and gh-error paths). False
1151
+ # only for genuinely unlanded work.
1152
+ work_is_landed() {
1153
+ local branch=$1
1154
+ pr_is_merged "$branch" && return 0
1155
+ content_in_default
1156
+ }
1157
+
1158
+ # The completion links this teardown already holds locally. A scout's
1159
+ # deliverable is its report, a local-only ship lands on local main, and every
1160
+ # other ship carries the PR recorded on its own record.
1161
+ BACKLOG_DONE_ARGS=()
1162
+ backlog_done_args() {
1163
+ local data_relative
1164
+ BACKLOG_DONE_ARGS=()
1165
+ case "$KIND" in
1166
+ scout)
1167
+ data_relative=$(fm_backlog_data_relative "$DATA") || return 1
1168
+ BACKLOG_DONE_ARGS=(--report "$data_relative/$ID/report.md")
1169
+ ;;
1170
+ *)
1171
+ if [ "$MODE" = local-only ]; then
1172
+ BACKLOG_DONE_ARGS=(--note "local main")
1173
+ elif [ -n "$PR_URL" ]; then
1174
+ BACKLOG_DONE_ARGS=(--pr "$PR_URL")
1175
+ fi
1176
+ ;;
1177
+ esac
1178
+ }
1179
+
1180
+ # Closing the backlog item is this script's own last act on the record, not a
1181
+ # printed instruction for a later turn (bin/fm-backlog-transition-lib.sh owns the
1182
+ # invariant). This prints what already happened, so the follow-up wording stays
1183
+ # only where a human still owes the edit.
1184
+ backlog_refresh_reminder() {
1185
+ local backlog_display
1186
+ [ "$KIND" = secondmate ] && return 0
1187
+ [ "$CLEANUP_RECOVERY" = orca ] && return 0
1188
+ if backlog_display=$(fm_backlog_file "$DATA"); then
1189
+ :
1190
+ else
1191
+ backlog_display="${DATA%/}/backlog.md"
1192
+ fi
1193
+ if [ "$BACKLOG_CLOSED" = 1 ]; then
1194
+ printf '%s\n' "Backlog: $ID is closed in $backlog_display. Run tasks-axi ready for dependency-cleared candidates, check date gates, and dispatch only work whose blockers are gone and date is due."
1195
+ else
1196
+ printf '%s\n' "Backlog: $ID just finished ($BACKLOG_SKIP_REASON). Update $backlog_display - move $ID to Done, keep Done to the 10 most recent, then re-scan Queued and dispatch only work whose blockers are gone and date is due."
1197
+ fi
1198
+ }
1199
+
1200
+ path_is_ancestor_of() {
1201
+ local ancestor=$1 path=$2
1202
+ [ -n "$ancestor" ] || return 1
1203
+ [ -n "$path" ] || return 1
1204
+ [ "$ancestor" != "$path" ] || return 1
1205
+ case "$path" in
1206
+ "$ancestor"/*) return 0 ;;
1207
+ esac
1208
+ return 1
1209
+ }
1210
+
1211
+ removal_target_abs_path() {
1212
+ local target=$1
1213
+ if [ -d "$target" ]; then
1214
+ cd "$target" && pwd -P
1215
+ else
1216
+ cd "$(dirname "$target")" && printf '%s/%s\n' "$(pwd -P)" "$(basename "$target")"
1217
+ fi
1218
+ }
1219
+
1220
+ worktree_registered_for_project() {
1221
+ local project=$1 target=$2 abs_target listed line listed_abs
1222
+ [ -n "$project" ] || return 1
1223
+ [ -d "$project" ] || return 1
1224
+ git -C "$project" rev-parse --git-dir >/dev/null 2>&1 || return 1
1225
+ abs_target=$(removal_target_abs_path "$target")
1226
+ listed=$(git -C "$project" -c core.quotePath=false worktree list --porcelain 2>/dev/null) || return 1
1227
+ while IFS= read -r line; do
1228
+ case "$line" in
1229
+ worktree\ *)
1230
+ listed_abs=$(removal_target_abs_path "${line#worktree }" 2>/dev/null || true)
1231
+ [ "$listed_abs" = "$abs_target" ] && return 0
1232
+ ;;
1233
+ esac
1234
+ done <<EOF
1235
+ $listed
1236
+ EOF
1237
+ return 1
1238
+ }
1239
+
1240
+ inspectable_git_worktree() {
1241
+ local target=$1 top
1242
+ [ -n "$target" ] || return 1
1243
+ [ -d "$target" ] || return 1
1244
+ top=$(git -C "$target" rev-parse --show-toplevel 2>/dev/null) || return 1
1245
+ [ -n "$top" ] || return 1
1246
+ [ -d "$top" ] || return 1
1247
+ git -C "$top" rev-parse --git-dir >/dev/null 2>&1
1248
+ }
1249
+
1250
+ canonical_existing_dir() {
1251
+ local target=$1
1252
+ [ -n "$target" ] || return 1
1253
+ [ -d "$target" ] || return 1
1254
+ ( cd "$target" && pwd -P )
1255
+ }
1256
+
1257
+ retry_wait_secs_is_valid() {
1258
+ [[ "$1" =~ ^([0-9]+([.][0-9]*)?|[.][0-9]+)$ ]]
1259
+ }
1260
+
1261
+ STALE_WORKTREE_LOCK_AGE_SECS=${FM_STALE_WORKTREE_LOCK_AGE_SECS:-30}
1262
+ # Bounded patience window for transient index.lock after killing a crew process.
1263
+ # New knobs are preferred; FM_STALE_WORKTREE_LOCK_RETRY_WAIT_SECS remains an alias
1264
+ # for the per-attempt wait so existing tests and operators keep working.
1265
+ TREEHOUSE_RETURN_LOCK_RETRIES=${FM_TREEHOUSE_RETURN_LOCK_RETRIES:-3}
1266
+ TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS=${FM_TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS:-${FM_STALE_WORKTREE_LOCK_RETRY_WAIT_SECS:-1}}
1267
+ if ! retry_wait_secs_is_valid "$TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS"; then
1268
+ echo "teardown: invalid treehouse return lock retry wait '$TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS'; using 1s" >&2
1269
+ TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS=1
1270
+ fi
1271
+ # Compatibility alias used by the safety-check wait path and older call sites.
1272
+ STALE_WORKTREE_LOCK_RETRY_WAIT_SECS=$TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS
1273
+ TEARDOWN_TREEHOUSE_LOCK_REFUSED=2
1274
+ TEARDOWN_WORKTREE_SAFETY_LOCK_BLOCKED=3
1275
+ TEARDOWN_PROCEVENT_RESTORE_FAILED=4
1276
+
1277
+ # True when treehouse/git stderr shows the transient index.lock "File exists" race.
1278
+ # Other return failures must not enter the retry path.
1279
+ treehouse_return_is_index_lock_error() {
1280
+ local text=$1
1281
+ printf '%s\n' "$text" | grep -Eq "Unable to create ['\"].*index\\.lock['\"]: File exists"
1282
+ }
1283
+
1284
+ # Absolute path to the git index lock for a worktree/repo dir, or empty when it
1285
+ # cannot be resolved (dir missing or not a git worktree at all).
1286
+ worktree_git_lock_path() {
1287
+ local dir=$1 lock abs_dir
1288
+ [ -n "$dir" ] && [ -d "$dir" ] || return 1
1289
+ lock=$(git -C "$dir" rev-parse --git-path index.lock 2>/dev/null) || return 1
1290
+ [ -n "$lock" ] || return 1
1291
+ case "$lock" in
1292
+ /*) printf '%s\n' "$lock" ;;
1293
+ *)
1294
+ abs_dir=$(canonical_existing_dir "$dir") || return 1
1295
+ printf '%s/%s\n' "$abs_dir" "$lock"
1296
+ ;;
1297
+ esac
1298
+ }
1299
+
1300
+ # The lock-staleness proof (lsof holder check, mtime age, fail-safe defaults)
1301
+ # is owned by bin/fm-lock-lib.sh's fm_lock_is_provably_stale, sourced above.
1302
+ # Teardown passes the worktree dir as the companion directory and its own
1303
+ # STALE_WORKTREE_LOCK_AGE_SECS threshold.
1304
+
1305
+ worktree_safety_blocked_by_lock() {
1306
+ local reason=$1 lock
1307
+ lock=$(worktree_git_lock_path "$WT") || lock=""
1308
+ [ -n "$lock" ] && [ -e "$lock" ] || return 1
1309
+ echo "teardown: cannot inspect worktree $WT for $reason while git lock $lock is present; checking whether the lock is stale" >&2
1310
+ return 0
1311
+ }
1312
+
1313
+ cleanup_stale_lock_for_safety_check() {
1314
+ local dir=$1 lock
1315
+ lock=$(worktree_git_lock_path "$dir") || lock=""
1316
+ [ -n "$lock" ] && [ -e "$lock" ] || return 0
1317
+
1318
+ echo "teardown: worktree safety check blocked by git lock $lock; waiting ${STALE_WORKTREE_LOCK_RETRY_WAIT_SECS}s and retrying (owning process may be exiting)" >&2
1319
+ sleep "$STALE_WORKTREE_LOCK_RETRY_WAIT_SECS"
1320
+
1321
+ if [ ! -e "$lock" ]; then
1322
+ echo "teardown: worktree safety check lock cleared on its own; retrying safety checks" >&2
1323
+ return 0
1324
+ fi
1325
+
1326
+ if fm_lock_is_provably_stale "$lock" "$dir" "$STALE_WORKTREE_LOCK_AGE_SECS"; then
1327
+ rm -f "$lock"
1328
+ echo "teardown: removed provably-stale git lock $lock (age >= ${STALE_WORKTREE_LOCK_AGE_SECS}s, no live holder) and retrying worktree safety checks" >&2
1329
+ return 0
1330
+ fi
1331
+
1332
+ echo "teardown: worktree safety check blocked by git lock $lock that is not provably stale (may belong to a live process); leaving it in place" >&2
1333
+ return "$TEARDOWN_TREEHOUSE_LOCK_REFUSED"
1334
+ }
1335
+
1336
+ # Return a worktree/home via `treehouse return --force`, tolerating a transient or
1337
+ # stale git index.lock left by a killed crew process. See the script header.
1338
+ teardown_treehouse_return() {
1339
+ local dir=$1 cd_dir=$2 label=$3 post_cleanup_check=${4:-}
1340
+ local out lock attempt=0 max_retries lock_desc
1341
+
1342
+ # Capture stdout+stderr so non-lock failures stay visible and lock failures can
1343
+ # be matched by signature even when the lock file is already gone mid-check.
1344
+ if out=$( ( cd "$cd_dir" && treehouse return --force "$dir" ) 2>&1 ); then
1345
+ [ -n "$out" ] && printf '%s\n' "$out"
1346
+ return 0
1347
+ fi
1348
+ [ -n "$out" ] && printf '%s\n' "$out" >&2
1349
+
1350
+ if ! treehouse_return_is_index_lock_error "$out"; then
1351
+ return 1
1352
+ fi
1353
+
1354
+ lock=$(worktree_git_lock_path "$dir") || lock=""
1355
+ if [ -n "$lock" ]; then
1356
+ lock_desc=$lock
1357
+ else
1358
+ lock_desc="index.lock"
1359
+ fi
1360
+
1361
+ max_retries=$TREEHOUSE_RETURN_LOCK_RETRIES
1362
+ case "$max_retries" in ''|*[!0-9]*) max_retries=3 ;; esac
1363
+
1364
+ while [ "$attempt" -lt "$max_retries" ]; do
1365
+ attempt=$(( attempt + 1 ))
1366
+ echo "teardown: $label return failed with transient git lock ($lock_desc); waiting ${TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS}s and retrying ($attempt/${max_retries})" >&2
1367
+ sleep "$TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS"
1368
+
1369
+ if out=$( ( cd "$cd_dir" && treehouse return --force "$dir" ) 2>&1 ); then
1370
+ [ -n "$out" ] && printf '%s\n' "$out"
1371
+ echo "teardown: $label return succeeded on retry; lock cleared on its own" >&2
1372
+ return 0
1373
+ fi
1374
+ [ -n "$out" ] && printf '%s\n' "$out" >&2
1375
+
1376
+ if ! treehouse_return_is_index_lock_error "$out"; then
1377
+ echo "teardown: $label return failed with a non-lock error after retry; aborting" >&2
1378
+ return 1
1379
+ fi
1380
+ done
1381
+
1382
+ # Refresh lock path after the patience window; it may have appeared, moved, or
1383
+ # cleared while we waited.
1384
+ lock=$(worktree_git_lock_path "$dir") || lock=""
1385
+ if [ -n "$lock" ] && [ -e "$lock" ]; then
1386
+ lock_desc=$lock
1387
+ if fm_lock_is_provably_stale "$lock" "$dir" "$STALE_WORKTREE_LOCK_AGE_SECS"; then
1388
+ rm -f "$lock"
1389
+ echo "teardown: removed provably-stale git lock $lock (age >= ${STALE_WORKTREE_LOCK_AGE_SECS}s, no live holder) and retrying $label return" >&2
1390
+ if [ -n "$post_cleanup_check" ]; then
1391
+ if ! "$post_cleanup_check"; then
1392
+ echo "teardown: $label return aborted after stale-lock cleanup because safety checks failed" >&2
1393
+ return 1
1394
+ fi
1395
+ fi
1396
+ if out=$( ( cd "$cd_dir" && treehouse return --force "$dir" ) 2>&1 ); then
1397
+ [ -n "$out" ] && printf '%s\n' "$out"
1398
+ echo "teardown: $label return succeeded after stale-lock cleanup" >&2
1399
+ return 0
1400
+ fi
1401
+ [ -n "$out" ] && printf '%s\n' "$out" >&2
1402
+ echo "teardown: $label return still failing after stale-lock cleanup" >&2
1403
+ return 1
1404
+ fi
1405
+
1406
+ echo "teardown: $label return failed: git lock $lock_desc persisted across ${max_retries} retries (waiting ${TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS}s each) and is not provably stale (may belong to a live process); leaving it in place" >&2
1407
+ return "$TEARDOWN_TREEHOUSE_LOCK_REFUSED"
1408
+ fi
1409
+
1410
+ echo "teardown: $label return failed: git index.lock signature persisted across ${max_retries} retries (waiting ${TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS}s each) even after the lock file disappeared" >&2
1411
+ return 1
1412
+ }
1413
+
1414
+ validate_worktree_teardown_safety() {
1415
+ local dirty_raw dirty unpushed_raw unpushed DEFAULT unmerged_raw unmerged branch
1416
+ [ -d "$WT" ] || return 0
1417
+ [ "$FORCE" != "--force" ] || return 0
1418
+ case "$KIND" in
1419
+ secondmate|scout) return 0 ;;
1420
+ esac
1421
+
1422
+ if ! dirty_raw=$(git -C "$WT" status --porcelain 2>/dev/null); then
1423
+ if worktree_safety_blocked_by_lock "uncommitted changes"; then
1424
+ return "$TEARDOWN_WORKTREE_SAFETY_LOCK_BLOCKED"
1425
+ fi
1426
+ echo "REFUSED: cannot inspect worktree $WT for uncommitted changes." >&2
1427
+ echo "Restore the git index state, or get the captain's explicit OK to discard, then --force." >&2
1428
+ return 1
1429
+ fi
1430
+ dirty=$(printf '%s\n' "$dirty_raw" | grep -vE '^\?\? (\.claude/|\.fm-(grok|kimi)-turnend$)' | head -1 || true)
1431
+
1432
+ if ! unpushed_raw=$(git -C "$WT" log --oneline HEAD --not --remotes -- 2>/dev/null); then
1433
+ if worktree_safety_blocked_by_lock "commits not on a remote"; then
1434
+ return "$TEARDOWN_WORKTREE_SAFETY_LOCK_BLOCKED"
1435
+ fi
1436
+ echo "REFUSED: cannot inspect worktree $WT for commits not on a remote." >&2
1437
+ echo "Restore the git index state, or get the captain's explicit OK to discard, then --force." >&2
1438
+ return 1
1439
+ fi
1440
+ unpushed=$(printf '%s\n' "$unpushed_raw" | head -5)
1441
+
1442
+ if [ -n "$unpushed" ] && [ "$MODE" = local-only ]; then
1443
+ DEFAULT=$(default_branch) || { echo "REFUSED: cannot determine default branch for $PROJ; expected origin/HEAD, main, or master." >&2; return 1; }
1444
+ if ! unmerged_raw=$(git -C "$WT" log --oneline HEAD --not "$DEFAULT" -- 2>/dev/null); then
1445
+ if worktree_safety_blocked_by_lock "commits not on $DEFAULT"; then
1446
+ return "$TEARDOWN_WORKTREE_SAFETY_LOCK_BLOCKED"
1447
+ fi
1448
+ echo "REFUSED: cannot inspect worktree $WT for commits not on $DEFAULT." >&2
1449
+ echo "Restore the git index state, or get the captain's explicit OK to discard, then --force." >&2
1450
+ return 1
1451
+ fi
1452
+ unmerged=$(printf '%s\n' "$unmerged_raw" | head -5)
1453
+ if [ -n "$dirty" ] || [ -n "$unmerged" ]; then
1454
+ echo "REFUSED: local-only worktree $WT has work not yet merged into $DEFAULT and not on any remote." >&2
1455
+ [ -n "$dirty" ] && echo "uncommitted changes present" >&2
1456
+ [ -n "$unmerged" ] && printf 'commits not yet on %s:\n%s\n' "$DEFAULT" "$unmerged" >&2
1457
+ echo "Merge the branch into local $DEFAULT first (bin/fm-merge-local.sh after the captain approves), or push to a fork/remote, or get the captain's explicit OK to discard, then --force." >&2
1458
+ return 1
1459
+ fi
1460
+ elif [ -n "$dirty" ]; then
1461
+ echo "REFUSED: worktree $WT has uncommitted changes." >&2
1462
+ echo "uncommitted changes present" >&2
1463
+ echo "Commit them (or get the captain's explicit OK to discard, then --force)." >&2
1464
+ return 1
1465
+ elif [ -n "$unpushed" ]; then
1466
+ branch=${TEARDOWN_WORKTREE_BRANCH_FOR_SAFETY:-}
1467
+ if [ -z "$branch" ]; then
1468
+ branch=$(git -C "$WT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)
1469
+ TEARDOWN_WORKTREE_BRANCH_FOR_SAFETY=$branch
1470
+ fi
1471
+ if ! work_is_landed "$branch"; then
1472
+ echo "REFUSED: worktree $WT has work not on any remote and not landed." >&2
1473
+ printf 'unpushed commits:\n%s\n' "$unpushed" >&2
1474
+ echo "Push the branch, land its PR, or get the captain's explicit OK to discard, then --force." >&2
1475
+ return 1
1476
+ fi
1477
+ fi
1478
+ }
1479
+
1480
+ # Fix 1 (see script header): does the active-or-most-recent no-mistakes run in
1481
+ # worktree $1 belong to THIS task, and is it parked at a gate awaiting an agent
1482
+ # that is about to be removed? Prints nothing; returns 0 only on a genuine
1483
+ # match so the caller knows it is safe to abort - never a guess.
1484
+ NM_TEARDOWN_TIMEOUT=${FM_TEARDOWN_NM_TIMEOUT:-10}
1485
+ case "$NM_TEARDOWN_TIMEOUT" in ''|*[!0-9]*) NM_TEARDOWN_TIMEOUT=10 ;; esac
1486
+ TASK_RUN_ID=
1487
+ task_status_is_own_parked_run() { # <worktree> <axi-status-output>
1488
+ local wt=$1 out=$2 branch run_id run_branch run_head status outcome awaiting has_gate
1489
+ TASK_RUN_ID=
1490
+ branch=$(git -C "$wt" symbolic-ref --quiet --short HEAD 2>/dev/null) || return 1
1491
+ [ -n "$branch" ] || return 1
1492
+ [ -n "$out" ] || return 1
1493
+ run_id=$(fm_nm_strip_quotes "$(fm_nm_field "$out" id)")
1494
+ [ -n "$run_id" ] || return 1
1495
+ run_branch=$(fm_nm_strip_quotes "$(fm_nm_field "$out" branch)")
1496
+ [ -n "$run_branch" ] && [ "$run_branch" = "$branch" ] || return 1
1497
+ run_head=$(fm_nm_strip_quotes "$(fm_nm_field "$out" head)")
1498
+ fm_nm_head_matches_worktree "$wt" "$run_head" || return 1
1499
+ outcome=$(fm_nm_strip_quotes "$(fm_nm_field "$out" outcome)")
1500
+ [ -z "$outcome" ] || return 1
1501
+ status=$(fm_nm_strip_quotes "$(fm_nm_field "$out" status)")
1502
+ awaiting=$(printf '%s\n' "$out" | grep -E '^[[:space:]]*awaiting_agent:' | head -1 || true)
1503
+ has_gate=$(printf '%s\n' "$out" | grep -Eq '^[[:space:]]*gate:[[:space:]]*' && echo 1 || echo 0)
1504
+ case "$status" in
1505
+ awaiting_approval|fix_review) TASK_RUN_ID=$run_id; return 0 ;;
1506
+ esac
1507
+ if [ -n "$awaiting" ] || [ "$has_gate" = 1 ]; then
1508
+ TASK_RUN_ID=$run_id
1509
+ return 0
1510
+ fi
1511
+ return 1
1512
+ }
1513
+
1514
+ task_run_is_own_parked_run() { # <worktree>
1515
+ local wt=$1 out
1516
+ # Accepted best-effort residual: query failures stay fail-open because making
1517
+ # no-mistakes availability a prerequisite would block ship tasks with no run.
1518
+ out=$(fm_nm_run "$wt" "$NM_TEARDOWN_TIMEOUT" axi status)
1519
+ task_status_is_own_parked_run "$wt" "$out"
1520
+ }
1521
+
1522
+ task_status_is_terminal_run() { # <axi-status-output> <run-id>
1523
+ local out=$1 expected_id=$2 run_id outcome
1524
+ run_id=$(fm_nm_strip_quotes "$(fm_nm_field "$out" id)")
1525
+ [ "$run_id" = "$expected_id" ] || return 1
1526
+ outcome=$(fm_nm_strip_quotes "$(fm_nm_field "$out" outcome)")
1527
+ case "$outcome" in
1528
+ cancelled|failed|passed|checks-passed) return 0 ;;
1529
+ esac
1530
+ return 1
1531
+ }
1532
+
1533
+ task_status_is_run_not_found() { # <status-error> <run-id>
1534
+ local actual expected
1535
+ actual=$(fm_nm_trim "$1")
1536
+ expected=$(printf 'error: "run \\"%s\\" not found"' "$2")
1537
+ [ "$actual" = "$expected" ]
1538
+ }
1539
+
1540
+ # Abort THIS task's own parked no-mistakes run before the worker that would
1541
+ # have answered its gate is removed, so no run is left orphaned holding a
1542
+ # fleet slot. Only KIND=ship drives a no-mistakes validation of its own
1543
+ # worktree (scouts and secondmates never do, mirroring bin/fm-crew-state.sh);
1544
+ # a run not attributed to this exact branch+head is left completely alone.
1545
+ conclude_task_no_mistakes_run() { # <worktree>
1546
+ local wt=$1 out run_id
1547
+ [ "$KIND" = ship ] || return 0
1548
+ [ -d "$wt" ] || return 0
1549
+ command -v no-mistakes >/dev/null 2>&1 || return 0
1550
+ task_run_is_own_parked_run "$wt" || return 0
1551
+ run_id=$TASK_RUN_ID
1552
+ echo "teardown: no-mistakes run for $ID is parked at a gate; aborting before the worker is removed" >&2
1553
+ # Accepted best-effort residual: abort supports run-id targeting but no atomic
1554
+ # live-state condition; fully closing the resume race needs upstream compare-and-cancel.
1555
+ fm_nm_run_checked "$wt" "$NM_TEARDOWN_TIMEOUT" axi abort --run "$run_id" >/dev/null 2>&1 || true
1556
+ if out=$(fm_nm_run_bounded "$wt" "$NM_TEARDOWN_TIMEOUT" axi status --run "$run_id" 2>&1); then
1557
+ task_status_is_terminal_run "$out" "$run_id" && return 0
1558
+ elif task_status_is_run_not_found "$out" "$run_id"; then
1559
+ return 0
1560
+ fi
1561
+ echo "REFUSED: no-mistakes run for $ID is still parked after axi abort; confirm it stopped (no-mistakes axi status) or abort it manually (no-mistakes axi abort --run <id>) before retrying teardown." >&2
1562
+ return 1
1563
+ }
1564
+
1565
+ # Fix 2 (see script header): pids of every process whose CURRENT WORKING
1566
+ # DIRECTORY is exactly $1 or under it, from one bounded system-wide `lsof -a
1567
+ # -d cwd` scan (never the recursive +D file-tree walk, which lsof itself
1568
+ # documents as slow). Never $$ (this script's own pid). Empty output when
1569
+ # nothing matches; failure means the scan could not establish a safe result.
1570
+ pids_with_cwd_under() { # <dir>
1571
+ local dir=$1 out pid path line
1572
+ [ -n "$dir" ] && [ -d "$dir" ] || return 0
1573
+ dir=$(cd "$dir" && pwd -P) || return 1
1574
+ out=$(lsof -a -d cwd -Fpn 2>/dev/null) || return 1
1575
+ [ -n "$out" ] || return 0
1576
+ pid=
1577
+ while IFS= read -r line; do
1578
+ case "$line" in
1579
+ p*)
1580
+ pid=${line#p}
1581
+ case "$pid" in ''|*[!0-9]*) return 1 ;; esac
1582
+ ;;
1583
+ fcwd) [ -n "$pid" ] || return 1 ;;
1584
+ n*)
1585
+ [ -n "$pid" ] || return 1
1586
+ path=${line#n}
1587
+ case "$path" in
1588
+ "$dir"|"$dir"/*)
1589
+ [ -n "$pid" ] && [ "$pid" != "$$" ] && printf '%s\n' "$pid"
1590
+ ;;
1591
+ esac
1592
+ ;;
1593
+ '') ;;
1594
+ *) return 1 ;;
1595
+ esac
1596
+ done <<EOF
1597
+ $out
1598
+ EOF
1599
+ }
1600
+
1601
+ task_process_identity() { # <pid>
1602
+ local pid=$1 proc_root stat_line starttime value
1603
+ local -a stat_fields
1604
+ proc_root=${FM_PROC_ROOT_OVERRIDE:-/proc}
1605
+ if [ -r "$proc_root/$pid/stat" ]; then
1606
+ stat_line=$(cat "$proc_root/$pid/stat" 2>/dev/null) || return 1
1607
+ read -r -a stat_fields <<< "${stat_line##*)}"
1608
+ [ "${#stat_fields[@]}" -ge 20 ] || return 1
1609
+ starttime=${stat_fields[19]}
1610
+ case "$starttime" in ''|*[!0-9]*) return 1 ;; esac
1611
+ printf 'starttime=%s\n' "$starttime"
1612
+ return 0
1613
+ fi
1614
+ value=$(LC_ALL=C ps -p "$pid" -o lstart= 2>/dev/null) || return 1
1615
+ value=$(fm_nm_trim "$value")
1616
+ [ -n "$value" ] || return 1
1617
+ case "$value" in *$'\n'*|*$'\r'*) return 1 ;; esac
1618
+ printf 'lstart=%s\n' "$value"
1619
+ }
1620
+
1621
+ task_process_identity_matches() { # <pid> <identity>
1622
+ local current
1623
+ current=$(task_process_identity "$1") || return 1
1624
+ [ "$current" = "$2" ]
1625
+ }
1626
+
1627
+ task_pid_list_contains() { # <pid-list> <pid>
1628
+ printf '%s\n' "$1" | grep -Fxq "$2"
1629
+ }
1630
+
1631
+ task_pids_under_roots() { # <dir>...
1632
+ TASK_PIDS=
1633
+ TASK_PIDS_FAILED_DIR=
1634
+ local dir dir_pids pids=""
1635
+ for dir in "$@"; do
1636
+ [ -n "$dir" ] || continue
1637
+ if ! dir_pids=$(pids_with_cwd_under "$dir"); then
1638
+ TASK_PIDS_FAILED_DIR=$dir
1639
+ return 1
1640
+ fi
1641
+ pids="$pids
1642
+ $dir_pids"
1643
+ done
1644
+ TASK_PIDS=$(printf '%s\n' "$pids" | grep -E '^[0-9]+$' | sort -un || true)
1645
+ }
1646
+
1647
+ reap_task_backend_process_group() { # <label>
1648
+ local label=$1 leader leader_start pgid current_pgid own_pgid
1649
+ if [ "$BACKEND" != tmux ]; then
1650
+ echo "warning: lsof is unavailable; cannot resolve a process-group fallback for $BACKEND task $ID" >&2
1651
+ return 0
1652
+ fi
1653
+ leader=$(tmux display-message -p -t "$T" '#{pane_pid}' 2>/dev/null) || leader=""
1654
+ case "$leader" in ''|*[!0-9]*)
1655
+ echo "warning: lsof is unavailable; cannot resolve the tmux pane process group for $ID" >&2
1656
+ return 0
1657
+ ;;
1658
+ esac
1659
+ leader_start=$(task_process_identity "$leader") || {
1660
+ echo "warning: lsof is unavailable; cannot identify the tmux pane process group for $ID" >&2
1661
+ return 0
1662
+ }
1663
+ pgid=$(ps -o pgid= -p "$leader" 2>/dev/null) || pgid=""
1664
+ pgid=$(printf '%s' "$pgid" | tr -d '[:space:]')
1665
+ case "$pgid" in ''|*[!0-9]*|0|1)
1666
+ echo "warning: lsof is unavailable; cannot resolve the tmux pane process group for $ID" >&2
1667
+ return 0
1668
+ ;;
1669
+ esac
1670
+ own_pgid=$(ps -o pgid= -p "$$" 2>/dev/null) || own_pgid=""
1671
+ own_pgid=$(printf '%s' "$own_pgid" | tr -d '[:space:]')
1672
+ if [ "$pgid" = "$own_pgid" ]; then
1673
+ echo "warning: lsof is unavailable; refusing to signal teardown's own process group for $ID" >&2
1674
+ return 0
1675
+ fi
1676
+ task_process_identity_matches "$leader" "$leader_start" || return 0
1677
+ current_pgid=$(ps -o pgid= -p "$leader" 2>/dev/null) || current_pgid=""
1678
+ current_pgid=$(printf '%s' "$current_pgid" | tr -d '[:space:]')
1679
+ [ "$current_pgid" = "$pgid" ] || return 0
1680
+ echo "teardown: reaping leaked $label process group for $ID: $pgid" >&2
1681
+ kill -TERM -- "-$pgid" 2>/dev/null || true
1682
+ sleep 1
1683
+ if task_process_identity_matches "$leader" "$leader_start" \
1684
+ && [ "$(ps -o pgid= -p "$leader" 2>/dev/null | tr -d '[:space:]')" = "$pgid" ] \
1685
+ && kill -0 -- "-$pgid" 2>/dev/null; then
1686
+ echo "teardown: force-killing leaked $label process group for $ID: $pgid" >&2
1687
+ kill -KILL -- "-$pgid" 2>/dev/null || true
1688
+ fi
1689
+ }
1690
+
1691
+ # Reap every process rooted (by cwd) under this task's own worktree or tasktmp
1692
+ # - both unique per task and never shared - before either is removed. TERM
1693
+ # first, then KILL after a short grace period for anything still alive; a
1694
+ # process that exits on its own between the two passes is simply absent from
1695
+ # the recheck. A missing lsof uses the backend process-group fallback; an lsof
1696
+ # scan error refuses before destructive teardown.
1697
+ reap_task_worktree_processes() { # <label> <dir>...
1698
+ local label=$1 pids pid identity current_pids i pass=1 max_passes=3
1699
+ local -a tracked_pids tracked_identities remaining_pids remaining_identities
1700
+ shift
1701
+ if ! command -v lsof >/dev/null 2>&1; then
1702
+ reap_task_backend_process_group "$label"
1703
+ return 0
1704
+ fi
1705
+ while [ "$pass" -le "$max_passes" ]; do
1706
+ if ! task_pids_under_roots "$@"; then
1707
+ echo "REFUSED: cannot determine leaked processes under ${TASK_PIDS_FAILED_DIR:-<missing>} for $ID (lsof failed); preserving the worktree/tasktmp for manual inspection or retry." >&2
1708
+ return 1
1709
+ fi
1710
+ pids=$TASK_PIDS
1711
+ [ -n "$pids" ] || return 0
1712
+ tracked_pids=()
1713
+ tracked_identities=()
1714
+ while IFS= read -r pid; do
1715
+ [ -n "$pid" ] || continue
1716
+ if ! identity=$(task_process_identity "$pid"); then
1717
+ if ! task_pids_under_roots "$@"; then
1718
+ echo "REFUSED: cannot determine leaked processes under ${TASK_PIDS_FAILED_DIR:-<missing>} for $ID (lsof failed); preserving the worktree/tasktmp for manual inspection or retry." >&2
1719
+ return 1
1720
+ fi
1721
+ if task_pid_list_contains "$TASK_PIDS" "$pid"; then
1722
+ echo "REFUSED: cannot verify leaked process $pid identity for $ID; preserving the worktree/tasktmp for manual inspection or retry." >&2
1723
+ return 1
1724
+ fi
1725
+ continue
1726
+ fi
1727
+ tracked_pids+=("$pid")
1728
+ tracked_identities+=("$identity")
1729
+ done <<EOF
1730
+ $pids
1731
+ EOF
1732
+ if [ "${#tracked_pids[@]}" -eq 0 ]; then
1733
+ pass=$((pass + 1))
1734
+ continue
1735
+ fi
1736
+ if ! task_pids_under_roots "$@"; then
1737
+ echo "REFUSED: cannot determine leaked processes under ${TASK_PIDS_FAILED_DIR:-<missing>} for $ID (lsof failed); preserving the worktree/tasktmp for manual inspection or retry." >&2
1738
+ return 1
1739
+ fi
1740
+ current_pids=$TASK_PIDS
1741
+ echo "teardown: reaping leaked $label process(es) for $ID: $(printf '%s' "$pids" | tr '\n' ' ')" >&2
1742
+ for i in "${!tracked_pids[@]}"; do
1743
+ pid=${tracked_pids[$i]}
1744
+ identity=${tracked_identities[$i]}
1745
+ if task_pid_list_contains "$current_pids" "$pid" \
1746
+ && task_process_identity_matches "$pid" "$identity"; then
1747
+ kill -TERM "$pid" 2>/dev/null || true
1748
+ fi
1749
+ done
1750
+ sleep 1
1751
+ if ! task_pids_under_roots "$@"; then
1752
+ echo "REFUSED: cannot determine leaked processes under ${TASK_PIDS_FAILED_DIR:-<missing>} for $ID (lsof failed); preserving the worktree/tasktmp for manual inspection or retry." >&2
1753
+ return 1
1754
+ fi
1755
+ current_pids=$TASK_PIDS
1756
+ remaining_pids=()
1757
+ remaining_identities=()
1758
+ for i in "${!tracked_pids[@]}"; do
1759
+ pid=${tracked_pids[$i]}
1760
+ identity=${tracked_identities[$i]}
1761
+ if task_pid_list_contains "$current_pids" "$pid" \
1762
+ && task_process_identity_matches "$pid" "$identity"; then
1763
+ remaining_pids+=("$pid")
1764
+ remaining_identities+=("$identity")
1765
+ fi
1766
+ done
1767
+ if [ "${#remaining_pids[@]}" -gt 0 ]; then
1768
+ echo "teardown: force-killing leaked $label process(es) for $ID: ${remaining_pids[*]}" >&2
1769
+ if ! task_pids_under_roots "$@"; then
1770
+ echo "REFUSED: cannot determine leaked processes under ${TASK_PIDS_FAILED_DIR:-<missing>} for $ID (lsof failed); preserving the worktree/tasktmp for manual inspection or retry." >&2
1771
+ return 1
1772
+ fi
1773
+ current_pids=$TASK_PIDS
1774
+ for i in "${!remaining_pids[@]}"; do
1775
+ pid=${remaining_pids[$i]}
1776
+ identity=${remaining_identities[$i]}
1777
+ if task_pid_list_contains "$current_pids" "$pid" \
1778
+ && task_process_identity_matches "$pid" "$identity"; then
1779
+ kill -KILL "$pid" 2>/dev/null || true
1780
+ fi
1781
+ done
1782
+ fi
1783
+ pass=$((pass + 1))
1784
+ done
1785
+ if ! task_pids_under_roots "$@"; then
1786
+ echo "REFUSED: cannot determine leaked processes under ${TASK_PIDS_FAILED_DIR:-<missing>} for $ID (lsof failed); preserving the worktree/tasktmp for manual inspection or retry." >&2
1787
+ return 1
1788
+ fi
1789
+ [ -z "$TASK_PIDS" ] && return 0
1790
+ echo "REFUSED: leaked $label processes for $ID remain after $max_passes reap attempts; preserving the worktree/tasktmp for manual inspection or retry." >&2
1791
+ return 1
1792
+ }
1793
+
1794
+ require_orca_worktree_path_match() {
1795
+ local worktree_id=$1 inspected=$2 resolved inspected_abs resolved_abs
1796
+ resolved=$(fm_backend_worktree_path orca "$worktree_id") || {
1797
+ echo "REFUSED: cannot resolve Orca worktree id $worktree_id to a path; preserving metadata." >&2
1798
+ return 1
1799
+ }
1800
+ inspected_abs=$(canonical_existing_dir "$inspected") || {
1801
+ echo "REFUSED: cannot canonicalize inspected worktree ${inspected:-<missing>}; preserving metadata." >&2
1802
+ return 1
1803
+ }
1804
+ resolved_abs=$(canonical_existing_dir "$resolved") || {
1805
+ echo "REFUSED: Orca worktree id $worktree_id resolved to uninspectable path ${resolved:-<missing>}; preserving metadata." >&2
1806
+ return 1
1807
+ }
1808
+ if [ "$resolved_abs" != "$inspected_abs" ]; then
1809
+ echo "REFUSED: Orca worktree id $worktree_id resolves to $resolved_abs, not inspected worktree $inspected_abs." >&2
1810
+ echo "Cannot verify dirty or unlanded work for the worktree Orca would remove; preserving metadata." >&2
1811
+ return 1
1812
+ fi
1813
+ }
1814
+
1815
+ require_orca_worktree_path_match_if_present() {
1816
+ local worktree_id=$1 inspected=$2
1817
+ [ -n "$inspected" ] && [ -e "$inspected" ] || return 0
1818
+ require_orca_worktree_path_match "$worktree_id" "$inspected"
1819
+ }
1820
+
1821
+ firstmate_home_has_treehouse_slot() {
1822
+ local home=$1
1823
+ worktree_registered_for_project "$FM_ROOT" "$home"
1824
+ }
1825
+
1826
+ validate_removal_target() {
1827
+ local target=$1 label=$2 abs_target abs_home abs_root
1828
+ [ -n "$target" ] || return 0
1829
+ [ -e "$target" ] || return 0
1830
+ abs_target=$(removal_target_abs_path "$target")
1831
+ if abs_home=$(cd "$FM_HOME" 2>/dev/null && pwd -P); then
1832
+ :
1833
+ else
1834
+ abs_home=
1835
+ fi
1836
+ abs_root=$(cd "$FM_ROOT" && pwd -P)
1837
+ case "$abs_target" in
1838
+ ''|/) echo "REFUSED: unsafe $label removal target $target" >&2; return 1 ;;
1839
+ esac
1840
+ if [ -n "$abs_home" ] && [ "$abs_target" = "$abs_home" ]; then
1841
+ echo "REFUSED: unsafe $label removal target $target is the active firstmate home" >&2
1842
+ return 1
1843
+ fi
1844
+ if [ "$abs_target" = "$abs_root" ]; then
1845
+ echo "REFUSED: unsafe $label removal target $target is the firstmate repo" >&2
1846
+ return 1
1847
+ fi
1848
+ if [ -n "$abs_home" ] && path_is_ancestor_of "$abs_target" "$abs_home"; then
1849
+ echo "REFUSED: unsafe $label removal target $target is an ancestor of the active firstmate home" >&2
1850
+ return 1
1851
+ fi
1852
+ if path_is_ancestor_of "$abs_target" "$abs_root"; then
1853
+ echo "REFUSED: unsafe $label removal target $target is an ancestor of the firstmate repo" >&2
1854
+ return 1
1855
+ fi
1856
+ if [ -n "$abs_home" ] && path_is_ancestor_of "$abs_home" "$abs_target"; then
1857
+ echo "REFUSED: unsafe $label removal target $target is inside the active firstmate home" >&2
1858
+ return 1
1859
+ fi
1860
+ if path_is_ancestor_of "$abs_root" "$abs_target"; then
1861
+ echo "REFUSED: unsafe $label removal target $target is inside the firstmate repo" >&2
1862
+ return 1
1863
+ fi
1864
+ printf '%s\n' "$abs_target"
1865
+ }
1866
+
1867
+ registered_descendant_home_for_removal() {
1868
+ local reg=$1 target=$2 line id registered_home registered_abs
1869
+ [ -f "$reg" ] || return 1
1870
+ if ! secondmate_registry_validate_bindings "$reg" secondmate_registry_path_key; then
1871
+ echo "REFUSED: $SECONDMATE_REGISTRY_ERROR" >&2
1872
+ return 2
1873
+ fi
1874
+ while IFS= read -r line || [ -n "$line" ]; do
1875
+ case "$line" in
1876
+ "- "*)
1877
+ secondmate_registry_parse_line "$line" || {
1878
+ echo "REFUSED: malformed secondmate registry entry: $line" >&2
1879
+ return 2
1880
+ }
1881
+ id=$SECONDMATE_REGISTRY_ID
1882
+ registered_home=$SECONDMATE_REGISTRY_HOME
1883
+ registered_abs=$(removal_target_abs_path "$registered_home" 2>/dev/null || true)
1884
+ [ -n "$registered_abs" ] || continue
1885
+ [ "$registered_abs" = "$target" ] && continue
1886
+ if path_is_ancestor_of "$target" "$registered_abs"; then
1887
+ printf '%s\t%s\n' "$id" "$registered_abs"
1888
+ return 0
1889
+ fi
1890
+ ;;
1891
+ esac
1892
+ done < "$reg"
1893
+ return 1
1894
+ }
1895
+
1896
+ validate_firstmate_operational_dirs_for_removal() {
1897
+ local home=$1 label=$2 name dir abs_home abs_dir
1898
+ abs_home=$(removal_target_abs_path "$home")
1899
+ for name in data state config projects; do
1900
+ dir="$home/$name"
1901
+ [ -e "$dir" ] || [ -L "$dir" ] || continue
1902
+ if [ -L "$dir" ] && [ ! -e "$dir" ]; then
1903
+ echo "REFUSED: unsafe $label $name directory $dir resolves outside the secondmate home" >&2
1904
+ return 1
1905
+ fi
1906
+ if [ -d "$dir" ]; then
1907
+ abs_dir=$(cd "$dir" && pwd -P)
1908
+ elif [ -e "$dir" ]; then
1909
+ echo "REFUSED: unsafe $label $name path $dir is not a directory" >&2
1910
+ return 1
1911
+ else
1912
+ abs_dir=
1913
+ fi
1914
+ if [ -z "$abs_dir" ] || ! path_is_ancestor_of "$abs_home" "$abs_dir"; then
1915
+ echo "REFUSED: unsafe $label $name directory $dir resolves outside the secondmate home" >&2
1916
+ return 1
1917
+ fi
1918
+ done
1919
+ }
1920
+
1921
+ validate_child_worktree_for_removal() {
1922
+ local target=$1 project=$2 abs_target abs_home abs_root
1923
+ [ -n "$target" ] || return 0
1924
+ [ -e "$target" ] || return 0
1925
+ abs_target=$(validate_removal_target "$target" "child worktree") || return 1
1926
+ if abs_home=$(cd "$FM_HOME" 2>/dev/null && pwd -P); then
1927
+ if path_is_ancestor_of "$abs_home" "$abs_target"; then
1928
+ echo "REFUSED: unsafe child worktree removal target $target is inside the active firstmate home" >&2
1929
+ return 1
1930
+ fi
1931
+ fi
1932
+ abs_root=$(cd "$FM_ROOT" && pwd -P)
1933
+ if path_is_ancestor_of "$abs_root" "$abs_target"; then
1934
+ echo "REFUSED: unsafe child worktree removal target $target is inside the firstmate repo" >&2
1935
+ return 1
1936
+ fi
1937
+ if ! worktree_registered_for_project "$project" "$target"; then
1938
+ echo "REFUSED: unsafe child worktree removal target $target is not a git worktree for ${project:-the recorded project}" >&2
1939
+ return 1
1940
+ fi
1941
+ printf '%s\n' "$abs_target"
1942
+ }
1943
+
1944
+ safe_rm_rf() {
1945
+ local target=$1 label=$2
1946
+ validate_removal_target "$target" "$label" >/dev/null || return 1
1947
+ rm -rf -- "$target"
1948
+ }
1949
+
1950
+ safe_rm_rf_child_worktree() {
1951
+ local target=$1 project=$2
1952
+ validate_child_worktree_for_removal "$target" "$project" >/dev/null || return 1
1953
+ rm -rf -- "$target"
1954
+ }
1955
+
1956
+ validate_firstmate_home_for_removal() {
1957
+ local home=$1 label=$2 expected_id=${3:-} abs_home_path marker_id conflict child_id child_home
1958
+ [ -n "$home" ] || return 0
1959
+ [ -e "$home" ] || return 0
1960
+ abs_home_path=$(validate_removal_target "$home" "$label") || return 1
1961
+ if [ ! -f "$abs_home_path/$SUB_HOME_MARKER" ]; then
1962
+ echo "REFUSED: unsafe $label removal target $home is not a seeded secondmate home" >&2
1963
+ return 1
1964
+ fi
1965
+ if [ -n "$expected_id" ]; then
1966
+ marker_id=$(cat "$abs_home_path/$SUB_HOME_MARKER" 2>/dev/null || true)
1967
+ if [ "$marker_id" != "$expected_id" ]; then
1968
+ echo "REFUSED: unsafe $label removal target $home is marked for secondmate ${marker_id:-unknown}, expected $expected_id" >&2
1969
+ return 1
1970
+ fi
1971
+ if [ -e "$SECONDMATE_REG" ] || [ -L "$SECONDMATE_REG" ]; then
1972
+ if ! secondmate_registry_validate_bindings "$SECONDMATE_REG" secondmate_registry_path_key "$expected_id" "$abs_home_path"; then
1973
+ case "$SECONDMATE_REGISTRY_ERROR" in
1974
+ overlapping\ secondmate\ home\ assignment:*)
1975
+ echo "REFUSED: unsafe $label removal target $home contains registered secondmate home; $SECONDMATE_REGISTRY_ERROR" >&2
1976
+ ;;
1977
+ *) echo "REFUSED: $SECONDMATE_REGISTRY_ERROR" >&2 ;;
1978
+ esac
1979
+ return 1
1980
+ fi
1981
+ fi
1982
+ fi
1983
+ validate_firstmate_operational_dirs_for_removal "$abs_home_path" "$label" || return 1
1984
+ conflict=
1985
+ if conflict=$(registered_descendant_home_for_removal "$SECONDMATE_REG" "$abs_home_path"); then
1986
+ :
1987
+ else
1988
+ conflict_rc=$?
1989
+ [ "$conflict_rc" -eq 1 ] || return 1
1990
+ fi
1991
+ if [ -z "$conflict" ]; then
1992
+ if conflict=$(registered_descendant_home_for_removal "$abs_home_path/data/secondmates.md" "$abs_home_path"); then
1993
+ :
1994
+ else
1995
+ conflict_rc=$?
1996
+ [ "$conflict_rc" -eq 1 ] || return 1
1997
+ fi
1998
+ fi
1999
+ if [ -n "$conflict" ]; then
2000
+ IFS=$'\t' read -r child_id child_home <<EOF
2001
+ $conflict
2002
+ EOF
2003
+ echo "REFUSED: unsafe $label removal target $home contains registered secondmate home $child_home for $child_id" >&2
2004
+ return 1
2005
+ fi
2006
+ printf '%s\n' "$abs_home_path"
2007
+ }
2008
+
2009
+ remove_firstmate_home() {
2010
+ local home=$1 label=$2 expected_id=${3:-} abs_home_path process_event_backup
2011
+ [ -n "$home" ] || return 0
2012
+ [ -e "$home" ] || return 0
2013
+ abs_home_path=$(validate_firstmate_home_for_removal "$home" "$label" "$expected_id") || return 1
2014
+ [ -n "$abs_home_path" ] || return 0
2015
+ process_event_backup=$(snapshot_firstmate_home_process_events "$abs_home_path" "$label") || return 1
2016
+ if ! cleanup_firstmate_home_process_events "$abs_home_path" "$label"; then
2017
+ restore_firstmate_home_process_events "$abs_home_path" "$label" "$process_event_backup" || return $?
2018
+ return 1
2019
+ fi
2020
+ if firstmate_home_has_treehouse_slot "$abs_home_path"; then
2021
+ command -v treehouse >/dev/null 2>&1 || {
2022
+ echo "error: treehouse command not found; cannot return $label $abs_home_path" >&2
2023
+ restore_firstmate_home_process_events "$abs_home_path" "$label" "$process_event_backup" || return $?
2024
+ return 1
2025
+ }
2026
+ teardown_treehouse_return "$abs_home_path" "$FM_ROOT" "$label" || {
2027
+ echo "error: treehouse return failed for $label $abs_home_path; lease may still be held" >&2
2028
+ restore_firstmate_home_process_events "$abs_home_path" "$label" "$process_event_backup" || return $?
2029
+ return 1
2030
+ }
2031
+ [ -z "$process_event_backup" ] || rm -rf -- "$process_event_backup"
2032
+ return 0
2033
+ fi
2034
+ if safe_rm_rf "$abs_home_path" "$label"; then
2035
+ [ -z "$process_event_backup" ] || rm -rf -- "$process_event_backup"
2036
+ return 0
2037
+ fi
2038
+ restore_firstmate_home_process_events "$abs_home_path" "$label" "$process_event_backup" || return $?
2039
+ return 1
2040
+ }
2041
+
2042
+ firstmate_home_has_process_events() {
2043
+ local home=$1 path owner claim_root
2044
+ for path in "$home/state/procevent"/*.source "$home/state/procevent"/*.runner; do
2045
+ if [ -e "$path" ] || [ -L "$path" ]; then
2046
+ return 0
2047
+ fi
2048
+ done
2049
+ claim_root=${FM_PROCEVENT_CLAIM_ROOT:-${XDG_STATE_HOME:-$HOME/.local/state}/firstmate/procevent-claims}
2050
+ for path in "$claim_root"/*.claim; do
2051
+ [ -f "$path" ] && [ ! -L "$path" ] || continue
2052
+ IFS= read -r owner < "$path" 2>/dev/null || continue
2053
+ [ "$owner" = "$home" ] && return 0
2054
+ done
2055
+ return 1
2056
+ }
2057
+
2058
+ snapshot_firstmate_home_process_events() {
2059
+ local home=$1 label=$2 backup path
2060
+ if ! firstmate_home_has_process_events "$home"; then
2061
+ printf '\n'
2062
+ return 0
2063
+ fi
2064
+ backup=$(umask 077; mktemp -d "${home%/*}/.fm-procevent-restore.XXXXXX") || {
2065
+ echo "REFUSED: cannot stage recoverable process-event state for $label $home" >&2
2066
+ return 1
2067
+ }
2068
+ for path in "$home/state/procevent"/*.source; do
2069
+ [ -e "$path" ] || continue
2070
+ if [ ! -f "$path" ] || [ -L "$path" ] || ! cp -p -- "$path" "$backup/"; then
2071
+ rm -rf -- "$backup"
2072
+ echo "REFUSED: cannot preserve process-event registrations for $label $home" >&2
2073
+ return 1
2074
+ fi
2075
+ done
2076
+ printf '%s\n' "$backup"
2077
+ }
2078
+
2079
+ restore_firstmate_home_process_events() {
2080
+ local home=$1 label=$2 backup=$3 reg source tmp runner
2081
+ [ -n "$backup" ] || return 0
2082
+ [ -d "$backup" ] && [ ! -L "$backup" ] || {
2083
+ echo "error: process-event restoration failed for $label $home; recovery backup is unavailable at $backup" >&2
2084
+ return "$TEARDOWN_PROCEVENT_RESTORE_FAILED"
2085
+ }
2086
+ reg="$home/state/procevent"
2087
+ (umask 077; mkdir -p "$reg") || {
2088
+ echo "error: process-event restoration failed for $label $home; recover registrations from $backup" >&2
2089
+ return "$TEARDOWN_PROCEVENT_RESTORE_FAILED"
2090
+ }
2091
+ [ -d "$reg" ] && [ ! -L "$reg" ] || {
2092
+ echo "error: process-event restoration failed for $label $home; recover registrations from $backup" >&2
2093
+ return "$TEARDOWN_PROCEVENT_RESTORE_FAILED"
2094
+ }
2095
+ for source in "$backup"/*.source; do
2096
+ [ -e "$source" ] || continue
2097
+ [ -f "$source" ] && [ ! -L "$source" ] || {
2098
+ echo "error: process-event restoration failed for $label $home; recover registrations from $backup" >&2
2099
+ return "$TEARDOWN_PROCEVENT_RESTORE_FAILED"
2100
+ }
2101
+ tmp=$(umask 077; mktemp "$reg/.restore.XXXXXX") || {
2102
+ echo "error: process-event restoration failed for $label $home; recover registrations from $backup" >&2
2103
+ return "$TEARDOWN_PROCEVENT_RESTORE_FAILED"
2104
+ }
2105
+ if ! cp -- "$source" "$tmp" || ! chmod 0600 "$tmp" || ! mv -f -- "$tmp" "$reg/${source##*/}"; then
2106
+ rm -f -- "$tmp"
2107
+ echo "error: process-event restoration failed for $label $home; recover registrations from $backup" >&2
2108
+ return "$TEARDOWN_PROCEVENT_RESTORE_FAILED"
2109
+ fi
2110
+ done
2111
+ runner="$home/bin/fm-procevent.sh"
2112
+ if [ ! -f "$runner" ] || [ -L "$runner" ] || [ ! -x "$runner" ]; then
2113
+ runner="$SCRIPT_DIR/fm-procevent.sh"
2114
+ fi
2115
+ if ! FM_HOME="$home" FM_ROOT_OVERRIDE="$FM_ROOT" "$runner" reconcile >/dev/null; then
2116
+ echo "error: process-event restoration could not rearm $label $home; active waits may remain retired; recover registrations from $backup" >&2
2117
+ return "$TEARDOWN_PROCEVENT_RESTORE_FAILED"
2118
+ fi
2119
+ rm -rf -- "$backup"
2120
+ }
2121
+
2122
+ cleanup_firstmate_home_process_events() {
2123
+ local home=$1 label=$2 runner="$1/bin/fm-procevent.sh"
2124
+ firstmate_home_has_process_events "$home" || return 0
2125
+ if [ ! -f "$runner" ] || [ -L "$runner" ] || [ ! -x "$runner" ]; then
2126
+ echo "REFUSED: $label $home has process-event state but no sweep-capable bin/fm-procevent.sh; restore the home script and rerun teardown" >&2
2127
+ return 1
2128
+ fi
2129
+ if ! FM_HOME="$home" FM_ROOT_OVERRIDE="$home" "$runner" sweep-home; then
2130
+ echo "REFUSED: process-event cleanup is incomplete for $label $home; preserving the home, lease, and retirement records for retry" >&2
2131
+ return 1
2132
+ fi
2133
+ if firstmate_home_has_process_events "$home"; then
2134
+ echo "REFUSED: process-event state remains for $label $home after its bounded sweep; preserving the home, lease, and retirement records for retry" >&2
2135
+ return 1
2136
+ fi
2137
+ }
2138
+
2139
+ preflight_firstmate_home_process_events() {
2140
+ local home=$1 label=$2 runner="$1/bin/fm-procevent.sh"
2141
+ firstmate_home_has_process_events "$home" || return 0
2142
+ if [ ! -f "$runner" ] || [ -L "$runner" ] || [ ! -x "$runner" ]; then
2143
+ echo "REFUSED: $label $home has process-event state but no sweep-capable bin/fm-procevent.sh; restore the home script and rerun teardown" >&2
2144
+ return 1
2145
+ fi
2146
+ if ! FM_HOME="$home" FM_ROOT_OVERRIDE="$home" "$runner" sweep-home --preflight >/dev/null; then
2147
+ echo "REFUSED: process-event cleanup cannot safely proceed for $label $home; preserving the home, lease, and retirement records for retry" >&2
2148
+ return 1
2149
+ fi
2150
+ }
2151
+
2152
+ preflight_firstmate_home_process_event_tree() {
2153
+ local home=$1 label=$2 sub_state child_meta child_kind child_home child_wt child_id
2154
+ sub_state="$home/state"
2155
+ if [ -d "$sub_state" ]; then
2156
+ for child_meta in "$sub_state"/*.meta; do
2157
+ [ -e "$child_meta" ] || continue
2158
+ child_kind=$(meta_value "$child_meta" kind)
2159
+ [ "$child_kind" = secondmate ] || continue
2160
+ child_id=$(basename "$child_meta" .meta)
2161
+ child_wt=$(meta_value "$child_meta" worktree)
2162
+ child_home=$(meta_value "$child_meta" home)
2163
+ [ -n "$child_home" ] || child_home=$child_wt
2164
+ preflight_firstmate_home_process_event_tree "$child_home" "child firstmate home for $child_id" || return 1
2165
+ done
2166
+ fi
2167
+ preflight_firstmate_home_process_events "$home" "$label"
2168
+ }
2169
+
2170
+ collect_descendant_task_locks() {
2171
+ local home=$1 sub_state child_meta child_id child_kind child_wt child_home task_set_lock
2172
+ local -a child_ids
2173
+ sub_state="$home/state"
2174
+ if [ -L "$sub_state" ]; then
2175
+ echo "REFUSED: secondmate home $home has a symbolic-link state path at $sub_state; forced teardown changed nothing" >&2
2176
+ return 1
2177
+ fi
2178
+ if [ -e "$sub_state" ] && [ ! -d "$sub_state" ]; then
2179
+ echo "REFUSED: secondmate home $home has a non-directory state path at $sub_state; forced teardown changed nothing" >&2
2180
+ return 1
2181
+ fi
2182
+ if ! mkdir -p -- "$sub_state"; then
2183
+ echo "REFUSED: secondmate home $home state directory could not be established at $sub_state; forced teardown changed nothing" >&2
2184
+ return 1
2185
+ fi
2186
+ if [ -L "$sub_state" ] || [ ! -d "$sub_state" ]; then
2187
+ echo "REFUSED: secondmate home $home state path is not a safe directory at $sub_state; forced teardown changed nothing" >&2
2188
+ return 1
2189
+ fi
2190
+ # Freeze this home's task SET before reading it. Everything below locks the
2191
+ # tasks that exist right now, but the later cleanup re-enumerates, so without
2192
+ # this a fresh spawn could publish a record into the gap and be mutated
2193
+ # without ever having been lifecycle-locked (bin/fm-wake-lib.sh's
2194
+ # fm_task_set_lock_path owns why). Taken per home, parent before child, and
2195
+ # held until this teardown exits.
2196
+ task_set_lock=$(fm_task_set_lock_path "$sub_state") || {
2197
+ echo "REFUSED: secondmate home $home has an invalid task-set lock path; forced teardown changed nothing" >&2
2198
+ return 1
2199
+ }
2200
+ if ! fm_lock_try_acquire "$task_set_lock"; then
2201
+ echo "REFUSED: secondmate home $home is publishing a task right now (task-set lock is held); forced teardown changed nothing" >&2
2202
+ return 1
2203
+ fi
2204
+ DESCENDANT_LOCK_PATHS+=("$task_set_lock")
2205
+ child_ids=()
2206
+ for child_meta in "$sub_state"/*.meta; do
2207
+ [ -e "$child_meta" ] || continue
2208
+ child_ids+=("$(basename "$child_meta" .meta)")
2209
+ done
2210
+ [ "${#child_ids[@]}" -gt 0 ] || return 0
2211
+ while IFS= read -r child_id; do
2212
+ child_meta="$sub_state/$child_id.meta"
2213
+ child_kind=$(meta_value "$child_meta" kind)
2214
+ [ -n "$child_kind" ] || child_kind=ship
2215
+ child_home=
2216
+ if [ "$child_kind" = secondmate ]; then
2217
+ child_wt=$(meta_value "$child_meta" worktree)
2218
+ child_home=$(meta_value "$child_meta" home)
2219
+ [ -n "$child_home" ] || child_home=$child_wt
2220
+ fi
2221
+ DESCENDANT_TASK_STATES+=("$sub_state")
2222
+ DESCENDANT_TASK_IDS+=("$child_id")
2223
+ DESCENDANT_TASK_KINDS+=("$child_kind")
2224
+ DESCENDANT_TASK_HOMES+=("$child_home")
2225
+ [ "$child_kind" != secondmate ] \
2226
+ || collect_descendant_task_locks "$child_home" \
2227
+ || return 1
2228
+ done < <(printf '%s\n' "${child_ids[@]}" | LC_ALL=C sort)
2229
+ }
2230
+
2231
+ preflight_descendant_task_locks() {
2232
+ local home=$1 i state task_id meta control_lock meta_lock kind child_wt child_home
2233
+ DESCENDANT_TASK_STATES=()
2234
+ DESCENDANT_TASK_IDS=()
2235
+ DESCENDANT_TASK_KINDS=()
2236
+ DESCENDANT_TASK_HOMES=()
2237
+ collect_descendant_task_locks "$home" || return 1
2238
+ # Acquisition order, which every other holder of these locks must match so
2239
+ # they cannot cycle: each home's task-set lock first (parent home before child
2240
+ # home, during collection above), then per-task locks in that same
2241
+ # parent-before-child preorder, sorted by id within each home, each control
2242
+ # lock before its matching metadata lock. No child lock holder ever reaches
2243
+ # back for a parent lock. bin/fm-spawn.sh takes the same task-set lock before
2244
+ # its own per-task locks when it publishes a fresh record.
2245
+ for ((i=0; i < ${#DESCENDANT_TASK_IDS[@]}; i++)); do
2246
+ state=${DESCENDANT_TASK_STATES[$i]}
2247
+ task_id=${DESCENDANT_TASK_IDS[$i]}
2248
+ meta="$state/$task_id.meta"
2249
+ control_lock="$state/.control-$task_id.lock"
2250
+ meta_lock=$(fm_meta_lock_path "$meta") || {
2251
+ echo "REFUSED: descendant task $task_id has an invalid metadata lock path; forced teardown changed nothing" >&2
2252
+ return 1
2253
+ }
2254
+ if ! fm_lock_try_acquire "$control_lock"; then
2255
+ echo "REFUSED: descendant task $task_id has a lifecycle action in flight (control lock is held); forced teardown changed nothing" >&2
2256
+ return 1
2257
+ fi
2258
+ DESCENDANT_LOCK_PATHS+=("$control_lock")
2259
+ if ! fm_lock_try_acquire "$meta_lock"; then
2260
+ echo "REFUSED: descendant task $task_id has a metadata update in flight (metadata lock is held); forced teardown changed nothing" >&2
2261
+ return 1
2262
+ fi
2263
+ DESCENDANT_LOCK_PATHS+=("$meta_lock")
2264
+ [ -f "$meta" ] || {
2265
+ echo "REFUSED: descendant task $task_id changed while forced teardown acquired its locks; forced teardown changed nothing" >&2
2266
+ return 1
2267
+ }
2268
+ kind=$(meta_value "$meta" kind)
2269
+ [ -n "$kind" ] || kind=ship
2270
+ [ "$kind" = "${DESCENDANT_TASK_KINDS[$i]}" ] || {
2271
+ echo "REFUSED: descendant task $task_id changed kind while forced teardown acquired its locks; forced teardown changed nothing" >&2
2272
+ return 1
2273
+ }
2274
+ if [ "$kind" = secondmate ]; then
2275
+ child_wt=$(meta_value "$meta" worktree)
2276
+ child_home=$(meta_value "$meta" home)
2277
+ [ -n "$child_home" ] || child_home=$child_wt
2278
+ [ "$child_home" = "${DESCENDANT_TASK_HOMES[$i]}" ] || {
2279
+ echo "REFUSED: descendant task $task_id changed home while forced teardown acquired its locks; forced teardown changed nothing" >&2
2280
+ return 1
2281
+ }
2282
+ fi
2283
+ done
2284
+ }
2285
+
2286
+ validate_firstmate_home_children_removal() {
2287
+ local home=$1 sub_state child_meta child_id child_wt child_proj child_kind child_home child_backend child_orca_worktree_id
2288
+ sub_state="$home/state"
2289
+ [ -d "$sub_state" ] || return 0
2290
+ for child_meta in "$sub_state"/*.meta; do
2291
+ [ -e "$child_meta" ] || continue
2292
+ child_id=$(basename "$child_meta" .meta)
2293
+ fm_backend_validate_task_endpoint "$child_meta" "$child_id" || return 1
2294
+ validate_pr_poll_cleanup "$sub_state" "$child_id" || return 1
2295
+ child_wt=$(meta_value "$child_meta" worktree)
2296
+ child_kind=$(meta_value "$child_meta" kind)
2297
+ [ -n "$child_kind" ] || child_kind=ship
2298
+ child_backend=$(fm_backend_of_meta "$child_meta")
2299
+ if [ "$child_kind" = secondmate ]; then
2300
+ child_home=$(meta_value "$child_meta" home)
2301
+ [ -n "$child_home" ] || child_home=$child_wt
2302
+ validate_firstmate_home_for_removal "$child_home" "child firstmate home" "$child_id" >/dev/null || return 1
2303
+ validate_firstmate_home_children_removal "$child_home" || return 1
2304
+ elif [ "$child_backend" = orca ]; then
2305
+ child_orca_worktree_id=$(require_orca_worktree_id "$child_meta") || return 1
2306
+ if [ -n "$child_wt" ] && [ -e "$child_wt" ]; then
2307
+ child_proj=$(meta_value "$child_meta" project)
2308
+ validate_child_worktree_for_removal "$child_wt" "$child_proj" >/dev/null || return 1
2309
+ require_orca_worktree_path_match "$child_orca_worktree_id" "$child_wt" || return 1
2310
+ fi
2311
+ elif [ -n "$child_wt" ] && [ -e "$child_wt" ]; then
2312
+ child_proj=$(meta_value "$child_meta" project)
2313
+ validate_child_worktree_for_removal "$child_wt" "$child_proj" >/dev/null || return 1
2314
+ fi
2315
+ done
2316
+ }
2317
+
2318
+ TEARDOWN_HERDR_LOCK_RECORDS=
2319
+ teardown_release_herdr_locks() {
2320
+ local lock_session lock_path
2321
+ [ -n "$TEARDOWN_HERDR_LOCK_RECORDS" ] || return 0
2322
+ while IFS=$'\t' read -r lock_session lock_path; do
2323
+ [ -n "$lock_path" ] || continue
2324
+ fm_lock_release "$lock_path" || true
2325
+ done <<FMEOF
2326
+ $TEARDOWN_HERDR_LOCK_RECORDS
2327
+ FMEOF
2328
+ TEARDOWN_HERDR_LOCK_RECORDS=
2329
+ }
2330
+
2331
+ teardown_herdr_session_lock_held() { # <session>
2332
+ local session=$1 lock_session lock_path
2333
+ [ -n "$TEARDOWN_HERDR_LOCK_RECORDS" ] || return 1
2334
+ while IFS=$'\t' read -r lock_session lock_path; do
2335
+ [ "$lock_session" != "$session" ] || return 0
2336
+ done <<FMEOF
2337
+ $TEARDOWN_HERDR_LOCK_RECORDS
2338
+ FMEOF
2339
+ return 1
2340
+ }
2341
+
2342
+ teardown_herdr_require_prerequisites() { # <task-id>
2343
+ local task_id=$1 prerequisite
2344
+ if ! fm_backend_source herdr; then
2345
+ echo "error: herdr teardown prerequisites are unavailable for $task_id; nothing was changed - restore the adapter and rerun teardown" >&2
2346
+ return 1
2347
+ fi
2348
+ for prerequisite in \
2349
+ fm_backend_herdr_parse_target \
2350
+ fm_backend_herdr_pane_presence_state \
2351
+ fm_backend_herdr_workspace_presence_state \
2352
+ fm_backend_herdr_endpoint_confirmed_gone \
2353
+ fm_backend_herdr_explicit_close_pane_confirmed \
2354
+ fm_backend_herdr_presentation_session_lock_path; do
2355
+ if ! declare -F "$prerequisite" >/dev/null 2>&1; then
2356
+ echo "error: herdr teardown prerequisites are unavailable for $task_id; nothing was changed - restore the adapter and rerun teardown" >&2
2357
+ return 1
2358
+ fi
2359
+ done
2360
+ if ! declare -F fm_lock_try_acquire >/dev/null 2>&1; then
2361
+ # shellcheck source=bin/fm-wake-lib.sh
2362
+ . "$SCRIPT_DIR/fm-wake-lib.sh"
2363
+ fi
2364
+ if ! declare -F fm_lock_try_acquire >/dev/null 2>&1 \
2365
+ || ! declare -F fm_lock_release >/dev/null 2>&1; then
2366
+ echo "error: herdr teardown lock machinery is unavailable for $task_id; nothing was changed - restore the lock support and rerun teardown" >&2
2367
+ return 1
2368
+ fi
2369
+ }
2370
+
2371
+ teardown_herdr_preflight_target() { # <target> <task-id>
2372
+ local target=$1 task_id=$2 session pane presence lock_path verified_lock_path lock_session held_path attempt
2373
+ teardown_herdr_require_prerequisites "$task_id" || return 1
2374
+ if ! fm_backend_herdr_parse_target "$target"; then
2375
+ echo "error: herdr endpoint $target for $task_id could not be parsed exactly; nothing was changed - repair the endpoint metadata and rerun teardown" >&2
2376
+ return 1
2377
+ fi
2378
+ session=$FM_BACKEND_HERDR_SESSION
2379
+ pane=$FM_BACKEND_HERDR_PANE
2380
+ presence=$(fm_backend_herdr_pane_presence_state "$session" "$pane")
2381
+ case "$presence" in
2382
+ dead|present) ;;
2383
+ *)
2384
+ echo "error: herdr endpoint $target for $task_id has ambiguous structured presence; nothing was changed - restore reliable endpoint inspection and rerun teardown" >&2
2385
+ return 1
2386
+ ;;
2387
+ esac
2388
+ if ! lock_path=$(fm_backend_herdr_presentation_session_lock_path "$session"); then
2389
+ echo "error: herdr session presentation lock could not be resolved for $task_id; nothing was changed - rerun teardown once the session is reachable and unambiguous" >&2
2390
+ return 1
2391
+ fi
2392
+ if [ -n "$TEARDOWN_HERDR_LOCK_RECORDS" ]; then
2393
+ while IFS=$'\t' read -r lock_session held_path; do
2394
+ if [ "$lock_session" = "$session" ]; then
2395
+ if [ "$held_path" != "$lock_path" ]; then
2396
+ echo "error: herdr session presentation lock changed during preflight for $task_id; nothing was changed - rerun teardown once session identity is stable" >&2
2397
+ return 1
2398
+ fi
2399
+ return 0
2400
+ fi
2401
+ done <<FMEOF
2402
+ $TEARDOWN_HERDR_LOCK_RECORDS
2403
+ FMEOF
2404
+ fi
2405
+ attempt=0
2406
+ while [ "$attempt" -lt 50 ]; do
2407
+ if fm_lock_try_acquire "$lock_path"; then
2408
+ if ! verified_lock_path=$(fm_backend_herdr_presentation_session_lock_path "$session") \
2409
+ || [ "$verified_lock_path" != "$lock_path" ]; then
2410
+ fm_lock_release "$lock_path" || true
2411
+ echo "error: herdr session presentation lock changed during preflight for $task_id; nothing was changed - rerun teardown once session identity is stable" >&2
2412
+ return 1
2413
+ fi
2414
+ if [ -n "$TEARDOWN_HERDR_LOCK_RECORDS" ]; then
2415
+ TEARDOWN_HERDR_LOCK_RECORDS="$TEARDOWN_HERDR_LOCK_RECORDS
2416
+ $session $lock_path"
2417
+ else
2418
+ TEARDOWN_HERDR_LOCK_RECORDS="$session $lock_path"
2419
+ fi
2420
+ return 0
2421
+ fi
2422
+ sleep 0.1
2423
+ attempt=$((attempt + 1))
2424
+ done
2425
+ echo "error: herdr session presentation lock is contended for $task_id; nothing was changed - rerun teardown once the contention clears" >&2
2426
+ return 1
2427
+ }
2428
+
2429
+ preflight_firstmate_home_herdr_children() { # <home>
2430
+ local home=$1 sub_state child_meta child_id child_backend child_target child_kind child_home child_wt
2431
+ sub_state="$home/state"
2432
+ [ -d "$sub_state" ] || return 0
2433
+ for child_meta in "$sub_state"/*.meta; do
2434
+ [ -e "$child_meta" ] || continue
2435
+ child_id=$(basename "$child_meta" .meta)
2436
+ fm_backend_validate_task_endpoint "$child_meta" "$child_id" || return 1
2437
+ child_backend=$FM_BACKEND_VALIDATED_BACKEND
2438
+ child_target=$FM_BACKEND_VALIDATED_TARGET
2439
+ if [ "$child_backend" = herdr ]; then
2440
+ teardown_herdr_preflight_target "$child_target" "$child_id" || return 1
2441
+ fi
2442
+ child_kind=$(meta_value "$child_meta" kind)
2443
+ [ -n "$child_kind" ] || child_kind=ship
2444
+ if [ "$child_kind" = secondmate ]; then
2445
+ child_wt=$(meta_value "$child_meta" worktree)
2446
+ child_home=$(meta_value "$child_meta" home)
2447
+ [ -n "$child_home" ] || child_home=$child_wt
2448
+ preflight_firstmate_home_herdr_children "$child_home" || return 1
2449
+ fi
2450
+ done
2451
+ }
2452
+
2453
+ cleanup_firstmate_home_children() {
2454
+ local home=$1 sub_state child_meta child_id child_t child_wt child_proj child_kind child_home child_backend child_orca_worktree_id child_return_rc child_busy_gen
2455
+ sub_state="$home/state"
2456
+ [ -d "$sub_state" ] || return 0
2457
+ for child_meta in "$sub_state"/*.meta; do
2458
+ [ -e "$child_meta" ] || continue
2459
+ child_id=$(basename "$child_meta" .meta)
2460
+ child_wt=$(meta_value "$child_meta" worktree)
2461
+ child_proj=$(meta_value "$child_meta" project)
2462
+ child_kind=$(meta_value "$child_meta" kind)
2463
+ [ -n "$child_kind" ] || child_kind=ship
2464
+ child_backend=$(fm_backend_of_meta "$child_meta")
2465
+ if [ "$child_backend" = orca ]; then
2466
+ child_t=$(meta_value "$child_meta" terminal)
2467
+ else
2468
+ child_t=$(fm_backend_target_of_meta "$child_meta")
2469
+ fi
2470
+ if [ "$child_backend" = orca ] && [ "$child_kind" != secondmate ]; then
2471
+ child_orca_worktree_id=$(require_orca_worktree_id "$child_meta") || return 1
2472
+ if [ -n "$child_wt" ] && [ -e "$child_wt" ]; then
2473
+ validate_child_worktree_for_removal "$child_wt" "$child_proj" >/dev/null || return 1
2474
+ fi
2475
+ fi
2476
+ if [ -n "$child_t" ]; then
2477
+ if [ "$child_backend" = herdr ]; then
2478
+ fm_backend_herdr_parse_target "$child_t" || return 1
2479
+ if ! teardown_herdr_session_lock_held "$FM_BACKEND_HERDR_SESSION"; then
2480
+ echo "error: herdr session presentation lock is not held for child $child_id; retaining that child's durable identity records and stopping forced cleanup" >&2
2481
+ return 1
2482
+ fi
2483
+ fm_backend_herdr_kill_serialized "$FM_BACKEND_HERDR_SESSION" "$FM_BACKEND_HERDR_PANE" 2>/dev/null || true
2484
+ if ! fm_backend_herdr_endpoint_confirmed_gone "$child_t"; then
2485
+ echo "error: herdr pane $child_t for child $child_id is not confirmed gone; retaining that child's durable identity records and stopping forced cleanup" >&2
2486
+ return 1
2487
+ fi
2488
+ elif [ "$child_backend" = zellij ]; then
2489
+ # Zellij titles are scoped by the owning home tag, so forced secondmate
2490
+ # cleanup must verify child tabs as that child home, not the parent.
2491
+ ( unset FM_ROOT_OVERRIDE; FM_HOME=$home FM_ROOT=$home fm_backend_kill "$child_backend" "$child_t" "$(meta_value "$child_meta" zellij_tab_id)" "fm-$child_id" ) 2>/dev/null || true
2492
+ else
2493
+ fm_backend_kill "$child_backend" "$child_t" "$(meta_value "$child_meta" zellij_tab_id)" "fm-$child_id" 2>/dev/null || true
2494
+ fi
2495
+ fi
2496
+ if [ "$child_kind" = secondmate ]; then
2497
+ child_home=$(meta_value "$child_meta" home)
2498
+ [ -n "$child_home" ] || child_home=$child_wt
2499
+ if [ -n "$child_home" ] && [ -d "$child_home" ]; then
2500
+ cleanup_firstmate_home_children "$child_home" || return $?
2501
+ remove_firstmate_home "$child_home" "child firstmate home" "$child_id" || return $?
2502
+ fi
2503
+ elif [ "$child_backend" = orca ]; then
2504
+ if [ -n "$child_wt" ] && [ -d "$child_wt" ]; then
2505
+ validate_child_worktree_for_removal "$child_wt" "$child_proj" >/dev/null || return 1
2506
+ rm -f "$child_wt/.claude/settings.local.json" "$child_wt/.opencode/plugins/fm-turn-end.js" \
2507
+ "$child_wt/.fm-grok-turnend" "$child_wt/.fm-kimi-turnend"
2508
+ fi
2509
+ fm_backend_remove_worktree "$child_backend" "$child_orca_worktree_id" || return 1
2510
+ elif [ -n "$child_wt" ] && [ -d "$child_wt" ]; then
2511
+ validate_child_worktree_for_removal "$child_wt" "$child_proj" >/dev/null || return 1
2512
+ rm -f "$child_wt/.claude/settings.local.json" "$child_wt/.opencode/plugins/fm-turn-end.js" \
2513
+ "$child_wt/.opencode/plugins/fm-busy-state.js" \
2514
+ "$child_wt/.fm-grok-turnend" "$child_wt/.fm-kimi-turnend"
2515
+ if [ -n "$child_proj" ] && [ -d "$child_proj" ] && command -v treehouse >/dev/null 2>&1; then
2516
+ if teardown_treehouse_return "$child_wt" "$child_proj" "child worktree"; then
2517
+ :
2518
+ else
2519
+ child_return_rc=$?
2520
+ if [ "$child_return_rc" -eq "$TEARDOWN_TREEHOUSE_LOCK_REFUSED" ]; then
2521
+ return "$child_return_rc"
2522
+ fi
2523
+ safe_rm_rf_child_worktree "$child_wt" "$child_proj"
2524
+ fi
2525
+ else
2526
+ safe_rm_rf_child_worktree "$child_wt" "$child_proj"
2527
+ fi
2528
+ fi
2529
+ remove_grok_turnend_auth "$sub_state" "$child_id" || return 1
2530
+ remove_kimi_turnend_auth "$sub_state" "$child_id" || return 1
2531
+ remove_pr_poll_artifacts "$sub_state" "$child_id" || return 1
2532
+ child_busy_gen=$(meta_value "$child_meta" busy_gen)
2533
+ if [ -z "$child_busy_gen" ]; then
2534
+ child_busy_gen=$(cat "$sub_state/$child_id.busy-gen" 2>/dev/null || true)
2535
+ fi
2536
+ retire_busy_state "$sub_state" "$child_id" "$child_busy_gen" || return 1
2537
+ status_retire_presentation_task "$sub_state" "$child_id" || return 1
2538
+ fm_backlog_atomic_transition remove "$sub_state/$child_id.meta" "task record" "$sub_state" || return 1
2539
+ rm -f "$sub_state/$child_id.turn-ended" \
2540
+ "$sub_state/$child_id.pi-ext.ts" \
2541
+ "$sub_state/$child_id.grok-turnend-token" "$sub_state/$child_id.kimi-turnend-token" \
2542
+ "$sub_state/$child_id.muse-session" "$sub_state/$child_id.muse-session-current" \
2543
+ "$sub_state/$child_id.cursor-session" "$sub_state/$child_id.reconcile-nudged"
2544
+ done
2545
+ }
2546
+
2547
+ remove_secondmate_registry_entry() {
2548
+ local id=$1 tmp lock rc=0 acquired=0
2549
+ [ -f "$SECONDMATE_REG" ] || return 0
2550
+ lock=$(secondmate_registry_lock_path "$STATE")
2551
+ if [ "$LOCAL_REGISTRY_LOCK" != "$lock" ]; then
2552
+ fm_lock_acquire_wait "$lock" || return 1
2553
+ acquired=1
2554
+ fi
2555
+ tmp="$SECONDMATE_REG.tmp.$$"
2556
+ grep -vE "^- $id( |$)" "$SECONDMATE_REG" > "$tmp" || true
2557
+ mv "$tmp" "$SECONDMATE_REG" || rc=$?
2558
+ [ "$acquired" -eq 0 ] || fm_lock_release "$lock"
2559
+ return "$rc"
2560
+ }
2561
+
2562
+ validate_pr_poll_cleanup "$STATE" "$ID" || exit 1
2563
+
2564
+ if [ "$KIND" = secondmate ]; then
2565
+ LOCAL_REGISTRY_LOCK=$(secondmate_registry_lock_path "$STATE")
2566
+ fm_lock_acquire_wait "$LOCAL_REGISTRY_LOCK" || exit 1
2567
+ LOCAL_HANDOFF_LOCK="$STATE/.backlog-handoff-$ID.lock"
2568
+ fm_lock_acquire_wait "$LOCAL_HANDOFF_LOCK" || exit 1
2569
+ [ -n "$HOME_PATH" ] || HOME_PATH=$WT
2570
+ handoff_wake_retire_stage_recover "$HOME_PATH" || exit 1
2571
+ handoff_wake_retire_validate || exit 1
2572
+ validate_firstmate_home_for_removal "$HOME_PATH" "secondmate home" "$ID" >/dev/null || exit 1
2573
+ if [ "$FORCE" = "--force" ]; then
2574
+ validate_firstmate_home_children_removal "$HOME_PATH" || exit 1
2575
+ preflight_descendant_task_locks "$HOME_PATH" || exit 1
2576
+ validate_firstmate_home_children_removal "$HOME_PATH" || exit 1
2577
+ if [ "$BACKEND" = herdr ]; then
2578
+ teardown_herdr_preflight_target "$T" "$ID" || exit 1
2579
+ fi
2580
+ preflight_firstmate_home_herdr_children "$HOME_PATH" || exit 1
2581
+ fi
2582
+ fi
2583
+
2584
+ if [ "$KIND" = secondmate ] && [ "$FORCE" != "--force" ]; then
2585
+ SUB_STATE="$HOME_PATH/state"
2586
+ if [ -d "$SUB_STATE" ]; then
2587
+ for child_meta in "$SUB_STATE"/*.meta; do
2588
+ [ -e "$child_meta" ] || continue
2589
+ echo "REFUSED: secondmate $ID still has in-flight work in $SUB_STATE." >&2
2590
+ echo "Found $(basename "$child_meta"). Let that home finish or explicitly discard with --force." >&2
2591
+ exit 1
2592
+ done
2593
+ fi
2594
+ fi
2595
+
2596
+ if [ "$KIND" = secondmate ]; then
2597
+ preflight_firstmate_home_process_event_tree "$HOME_PATH" "secondmate home" || exit 1
2598
+ fi
2599
+
2600
+ if [ "$KIND" = secondmate ] && [ "$FORCE" = "--force" ]; then
2601
+ cleanup_firstmate_home_children "$HOME_PATH" || exit $?
2602
+ fi
2603
+
2604
+ if [ "$KIND" = scout ] && [ "$FORCE" != "--force" ]; then
2605
+ REPORT="$DATA/$ID/report.md"
2606
+ if [ ! -f "$REPORT" ]; then
2607
+ echo "REFUSED: scout task $ID has no report at $REPORT." >&2
2608
+ echo "The report is the work product. Have the crewmate write it, or use --force after explicit discard approval." >&2
2609
+ exit 1
2610
+ fi
2611
+ if ! FM_HOME="$FM_HOME" FM_STATE_OVERRIDE="$STATE" FM_DATA_OVERRIDE="$DATA" \
2612
+ FM_CONFIG_OVERRIDE="$CONFIG" "$SCRIPT_DIR/fm-captain-hold.sh" verify "$ID" >/dev/null; then
2613
+ echo "REFUSED: scout task $ID has not passed the captain-call completion gate." >&2
2614
+ echo "Inventory its report and any visual review through bin/fm-captain-hold.sh before teardown." >&2
2615
+ exit 1
2616
+ fi
2617
+ fi
2618
+
2619
+ # A public commitment is not kept until its final reply lands in the ORIGINAL
2620
+ # thread, and this cleanup removes the task records that make the promise
2621
+ # reconcilable. Refuse while this home still owes a public reply for exactly this
2622
+ # work. Both gates live in bin/fm-public-followup-lib.sh, so a home that never
2623
+ # opted into the myfirstmate relay runs one [ -f ] test and nothing else here.
2624
+ if [ "$FORCE" != "--force" ] && [ "$PUBLIC_FOLLOWUP_PARENT_UNRESOLVED" = 1 ]; then
2625
+ echo "REFUSED: cannot resolve the primary home for marked secondmate $SECOND_MATE_ID; refusing cleanup without its durable parent binding." >&2
2626
+ exit 1
2627
+ fi
2628
+ if [ "$FORCE" != "--force" ] \
2629
+ && [ -n "$PUBLIC_FOLLOWUP_STATE" ] \
2630
+ && [ "$PUBLIC_FOLLOWUP_RELAY_ACTIVE" = 1 ] \
2631
+ && fm_pf_has_registrations "$PUBLIC_FOLLOWUP_STATE"; then
2632
+ if ! PUBLIC_FOLLOWUP_BLOCKING=$(FM_HOME="$PUBLIC_FOLLOWUP_HOME" FM_STATE_OVERRIDE="$PUBLIC_FOLLOWUP_STATE" \
2633
+ "$SCRIPT_DIR/fm-public-followup.sh" guard-work "$PUBLIC_FOLLOWUP_WORK_HOME" "$ID" 2>/dev/null); then
2634
+ echo "REFUSED: task $ID still owes a public reply through the myfirstmate relay." >&2
2635
+ printf '%s\n' "$PUBLIC_FOLLOWUP_BLOCKING" >&2
2636
+ echo "Deliver it with bin/fm-public-followup.sh deliver <obligation-id>, waive it with tasks-axi public-followup waive, or use --force after explicit discard approval." >&2
2637
+ exit 1
2638
+ fi
2639
+ fi
2640
+
2641
+ # Non-blocking: a delivered public loop is not a teardown refusal (guard-work
2642
+ # already passed), but tearing down a ship whose PR merged while a loop is still
2643
+ # open with nothing owed is the moment the drop is detectable.
2644
+ if [ "$KIND" = ship ] && [ -n "$PR_URL" ] \
2645
+ && [ -n "$PUBLIC_FOLLOWUP_STATE" ] \
2646
+ && [ "${PUBLIC_FOLLOWUP_RELAY_ACTIVE:-0}" = 1 ] \
2647
+ && fm_pf_has_delivered_open_loops "$PUBLIC_FOLLOWUP_STATE"; then
2648
+ echo "warning: an open public loop with nothing owed is still recorded in the consent-holding home while cleaning up ship task $ID. Hand it on with bin/fm-public-followup.sh rechain or close it with retire --reason." >&2
2649
+ fi
2650
+
2651
+ # Non-blocking: the legacy Relay link is not guarded as a refusal.
2652
+ X_REQUEST=$(grep '^x_request=' "$META" 2>/dev/null | tail -1 | cut -d= -f2- || true)
2653
+ if [ -n "$X_REQUEST" ]; then
2654
+ echo "warning: task $ID still carries an unreconciled Relay request link ($X_REQUEST) on its task record." >&2
2655
+ fi
2656
+
2657
+ if [ "$BACKEND" = orca ] && [ "$KIND" != scout ] && [ "$KIND" != secondmate ] && [ "$FORCE" != "--force" ]; then
2658
+ if ! inspectable_git_worktree "$WT"; then
2659
+ echo "REFUSED: Orca ship task $ID has no inspectable git worktree at ${WT:-<missing>}." >&2
2660
+ echo "Cannot verify dirty or unlanded work; restore the worktree path or get explicit OK to discard, then --force." >&2
2661
+ exit 1
2662
+ fi
2663
+ require_orca_worktree_path_match "$ORCA_WORKTREE_ID" "$WT" || exit 1
2664
+ ORCA_PATH_MATCH_VERIFIED=1
2665
+ fi
2666
+
2667
+ if [ -d "$WT" ] && [ "$FORCE" != "--force" ]; then
2668
+ if validate_worktree_teardown_safety; then
2669
+ :
2670
+ else
2671
+ safety_rc=$?
2672
+ if [ "$safety_rc" -eq "$TEARDOWN_WORKTREE_SAFETY_LOCK_BLOCKED" ]; then
2673
+ cleanup_stale_lock_for_safety_check "$WT" || exit 1
2674
+ validate_worktree_teardown_safety || exit 1
2675
+ else
2676
+ exit 1
2677
+ fi
2678
+ fi
2679
+ fi
2680
+
2681
+ # A Herdr close may reposition shared workspace order, so the whole
2682
+ # destructive sequence below (worktree return, pane close, record removal)
2683
+ # runs under the named-session presentation lock, acquired BEFORE anything is
2684
+ # returned or erased: a contended lock refuses here while the isolated copy,
2685
+ # every durable record, and the endpoint are all still intact for a plain
2686
+ # rerun. An unresolvable lock path (for example an unreachable server) also
2687
+ # refuses before any destructive step.
2688
+ TEARDOWN_HERDR_SESSION=
2689
+ TEARDOWN_HERDR_PANE=
2690
+ if [ "$BACKEND" = herdr ]; then
2691
+ teardown_herdr_preflight_target "$T" "$ID" || exit 1
2692
+ fm_backend_herdr_parse_target "$T" || exit 1
2693
+ TEARDOWN_HERDR_SESSION=$FM_BACKEND_HERDR_SESSION
2694
+ TEARDOWN_HERDR_PANE=$FM_BACKEND_HERDR_PANE
2695
+ fi
2696
+
2697
+ BACKLOG_CLOSED=0
2698
+ BACKLOG_SKIP_REASON=
2699
+ if [ "$TEARDOWN_BACKLOG_APPLIES" = 1 ]; then
2700
+ backlog_done_args || {
2701
+ echo "error: the pending backlog close for $ID is not replayable; refusing destructive teardown" >&2
2702
+ exit 1
2703
+ }
2704
+ BACKLOG_CLOSED=1
2705
+ META_SPAWN_GEN=$TEARDOWN_META_SPAWN_GEN
2706
+ fm_backlog_close_marker_write "$STATE" "$ID" "$DATA" "$META_SPAWN_GEN" \
2707
+ "${BACKLOG_DONE_ARGS[@]+"${BACKLOG_DONE_ARGS[@]}"}" \
2708
+ || { echo "error: the pending backlog close for $ID could not be recorded ($FM_BACKLOG_TRANSITION_ERROR); retaining every durable task record" >&2; exit 1; }
2709
+ else
2710
+ if [ "$CLEANUP_RECOVERY" = orca ]; then
2711
+ BACKLOG_SKIP_REASON="Orca cleanup recovery is not a launched backlog worker"
2712
+ else
2713
+ BACKLOG_SKIP_REASON=$TEARDOWN_BACKLOG_SKIP_REASON
2714
+ fi
2715
+ fi
2716
+
2717
+ # Every landed/discard-work refusal above has now passed (or --force skipped
2718
+ # them). Fix 1 and Fix 2 (see script header) run here, unconditionally on
2719
+ # --force, and before ANY destructive step below - a still-parked run or a
2720
+ # leaked process can own live work in this exact worktree. Not for
2721
+ # kind=secondmate: a secondmate home's own runtime lifecycle is owned by the
2722
+ # dedicated process-event and firstmate-home removal machinery further below,
2723
+ # not by task-worktree cleanup.
2724
+ if [ "$KIND" != secondmate ]; then
2725
+ conclude_task_no_mistakes_run "$WT"
2726
+ reap_task_worktree_processes worktree "$WT" "$TASK_TMP"
2727
+ fi
2728
+
2729
+ # Fix 3 (see script header): sweep remote job workers abandoned by an already
2730
+ # pruned code root. Best effort - a sweep failure never blocks this teardown.
2731
+ "$SCRIPT_DIR/fm-remote-job-reap-orphans.sh" >&2 || true
2732
+
2733
+ # Best-effort: drop the local task branch so the shared repo does not accumulate refs.
2734
+ if [ "$BACKEND" = orca ] && [ "$KIND" != secondmate ]; then
2735
+ if [ "$ORCA_PATH_MATCH_VERIFIED" != 1 ]; then
2736
+ require_orca_worktree_path_match_if_present "$ORCA_WORKTREE_ID" "$WT" || exit 1
2737
+ ORCA_PATH_MATCH_VERIFIED=1
2738
+ fi
2739
+ if [ -d "$WT" ]; then
2740
+ branch=$(git -C "$WT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)
2741
+ if [ "$branch" != "HEAD" ]; then
2742
+ if git -C "$WT" checkout --detach -q 2>/dev/null; then
2743
+ git -C "$WT" branch -D "$branch" >/dev/null 2>&1 || true
2744
+ fi
2745
+ fi
2746
+ rm -f "$WT/.claude/settings.local.json" "$WT/.opencode/plugins/fm-turn-end.js" \
2747
+ "$WT/.opencode/plugins/fm-busy-state.js" \
2748
+ "$WT/.fm-grok-turnend" "$WT/.fm-kimi-turnend"
2749
+ fi
2750
+ [ -z "$T_ORCA" ] || fm_backend_kill "$BACKEND" "$T" "$(meta_value "$META" zellij_tab_id)" "fm-$ID" 2>/dev/null || true
2751
+ fm_backend_remove_worktree "$BACKEND" "$ORCA_WORKTREE_ID"
2752
+ elif [ -d "$WT" ] && [ "$KIND" != secondmate ]; then
2753
+ branch=$(git -C "$WT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)
2754
+ if [ "$branch" != "HEAD" ]; then
2755
+ if git -C "$WT" checkout --detach -q 2>/dev/null; then
2756
+ git -C "$WT" branch -D "$branch" >/dev/null 2>&1 || true
2757
+ fi
2758
+ fi
2759
+ # Remove our hook file so a reused pool worktree cannot fire signals for a dead task.
2760
+ rm -f "$WT/.claude/settings.local.json" "$WT/.opencode/plugins/fm-turn-end.js" \
2761
+ "$WT/.fm-grok-turnend" "$WT/.fm-kimi-turnend"
2762
+ # Kills remaining processes in the worktree (including the agent), resets, returns
2763
+ # to pool. treehouse resolves the pool from the working directory, so run it from
2764
+ # the project. teardown_treehouse_return tolerates transient and stale git locks
2765
+ # left by a killed crew process; see the script header for retry and stale-lock proof.
2766
+ post_lock_cleanup_check=
2767
+ if [ "$FORCE" != "--force" ] && [ "$KIND" != scout ] && [ "$KIND" != secondmate ]; then
2768
+ post_lock_cleanup_check=validate_worktree_teardown_safety
2769
+ fi
2770
+ teardown_treehouse_return "$WT" "$PROJ" "worktree" "$post_lock_cleanup_check" || {
2771
+ echo "error: treehouse return failed for worktree $WT; teardown aborted" >&2
2772
+ exit 1
2773
+ }
2774
+ fi
2775
+
2776
+ HERDR_PRESENTATION_JOURNAL="$STATE/$ID.herdr-presentation"
2777
+ HERDR_PRESENTATION_RETIRE_CANDIDATE=0
2778
+ HERDR_PRESENTATION_SESSION=
2779
+ HERDR_PRESENTATION_PANE=
2780
+ if [ "$BACKEND" = herdr ] \
2781
+ && { [ -e "$HERDR_PRESENTATION_JOURNAL" ] || [ -L "$HERDR_PRESENTATION_JOURNAL" ]; }; then
2782
+ fm_backend_source herdr || true
2783
+ HERDR_PRESENTATION_SESSION=$(meta_value "$META" herdr_session)
2784
+ HERDR_PRESENTATION_WORKSPACE=$(meta_value "$META" herdr_workspace_id)
2785
+ HERDR_PRESENTATION_PANE=$(meta_value "$META" herdr_pane_id)
2786
+ if [ -n "$HERDR_PRESENTATION_SESSION" ] \
2787
+ && [ -n "$HERDR_PRESENTATION_WORKSPACE" ] \
2788
+ && [ -n "$HERDR_PRESENTATION_PANE" ] \
2789
+ && [ "$T" = "$HERDR_PRESENTATION_SESSION:$HERDR_PRESENTATION_PANE" ] \
2790
+ && fm_backend_herdr_projection_endpoint_matches_journal \
2791
+ "$HERDR_PRESENTATION_SESSION" "$HERDR_PRESENTATION_WORKSPACE" \
2792
+ "$HERDR_PRESENTATION_JOURNAL" "$ID"; then
2793
+ HERDR_PRESENTATION_RETIRE_CANDIDATE=1
2794
+ fi
2795
+ fi
2796
+
2797
+ if [ "$HERDR_PRESENTATION_RETIRE_CANDIDATE" = 1 ]; then
2798
+ # The presentation lock was acquired before the worktree return above; a
2799
+ # contended lock already refused this teardown while everything was intact.
2800
+ if teardown_herdr_session_lock_held "$HERDR_PRESENTATION_SESSION"; then
2801
+ # stderr is deliberately NOT discarded here. This is the highest-frequency
2802
+ # projected-close call site, and the helper's only stderr output is a real
2803
+ # warning - unverifiable workspace.move support, a refused focus-unsafe
2804
+ # close, an unconfirmed repositioned-workspace removal, or a failed exact
2805
+ # restore.
2806
+ # Swallowing them left a wrong active workspace with no operator-visible
2807
+ # signal at all. The close stays non-fatal exactly as before: the presence
2808
+ # gate below is what decides whether any durable record may be removed.
2809
+ fm_backend_herdr_projection_close_pane_focus_preserving \
2810
+ "$HERDR_PRESENTATION_SESSION" "$HERDR_PRESENTATION_PANE" || true
2811
+ else
2812
+ echo "warning: herdr presentation focus lock unavailable; refusing a concurrent focus-unsafe pane close" >&2
2813
+ fi
2814
+ elif [ "$BACKEND" = herdr ]; then
2815
+ if teardown_herdr_session_lock_held "$TEARDOWN_HERDR_SESSION"; then
2816
+ fm_backend_herdr_kill_serialized "$TEARDOWN_HERDR_SESSION" "$TEARDOWN_HERDR_PANE" 2>/dev/null || true
2817
+ else
2818
+ echo "warning: herdr session presentation lock path is unavailable; skipping the pane close rather than closing unlocked" >&2
2819
+ fi
2820
+ elif [ "$BACKEND" != orca ]; then
2821
+ fm_backend_kill "$BACKEND" "$T" "$(meta_value "$META" zellij_tab_id)" "fm-$ID" 2>/dev/null || true
2822
+ fi
2823
+ if [ "$HERDR_PRESENTATION_RETIRE_CANDIDATE" = 1 ]; then
2824
+ if [ "$(fm_backend_herdr_pane_agent_state "$HERDR_PRESENTATION_SESSION" "$HERDR_PRESENTATION_PANE")" = dead ]; then
2825
+ rm -f "$HERDR_PRESENTATION_JOURNAL"
2826
+ else
2827
+ echo "warning: exact herdr task-pane close could not be confirmed for $ID; retaining the presentation journal and attempting no workspace cleanup" >&2
2828
+ fi
2829
+ elif [ "$BACKEND" = herdr ] \
2830
+ && { [ -e "$HERDR_PRESENTATION_JOURNAL" ] || [ -L "$HERDR_PRESENTATION_JOURNAL" ]; }; then
2831
+ echo "warning: herdr presentation journal for $ID remains quarantined; no workspace cleanup was attempted" >&2
2832
+ fi
2833
+ # A refused, skipped, or failed Herdr close must never erase a live task's
2834
+ # durable endpoint identity: unless the exact pane is confirmed gone, retain
2835
+ # every record and stop before any removal below so a later rerun can retry
2836
+ # the locked close. Only a structured not-found proves the pane gone; unknown
2837
+ # presence, missing or malformed endpoint identity, and missing confirmation
2838
+ # machinery all refuse.
2839
+ if [ "$BACKEND" = herdr ]; then
2840
+ fm_backend_source herdr || true
2841
+ if ! declare -F fm_backend_herdr_endpoint_confirmed_gone >/dev/null 2>&1; then
2842
+ echo "error: herdr endpoint confirmation is unavailable for $ID; retaining every durable task record" >&2
2843
+ exit 1
2844
+ fi
2845
+ if ! fm_backend_herdr_endpoint_confirmed_gone "$T"; then
2846
+ echo "error: herdr pane $T for $ID is not confirmed gone after its close was refused, skipped, or failed; retaining every durable task record - rerun teardown once the close can run under the session lock" >&2
2847
+ exit 1
2848
+ fi
2849
+ fi
2850
+ if [ "$KIND" = secondmate ]; then
2851
+ [ -n "$HOME_PATH" ] || HOME_PATH=$WT
2852
+ handoff_wake_retire_stage \
2853
+ || { echo "error: receiver wake cleanup could not be staged; preserving the secondmate home and route" >&2; exit 1; }
2854
+ if remove_firstmate_home "$HOME_PATH" "secondmate home" "$ID"; then
2855
+ :
2856
+ else
2857
+ rc=$?
2858
+ handoff_wake_retire_stage_restore \
2859
+ || echo "error: receiver wake restoration failed; recovery state remains at $HANDOFF_WAKE_RETIRE_STAGE" >&2
2860
+ exit "$rc"
2861
+ fi
2862
+ handoff_wake_retire_stage_commit \
2863
+ || { echo "error: receiver wake cleanup failed; preserving the secondmate route for retry" >&2; exit 1; }
2864
+ remove_secondmate_registry_entry "$ID"
2865
+ fi
2866
+ remove_grok_turnend_auth "$STATE" "$ID" || exit 1
2867
+ remove_kimi_turnend_auth "$STATE" "$ID" || exit 1
2868
+ fm_backend_clear_transition "$BACKEND" "$STATE" "$T" || true
2869
+ # Remove the per-task temp root (/tmp/fm-<id>/, incl. its gotmp/) recorded by spawn.
2870
+ # Read before the state-file rm below; empty (pre-fix tasks without tasktmp=) is a no-op.
2871
+ [ -n "$TASK_TMP" ] && rm -rf "$TASK_TMP"
2872
+ remove_pr_poll_artifacts "$STATE" "$ID" || exit 1
2873
+ retire_busy_state "$STATE" "$ID" "$BUSY_GEN" || exit 1
2874
+ status_retire_presentation_task "$STATE" "$ID" || exit 1
2875
+ rm -f "$STATE/$ID.turn-ended" \
2876
+ "$STATE/$ID.pi-ext.ts" "$STATE/$ID.grok-turnend-token" \
2877
+ "$STATE/$ID.kimi-turnend-token" "$STATE/$ID.muse-session" \
2878
+ "$STATE/$ID.muse-session-current" "$STATE/$ID.cursor-session" \
2879
+ "$STATE/$ID.control-relaunch" "$STATE/$ID.control-relaunch.meta-prior" \
2880
+ "$STATE/$ID.control-relaunch.brief-prior" "$STATE/$ID.control-relaunch.note" \
2881
+ "$STATE/$ID.reconcile-nudged"
2882
+ # The steering inbox (bin/fm-task-inbox-lib.sh) is runtime state for the
2883
+ # retired endpoint; teardown only runs after landing is confirmed, so any
2884
+ # leftover unhandled steer here is moot rather than unlanded work.
2885
+ rm -rf "$STATE/$ID.inbox"
2886
+ # The record is gone, so the backlog must not still show this task in flight
2887
+ # when teardown reports success. Still under this task's meta lock, so a steer
2888
+ # racing the same id stays serialized exactly as it was before.
2889
+ if [ "$BACKLOG_CLOSED" = 1 ]; then
2890
+ BACKLOG_CLOSE_MARKER=$(fm_backlog_close_marker_path "$STATE" "$ID") || exit 1
2891
+ if ! fm_backlog_atomic_transition close "$STATE/$ID.meta" "$BACKLOG_CLOSE_MARKER" \
2892
+ "$DATA" "$ID" "$STATE" "${BACKLOG_DONE_ARGS[@]+"${BACKLOG_DONE_ARGS[@]}"}"; then
2893
+ fm_lock_release "$META_LOCK"
2894
+ META_LOCK_HELD=0
2895
+ echo "error: $ID's endpoint and local copy are cleaned up, but its backlog item could not be closed atomically ($FM_BACKLOG_TRANSITION_ERROR); the pending close is recorded and the next session start retries it" >&2
2896
+ exit 1
2897
+ fi
2898
+ elif [ "$KIND" = secondmate ] && [ ! -e "$STATE" ] && [ ! -L "$STATE" ]; then
2899
+ # A nested remote retirement can keep its route record inside the home being
2900
+ # removed. remove_firstmate_home above already performed that physical
2901
+ # deletion; do not turn its confirmed absence into a false cleanup failure.
2902
+ :
2903
+ else
2904
+ if ! fm_backlog_atomic_transition remove "$STATE/$ID.meta" "task record" "$STATE"; then
2905
+ fm_lock_release "$META_LOCK"
2906
+ META_LOCK_HELD=0
2907
+ echo "error: $ID's endpoint and local copy are cleaned up, but its task record could not be removed ($FM_BACKLOG_TRANSITION_ERROR)" >&2
2908
+ exit 1
2909
+ fi
2910
+ fi
2911
+ fm_lock_release "$META_LOCK"
2912
+ META_LOCK_HELD=0
2913
+ if [ "$KIND" != scout ] && [ "$KIND" != secondmate ] && [ "$MODE" != local-only ]; then
2914
+ "$FM_ROOT/bin/fm-fleet-sync.sh" "$PROJ" || true
2915
+ fi
2916
+ # A secondmate retirement may remove the home containing an overridden control
2917
+ # state directory. Do not let the side-band refresh recreate that retired home.
2918
+ if [ -d "$STATE" ]; then
2919
+ "$SCRIPT_DIR/fm-home-summary-refresh.sh" --best-effort || true
2920
+ fi
2921
+ echo "teardown $ID complete (window $T, worktree $WT)"
2922
+ backlog_refresh_reminder