@zerwiz/ymir 0.1.4 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (865) hide show
  1. package/.agents/README.md +291 -0
  2. package/.agents/agents/bragi-marketer.md +111 -0
  3. package/.agents/agents/brokk.md +352 -0
  4. package/.agents/agents/forseti-reviewer.md +93 -0
  5. package/.agents/agents/frigg-consent.md +77 -0
  6. package/.agents/agents/groa-update.md +77 -0
  7. package/.agents/agents/hnoss-designer.md +69 -0
  8. package/.agents/agents/huginn-researcher.md +99 -0
  9. package/.agents/agents/jord-projects.md +78 -0
  10. package/.agents/agents/kvasir-scout.md +91 -0
  11. package/.agents/agents/mimir-planner.md +92 -0
  12. package/.agents/agents/muninn-stow.md +77 -0
  13. package/.agents/agents/saga-bearings.md +78 -0
  14. package/.agents/agents/sindri-developer.md +114 -0
  15. package/.agents/agents/snotra-documenter.md +105 -0
  16. package/.agents/agents/syn-recovery.md +78 -0
  17. package/.agents/agents/tyr-check.md +78 -0
  18. package/.agents/agents/urdh-hold.md +77 -0
  19. package/.agents/agents/volundr-smith.md +80 -0
  20. package/.agents/agents/vor-diagnostics.md +77 -0
  21. package/.agents/assets/agents/naming.md +50 -0
  22. package/.agents/assets/agents/registry.md +93 -0
  23. package/.agents/assets/agents/runtime.md +36 -0
  24. package/.agents/assets/agents/toon-tasks-cli.md +42 -0
  25. package/.agents/assets/templates/company_entity.template.md +37 -0
  26. package/.agents/backend/README.md +74 -0
  27. package/.agents/backend/backends/cmux.sh +649 -0
  28. package/.agents/backend/backends/herdr-eventwait.py +157 -0
  29. package/.agents/backend/backends/herdr-workspace-move.py +114 -0
  30. package/.agents/backend/backends/herdr.sh +3386 -0
  31. package/.agents/backend/backends/orca.sh +290 -0
  32. package/.agents/backend/backends/tmux.sh +357 -0
  33. package/.agents/backend/backends/zellij.sh +700 -0
  34. package/.agents/backend/fm-afk-launch.sh +654 -0
  35. package/.agents/backend/fm-afk-return.sh +241 -0
  36. package/.agents/backend/fm-afk-start.sh +172 -0
  37. package/.agents/backend/fm-arm-command-policy.mjs +960 -0
  38. package/.agents/backend/fm-arm-pretool-check.sh +200 -0
  39. package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
  40. package/.agents/backend/fm-backend.sh +981 -0
  41. package/.agents/backend/fm-backlog-handoff.sh +909 -0
  42. package/.agents/backend/fm-backlog-receive.sh +187 -0
  43. package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
  44. package/.agents/backend/fm-bearings-board.sh +204 -0
  45. package/.agents/backend/fm-bearings-snapshot.sh +546 -0
  46. package/.agents/backend/fm-bootstrap.sh +1568 -0
  47. package/.agents/backend/fm-branch-outcome.sh +203 -0
  48. package/.agents/backend/fm-branch-prompt.sh +101 -0
  49. package/.agents/backend/fm-brief.sh +452 -0
  50. package/.agents/backend/fm-busy-event.sh +234 -0
  51. package/.agents/backend/fm-busy-lib.sh +986 -0
  52. package/.agents/backend/fm-captain-hold.sh +1001 -0
  53. package/.agents/backend/fm-cd-command-policy.mjs +153 -0
  54. package/.agents/backend/fm-cd-pretool-check.sh +190 -0
  55. package/.agents/backend/fm-check-lib.sh +74 -0
  56. package/.agents/backend/fm-check-register.sh +42 -0
  57. package/.agents/backend/fm-check-unregister.sh +52 -0
  58. package/.agents/backend/fm-classify-lib.sh +1770 -0
  59. package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
  60. package/.agents/backend/fm-composer-lib.sh +1416 -0
  61. package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
  62. package/.agents/backend/fm-config-push.sh +254 -0
  63. package/.agents/backend/fm-control-lib.sh +251 -0
  64. package/.agents/backend/fm-control.sh +878 -0
  65. package/.agents/backend/fm-crew-state.sh +625 -0
  66. package/.agents/backend/fm-cursor-lib.sh +243 -0
  67. package/.agents/backend/fm-decision-hold.sh +232 -0
  68. package/.agents/backend/fm-doc-audience-check.sh +269 -0
  69. package/.agents/backend/fm-dod-lib.sh +67 -0
  70. package/.agents/backend/fm-ensure-agents-md.sh +253 -0
  71. package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
  72. package/.agents/backend/fm-extension.mjs +2577 -0
  73. package/.agents/backend/fm-extension.sh +16 -0
  74. package/.agents/backend/fm-ff-lib.sh +420 -0
  75. package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
  76. package/.agents/backend/fm-fleet-sync.sh +458 -0
  77. package/.agents/backend/fm-fleet-view.sh +96 -0
  78. package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
  79. package/.agents/backend/fm-guard.sh +242 -0
  80. package/.agents/backend/fm-harness.sh +195 -0
  81. package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
  82. package/.agents/backend/fm-herdr-lab.sh +346 -0
  83. package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
  84. package/.agents/backend/fm-home-seed.sh +984 -0
  85. package/.agents/backend/fm-home-summary-refresh.sh +255 -0
  86. package/.agents/backend/fm-hook-host-lib.sh +36 -0
  87. package/.agents/backend/fm-inactive-reconcile.sh +531 -0
  88. package/.agents/backend/fm-inbox.sh +399 -0
  89. package/.agents/backend/fm-install-actionlint.sh +84 -0
  90. package/.agents/backend/fm-install-herdr.sh +96 -0
  91. package/.agents/backend/fm-install-shellcheck.sh +84 -0
  92. package/.agents/backend/fm-install-treehouse.sh +95 -0
  93. package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
  94. package/.agents/backend/fm-lease-lib.sh +218 -0
  95. package/.agents/backend/fm-lease.sh +189 -0
  96. package/.agents/backend/fm-line-cap-lib.sh +51 -0
  97. package/.agents/backend/fm-lint-workflows.sh +137 -0
  98. package/.agents/backend/fm-lint.sh +591 -0
  99. package/.agents/backend/fm-lock-lib.sh +104 -0
  100. package/.agents/backend/fm-lock.sh +107 -0
  101. package/.agents/backend/fm-marker-lib.sh +12 -0
  102. package/.agents/backend/fm-merge-local.sh +74 -0
  103. package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
  104. package/.agents/backend/fm-nm-run-lib.sh +124 -0
  105. package/.agents/backend/fm-on.sh +125 -0
  106. package/.agents/backend/fm-operational-input.sh +253 -0
  107. package/.agents/backend/fm-peek.sh +46 -0
  108. package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
  109. package/.agents/backend/fm-pr-check.sh +135 -0
  110. package/.agents/backend/fm-pr-lib.sh +1016 -0
  111. package/.agents/backend/fm-pr-merge.sh +707 -0
  112. package/.agents/backend/fm-pr-poll.sh +110 -0
  113. package/.agents/backend/fm-primary-scope-lib.sh +33 -0
  114. package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
  115. package/.agents/backend/fm-procevent-lavish.sh +628 -0
  116. package/.agents/backend/fm-procevent-lib.sh +892 -0
  117. package/.agents/backend/fm-procevent-quota.sh +290 -0
  118. package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
  119. package/.agents/backend/fm-procevent-when.sh +504 -0
  120. package/.agents/backend/fm-procevent.sh +1466 -0
  121. package/.agents/backend/fm-project-mode.sh +92 -0
  122. package/.agents/backend/fm-project-origin-lib.sh +180 -0
  123. package/.agents/backend/fm-promote.sh +282 -0
  124. package/.agents/backend/fm-public-followup-emit.sh +260 -0
  125. package/.agents/backend/fm-public-followup-lib.sh +398 -0
  126. package/.agents/backend/fm-public-followup.sh +1330 -0
  127. package/.agents/backend/fm-push-transition-lib.sh +168 -0
  128. package/.agents/backend/fm-quota-axi-lib.sh +93 -0
  129. package/.agents/backend/fm-quota-choose.sh +384 -0
  130. package/.agents/backend/fm-remote-delta-read.sh +189 -0
  131. package/.agents/backend/fm-remote-doctor.sh +800 -0
  132. package/.agents/backend/fm-remote-entrypoint.sh +193 -0
  133. package/.agents/backend/fm-remote-file.sh +234 -0
  134. package/.agents/backend/fm-remote-home-provision.sh +259 -0
  135. package/.agents/backend/fm-remote-home-seed.sh +259 -0
  136. package/.agents/backend/fm-remote-inherit-push.sh +90 -0
  137. package/.agents/backend/fm-remote-inherit.sh +179 -0
  138. package/.agents/backend/fm-remote-job-lib.sh +1241 -0
  139. package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
  140. package/.agents/backend/fm-remote-job-worker.sh +1124 -0
  141. package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
  142. package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
  143. package/.agents/backend/fm-review-diff.sh +158 -0
  144. package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
  145. package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
  146. package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
  147. package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
  148. package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
  149. package/.agents/backend/fm-secondmate-report.sh +88 -0
  150. package/.agents/backend/fm-send.sh +1059 -0
  151. package/.agents/backend/fm-session-lock-lib.sh +176 -0
  152. package/.agents/backend/fm-session-start.sh +970 -0
  153. package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
  154. package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
  155. package/.agents/backend/fm-sessionstart-run.sh +145 -0
  156. package/.agents/backend/fm-spawn.sh +3153 -0
  157. package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
  158. package/.agents/backend/fm-startup-memory-budget.sh +94 -0
  159. package/.agents/backend/fm-startup-network.sh +642 -0
  160. package/.agents/backend/fm-stow-cascade.sh +251 -0
  161. package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
  162. package/.agents/backend/fm-supervise-daemon.sh +1732 -0
  163. package/.agents/backend/fm-supervision-instructions.sh +215 -0
  164. package/.agents/backend/fm-supervision-lib.sh +90 -0
  165. package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
  166. package/.agents/backend/fm-tangle-lib.sh +53 -0
  167. package/.agents/backend/fm-task-inbox-lib.sh +402 -0
  168. package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
  169. package/.agents/backend/fm-teardown.sh +2922 -0
  170. package/.agents/backend/fm-test-isolation-proof.sh +588 -0
  171. package/.agents/backend/fm-test-run.sh +2191 -0
  172. package/.agents/backend/fm-timeout-lib.sh +141 -0
  173. package/.agents/backend/fm-timing-lib.sh +179 -0
  174. package/.agents/backend/fm-tmux-lib.sh +291 -0
  175. package/.agents/backend/fm-tool-update-check.sh +898 -0
  176. package/.agents/backend/fm-trace-context-lib.sh +227 -0
  177. package/.agents/backend/fm-transition-lib.sh +103 -0
  178. package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
  179. package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
  180. package/.agents/backend/fm-turnend-guard.sh +432 -0
  181. package/.agents/backend/fm-update.sh +110 -0
  182. package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
  183. package/.agents/backend/fm-voice-client.py +1373 -0
  184. package/.agents/backend/fm-voice-relay.py +1256 -0
  185. package/.agents/backend/fm-wake-drain.sh +592 -0
  186. package/.agents/backend/fm-wake-grant.sh +127 -0
  187. package/.agents/backend/fm-wake-lib.sh +1849 -0
  188. package/.agents/backend/fm-watch-arm.sh +602 -0
  189. package/.agents/backend/fm-watch-checkpoint.sh +109 -0
  190. package/.agents/backend/fm-watch.sh +1962 -0
  191. package/.agents/backend/fm-x-dismiss.sh +116 -0
  192. package/.agents/backend/fm-x-followup.sh +286 -0
  193. package/.agents/backend/fm-x-lib.sh +1000 -0
  194. package/.agents/backend/fm-x-link.sh +234 -0
  195. package/.agents/backend/fm-x-poll.sh +185 -0
  196. package/.agents/backend/fm-x-reply.sh +385 -0
  197. package/.agents/backend/fm_voice_frame.py +166 -0
  198. package/.agents/backend/fm_voice_records.py +574 -0
  199. package/.agents/backend/model-bridge.py +360 -0
  200. package/.agents/backend/opencode-go-bridge.py +30 -0
  201. package/.agents/bus/README.md +12 -0
  202. package/.agents/bus/messages.json +1 -0
  203. package/.agents/config/agents.machine.example.yaml +16 -0
  204. package/.agents/config/agents.yaml.example +72 -0
  205. package/.agents/config/crew-dispatch.json +22 -0
  206. package/.agents/config/cron.yaml +11 -0
  207. package/.agents/config/eindri-dispatch.json +67 -0
  208. package/.agents/config/eindri-harness +1 -0
  209. package/.agents/config/startup-memory-budget +1 -0
  210. package/.agents/filebrowser/.filebrowser.json +6 -0
  211. package/.agents/filebrowser/README.md +11 -0
  212. package/.agents/gateway/README.md +12 -0
  213. package/.agents/github/README.md +15 -0
  214. package/.agents/github/webhooks/.gitkeep +0 -0
  215. package/.agents/github/workflows/.gitkeep +0 -0
  216. package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
  217. package/.agents/harness/opencode/plugins/package.json +4 -0
  218. package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
  219. package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
  220. package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
  221. package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
  222. package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
  223. package/.agents/memory/README.md +46 -0
  224. package/.agents/memory/runes_audit.md +5 -0
  225. package/.agents/migrations/0001-hodd-layout.sh +38 -0
  226. package/.agents/migrations/0002-a2a-mcp.sh +27 -0
  227. package/.agents/migrations/0003-private-data-separation.sh +121 -0
  228. package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
  229. package/.agents/migrations/0005-roots-out-of-tree.sh +158 -0
  230. package/.agents/sandbox/Dockerfile.utgard +19 -0
  231. package/.agents/sandbox/README.md +14 -0
  232. package/.agents/sandbox/utgard.config.json +14 -0
  233. package/.agents/skills/README.md +59 -0
  234. package/.agents/skills/bragi-marketing/README.md +3 -0
  235. package/.agents/skills/bragi-marketing/SKILL.md +82 -0
  236. package/.agents/skills/eindri-homes/README.md +3 -0
  237. package/.agents/skills/eindri-homes/SKILL.md +291 -0
  238. package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
  239. package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
  240. package/.agents/skills/frigg-consent/README.md +3 -0
  241. package/.agents/skills/frigg-consent/SKILL.md +56 -0
  242. package/.agents/skills/galdr-ymirsystem/README.md +3 -0
  243. package/.agents/skills/galdr-ymirsystem/SKILL.md +159 -0
  244. package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
  245. package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
  246. package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
  247. package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
  248. package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
  249. package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
  250. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +736 -0
  251. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
  252. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
  253. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
  254. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
  255. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
  256. package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +539 -0
  257. package/.agents/skills/galdr-ymirsystem/assets/installation.md +763 -0
  258. package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
  259. package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
  260. package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +163 -0
  261. package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +415 -0
  262. package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
  263. package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +133 -0
  264. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
  265. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
  266. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +101 -0
  267. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
  268. package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
  269. package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
  270. package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
  271. package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
  272. package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
  273. package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
  274. package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
  275. package/.agents/skills/galdr-ymirsystem/assets/smidja.md +180 -0
  276. package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
  277. package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
  278. package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
  279. package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +503 -0
  280. package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
  281. package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
  282. package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
  283. package/.agents/skills/gjallarhorn-relay/README.md +3 -0
  284. package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
  285. package/.agents/skills/groa-update/SKILL.md +90 -0
  286. package/.agents/skills/hamr-adapters/README.md +3 -0
  287. package/.agents/skills/hamr-adapters/SKILL.md +123 -0
  288. package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
  289. package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
  290. package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
  291. package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
  292. package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
  293. package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
  294. package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
  295. package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
  296. package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
  297. package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
  298. package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
  299. package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
  300. package/.agents/skills/herdr-panes/README.md +3 -0
  301. package/.agents/skills/herdr-panes/SKILL.md +92 -0
  302. package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
  303. package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
  304. package/.agents/skills/hnoss-design/DESIGN.md +19 -0
  305. package/.agents/skills/hnoss-design/README.md +3 -0
  306. package/.agents/skills/hnoss-design/SKILL.md +83 -0
  307. package/.agents/skills/hvild-afk/README.md +3 -0
  308. package/.agents/skills/hvild-afk/SKILL.md +241 -0
  309. package/.agents/skills/jord-projects/README.md +3 -0
  310. package/.agents/skills/jord-projects/SKILL.md +92 -0
  311. package/.agents/skills/lifecycle/SKILL.md +56 -0
  312. package/.agents/skills/lifecycle/smoke_test.sh +57 -0
  313. package/.agents/skills/lifecycle/start.sh +21 -0
  314. package/.agents/skills/lifecycle/status.sh +35 -0
  315. package/.agents/skills/lifecycle/stop.sh +16 -0
  316. package/.agents/skills/muninn-stow/README.md +3 -0
  317. package/.agents/skills/muninn-stow/SKILL.md +309 -0
  318. package/.agents/skills/no-mistakes/SKILL.md +379 -0
  319. package/.agents/skills/nornir-schedule/README.md +3 -0
  320. package/.agents/skills/nornir-schedule/SKILL.md +25 -0
  321. package/.agents/skills/nornir-schedule/assets/events.md +137 -0
  322. package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
  323. package/.agents/skills/nsr-compliance/README.md +3 -0
  324. package/.agents/skills/nsr-compliance/SKILL.md +29 -0
  325. package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
  326. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
  327. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
  328. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
  329. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
  330. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
  331. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
  332. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
  333. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
  334. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
  335. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
  336. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
  337. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
  338. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
  339. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
  340. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
  341. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
  342. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
  343. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
  344. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
  345. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
  346. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
  347. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
  348. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  349. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
  350. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
  351. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
  352. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
  353. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
  354. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
  355. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
  356. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
  357. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
  358. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
  359. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
  360. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
  361. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
  362. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
  363. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
  364. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
  365. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
  366. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
  367. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
  368. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
  369. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
  370. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
  371. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
  372. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
  373. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
  374. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
  375. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
  376. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
  377. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
  378. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
  379. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
  380. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
  381. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
  382. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
  383. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
  384. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
  385. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
  386. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
  387. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
  388. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
  389. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
  390. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
  391. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
  392. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
  393. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
  394. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
  395. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
  396. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
  397. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
  398. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
  399. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
  400. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
  401. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
  402. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
  403. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
  404. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
  405. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
  406. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
  407. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
  408. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
  409. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
  410. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
  411. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
  412. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
  413. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
  414. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
  415. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
  416. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
  417. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
  418. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
  419. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
  420. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
  421. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
  422. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
  423. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
  424. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
  425. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
  426. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
  427. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
  428. package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
  429. package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
  430. package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
  431. package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
  432. package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
  433. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
  434. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
  435. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
  436. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
  437. package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
  438. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
  439. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
  440. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
  441. package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
  442. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
  443. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
  444. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
  445. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
  446. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
  447. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
  448. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
  449. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  450. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
  451. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
  452. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
  453. package/.agents/skills/pr-ops/README.md +3 -0
  454. package/.agents/skills/pr-ops/SKILL.md +109 -0
  455. package/.agents/skills/ratatoskr-a2a/README.md +3 -0
  456. package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
  457. package/.agents/skills/rules-check-drift/SKILL.md +81 -0
  458. package/.agents/skills/saga-bearings/README.md +3 -0
  459. package/.agents/skills/saga-bearings/SKILL.md +26 -0
  460. package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
  461. package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
  462. package/.agents/skills/saga-bearings/assets/recap.md +56 -0
  463. package/.agents/skills/syn-recovery/README.md +3 -0
  464. package/.agents/skills/syn-recovery/SKILL.md +54 -0
  465. package/.agents/skills/tyr-check/README.md +3 -0
  466. package/.agents/skills/tyr-check/SKILL.md +218 -0
  467. package/.agents/skills/urdh-hold/README.md +3 -0
  468. package/.agents/skills/urdh-hold/SKILL.md +25 -0
  469. package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
  470. package/.agents/skills/urdh-hold/assets/hold.md +54 -0
  471. package/.agents/skills/vor-diagnostics/README.md +3 -0
  472. package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
  473. package/.agents/skills/ymir-host/README.md +3 -0
  474. package/.agents/skills/ymir-host/SKILL.md +35 -0
  475. package/.agents/skills/ymir-host/assets/desktop.md +36 -0
  476. package/.agents/skills/ymir-host/assets/install.md +69 -0
  477. package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
  478. package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
  479. package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
  480. package/.agents/skills/ymir-host/assets/update.md +64 -0
  481. package/.agents/tests/assets/board-render-harness.mjs +117 -0
  482. package/.agents/tests/cmux-test-safety.sh +51 -0
  483. package/.agents/tests/fixtures.sh +291 -0
  484. package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
  485. package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
  486. package/.agents/tests/fm-afk-launch.test.sh +961 -0
  487. package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
  488. package/.agents/tests/fm-afk-return.test.sh +283 -0
  489. package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
  490. package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
  491. package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
  492. package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
  493. package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
  494. package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
  495. package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
  496. package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
  497. package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
  498. package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
  499. package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
  500. package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
  501. package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
  502. package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
  503. package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
  504. package/.agents/tests/fm-backend-orca.test.sh +1357 -0
  505. package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
  506. package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
  507. package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
  508. package/.agents/tests/fm-backend.test.sh +1142 -0
  509. package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
  510. package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
  511. package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
  512. package/.agents/tests/fm-bearings-board.test.sh +414 -0
  513. package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
  514. package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
  515. package/.agents/tests/fm-bootstrap.test.sh +1178 -0
  516. package/.agents/tests/fm-branch-supervision.test.sh +552 -0
  517. package/.agents/tests/fm-brief.test.sh +785 -0
  518. package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
  519. package/.agents/tests/fm-busy-state.test.sh +465 -0
  520. package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
  521. package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
  522. package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
  523. package/.agents/tests/fm-check-unregister.test.sh +198 -0
  524. package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
  525. package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
  526. package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
  527. package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
  528. package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
  529. package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
  530. package/.agents/tests/fm-composer-ghost.test.sh +716 -0
  531. package/.agents/tests/fm-composer-lib.test.sh +666 -0
  532. package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
  533. package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
  534. package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
  535. package/.agents/tests/fm-control.test.sh +910 -0
  536. package/.agents/tests/fm-crew-state.test.sh +1610 -0
  537. package/.agents/tests/fm-cursor-harness.test.sh +404 -0
  538. package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
  539. package/.agents/tests/fm-cursor-primary.test.sh +708 -0
  540. package/.agents/tests/fm-daemon.test.sh +2740 -0
  541. package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
  542. package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
  543. package/.agents/tests/fm-extension-binding.test.sh +2187 -0
  544. package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
  545. package/.agents/tests/fm-fleet-sync.test.sh +721 -0
  546. package/.agents/tests/fm-gate-refuse.test.sh +356 -0
  547. package/.agents/tests/fm-gitignore-config.test.sh +89 -0
  548. package/.agents/tests/fm-gotmp.test.sh +230 -0
  549. package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
  550. package/.agents/tests/fm-grok-harness.test.sh +117 -0
  551. package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
  552. package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
  553. package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
  554. package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
  555. package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
  556. package/.agents/tests/fm-herdr-lab.test.sh +243 -0
  557. package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
  558. package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
  559. package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
  560. package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
  561. package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
  562. package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
  563. package/.agents/tests/fm-kimi-harness.test.sh +687 -0
  564. package/.agents/tests/fm-lint-workflows.test.sh +532 -0
  565. package/.agents/tests/fm-lint.test.sh +1023 -0
  566. package/.agents/tests/fm-muse-harness.test.sh +932 -0
  567. package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
  568. package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
  569. package/.agents/tests/fm-on.test.sh +507 -0
  570. package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
  571. package/.agents/tests/fm-operational-input.test.sh +160 -0
  572. package/.agents/tests/fm-peek-remote.test.sh +110 -0
  573. package/.agents/tests/fm-pending-reply.test.sh +1288 -0
  574. package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
  575. package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
  576. package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
  577. package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
  578. package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
  579. package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
  580. package/.agents/tests/fm-pr-merge.test.sh +2135 -0
  581. package/.agents/tests/fm-procevent-quota.test.sh +225 -0
  582. package/.agents/tests/fm-procevent-when.test.sh +406 -0
  583. package/.agents/tests/fm-procevent.test.sh +1745 -0
  584. package/.agents/tests/fm-project-origin.test.sh +110 -0
  585. package/.agents/tests/fm-public-followup.test.sh +2272 -0
  586. package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
  587. package/.agents/tests/fm-quota-choose.test.sh +626 -0
  588. package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
  589. package/.agents/tests/fm-remote-doctor.test.sh +625 -0
  590. package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
  591. package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
  592. package/.agents/tests/fm-remote-job.test.sh +768 -0
  593. package/.agents/tests/fm-remote-reply.test.sh +520 -0
  594. package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
  595. package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
  596. package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
  597. package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
  598. package/.agents/tests/fm-review-diff.test.sh +176 -0
  599. package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
  600. package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
  601. package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
  602. package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
  603. package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
  604. package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
  605. package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
  606. package/.agents/tests/fm-send-inbox.test.sh +352 -0
  607. package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
  608. package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
  609. package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
  610. package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
  611. package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
  612. package/.agents/tests/fm-send-settle.test.sh +147 -0
  613. package/.agents/tests/fm-send-strict.test.sh +241 -0
  614. package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
  615. package/.agents/tests/fm-session-start.test.sh +2515 -0
  616. package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
  617. package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
  618. package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
  619. package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
  620. package/.agents/tests/fm-spawn-batch.test.sh +148 -0
  621. package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
  622. package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
  623. package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
  624. package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
  625. package/.agents/tests/fm-startup-network.test.sh +712 -0
  626. package/.agents/tests/fm-stow-cascade.test.sh +370 -0
  627. package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
  628. package/.agents/tests/fm-supervision-events.test.sh +156 -0
  629. package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
  630. package/.agents/tests/fm-tangle-guard.test.sh +277 -0
  631. package/.agents/tests/fm-task-delivery.test.sh +420 -0
  632. package/.agents/tests/fm-task-inbox.test.sh +513 -0
  633. package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
  634. package/.agents/tests/fm-teardown.test.sh +2665 -0
  635. package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
  636. package/.agents/tests/fm-test-fixtures.test.sh +132 -0
  637. package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
  638. package/.agents/tests/fm-test-run.test.sh +1196 -0
  639. package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
  640. package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
  641. package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
  642. package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
  643. package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
  644. package/.agents/tests/fm-transition-lib.test.sh +54 -0
  645. package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
  646. package/.agents/tests/fm-update.test.sh +304 -0
  647. package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
  648. package/.agents/tests/fm-voice-relay.test.sh +4741 -0
  649. package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
  650. package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
  651. package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
  652. package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
  653. package/.agents/tests/fm-wake-queue.test.sh +1224 -0
  654. package/.agents/tests/fm-watch-arm.test.sh +821 -0
  655. package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
  656. package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
  657. package/.agents/tests/fm-watch-triage.test.sh +3889 -0
  658. package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
  659. package/.agents/tests/fm-x-mode.test.sh +3042 -0
  660. package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
  661. package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
  662. package/.agents/tests/herdr-test-safety.sh +42 -0
  663. package/.agents/tests/lib.sh +315 -0
  664. package/.agents/tests/remote-herdr-fixture.sh +125 -0
  665. package/.agents/tests/secondmate-helpers.sh +193 -0
  666. package/.agents/tests/smoke.test.sh +93 -0
  667. package/.agents/tests/wake-helpers.sh +338 -0
  668. package/.agents/tests/zellij-test-safety.sh +60 -0
  669. package/.agents/tools/README.md +10 -0
  670. package/.agents/tools/bin/tasks-cli +267 -0
  671. package/.agents/tools/tasks-cli.ts +216 -0
  672. package/AGENTS.md +498 -0
  673. package/CHANGELOG.md +1913 -0
  674. package/LICENSE +1 -1
  675. package/NOTICE +63 -0
  676. package/README.md +114 -60
  677. package/RULES/01-domains.md +51 -0
  678. package/RULES/02-agents.md +78 -0
  679. package/RULES/03-houses.md +61 -0
  680. package/RULES/04-hoard.md +97 -0
  681. package/RULES/05-platforms.md +67 -0
  682. package/RULES/06-append-only.md +108 -0
  683. package/RULES/07-config.md +101 -0
  684. package/RULES/08-delivery-gate.md +75 -0
  685. package/RULES/09-electron.md +58 -0
  686. package/RULES/README.md +22 -0
  687. package/bin/README.md +107 -0
  688. package/bin/a2a-mcp.sh +133 -0
  689. package/bin/a2a-serve.py +103 -0
  690. package/bin/a2a-serve.sh +8 -0
  691. package/bin/a2a-talk.sh +50 -0
  692. package/bin/a2abridge-ensure.sh +108 -0
  693. package/bin/agent-run.sh +43 -0
  694. package/bin/agents-config.sh +256 -0
  695. package/bin/apodex-smoke-test.sh +105 -0
  696. package/bin/bifrost-bridge.sh +131 -0
  697. package/bin/bifrost-ingress.sh +77 -0
  698. package/bin/bootstrap-macos.sh +112 -0
  699. package/bin/bootstrap-windows.ps1 +112 -0
  700. package/bin/branch-guard.sh +64 -0
  701. package/bin/brokk +73 -0
  702. package/bin/brokk-classify-lib.sh +1770 -0
  703. package/bin/brokk-lease-lib.sh +222 -0
  704. package/bin/brokk-lease.sh +193 -0
  705. package/bin/brokk-lint.sh +65 -0
  706. package/bin/brokk-send.sh +51 -0
  707. package/bin/brokk-update.sh +9 -0
  708. package/bin/brokk-wake-grant.sh +127 -0
  709. package/bin/brokk-wake-lib.sh +1849 -0
  710. package/bin/changelog-assemble.sh +154 -0
  711. package/bin/changelog-guard.sh +197 -0
  712. package/bin/crash-sense.sh +173 -0
  713. package/bin/design-check.sh +80 -0
  714. package/bin/design-icon.sh +153 -0
  715. package/bin/desktop-place.sh +224 -0
  716. package/bin/docs-guard.sh +39 -0
  717. package/bin/editor-place.sh +225 -0
  718. package/bin/eindri-acclaim.sh +47 -0
  719. package/bin/eindri-control.sh +28 -0
  720. package/bin/eindri-role.sh +83 -0
  721. package/bin/eindri-seat.sh +83 -0
  722. package/bin/eindri-seen.sh +54 -0
  723. package/bin/eindri-send.sh +15 -0
  724. package/bin/eindri-start.sh +98 -0
  725. package/bin/eindri-watch.sh +57 -0
  726. package/bin/einherjar-spawn.sh +576 -0
  727. package/bin/eir-doctor.sh +158 -0
  728. package/bin/erindi-brief.sh +271 -0
  729. package/bin/feature-inventory.sh +73 -0
  730. package/bin/fleet-apply.sh +81 -0
  731. package/bin/github-deploy.sh +94 -0
  732. package/bin/gjallarhorn-expose.sh +136 -0
  733. package/bin/gjallarhorn-notify.sh +71 -0
  734. package/bin/gjallarhorn-purge.sh +67 -0
  735. package/bin/gjallarhorn-tunnel.sh +85 -0
  736. package/bin/gleipnir-lock-lib.sh +284 -0
  737. package/bin/groa-update.sh +130 -0
  738. package/bin/hall-snapshot.sh +111 -0
  739. package/bin/hamr-harness.sh +246 -0
  740. package/bin/heimdall-ssh-keys.sh +374 -0
  741. package/bin/herdr-agents.py +44 -0
  742. package/bin/herdr-ensure.sh +104 -0
  743. package/bin/herdr-run.sh +452 -0
  744. package/bin/hermes-ensure.sh +88 -0
  745. package/bin/hlidskjalf-agents.sh +172 -0
  746. package/bin/hlidskjalf-usage.sh +196 -0
  747. package/bin/hoard-guard.sh +135 -0
  748. package/bin/hoard-lib.sh +105 -0
  749. package/bin/hodd.sh +102 -0
  750. package/bin/host-sense.sh +199 -0
  751. package/bin/huginn-research-worker.sh +121 -0
  752. package/bin/local-model-lock.sh +70 -0
  753. package/bin/mimir-bridge.py +259 -0
  754. package/bin/mimir-bridge.sh +100 -0
  755. package/bin/mimir-ingest.sh +131 -0
  756. package/bin/mimir-reflect.sh +27 -0
  757. package/bin/mimir.sh +113 -0
  758. package/bin/mjollnir-webhook.sh +72 -0
  759. package/bin/mjollnir.sh +104 -0
  760. package/bin/model-hardware.sh +96 -0
  761. package/bin/model-resolve.sh +160 -0
  762. package/bin/models-detect.sh +178 -0
  763. package/bin/nornir-cron-start.sh +162 -0
  764. package/bin/nornir-job-daily-briefing.sh +191 -0
  765. package/bin/nornir-job-git-sync.sh +152 -0
  766. package/bin/nornir-job-hall-snapshot.sh +28 -0
  767. package/bin/nornir-job-memory-housekeeping.sh +114 -0
  768. package/bin/nornir-job-nsr-compliance.sh +54 -0
  769. package/bin/nornir-job-observer.sh +166 -0
  770. package/bin/nornir-job-skillopt-sleep.sh +76 -0
  771. package/bin/npm-publish.sh +136 -0
  772. package/bin/omarchy-hook-install.sh +60 -0
  773. package/bin/omarchy-install.sh +161 -0
  774. package/bin/omarchy-plugins.sh +187 -0
  775. package/bin/omarchy-sense.sh +162 -0
  776. package/bin/perm-guard.sh +30 -0
  777. package/bin/pi-agent.sh +68 -0
  778. package/bin/pi-ensure.sh +77 -0
  779. package/bin/pi-local.sh +37 -0
  780. package/bin/pi-model-agnostic.sh +71 -0
  781. package/bin/pi-seat.sh +63 -0
  782. package/bin/prereq-ensure.sh +166 -0
  783. package/bin/private-guard.sh +58 -0
  784. package/bin/project-git.sh +69 -0
  785. package/bin/public-guard.sh +71 -0
  786. package/bin/ratatoskr.sh +49 -0
  787. package/bin/realm-lib.sh +32 -0
  788. package/bin/repo-scrub.sh +62 -0
  789. package/bin/rodd-operational-input.sh +250 -0
  790. package/bin/runes-append.sh +198 -0
  791. package/bin/saga-session-start.sh +172 -0
  792. package/bin/saga-sessionstart-run.sh +79 -0
  793. package/bin/saga-wake-drain.sh +41 -0
  794. package/bin/secret-guard.sh +63 -0
  795. package/bin/sessrumnir-ensure.sh +100 -0
  796. package/bin/sessrumnir-sync.sh +237 -0
  797. package/bin/sessrumnir.sh +104 -0
  798. package/bin/skillopt-setup.sh +76 -0
  799. package/bin/skuld-branch-outcome.sh +203 -0
  800. package/bin/skuld-branch-prompt.sh +101 -0
  801. package/bin/smidja-bootstrap.sh +94 -0
  802. package/bin/smidja-observe.sh +86 -0
  803. package/bin/syn-arm-pretool-check.sh +57 -0
  804. package/bin/syn-asset-pretool-check.sh +89 -0
  805. package/bin/syn-cd-pretool-check.sh +28 -0
  806. package/bin/syn-guard-pretool-check.sh +83 -0
  807. package/bin/syn-spawn-pretool-check.sh +58 -0
  808. package/bin/syn-turnend-guard.sh +36 -0
  809. package/bin/syn-watch-arm.sh +99 -0
  810. package/bin/tailscale-sync.sh +203 -0
  811. package/bin/telegram-bot.sh +82 -0
  812. package/bin/toolchain.sh +74 -0
  813. package/bin/ui-truth-check.sh +75 -0
  814. package/bin/utgard.sh +111 -0
  815. package/bin/valhalla.sh +115 -0
  816. package/bin/valknut-load.sh +298 -0
  817. package/bin/vor-crew-state.sh +124 -0
  818. package/bin/wedge-notify.sh +60 -0
  819. package/bin/workspace-provision.sh +80 -0
  820. package/bin/workspace-rag.sh +163 -0
  821. package/bin/wyrd-db.sh +47 -0
  822. package/bin/yggdrasil.sh +152 -0
  823. package/bin/ymir-edit.sh +145 -0
  824. package/bin/ymir-install.sh +806 -0
  825. package/bin/ymir-invite.sh +93 -0
  826. package/bin/ymir-isolation.sh +88 -0
  827. package/bin/ymir-migrate.sh +61 -0
  828. package/bin/ymir-plan.sh +356 -0
  829. package/bin/ymir-platform.sh +348 -0
  830. package/bin/ymir-say.sh +110 -0
  831. package/bin/ymir-setup-auth.sh +119 -0
  832. package/bin/ymir-validate.sh +193 -0
  833. package/bin/ymir.js +74 -16
  834. package/docs/Architecture.md +339 -0
  835. package/docs/design.md +351 -0
  836. package/docs/installations/README.md +63 -0
  837. package/docs/installations/ymir-home.md +93 -0
  838. package/docs/integration.md +105 -0
  839. package/docs/lore.md +1060 -0
  840. package/docs/research/agent-harnesses.md +290 -0
  841. package/docs/runbooks/README.md +19 -0
  842. package/docs/runbooks/a2a.md +91 -0
  843. package/docs/runbooks/agent-permissions.md +99 -0
  844. package/docs/runbooks/agents.md +57 -0
  845. package/docs/runbooks/models.md +69 -0
  846. package/docs/runbooks/realm-onboarding.md +60 -0
  847. package/docs/runbooks/secrets-and-hoard.md +60 -0
  848. package/docs/runbooks/updates-and-migrations.md +110 -0
  849. package/docs/runbooks/worktrees-isolation.md +72 -0
  850. package/docs/session-start.md +105 -0
  851. package/docs/skillopt-integration.md +321 -0
  852. package/docs/sync-system.md +66 -0
  853. package/docs/workspaces.md +104 -0
  854. package/docs/ymir-rut.md +260 -0
  855. package/package.json +54 -14
  856. package/packaging/build.sh +74 -0
  857. package/packaging/macos/Ymir Installer.command +43 -0
  858. package/packaging/windows/ymir-setup.nsi +74 -0
  859. package/scripts/README.md +22 -0
  860. package/scripts/electron.sh +269 -0
  861. package/scripts/lower.sh +34 -0
  862. package/scripts/raise.sh +42 -0
  863. package/scripts/start.sh +217 -0
  864. package/scripts/stop.sh +113 -0
  865. package/install.sh +0 -35
@@ -0,0 +1,1000 @@
1
+ #!/usr/bin/env bash
2
+ # Shared config resolution for the X-mode connector client (fm-x-poll.sh and
3
+ # fm-x-reply.sh). X mode is opt-in: a user drops a non-empty FMX_PAIRING_TOKEN
4
+ # into the firstmate home's .env. FMX_ENV_FILE can point direct client calls at
5
+ # another .env-style file, but bootstrap activation still checks $FM_HOME/.env.
6
+ # Until then polling is a hard no-op; replies can still run in FMX_DRY_RUN
7
+ # preview mode without a token.
8
+ #
9
+ # This file is sourced, never executed. It defines:
10
+ # fmx_env_get <key> <file> - read one KEY=VALUE from a .env-style file
11
+ # fmx_load_config - resolve FMX_TOKEN, FMX_RELAY, FMX_DRY, FMX_MAX,
12
+ # and FMX_THREAD_MAX (env wins over .env)
13
+ # fmx_auth_header_file - write the bearer header to a 0600 temp file
14
+ # fmx_extract_reply_context <json-file> - the single owner of reply-context
15
+ # extraction: infer {platform, reply_max_chars}
16
+ # from any mention/relay payload file
17
+ # fmx_request_inbox_context <state> <request_id> - reply context from a stashed
18
+ # mention payload (wrapper over the extractor)
19
+ # fmx_request_relay_context <request_id> - resolve reply platform/limit
20
+ # AUTHORITATIVELY from the relay by request_id when
21
+ # no local inbox payload survives
22
+ # fmx_context_registry_set <state> <request_id> <platform> <reply-max> [refresh]
23
+ # - persist the durable per-request reply context;
24
+ # refresh=1 resets its retention timestamp
25
+ # fmx_offer_registry_claim <state> <request_id> - atomically claim the durable
26
+ # one-wake offer marker; 0=new, 1=existing, 2=error
27
+ # fmx_context_registry_prune <state> - remove records older than seven days
28
+ # fmx_context_registry_get <state> <request_id> - read the durable per-request
29
+ # reply context, or the empty shape when absent
30
+ # fmx_context_registry_clear <state> <request_id> - drop the durable record
31
+ # fmx_resolve_reply_context <state> <request_id> <allow-relay> - resolve reply
32
+ # context through registry -> inbox -> relay
33
+ # fmx_reply_limit_for_platform <platform> <explicit-limit> - pick split budget
34
+ # fmx_split_thread <max> <cap> - split a reply (stdin) into a numbered thread
35
+ # fmx_image_payload_file <path> <client> <payload-file> - encode one image
36
+ # attachment to a JSON file and print preview JSON
37
+ # fmx_reply_payload_json <request_id> <chunks> <n> [image-json-file]
38
+ # - build the answer/followup POST body
39
+ # fmx_reply_outbox_json <request_id> <chunks> <n> <followup-0|1> [image-preview-json]
40
+ # - build the dry-run record without image bytes
41
+ # fmx_post_json <endpoint> <payload-file> [body-file] - POST JSON to the relay,
42
+ # printing HTTP code and writing response body
43
+ # fmx_meta_get <meta> <key> - read one key=value line from a task meta file
44
+ # fmx_meta_link_set <meta> <request_id> <epoch> [followups] [platform] [max]
45
+ # - (re)write the X-request link, defaulting
46
+ # followups to 0
47
+ # fmx_meta_followups_set <meta> <n> - rewrite just the follow-up counter
48
+ # fmx_meta_link_clear <meta> - remove the X-request link entirely
49
+ # Callers must have FM_HOME set before calling fmx_load_config.
50
+
51
+ _FM_X_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
52
+ if ! command -v fm_backlog_atomic_transition >/dev/null 2>&1; then
53
+ # shellcheck source=bin/fm-tasks-axi-lib.sh
54
+ . "$_FM_X_LIB_DIR/fm-tasks-axi-lib.sh"
55
+ # shellcheck source=bin/fm-backlog-transition-lib.sh
56
+ . "$_FM_X_LIB_DIR/fm-backlog-transition-lib.sh"
57
+ fi
58
+
59
+ # Read the value of KEY from a .env-style file: last assignment wins; tolerates a
60
+ # leading "export ", surrounding whitespace, and one layer of matching single or
61
+ # double quotes. Prints nothing (and succeeds) when the file or key is absent, so
62
+ # callers can treat empty output as "unset".
63
+ fmx_env_get() {
64
+ local key=$1 file=$2 line val
65
+ [ -f "$file" ] || return 0
66
+ line=$(grep -E "^[[:space:]]*(export[[:space:]]+)?${key}=" "$file" 2>/dev/null | tail -n1) || return 0
67
+ [ -n "$line" ] || return 0
68
+ val=${line#*=}
69
+ val=${val#"${val%%[![:space:]]*}"} # strip leading whitespace
70
+ val=${val%"${val##*[![:space:]]}"} # strip trailing whitespace (incl. CR)
71
+ case "$val" in
72
+ \"*\") val=${val#\"}; val=${val%\"} ;;
73
+ \'*\') val=${val#\'}; val=${val%\'} ;;
74
+ esac
75
+ printf '%s' "$val"
76
+ }
77
+
78
+ fmx_poll_shim_content() {
79
+ local home=$1 root=$2
80
+ printf '%s\n' \
81
+ '#!/usr/bin/env bash' \
82
+ '# Auto-generated by fm-bootstrap.sh - X mode connector poll shim.' \
83
+ '# The watcher validates these bytes, then dispatches the trusted poll script.' \
84
+ "export FM_HOME=$(printf '%q' "$home")" \
85
+ "exec $(printf '%q' "$root/bin/fm-x-poll.sh")"
86
+ }
87
+
88
+ fmx_single_link_file_valid() {
89
+ local file=$1 expected_device=${2-} links device
90
+ [ -f "$file" ] && [ ! -L "$file" ] || return 1
91
+ if [ "$(uname)" = Darwin ]; then
92
+ links=$(stat -f %l "$file" 2>/dev/null) || return 1
93
+ device=$(stat -f %d "$file" 2>/dev/null) || return 1
94
+ else
95
+ links=$(stat -c %h "$file" 2>/dev/null) || return 1
96
+ device=$(stat -c %d "$file" 2>/dev/null) || return 1
97
+ fi
98
+ [ "$links" = 1 ] || return 1
99
+ [ -z "$expected_device" ] || [ "$device" = "$expected_device" ]
100
+ }
101
+
102
+ fmx_single_link_file_mode_valid() {
103
+ local file=$1 expected_mode=$2 expected_device=${3-} mode
104
+ fmx_single_link_file_valid "$file" "$expected_device" || return 1
105
+ if [ "$(uname)" = Darwin ]; then
106
+ mode=$(stat -f %Lp "$file" 2>/dev/null) || return 1
107
+ else
108
+ mode=$(stat -c %a "$file" 2>/dev/null) || return 1
109
+ fi
110
+ [ "$mode" = "$expected_mode" ]
111
+ }
112
+
113
+ fmx_private_artifact_dir_device() {
114
+ local dir=$1 mode device
115
+ [ -d "$dir" ] && [ ! -L "$dir" ] || return 1
116
+ if [ "$(uname)" = Darwin ]; then
117
+ mode=$(stat -f %Lp "$dir" 2>/dev/null) || return 1
118
+ device=$(stat -f %d "$dir" 2>/dev/null) || return 1
119
+ else
120
+ mode=$(stat -c %a "$dir" 2>/dev/null) || return 1
121
+ device=$(stat -c %d "$dir" 2>/dev/null) || return 1
122
+ fi
123
+ [ "$mode" = 700 ] || return 1
124
+ printf '%s\n' "$device"
125
+ }
126
+
127
+ fmx_private_artifact_dir_prepare() {
128
+ local dir=$1 parent
129
+ parent=${dir%/*}
130
+ if [ "$parent" != "$dir" ]; then
131
+ if [ -e "$parent" ] || [ -L "$parent" ]; then
132
+ [ -d "$parent" ] && [ ! -L "$parent" ] || return 1
133
+ else
134
+ (umask 077; mkdir -p "$parent" 2>/dev/null) || return 1
135
+ [ -d "$parent" ] && [ ! -L "$parent" ] || return 1
136
+ fi
137
+ fi
138
+ if [ -e "$dir" ] || [ -L "$dir" ]; then
139
+ [ -d "$dir" ] && [ ! -L "$dir" ] || return 1
140
+ else
141
+ (umask 077; mkdir -p "$dir" 2>/dev/null) || return 1
142
+ fi
143
+ fmx_private_artifact_dir_device "$dir"
144
+ }
145
+
146
+ fmx_private_artifact_publish_stdin() {
147
+ local dir=$1 base=$2 mode=$3 device tmp dest
148
+ case "$base" in
149
+ ''|.*|*/*) return 1 ;;
150
+ esac
151
+ case "$mode" in
152
+ 600|700) ;;
153
+ *) return 1 ;;
154
+ esac
155
+ device=$(fmx_private_artifact_dir_prepare "$dir") || return 1
156
+ dest="$dir/$base"
157
+ tmp=$(umask 077; mktemp "$dir/.${base}.fm-x.XXXXXX" 2>/dev/null) || return 1
158
+ if ! cat > "$tmp" \
159
+ || ! chmod "$mode" "$tmp" 2>/dev/null \
160
+ || ! fmx_single_link_file_mode_valid "$tmp" "$mode" "$device"; then
161
+ rm -f -- "$tmp"
162
+ return 1
163
+ fi
164
+ if { [ -e "$dest" ] || [ -L "$dest" ]; } \
165
+ && ! fmx_single_link_file_mode_valid "$dest" "$mode" "$device"; then
166
+ rm -f -- "$tmp"
167
+ return 1
168
+ fi
169
+ if ! mv -f -- "$tmp" "$dest" 2>/dev/null; then
170
+ rm -f -- "$tmp"
171
+ return 1
172
+ fi
173
+ if ! fmx_single_link_file_mode_valid "$dest" "$mode" "$device"; then
174
+ rm -f -- "$dest"
175
+ return 1
176
+ fi
177
+ }
178
+
179
+ # Publish stdin as a new private artifact without replacing an existing path.
180
+ # The hard-link claim is atomic within the prepared directory, so concurrent
181
+ # callers cannot both create the destination. Returns 0 when this caller created
182
+ # it, 1 when another valid private artifact already owns the path, and 2 on an
183
+ # unsafe path or publication failure.
184
+ fmx_private_artifact_publish_stdin_once() {
185
+ local dir=$1 base=$2 mode=$3 device tmp dest
186
+ case "$base" in
187
+ ''|.*|*/*) return 2 ;;
188
+ esac
189
+ case "$mode" in
190
+ 600|700) ;;
191
+ *) return 2 ;;
192
+ esac
193
+ device=$(fmx_private_artifact_dir_prepare "$dir") || return 2
194
+ dest="$dir/$base"
195
+ tmp=$(umask 077; mktemp "$dir/.${base}.fm-x.XXXXXX" 2>/dev/null) || return 2
196
+ if ! cat > "$tmp" \
197
+ || ! chmod "$mode" "$tmp" 2>/dev/null \
198
+ || ! fmx_single_link_file_mode_valid "$tmp" "$mode" "$device"; then
199
+ rm -f -- "$tmp"
200
+ return 2
201
+ fi
202
+ if ln -- "$tmp" "$dest" 2>/dev/null; then
203
+ rm -f -- "$tmp"
204
+ if fmx_single_link_file_mode_valid "$dest" "$mode" "$device"; then
205
+ return 0
206
+ fi
207
+ rm -f -- "$dest"
208
+ return 2
209
+ fi
210
+ rm -f -- "$tmp"
211
+ if fmx_single_link_file_mode_valid "$dest" "$mode" "$device"; then
212
+ return 1
213
+ fi
214
+ return 2
215
+ }
216
+
217
+ fmx_private_artifact_file_valid() {
218
+ local dir=$1 base=$2 mode=$3 device
219
+ case "$base" in
220
+ ''|.*|*/*) return 1 ;;
221
+ esac
222
+ case "$mode" in
223
+ 600|700) ;;
224
+ *) return 1 ;;
225
+ esac
226
+ device=$(fmx_private_artifact_dir_device "$dir") || return 1
227
+ fmx_single_link_file_mode_valid "$dir/$base" "$mode" "$device"
228
+ }
229
+
230
+ fmx_poll_shim_identity_valid() {
231
+ fmx_single_link_file_mode_valid "$1" "$2" "${3-}"
232
+ }
233
+
234
+ fmx_poll_shim_private_identity_valid() {
235
+ fmx_poll_shim_identity_valid "$1" 700
236
+ }
237
+
238
+ fmx_poll_shim_valid() {
239
+ local file=$1 home=$2 root=$3
240
+ fmx_poll_shim_private_identity_valid "$file" || return 1
241
+ cmp -s "$file" <(fmx_poll_shim_content "$home" "$root")
242
+ }
243
+
244
+ # Resolve the X-mode settings into FMX_TOKEN, FMX_RELAY, FMX_DRY, FMX_MAX,
245
+ # FMX_DISCORD_MAX, and FMX_THREAD_MAX. An explicit environment variable always
246
+ # wins over the .env file; the relay URL defaults to the production host so a
247
+ # normal user configures only the token. FMX_RELAY has any trailing slash trimmed
248
+ # so callers can append "/connector/..." cleanly.
249
+ # FMX_DRY is set to "1" when FMX_DRY_RUN is a truthy value (anything other than
250
+ # unset/empty/0/false/no/off), and "" otherwise: preview mode, where the client
251
+ # composes a reply but records it instead of posting (see fm-x-reply.sh).
252
+ fmx_load_config() {
253
+ local env_file="${FMX_ENV_FILE:-$FM_HOME/.env}" dry
254
+ if [ -n "${FMX_PAIRING_TOKEN+x}" ]; then
255
+ FMX_TOKEN=${FMX_PAIRING_TOKEN-}
256
+ else
257
+ FMX_TOKEN=$(fmx_env_get FMX_PAIRING_TOKEN "$env_file")
258
+ fi
259
+ if [ -n "${FMX_RELAY_URL+x}" ]; then
260
+ FMX_RELAY=${FMX_RELAY_URL-}
261
+ else
262
+ FMX_RELAY=$(fmx_env_get FMX_RELAY_URL "$env_file")
263
+ fi
264
+ [ -n "$FMX_RELAY" ] || FMX_RELAY="https://myfirstmate.io"
265
+ FMX_RELAY=${FMX_RELAY%/}
266
+ if [ -n "${FMX_DRY_RUN+x}" ]; then
267
+ dry=${FMX_DRY_RUN-}
268
+ else
269
+ dry=$(fmx_env_get FMX_DRY_RUN "$env_file")
270
+ fi
271
+ # shellcheck disable=SC2034 # FMX_DRY is read by callers (fm-x-reply.sh) after sourcing.
272
+ case "$(printf '%s' "$dry" | tr '[:upper:]' '[:lower:]')" in
273
+ ''|0|false|no|off) FMX_DRY="" ;;
274
+ *) FMX_DRY=1 ;;
275
+ esac
276
+
277
+ # Per-message character budgets for thread-splitting, and the maximum number
278
+ # of messages in one auto-split thread (anti-spam cap).
279
+ local maxraw discordraw threadraw
280
+ if [ -n "${FMX_X_REPLY_MAX_CHARS+x}" ]; then maxraw=${FMX_X_REPLY_MAX_CHARS-}; else maxraw=$(fmx_env_get FMX_X_REPLY_MAX_CHARS "$env_file"); fi
281
+ case "$maxraw" in ''|*[!0-9]*) maxraw=280 ;; esac
282
+ [ "$maxraw" -ge 50 ] 2>/dev/null || maxraw=50
283
+ # shellcheck disable=SC2034 # FMX_MAX is read by callers (fm-x-reply.sh) after sourcing.
284
+ FMX_MAX=$maxraw
285
+ if [ -n "${FMX_DISCORD_REPLY_MAX_CHARS+x}" ]; then discordraw=${FMX_DISCORD_REPLY_MAX_CHARS-}; else discordraw=$(fmx_env_get FMX_DISCORD_REPLY_MAX_CHARS "$env_file"); fi
286
+ case "$discordraw" in ''|*[!0-9]*) discordraw=1900 ;; esac
287
+ [ "$discordraw" -ge 50 ] 2>/dev/null || discordraw=50
288
+ [ "$discordraw" -le 2000 ] 2>/dev/null || discordraw=1900
289
+ # shellcheck disable=SC2034 # FMX_DISCORD_MAX is read by callers (fm-x-reply.sh) after sourcing.
290
+ FMX_DISCORD_MAX=$discordraw
291
+ if [ -n "${FMX_X_THREAD_MAX+x}" ]; then threadraw=${FMX_X_THREAD_MAX-}; else threadraw=$(fmx_env_get FMX_X_THREAD_MAX "$env_file"); fi
292
+ case "$threadraw" in ''|*[!0-9]*) threadraw=25 ;; esac
293
+ [ "$threadraw" -ge 1 ] 2>/dev/null || threadraw=25
294
+ # shellcheck disable=SC2034 # FMX_THREAD_MAX is read by callers (fm-x-reply.sh) after sourcing.
295
+ FMX_THREAD_MAX=$threadraw
296
+ }
297
+
298
+ # fmx_extract_reply_context <json-file>: the SINGLE owner of reply-context
299
+ # extraction. Print {"platform":"...","reply_max_chars":"..."} inferred from a
300
+ # mention/relay payload file. Explicit relay-provided platform/limit fields win;
301
+ # absent those, the legacy tweet_id shape is used ("discord:<channel>:<message>"
302
+ # means Discord, a numeric id means X). Empty fields mean unknown, and callers
303
+ # must default safely. A missing file yields the empty shape. The inbox, relay,
304
+ # and poll paths all feed their payload through this one function so platform
305
+ # inference can never drift between them.
306
+ fmx_extract_reply_context() {
307
+ local file=$1
308
+ if [ ! -f "$file" ]; then
309
+ printf '{"platform":"","reply_max_chars":""}\n'
310
+ return 0
311
+ fi
312
+ jq -c '
313
+ def norm_platform:
314
+ tostring | ascii_downcase
315
+ | if . == "discord" or . == "discordapp" then "discord"
316
+ elif . == "x" or . == "twitter" then "x"
317
+ else "" end;
318
+ def first_string($items):
319
+ [$items[] | select(type == "string" and length > 0)][0] // "";
320
+ def first_limit($items):
321
+ [$items[]
322
+ | select(type == "number" or type == "string")
323
+ | tostring
324
+ | select(test("^[0-9]+$"))][0] // "";
325
+ (first_string([.reply_platform, .platform, .target_platform, .source_platform, .provider]) | norm_platform) as $explicit_platform
326
+ | ((.tweet_id // "") | tostring) as $tweet_id
327
+ | {
328
+ platform: (if $explicit_platform != "" then $explicit_platform
329
+ elif ($tweet_id | startswith("discord:")) then "discord"
330
+ elif ($tweet_id | test("^[0-9]+$")) then "x"
331
+ else "" end),
332
+ reply_max_chars: first_limit([.reply_max_chars, .reply_max_characters, .message_max_chars, .message_limit, .max_chars])
333
+ }
334
+ ' "$file"
335
+ }
336
+
337
+ # fmx_request_inbox_context <state> <request_id>: reply context from a stashed
338
+ # mention payload (state/x-inbox/<request_id>.json), or the empty shape when the
339
+ # inbox file is absent. Thin wrapper over fmx_extract_reply_context.
340
+ fmx_request_inbox_context() {
341
+ local state=$1 rid=$2
342
+ if ! fmx_private_artifact_file_valid "$state/x-inbox" "$rid.json" 600; then
343
+ printf '{"platform":"","reply_max_chars":""}\n'
344
+ return 0
345
+ fi
346
+ fmx_extract_reply_context "$state/x-inbox/$rid.json"
347
+ }
348
+
349
+ # fmx_request_relay_context <request_id>: resolve the reply platform/limit
350
+ # AUTHORITATIVELY from the relay by request_id when local per-request registry or
351
+ # inbox context is missing an axis. The request_id is the durable key the relay
352
+ # still holds within the follow-up window, so live follow-ups can recover missing
353
+ # context without using a local platform or budget default.
354
+ #
355
+ # POSTs {request_id} to $RELAY/connector/request-context and prints
356
+ # {"platform":"...","reply_max_chars":"..."} in the SAME shape as
357
+ # fmx_request_inbox_context, so callers feed both through the identical
358
+ # normalization and fmx_reply_limit_for_platform path.
359
+ #
360
+ # Best-effort by design: it prints the empty-context shape and returns non-zero
361
+ # when the query cannot run (no token, no curl/jq) or the relay does not resolve
362
+ # it (non-2xx - e.g. an older relay without this endpoint, or a request already
363
+ # swept past its window). Callers must treat that as "unknown" and warn loudly
364
+ # rather than silently defaulting to the X budget. Requires fmx_load_config to
365
+ # have populated FMX_TOKEN and FMX_RELAY first.
366
+ fmx_request_relay_context() {
367
+ local rid=$1 payload_file body_file code rc ctx empty='{"platform":"","reply_max_chars":""}'
368
+ [ -n "${FMX_TOKEN:-}" ] || { printf '%s\n' "$empty"; return 1; }
369
+ command -v curl >/dev/null 2>&1 || { printf '%s\n' "$empty"; return 1; }
370
+ command -v jq >/dev/null 2>&1 || { printf '%s\n' "$empty"; return 1; }
371
+ payload_file=$(mktemp "${TMPDIR:-/tmp}/fm-x-reqctx.XXXXXX") || { printf '%s\n' "$empty"; return 1; }
372
+ body_file=$(mktemp "${TMPDIR:-/tmp}/fm-x-reqctx-body.XXXXXX") || { rm -f "$payload_file"; printf '%s\n' "$empty"; return 1; }
373
+ if ! jq -cn --arg rid "$rid" '{request_id:$rid}' > "$payload_file" 2>/dev/null; then
374
+ rm -f "$payload_file" "$body_file"; printf '%s\n' "$empty"; return 1
375
+ fi
376
+ code=$(fmx_post_json request-context "$payload_file" "$body_file"); rc=$?
377
+ rm -f "$payload_file"
378
+ if [ "$rc" != 0 ]; then rm -f "$body_file"; printf '%s\n' "$empty"; return 1; fi
379
+ case "$code" in
380
+ 2[0-9][0-9]) ;;
381
+ *) rm -f "$body_file"; printf '%s\n' "$empty"; return 1 ;;
382
+ esac
383
+ # Same extraction as the inbox path, so a relay-resolved context and an
384
+ # inbox-resolved one normalize identically.
385
+ ctx=$(fmx_extract_reply_context "$body_file" 2>/dev/null) || ctx=
386
+ rm -f "$body_file"
387
+ [ -n "$ctx" ] || { printf '%s\n' "$empty"; return 1; }
388
+ # A 200 that resolved neither a platform nor a limit is treated as unresolved so
389
+ # the caller warns instead of recording a link with no split budget.
390
+ if [ "$(printf '%s' "$ctx" | jq -r '.platform // ""')" = "" ] \
391
+ && [ "$(printf '%s' "$ctx" | jq -r '.reply_max_chars // ""')" = "" ]; then
392
+ printf '%s\n' "$empty"; return 1
393
+ fi
394
+ printf '%s\n' "$ctx"
395
+ }
396
+
397
+ # --- durable per-request reply-context registry (state/x-context/<rid>.json) ---
398
+ #
399
+ # A single x_request per task collides across concurrent public requests routed
400
+ # through one persistent secondmate: linking request B onto a task overwrites
401
+ # request A's recorded platform/budget, so A's later follow-up loses its context.
402
+ # And the inbox payload is drained right after the acknowledgement, so a delayed
403
+ # request-id follow-up has no local platform source at all. This registry is the
404
+ # durable fix: one small JSON per request_id, keyed independently of any task
405
+ # link, written at poll time from the authoritative relay payload. It survives
406
+ # inbox cleanup, process restart, and concurrent requests, so
407
+ # fmx_resolve_reply_context can always recover the ORIGINAL platform/budget for a
408
+ # request without depending on task-link state. Entries are volatile runtime
409
+ # state under state/ and are pruned after the relay's 7-day follow-up window.
410
+
411
+ fmx_context_registry_mtime() {
412
+ local file=$1 mtime
413
+ mtime=$(stat -f '%m' "$file" 2>/dev/null) || mtime=$(stat -c '%Y' "$file" 2>/dev/null) || return 1
414
+ case "$mtime" in
415
+ ''|*[!0-9]*) return 1 ;;
416
+ esac
417
+ printf '%s\n' "$mtime"
418
+ }
419
+
420
+ fmx_context_registry_recorded_at() {
421
+ local file=$1 now=${2:-} recorded_at
422
+ recorded_at=$(jq -r '
423
+ .recorded_at
424
+ | if type == "number" and floor == . and . >= 0 then tostring
425
+ elif type == "string" and test("^[0-9]+$") then .
426
+ else "" end
427
+ ' "$file" 2>/dev/null) || recorded_at=
428
+ case "$recorded_at" in
429
+ ''|*[!0-9]*) recorded_at= ;;
430
+ esac
431
+ [ "${#recorded_at}" -le 18 ] || recorded_at=
432
+ if [ -n "$recorded_at" ] && [ -n "$now" ] && [ "$recorded_at" -gt "$now" ]; then
433
+ recorded_at=
434
+ fi
435
+ if [ -z "$recorded_at" ]; then
436
+ recorded_at=$(fmx_context_registry_mtime "$file") || return 1
437
+ if [ -n "$now" ] && [ "$recorded_at" -gt "$now" ]; then
438
+ return 1
439
+ fi
440
+ fi
441
+ printf '%s\n' "$recorded_at"
442
+ }
443
+
444
+ fmx_context_registry_prune() {
445
+ local state=$1 dir now max_age file recorded_at age dir_device
446
+ dir="$state/x-context"
447
+ dir_device=$(fmx_private_artifact_dir_device "$dir" 2>/dev/null) || return 0
448
+ now=${FMX_NOW_OVERRIDE:-$(date +%s)}
449
+ case "$now" in
450
+ ''|*[!0-9]*) return 0 ;;
451
+ esac
452
+ [ "${#now}" -le 18 ] || return 0
453
+ max_age=${FMX_FOLLOWUP_MAX_AGE_SECS:-604800}
454
+ case "$max_age" in
455
+ ''|*[!0-9]*) max_age=604800 ;;
456
+ esac
457
+ [ "${#max_age}" -le 18 ] || max_age=604800
458
+ [ "$max_age" -le 604800 ] || max_age=604800
459
+ while IFS= read -r -d '' file; do
460
+ if ! fmx_single_link_file_mode_valid "$file" 600 "$dir_device"; then
461
+ rm -f -- "$file" 2>/dev/null || true
462
+ continue
463
+ fi
464
+ if ! recorded_at=$(fmx_context_registry_recorded_at "$file" "$now"); then
465
+ rm -f -- "$file" 2>/dev/null || true
466
+ continue
467
+ fi
468
+ age=$((10#$now - 10#$recorded_at))
469
+ if [ "$age" -gt "$max_age" ]; then
470
+ rm -f -- "$file" 2>/dev/null || true
471
+ fi
472
+ done < <(find "$dir" -type f -name '*.json' -print0 2>/dev/null)
473
+ return 0
474
+ }
475
+
476
+ # fmx_context_registry_set <state> <request_id> <platform> <reply-max> [refresh]:
477
+ # persist the durable per-request reply context atomically. Normalizes platform
478
+ # (twitter -> x, anything unrecognized -> empty) and requires a numeric budget.
479
+ # A refresh value of 1 resets the retention timestamp; ordinary writes preserve
480
+ # it. A no-op (success) when neither a platform nor a budget is known, so callers
481
+ # never write an empty, useless record. Returns non-zero only on invalid input or
482
+ # a write failure; callers treat the write as best-effort.
483
+ fmx_context_registry_set() {
484
+ local state=$1 rid=$2 platform=${3:-} reply_max=${4:-} refresh=${5:-0} dir file dir_device now recorded_at
485
+ case "$rid" in
486
+ ''|.*|*[!A-Za-z0-9._-]*) return 1 ;;
487
+ esac
488
+ case "$platform" in
489
+ discord|x) ;;
490
+ twitter) platform=x ;;
491
+ *) platform= ;;
492
+ esac
493
+ case "$reply_max" in
494
+ ''|*[!0-9]*) reply_max= ;;
495
+ esac
496
+ case "$refresh" in
497
+ 0|1) ;;
498
+ *) return 1 ;;
499
+ esac
500
+ if [ -z "$platform" ] && [ -z "$reply_max" ]; then
501
+ return 0
502
+ fi
503
+ dir="$state/x-context"
504
+ dir_device=$(fmx_private_artifact_dir_prepare "$dir") || return 1
505
+ file="$dir/$rid.json"
506
+ if { [ -e "$file" ] || [ -L "$file" ]; } \
507
+ && ! fmx_single_link_file_mode_valid "$file" 600 "$dir_device"; then
508
+ return 1
509
+ fi
510
+ fmx_context_registry_prune "$state"
511
+ now=${FMX_NOW_OVERRIDE:-$(date +%s)}
512
+ case "$now" in
513
+ ''|*[!0-9]*) return 1 ;;
514
+ esac
515
+ [ "${#now}" -le 18 ] || return 1
516
+ recorded_at=
517
+ if [ "$refresh" = 0 ] && [ -f "$file" ]; then
518
+ recorded_at=$(fmx_context_registry_recorded_at "$file" "$now") || recorded_at=
519
+ fi
520
+ if [ -z "$recorded_at" ]; then
521
+ recorded_at=$now
522
+ fi
523
+ (set -o pipefail; jq -cn --arg rid "$rid" --arg platform "$platform" --arg max "$reply_max" \
524
+ --argjson recorded_at "$recorded_at" \
525
+ '{request_id:$rid, platform:$platform, reply_max_chars:$max, recorded_at:$recorded_at}' \
526
+ | fmx_private_artifact_publish_stdin "$dir" "$rid.json" 600) || return 1
527
+ }
528
+
529
+ # fmx_offer_registry_claim <state> <request_id>: atomically claim the durable
530
+ # one-wake marker at state/x-context/<request_id>.offered.json. The marker uses
531
+ # the context registry's recorded_at retention contract, so its first claim
532
+ # survives inbox cleanup and expires with the relay's bounded follow-up window.
533
+ # Returns 0 only to the caller that created the marker, 1 when a valid marker
534
+ # already exists, and 2 on invalid input or a publication failure.
535
+ fmx_offer_registry_claim() {
536
+ local state=$1 rid=$2 dir now record rc
537
+ case "$rid" in
538
+ ''|.*|*[!A-Za-z0-9._-]*) return 2 ;;
539
+ esac
540
+ fmx_context_registry_prune "$state"
541
+ now=${FMX_NOW_OVERRIDE:-$(date +%s)}
542
+ case "$now" in
543
+ ''|*[!0-9]*) return 2 ;;
544
+ esac
545
+ [ "${#now}" -le 18 ] || return 2
546
+ record=$(jq -cn --arg rid "$rid" --argjson recorded_at "$now" \
547
+ '{request_id:$rid, recorded_at:$recorded_at}') || return 2
548
+ dir="$state/x-context"
549
+ printf '%s\n' "$record" \
550
+ | fmx_private_artifact_publish_stdin_once "$dir" "$rid.offered.json" 600
551
+ rc=$?
552
+ return "$rc"
553
+ }
554
+
555
+ # fmx_context_registry_get <state> <request_id>: print the durable per-request
556
+ # reply context as {"platform":"...","reply_max_chars":"..."} (the same shape as
557
+ # the inbox and relay extractors), or the empty shape when no record exists.
558
+ fmx_context_registry_get() {
559
+ local state=$1 rid=$2 dir file
560
+ case "$rid" in
561
+ ''|.*|*[!A-Za-z0-9._-]*) printf '{"platform":"","reply_max_chars":""}\n'; return 0 ;;
562
+ esac
563
+ dir="$state/x-context"
564
+ file="$dir/$rid.json"
565
+ if ! fmx_private_artifact_file_valid "$dir" "$rid.json" 600; then
566
+ printf '{"platform":"","reply_max_chars":""}\n'
567
+ return 0
568
+ fi
569
+ fmx_context_registry_prune "$state"
570
+ jq -c '{platform:(.platform // ""), reply_max_chars:(.reply_max_chars // "")}' "$file" 2>/dev/null \
571
+ || printf '{"platform":"","reply_max_chars":""}\n'
572
+ }
573
+
574
+ # fmx_context_registry_clear <state> <request_id>: drop the durable record.
575
+ # Idempotent and best-effort; a dismiss (no follow-up will ever come) uses it so
576
+ # a skipped mention leaves no stray context behind.
577
+ fmx_context_registry_clear() {
578
+ local state=$1 rid=$2 dir
579
+ case "$rid" in
580
+ ''|.*|*[!A-Za-z0-9._-]*) return 0 ;;
581
+ esac
582
+ dir="$state/x-context"
583
+ [ -d "$dir" ] && [ ! -L "$dir" ] || return 0
584
+ rm -f "$dir/$rid.json" 2>/dev/null || true
585
+ return 0
586
+ }
587
+
588
+ # fmx_resolve_reply_context <state> <request_id> <allow-relay>: resolve the reply
589
+ # platform/budget for a request through the durable sources, in order:
590
+ # 1. the per-request context registry (durable, survives inbox cleanup, restart,
591
+ # and concurrent requests - the primary source after this fix);
592
+ # 2. the still-present inbox payload;
593
+ # 3. when <allow-relay> is 1, an AUTHORITATIVE relay lookup by request_id.
594
+ # Prints {"platform":"...","reply_max_chars":"..."}; each axis is filled from
595
+ # the first source that provides it, continuing through later sources until both
596
+ # are present or the sources are exhausted. <allow-relay>
597
+ # must be 0 in dry-run / no-token / no-network contexts; the caller gates it
598
+ # (typically: follow-up + live + token) so the answer path and dry-run stay
599
+ # network-free. Requires fmx_load_config to have run when <allow-relay> is 1.
600
+ fmx_resolve_reply_context() {
601
+ # Bash local accepts p= and m= as explicit empty assignment arguments.
602
+ # shellcheck disable=SC1007
603
+ local state=$1 rid=$2 allow_relay=${3:-0} src ctx source_p source_m p= m=
604
+ for src in registry inbox relay; do
605
+ case "$src" in
606
+ registry) ctx=$(fmx_context_registry_get "$state" "$rid" 2>/dev/null) || ctx= ;;
607
+ inbox) ctx=$(fmx_request_inbox_context "$state" "$rid" 2>/dev/null) || ctx= ;;
608
+ relay)
609
+ [ "$allow_relay" = 1 ] || continue
610
+ ctx=$(fmx_request_relay_context "$rid" 2>/dev/null) || ctx=
611
+ ;;
612
+ esac
613
+ [ -n "$ctx" ] || continue
614
+ source_p=$(printf '%s' "$ctx" | jq -r '.platform // ""' 2>/dev/null) || source_p=
615
+ source_m=$(printf '%s' "$ctx" | jq -r '.reply_max_chars // ""' 2>/dev/null) || source_m=
616
+ case "$source_p" in discord|x) [ -n "$p" ] || p=$source_p ;; esac
617
+ case "$source_m" in ''|*[!0-9]*) ;; *) [ -n "$m" ] || m=$source_m ;; esac
618
+ [ -n "$p" ] && [ -n "$m" ] && break
619
+ done
620
+ jq -cn --arg platform "$p" --arg max "$m" \
621
+ '{platform:$platform, reply_max_chars:$max}'
622
+ return 0
623
+ }
624
+
625
+ # fmx_reply_limit_for_platform <platform> <explicit-limit>: choose the split
626
+ # budget for one outbound message. X keeps the existing FMX_X_REPLY_MAX_CHARS
627
+ # default of 280. Discord uses 1900 by default, below Discord's 2000-character
628
+ # message limit so relay/client metadata or small counting differences have
629
+ # headroom. A relay-provided explicit limit is honored when present.
630
+ fmx_reply_limit_for_platform() {
631
+ local platform=${1:-} explicit=${2:-}
632
+ case "$explicit" in
633
+ ''|*[!0-9]*) ;;
634
+ *) [ "$explicit" -ge 50 ] 2>/dev/null && { printf '%s\n' "$explicit"; return 0; } ;;
635
+ esac
636
+ case "$platform" in
637
+ discord) printf '%s\n' "${FMX_DISCORD_MAX:-1900}" ;;
638
+ *) printf '%s\n' "${FMX_MAX:-280}" ;;
639
+ esac
640
+ }
641
+
642
+ # Split a reply into a numbered thread of <=<max>-codepoint chunks, packing first
643
+ # on fenced-code, paragraph, and line boundaries, then on word boundaries, and
644
+ # hard-splitting only a single over-long unit. A reply that already fits in one
645
+ # message is returned as a single UNNUMBERED chunk; longer replies get " (k/n)"
646
+ # suffixes. At most <cap> messages are produced; if the reply would need more,
647
+ # the last kept message is marked with an ellipsis. Reads the reply text on stdin
648
+ # and prints a compact JSON array of chunks. Length is codepoint-based (via jq);
649
+ # the relay remains the final authority and trims.
650
+ fmx_split_thread() {
651
+ jq -Rsc --argjson limit "$1" --argjson cap "$2" '
652
+ def trim: gsub("^[[:space:]]+|[[:space:]]+$"; "");
653
+ def fence_marker: test("^[[:space:]]*```");
654
+ def fence_count: ((split("```") | length) - 1);
655
+ def numbered($i; $n):
656
+ "(\($i + 1)/\($n))" as $mark
657
+ | if ((fence_count % 2) == 0) and (split("\n")[-1] | fence_marker)
658
+ then . + "\n" + $mark
659
+ else . + " " + $mark
660
+ end;
661
+ def hardsplit($b): . as $s | [range(0; ($s|length); $b) as $i | $s[$i:$i+$b]];
662
+ def wordsplit($b):
663
+ (gsub("[[:space:]]+"; " ") | trim) as $norm
664
+ | if ($norm | length) == 0 then []
665
+ else
666
+ [ $norm | split(" ")[] | if (length > $b) then hardsplit($b)[] else . end ] as $words
667
+ | (reduce $words[] as $w ({chunks: [], cur: ""};
668
+ (if .cur == "" then $w else .cur + " " + $w end) as $cand
669
+ | if ($cand | length) <= $b then .cur = $cand
670
+ else .chunks += (if .cur == "" then [] else [.cur] end) | .cur = $w end
671
+ )) as $st
672
+ | $st.chunks + (if $st.cur != "" then [$st.cur] else [] end)
673
+ end;
674
+ def split_units:
675
+ split("\n") as $lines
676
+ | (reduce $lines[] as $line ({units: [], cur: "", fence: false};
677
+ if .fence then
678
+ .cur = (if .cur == "" then $line else .cur + "\n" + $line end)
679
+ | if ($line | fence_marker) then .units += [.cur] | .cur = "" | .fence = false else . end
680
+ elif ($line | fence_marker) then
681
+ (if .cur != "" then .units += [.cur] | .cur = "" else . end)
682
+ | .cur = $line
683
+ | .fence = true
684
+ elif ($line | test("^[[:space:]]*$")) then
685
+ if .cur != "" then .units += [.cur] | .cur = "" else . end
686
+ else
687
+ ($line | trim) as $clean
688
+ | .cur = (if .cur == "" then $clean else .cur + " " + $clean end)
689
+ end
690
+ )) as $st
691
+ | ($st.units + (if $st.cur != "" then [$st.cur] else [] end))
692
+ | map(select((trim | length) > 0));
693
+ def pack_units($units; $b):
694
+ (reduce $units[] as $u ({chunks: [], cur: ""};
695
+ if ($u | length) > $b then
696
+ (if .cur != "" then .chunks += [.cur] | .cur = "" else . end)
697
+ | .chunks += ($u | wordsplit($b))
698
+ else
699
+ (if .cur == "" then $u else .cur + "\n\n" + $u end) as $cand
700
+ | if ($cand | length) <= $b then .cur = $cand
701
+ else .chunks += (if .cur == "" then [] else [.cur] end) | .cur = $u end
702
+ end
703
+ )) as $st
704
+ | $st.chunks + (if $st.cur != "" then [$st.cur] else [] end);
705
+ def split_thread($limit; $cap):
706
+ trim as $norm
707
+ | if ($norm | length) == 0 then []
708
+ elif ($norm | length) <= $limit then [$norm]
709
+ else
710
+ ($cap | tostring | length) as $digits
711
+ | (4 + 2 * $digits) as $suffixw
712
+ | (if ($limit - $suffixw - 1) < 1 then 1 else ($limit - $suffixw - 1) end) as $budget
713
+ | ($norm | split_units) as $units
714
+ | pack_units($units; $budget) as $raw
715
+ | (if ($raw | length) > $cap
716
+ then ($raw[0:$cap] | (.[($cap - 1)] += "…"))
717
+ else $raw end) as $kept
718
+ | ($kept | length) as $n
719
+ | [ range(0; $n) as $i | $kept[$i] | numbered($i; $n) ]
720
+ end;
721
+ split_thread($limit; $cap)
722
+ '
723
+ }
724
+
725
+ fmx_auth_header_file() {
726
+ local file
727
+ case "$FMX_TOKEN" in
728
+ *$'\n'*|*$'\r'*) return 1 ;;
729
+ esac
730
+ file=$(umask 077; mktemp "${TMPDIR:-/tmp}/fm-x-auth.XXXXXX") || return 1
731
+ chmod 600 "$file" 2>/dev/null || { rm -f "$file"; return 1; }
732
+ printf 'Authorization: Bearer %s\n' "$FMX_TOKEN" > "$file" || { rm -f "$file"; return 1; }
733
+ printf '%s\n' "$file"
734
+ }
735
+
736
+ fmx_image_media_type_from_path() {
737
+ local path=$1 lower detected
738
+ lower=$(printf '%s' "$path" | tr '[:upper:]' '[:lower:]')
739
+ case "$lower" in
740
+ *.png) printf 'image/png\n' ;;
741
+ *.jpg|*.jpeg) printf 'image/jpeg\n' ;;
742
+ *.gif) printf 'image/gif\n' ;;
743
+ *.webp) printf 'image/webp\n' ;;
744
+ *.bmp) printf 'image/bmp\n' ;;
745
+ *.tif|*.tiff) printf 'image/tiff\n' ;;
746
+ *)
747
+ if command -v file >/dev/null 2>&1; then
748
+ detected=$(file --mime-type -b -- "$path" 2>/dev/null | tr '[:upper:]' '[:lower:]')
749
+ case "$detected" in
750
+ image/png|image/jpeg|image/pjpeg|image/gif|image/webp|image/bmp|image/tiff) printf '%s\n' "$detected" ;;
751
+ *) return 1 ;;
752
+ esac
753
+ else
754
+ return 1
755
+ fi
756
+ ;;
757
+ esac
758
+ }
759
+
760
+ # fmx_image_payload_file <path> <client-name> <payload-file>: validate and encode
761
+ # a local outbound image. The relay payload object is written to <payload-file>.
762
+ # The compact preview object is printed for FMX_DRY_RUN outbox records.
763
+ fmx_image_payload_file() {
764
+ local path=$1 client=${2:-fm-x-reply} payload_file=${3:-} media_type bytes
765
+ if [ -z "$payload_file" ]; then
766
+ echo "$client: missing image payload destination" >&2
767
+ return 1
768
+ fi
769
+ if [ ! -e "$path" ]; then
770
+ echo "$client: image file does not exist: $path" >&2
771
+ return 1
772
+ fi
773
+ if [ ! -f "$path" ]; then
774
+ echo "$client: image path is not a regular file: $path" >&2
775
+ return 1
776
+ fi
777
+ if [ ! -r "$path" ]; then
778
+ echo "$client: image file is not readable: $path" >&2
779
+ return 1
780
+ fi
781
+ media_type=$(fmx_image_media_type_from_path "$path") || {
782
+ echo "$client: unsupported image media type for: $path" >&2
783
+ return 1
784
+ }
785
+ command -v base64 >/dev/null 2>&1 || {
786
+ echo "$client: base64 not found" >&2
787
+ return 1
788
+ }
789
+ bytes=$(wc -c < "$path" | tr -d '[:space:]') || {
790
+ echo "$client: cannot stat image file: $path" >&2
791
+ return 1
792
+ }
793
+ if [ "$bytes" = 0 ]; then
794
+ echo "$client: image file is empty: $path" >&2
795
+ return 1
796
+ fi
797
+ if ! (set -o pipefail; base64 < "$path" | tr -d '\n\r' \
798
+ | jq -Rsc --arg media_type "$media_type" \
799
+ '{media_type:$media_type,data_base64:.}' > "$payload_file"); then
800
+ rm -f "$payload_file"
801
+ echo "$client: cannot read image file: $path" >&2
802
+ return 1
803
+ fi
804
+ jq -cn \
805
+ --arg media_type "$media_type" \
806
+ --arg source_path "$path" \
807
+ --argjson bytes "$bytes" \
808
+ '{media_type:$media_type,bytes:$bytes,source_path:$source_path}'
809
+ }
810
+
811
+ fmx_reply_payload_json() {
812
+ local rid=$1 chunks=$2 n=$3 image_json_file=${4:-}
813
+ if [ -n "$image_json_file" ]; then
814
+ if [ "$n" -le 1 ]; then
815
+ printf '%s' "$chunks" | jq -c --arg rid "$rid" --slurpfile image "$image_json_file" \
816
+ '{request_id:$rid, text:(.[0] // ""), image:$image[0]}'
817
+ else
818
+ printf '%s' "$chunks" | jq -c --arg rid "$rid" --slurpfile image "$image_json_file" \
819
+ '{request_id:$rid, text:.[0], texts:., image:$image[0]}'
820
+ fi
821
+ else
822
+ if [ "$n" -le 1 ]; then
823
+ printf '%s' "$chunks" | jq -c --arg rid "$rid" '{request_id:$rid, text:(.[0] // "")}'
824
+ else
825
+ printf '%s' "$chunks" | jq -c --arg rid "$rid" '{request_id:$rid, text:.[0], texts:.}'
826
+ fi
827
+ fi
828
+ }
829
+
830
+ fmx_reply_outbox_json() {
831
+ local rid=$1 chunks=$2 n=$3 followup=$4 image_preview_json=${5:-}
832
+ if [ -n "$image_preview_json" ]; then
833
+ if [ "$followup" = 1 ]; then
834
+ if [ "$n" -le 1 ]; then
835
+ printf '%s' "$chunks" | jq -c --arg rid "$rid" --argjson image "$image_preview_json" \
836
+ '{request_id:$rid, text:(.[0] // ""), image:$image, endpoint:"followup"}'
837
+ else
838
+ printf '%s' "$chunks" | jq -c --arg rid "$rid" --argjson image "$image_preview_json" \
839
+ '{request_id:$rid, text:.[0], texts:., image:$image, endpoint:"followup"}'
840
+ fi
841
+ else
842
+ if [ "$n" -le 1 ]; then
843
+ printf '%s' "$chunks" | jq -c --arg rid "$rid" --argjson image "$image_preview_json" \
844
+ '{request_id:$rid, text:(.[0] // ""), image:$image}'
845
+ else
846
+ printf '%s' "$chunks" | jq -c --arg rid "$rid" --argjson image "$image_preview_json" \
847
+ '{request_id:$rid, text:.[0], texts:., image:$image}'
848
+ fi
849
+ fi
850
+ else
851
+ if [ "$followup" = 1 ]; then
852
+ if [ "$n" -le 1 ]; then
853
+ printf '%s' "$chunks" | jq -c --arg rid "$rid" \
854
+ '{request_id:$rid, text:(.[0] // ""), endpoint:"followup"}'
855
+ else
856
+ printf '%s' "$chunks" | jq -c --arg rid "$rid" \
857
+ '{request_id:$rid, text:.[0], texts:., endpoint:"followup"}'
858
+ fi
859
+ else
860
+ if [ "$n" -le 1 ]; then
861
+ printf '%s' "$chunks" | jq -c --arg rid "$rid" '{request_id:$rid, text:(.[0] // "")}'
862
+ else
863
+ printf '%s' "$chunks" | jq -c --arg rid "$rid" '{request_id:$rid, text:.[0], texts:.}'
864
+ fi
865
+ fi
866
+ fi
867
+ }
868
+
869
+ fmx_post_json() (
870
+ local endpoint=$1 payload_file=$2 body_file=${3:-/dev/null} auth_header_file code rc
871
+ command -v curl >/dev/null 2>&1 || return 127
872
+ [ -r "$payload_file" ] || return 2
873
+ auth_header_file=$(fmx_auth_header_file) || return 3
874
+ trap 'rm -f "$auth_header_file"' EXIT
875
+ trap 'rm -f "$auth_header_file"; exit 143' HUP INT TERM
876
+ code=$(curl -m 10 -s -o "$body_file" -w '%{http_code}' \
877
+ -X POST \
878
+ -H "@$auth_header_file" \
879
+ -H 'Content-Type: application/json' \
880
+ --data-binary "@$payload_file" \
881
+ "$FMX_RELAY/connector/$endpoint" 2>/dev/null)
882
+ rc=$?
883
+ rm -f "$auth_header_file"
884
+ trap - EXIT HUP INT TERM
885
+ [ "$rc" = 0 ] || return 4
886
+ printf '%s\n' "$code"
887
+ )
888
+
889
+ # --- task <-> X-request link (state/<id>.meta backed) -----------------------
890
+ #
891
+ # When an X/Discord mention spawns real work, the task is linked to its
892
+ # originating mention by state/<id>.meta lines:
893
+ # x_request=<request_id> the relay-issued id the follow-up posts against
894
+ # x_request_ts=<epoch> when the link was made, for the 7-day follow-up window
895
+ # x_followups=<n> follow-ups already posted against this binding (0..3)
896
+ # x_platform=<platform> optional reply platform for follow-up split budget
897
+ # x_reply_max_chars=<n> optional recorded per-message split budget
898
+ # fm-x-followup.sh posts against that link (within the window, up to the cap),
899
+ # then either records the incremented count or clears the link. These helpers
900
+ # own the read/write/clear so fm-x-link.sh and fm-x-followup.sh never hand-edit
901
+ # meta and the rewrite stays atomic and preserves every other meta line.
902
+
903
+ # fmx_meta_get <meta> <key>: print the value of the last "key=value" line in
904
+ # <meta>, or nothing (and succeed) when the file or key is absent. Callers treat
905
+ # empty output as "unset".
906
+ fmx_meta_get() {
907
+ local meta=$1 key=$2 line
908
+ [ -f "$meta" ] || return 0
909
+ line=$(grep -E "^${key}=" "$meta" 2>/dev/null | tail -n1) || return 0
910
+ [ -n "$line" ] || return 0
911
+ printf '%s' "${line#*=}"
912
+ }
913
+
914
+ fmx_meta_tmp() {
915
+ local meta=$1 dir base
916
+ dir=${meta%/*}
917
+ base=${meta##*/}
918
+ [ "$dir" != "$meta" ] || dir=.
919
+ [ -d "$dir" ] || return 1
920
+ mktemp "$dir/.${base}.fm-x.XXXXXX"
921
+ }
922
+
923
+ # fmx_meta_link_set <meta> <request_id> <epoch> [followups] [platform] [max]:
924
+ # atomically (re)write the x_request/x_request_ts/x_followups lines plus optional
925
+ # reply-platform context, dropping any prior link and preserving every other meta
926
+ # line. <followups> defaults to 0 (a fresh link); pass the prior task's count to
927
+ # carry it forward onto a successor task instead of granting a fresh follow-up
928
+ # budget against a binding the relay already knows about. Returns non-zero if
929
+ # <meta> is missing or the rewrite fails.
930
+ fmx_meta_link_set() {
931
+ local meta=$1 rid=$2 ts=$3 followups=${4:-0} platform=${5:-} reply_max=${6:-} tmp lock
932
+ [ -f "$meta" ] || return 1
933
+ lock=$(fm_meta_lock_path "$meta") || return 1
934
+ fm_lock_acquire_wait "$lock"
935
+ [ -f "$meta" ] || { fm_lock_release "$lock"; return 1; }
936
+ tmp=$(fmx_meta_tmp "$meta") || { fm_lock_release "$lock"; return 1; }
937
+ if ! { grep -vE '^x_request=|^x_request_ts=|^x_followups=|^x_platform=|^x_reply_max_chars=' "$meta" || true; } > "$tmp"; then
938
+ rm -f "$tmp"; fm_lock_release "$lock"; return 1
939
+ fi
940
+ printf 'x_request=%s\n' "$rid" >> "$tmp" || { rm -f "$tmp"; fm_lock_release "$lock"; return 1; }
941
+ printf 'x_request_ts=%s\n' "$ts" >> "$tmp" || { rm -f "$tmp"; fm_lock_release "$lock"; return 1; }
942
+ printf 'x_followups=%s\n' "$followups" >> "$tmp" || { rm -f "$tmp"; fm_lock_release "$lock"; return 1; }
943
+ if [ -n "$platform" ]; then
944
+ printf 'x_platform=%s\n' "$platform" >> "$tmp" || { rm -f "$tmp"; fm_lock_release "$lock"; return 1; }
945
+ fi
946
+ case "$reply_max" in
947
+ ''|*[!0-9]*) ;;
948
+ *) printf 'x_reply_max_chars=%s\n' "$reply_max" >> "$tmp" || { rm -f "$tmp"; fm_lock_release "$lock"; return 1; } ;;
949
+ esac
950
+ # STATE is the caller's authorized state directory, never dirname of $meta.
951
+ # shellcheck disable=SC2153
952
+ if ! fm_backlog_atomic_transition publish "$tmp" "$meta" "task record" "$STATE"; then
953
+ rm -f "$tmp"; fm_lock_release "$lock"; return 1
954
+ fi
955
+ fm_lock_release "$lock"
956
+ }
957
+
958
+ # fmx_meta_followups_set <meta> <n>: atomically rewrite just the x_followups
959
+ # line, preserving every other meta line including link and reply context.
960
+ # Returns non-zero if <meta> is missing or the rewrite fails.
961
+ fmx_meta_followups_set() {
962
+ local meta=$1 n=$2 tmp lock
963
+ [ -f "$meta" ] || return 1
964
+ lock=$(fm_meta_lock_path "$meta") || return 1
965
+ fm_lock_acquire_wait "$lock"
966
+ [ -f "$meta" ] || { fm_lock_release "$lock"; return 1; }
967
+ tmp=$(fmx_meta_tmp "$meta") || { fm_lock_release "$lock"; return 1; }
968
+ if ! { grep -vE '^x_followups=' "$meta" || true; } > "$tmp"; then
969
+ rm -f "$tmp"; fm_lock_release "$lock"; return 1
970
+ fi
971
+ printf 'x_followups=%s\n' "$n" >> "$tmp" || { rm -f "$tmp"; fm_lock_release "$lock"; return 1; }
972
+ # shellcheck disable=SC2153
973
+ if ! fm_backlog_atomic_transition publish "$tmp" "$meta" "task record" "$STATE"; then
974
+ rm -f "$tmp"; fm_lock_release "$lock"; return 1
975
+ fi
976
+ fm_lock_release "$lock"
977
+ }
978
+
979
+ # fmx_meta_link_clear <meta>: atomically remove the x_request/x_request_ts/
980
+ # x_followups and reply-platform lines while preserving every other meta line. Idempotent:
981
+ # succeeds whether or not a link is present, and is a no-op when <meta> is
982
+ # missing.
983
+ fmx_meta_link_clear() {
984
+ local meta=$1 tmp lock
985
+ [ ! -L "$meta" ] || return 1
986
+ [ -f "$meta" ] || return 0
987
+ lock=$(fm_meta_lock_path "$meta") || return 1
988
+ fm_lock_acquire_wait "$lock"
989
+ [ ! -L "$meta" ] || { fm_lock_release "$lock"; return 1; }
990
+ [ -f "$meta" ] || { fm_lock_release "$lock"; return 0; }
991
+ tmp=$(fmx_meta_tmp "$meta") || { fm_lock_release "$lock"; return 1; }
992
+ if ! { grep -vE '^x_request=|^x_request_ts=|^x_followups=|^x_platform=|^x_reply_max_chars=' "$meta" || true; } > "$tmp"; then
993
+ rm -f "$tmp"; fm_lock_release "$lock"; return 1
994
+ fi
995
+ # shellcheck disable=SC2153
996
+ if ! fm_backlog_atomic_transition publish "$tmp" "$meta" "task record" "$STATE"; then
997
+ rm -f "$tmp"; fm_lock_release "$lock"; return 1
998
+ fi
999
+ fm_lock_release "$lock"
1000
+ }