@zerwiz/ymir 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (850) hide show
  1. package/.agents/README.md +291 -0
  2. package/.agents/agents/bragi-marketer.md +111 -0
  3. package/.agents/agents/brokk.md +352 -0
  4. package/.agents/agents/forseti-reviewer.md +93 -0
  5. package/.agents/agents/frigg-consent.md +77 -0
  6. package/.agents/agents/groa-update.md +77 -0
  7. package/.agents/agents/hnoss-designer.md +69 -0
  8. package/.agents/agents/huginn-researcher.md +99 -0
  9. package/.agents/agents/jord-projects.md +78 -0
  10. package/.agents/agents/kvasir-scout.md +91 -0
  11. package/.agents/agents/mimir-planner.md +92 -0
  12. package/.agents/agents/muninn-stow.md +77 -0
  13. package/.agents/agents/saga-bearings.md +78 -0
  14. package/.agents/agents/sindri-developer.md +114 -0
  15. package/.agents/agents/snotra-documenter.md +105 -0
  16. package/.agents/agents/syn-recovery.md +78 -0
  17. package/.agents/agents/tyr-check.md +78 -0
  18. package/.agents/agents/urdh-hold.md +77 -0
  19. package/.agents/agents/volundr-smith.md +80 -0
  20. package/.agents/agents/vor-diagnostics.md +77 -0
  21. package/.agents/assets/agents/naming.md +50 -0
  22. package/.agents/assets/agents/registry.md +93 -0
  23. package/.agents/assets/agents/runtime.md +36 -0
  24. package/.agents/assets/agents/toon-tasks-cli.md +42 -0
  25. package/.agents/assets/templates/company_entity.template.md +37 -0
  26. package/.agents/backend/backends/cmux.sh +649 -0
  27. package/.agents/backend/backends/herdr-eventwait.py +157 -0
  28. package/.agents/backend/backends/herdr-workspace-move.py +114 -0
  29. package/.agents/backend/backends/herdr.sh +3386 -0
  30. package/.agents/backend/backends/orca.sh +290 -0
  31. package/.agents/backend/backends/tmux.sh +357 -0
  32. package/.agents/backend/backends/zellij.sh +700 -0
  33. package/.agents/backend/fm-afk-launch.sh +654 -0
  34. package/.agents/backend/fm-afk-return.sh +241 -0
  35. package/.agents/backend/fm-afk-start.sh +172 -0
  36. package/.agents/backend/fm-arm-command-policy.mjs +960 -0
  37. package/.agents/backend/fm-arm-pretool-check.sh +200 -0
  38. package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
  39. package/.agents/backend/fm-backend.sh +981 -0
  40. package/.agents/backend/fm-backlog-handoff.sh +909 -0
  41. package/.agents/backend/fm-backlog-receive.sh +187 -0
  42. package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
  43. package/.agents/backend/fm-bearings-board.sh +204 -0
  44. package/.agents/backend/fm-bearings-snapshot.sh +546 -0
  45. package/.agents/backend/fm-bootstrap.sh +1568 -0
  46. package/.agents/backend/fm-branch-outcome.sh +203 -0
  47. package/.agents/backend/fm-branch-prompt.sh +101 -0
  48. package/.agents/backend/fm-brief.sh +452 -0
  49. package/.agents/backend/fm-busy-event.sh +234 -0
  50. package/.agents/backend/fm-busy-lib.sh +986 -0
  51. package/.agents/backend/fm-captain-hold.sh +1001 -0
  52. package/.agents/backend/fm-cd-command-policy.mjs +153 -0
  53. package/.agents/backend/fm-cd-pretool-check.sh +190 -0
  54. package/.agents/backend/fm-check-lib.sh +74 -0
  55. package/.agents/backend/fm-check-register.sh +42 -0
  56. package/.agents/backend/fm-check-unregister.sh +52 -0
  57. package/.agents/backend/fm-classify-lib.sh +1770 -0
  58. package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
  59. package/.agents/backend/fm-composer-lib.sh +1416 -0
  60. package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
  61. package/.agents/backend/fm-config-push.sh +254 -0
  62. package/.agents/backend/fm-control-lib.sh +251 -0
  63. package/.agents/backend/fm-control.sh +878 -0
  64. package/.agents/backend/fm-crew-state.sh +625 -0
  65. package/.agents/backend/fm-cursor-lib.sh +243 -0
  66. package/.agents/backend/fm-decision-hold.sh +232 -0
  67. package/.agents/backend/fm-doc-audience-check.sh +269 -0
  68. package/.agents/backend/fm-dod-lib.sh +67 -0
  69. package/.agents/backend/fm-ensure-agents-md.sh +253 -0
  70. package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
  71. package/.agents/backend/fm-extension.mjs +2577 -0
  72. package/.agents/backend/fm-extension.sh +16 -0
  73. package/.agents/backend/fm-ff-lib.sh +420 -0
  74. package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
  75. package/.agents/backend/fm-fleet-sync.sh +458 -0
  76. package/.agents/backend/fm-fleet-view.sh +96 -0
  77. package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
  78. package/.agents/backend/fm-guard.sh +242 -0
  79. package/.agents/backend/fm-harness.sh +195 -0
  80. package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
  81. package/.agents/backend/fm-herdr-lab.sh +346 -0
  82. package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
  83. package/.agents/backend/fm-home-seed.sh +984 -0
  84. package/.agents/backend/fm-home-summary-refresh.sh +255 -0
  85. package/.agents/backend/fm-hook-host-lib.sh +36 -0
  86. package/.agents/backend/fm-inactive-reconcile.sh +531 -0
  87. package/.agents/backend/fm-inbox.sh +399 -0
  88. package/.agents/backend/fm-install-actionlint.sh +84 -0
  89. package/.agents/backend/fm-install-herdr.sh +96 -0
  90. package/.agents/backend/fm-install-shellcheck.sh +84 -0
  91. package/.agents/backend/fm-install-treehouse.sh +95 -0
  92. package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
  93. package/.agents/backend/fm-lease-lib.sh +218 -0
  94. package/.agents/backend/fm-lease.sh +189 -0
  95. package/.agents/backend/fm-line-cap-lib.sh +51 -0
  96. package/.agents/backend/fm-lint-workflows.sh +137 -0
  97. package/.agents/backend/fm-lint.sh +591 -0
  98. package/.agents/backend/fm-lock-lib.sh +104 -0
  99. package/.agents/backend/fm-lock.sh +107 -0
  100. package/.agents/backend/fm-marker-lib.sh +12 -0
  101. package/.agents/backend/fm-merge-local.sh +74 -0
  102. package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
  103. package/.agents/backend/fm-nm-run-lib.sh +124 -0
  104. package/.agents/backend/fm-on.sh +125 -0
  105. package/.agents/backend/fm-operational-input.sh +253 -0
  106. package/.agents/backend/fm-peek.sh +46 -0
  107. package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
  108. package/.agents/backend/fm-pr-check.sh +135 -0
  109. package/.agents/backend/fm-pr-lib.sh +1016 -0
  110. package/.agents/backend/fm-pr-merge.sh +707 -0
  111. package/.agents/backend/fm-pr-poll.sh +110 -0
  112. package/.agents/backend/fm-primary-scope-lib.sh +33 -0
  113. package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
  114. package/.agents/backend/fm-procevent-lavish.sh +628 -0
  115. package/.agents/backend/fm-procevent-lib.sh +892 -0
  116. package/.agents/backend/fm-procevent-quota.sh +290 -0
  117. package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
  118. package/.agents/backend/fm-procevent-when.sh +504 -0
  119. package/.agents/backend/fm-procevent.sh +1466 -0
  120. package/.agents/backend/fm-project-mode.sh +92 -0
  121. package/.agents/backend/fm-project-origin-lib.sh +180 -0
  122. package/.agents/backend/fm-promote.sh +282 -0
  123. package/.agents/backend/fm-public-followup-emit.sh +260 -0
  124. package/.agents/backend/fm-public-followup-lib.sh +398 -0
  125. package/.agents/backend/fm-public-followup.sh +1330 -0
  126. package/.agents/backend/fm-push-transition-lib.sh +168 -0
  127. package/.agents/backend/fm-quota-axi-lib.sh +93 -0
  128. package/.agents/backend/fm-quota-choose.sh +384 -0
  129. package/.agents/backend/fm-remote-delta-read.sh +189 -0
  130. package/.agents/backend/fm-remote-doctor.sh +800 -0
  131. package/.agents/backend/fm-remote-entrypoint.sh +193 -0
  132. package/.agents/backend/fm-remote-file.sh +234 -0
  133. package/.agents/backend/fm-remote-home-provision.sh +259 -0
  134. package/.agents/backend/fm-remote-home-seed.sh +259 -0
  135. package/.agents/backend/fm-remote-inherit-push.sh +90 -0
  136. package/.agents/backend/fm-remote-inherit.sh +179 -0
  137. package/.agents/backend/fm-remote-job-lib.sh +1241 -0
  138. package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
  139. package/.agents/backend/fm-remote-job-worker.sh +1124 -0
  140. package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
  141. package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
  142. package/.agents/backend/fm-review-diff.sh +158 -0
  143. package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
  144. package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
  145. package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
  146. package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
  147. package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
  148. package/.agents/backend/fm-secondmate-report.sh +88 -0
  149. package/.agents/backend/fm-send.sh +1059 -0
  150. package/.agents/backend/fm-session-lock-lib.sh +176 -0
  151. package/.agents/backend/fm-session-start.sh +970 -0
  152. package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
  153. package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
  154. package/.agents/backend/fm-sessionstart-run.sh +145 -0
  155. package/.agents/backend/fm-spawn.sh +3153 -0
  156. package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
  157. package/.agents/backend/fm-startup-memory-budget.sh +94 -0
  158. package/.agents/backend/fm-startup-network.sh +642 -0
  159. package/.agents/backend/fm-stow-cascade.sh +251 -0
  160. package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
  161. package/.agents/backend/fm-supervise-daemon.sh +1732 -0
  162. package/.agents/backend/fm-supervision-instructions.sh +215 -0
  163. package/.agents/backend/fm-supervision-lib.sh +90 -0
  164. package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
  165. package/.agents/backend/fm-tangle-lib.sh +53 -0
  166. package/.agents/backend/fm-task-inbox-lib.sh +402 -0
  167. package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
  168. package/.agents/backend/fm-teardown.sh +2922 -0
  169. package/.agents/backend/fm-test-isolation-proof.sh +588 -0
  170. package/.agents/backend/fm-test-run.sh +2191 -0
  171. package/.agents/backend/fm-timeout-lib.sh +141 -0
  172. package/.agents/backend/fm-timing-lib.sh +179 -0
  173. package/.agents/backend/fm-tmux-lib.sh +291 -0
  174. package/.agents/backend/fm-tool-update-check.sh +898 -0
  175. package/.agents/backend/fm-trace-context-lib.sh +227 -0
  176. package/.agents/backend/fm-transition-lib.sh +103 -0
  177. package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
  178. package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
  179. package/.agents/backend/fm-turnend-guard.sh +432 -0
  180. package/.agents/backend/fm-update.sh +110 -0
  181. package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
  182. package/.agents/backend/fm-voice-client.py +1373 -0
  183. package/.agents/backend/fm-voice-relay.py +1256 -0
  184. package/.agents/backend/fm-wake-drain.sh +592 -0
  185. package/.agents/backend/fm-wake-grant.sh +127 -0
  186. package/.agents/backend/fm-wake-lib.sh +1849 -0
  187. package/.agents/backend/fm-watch-arm.sh +602 -0
  188. package/.agents/backend/fm-watch-checkpoint.sh +109 -0
  189. package/.agents/backend/fm-watch.sh +1962 -0
  190. package/.agents/backend/fm-x-dismiss.sh +116 -0
  191. package/.agents/backend/fm-x-followup.sh +286 -0
  192. package/.agents/backend/fm-x-lib.sh +1000 -0
  193. package/.agents/backend/fm-x-link.sh +234 -0
  194. package/.agents/backend/fm-x-poll.sh +185 -0
  195. package/.agents/backend/fm-x-reply.sh +385 -0
  196. package/.agents/backend/fm_voice_frame.py +166 -0
  197. package/.agents/backend/fm_voice_records.py +574 -0
  198. package/.agents/backend/model-bridge.py +290 -0
  199. package/.agents/backend/opencode-go-bridge.py +30 -0
  200. package/.agents/bus/README.md +12 -0
  201. package/.agents/bus/messages.json +1 -0
  202. package/.agents/config/agents.machine.example.yaml +16 -0
  203. package/.agents/config/agents.yaml +50 -0
  204. package/.agents/config/agents.yaml.example +72 -0
  205. package/.agents/config/crew-dispatch.json +22 -0
  206. package/.agents/config/cron.yaml +9 -0
  207. package/.agents/config/eindri-dispatch.json +67 -0
  208. package/.agents/config/eindri-harness +1 -0
  209. package/.agents/config/startup-memory-budget +1 -0
  210. package/.agents/config/tailscale-sync.yaml +27 -0
  211. package/.agents/filebrowser/.filebrowser.json +6 -0
  212. package/.agents/filebrowser/README.md +11 -0
  213. package/.agents/gateway/README.md +12 -0
  214. package/.agents/github/README.md +15 -0
  215. package/.agents/github/webhooks/.gitkeep +0 -0
  216. package/.agents/github/workflows/.gitkeep +0 -0
  217. package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
  218. package/.agents/harness/opencode/plugins/package.json +4 -0
  219. package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
  220. package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
  221. package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
  222. package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
  223. package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
  224. package/.agents/memory/README.md +46 -0
  225. package/.agents/memory/kaia.engram +0 -0
  226. package/.agents/memory/kaia.engram-shm +0 -0
  227. package/.agents/memory/kaia.engram-wal +0 -0
  228. package/.agents/memory/runes_audit.md +5 -0
  229. package/.agents/memory/well/episodes.jsonl +367 -0
  230. package/.agents/memory/well/workspace.jsonl +27 -0
  231. package/.agents/migrations/0001-hodd-layout.sh +38 -0
  232. package/.agents/migrations/0002-a2a-mcp.sh +27 -0
  233. package/.agents/migrations/0003-private-data-separation.sh +111 -0
  234. package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
  235. package/.agents/sandbox/Dockerfile.utgard +19 -0
  236. package/.agents/sandbox/README.md +14 -0
  237. package/.agents/sandbox/utgard.config.json +14 -0
  238. package/.agents/skills/README.md +59 -0
  239. package/.agents/skills/bragi-marketing/README.md +3 -0
  240. package/.agents/skills/bragi-marketing/SKILL.md +82 -0
  241. package/.agents/skills/eindri-homes/README.md +3 -0
  242. package/.agents/skills/eindri-homes/SKILL.md +291 -0
  243. package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
  244. package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
  245. package/.agents/skills/frigg-consent/README.md +3 -0
  246. package/.agents/skills/frigg-consent/SKILL.md +56 -0
  247. package/.agents/skills/galdr-ymirsystem/README.md +3 -0
  248. package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
  249. package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
  250. package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
  251. package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
  252. package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
  253. package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
  254. package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
  255. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
  256. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
  257. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
  258. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
  259. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
  260. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
  261. package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
  262. package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
  263. package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
  264. package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
  265. package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
  266. package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
  267. package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
  268. package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
  269. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
  270. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
  271. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
  272. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
  273. package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
  274. package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
  275. package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
  276. package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
  277. package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
  278. package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
  279. package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
  280. package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
  281. package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
  282. package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
  283. package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
  284. package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
  285. package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
  286. package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
  287. package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
  288. package/.agents/skills/gjallarhorn-relay/README.md +3 -0
  289. package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
  290. package/.agents/skills/groa-update/SKILL.md +70 -0
  291. package/.agents/skills/hamr-adapters/README.md +3 -0
  292. package/.agents/skills/hamr-adapters/SKILL.md +123 -0
  293. package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
  294. package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
  295. package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
  296. package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
  297. package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
  298. package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
  299. package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
  300. package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
  301. package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
  302. package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
  303. package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
  304. package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
  305. package/.agents/skills/herdr-panes/README.md +3 -0
  306. package/.agents/skills/herdr-panes/SKILL.md +92 -0
  307. package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
  308. package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
  309. package/.agents/skills/hnoss-design/DESIGN.md +19 -0
  310. package/.agents/skills/hnoss-design/README.md +3 -0
  311. package/.agents/skills/hnoss-design/SKILL.md +83 -0
  312. package/.agents/skills/hvild-afk/README.md +3 -0
  313. package/.agents/skills/hvild-afk/SKILL.md +241 -0
  314. package/.agents/skills/jord-projects/README.md +3 -0
  315. package/.agents/skills/jord-projects/SKILL.md +92 -0
  316. package/.agents/skills/lifecycle/SKILL.md +56 -0
  317. package/.agents/skills/lifecycle/smoke_test.sh +52 -0
  318. package/.agents/skills/lifecycle/start.sh +21 -0
  319. package/.agents/skills/lifecycle/status.sh +35 -0
  320. package/.agents/skills/lifecycle/stop.sh +16 -0
  321. package/.agents/skills/muninn-stow/README.md +3 -0
  322. package/.agents/skills/muninn-stow/SKILL.md +309 -0
  323. package/.agents/skills/no-mistakes/SKILL.md +379 -0
  324. package/.agents/skills/nornir-schedule/README.md +3 -0
  325. package/.agents/skills/nornir-schedule/SKILL.md +25 -0
  326. package/.agents/skills/nornir-schedule/assets/events.md +137 -0
  327. package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
  328. package/.agents/skills/nsr-compliance/README.md +3 -0
  329. package/.agents/skills/nsr-compliance/SKILL.md +29 -0
  330. package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
  331. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
  332. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
  333. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
  334. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
  335. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
  336. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
  337. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
  338. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
  339. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
  340. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
  341. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
  342. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
  343. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
  344. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
  345. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
  346. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
  347. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
  348. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
  349. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
  350. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
  351. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
  352. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
  353. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  354. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
  355. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
  356. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
  357. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
  358. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
  359. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
  360. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
  361. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
  362. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
  363. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
  364. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
  365. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
  366. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
  367. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
  368. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
  369. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
  370. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
  371. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
  372. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
  373. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
  374. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
  375. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
  376. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
  377. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
  378. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
  379. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
  380. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
  381. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
  382. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
  383. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
  384. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
  385. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
  386. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
  387. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
  388. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
  389. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
  390. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
  391. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
  392. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
  393. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
  394. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
  395. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
  396. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
  397. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
  398. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
  399. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
  400. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
  401. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
  402. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
  403. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
  404. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
  405. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
  406. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
  407. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
  408. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
  409. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
  410. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
  411. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
  412. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
  413. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
  414. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
  415. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
  416. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
  417. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
  418. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
  419. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
  420. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
  421. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
  422. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
  423. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
  424. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
  425. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
  426. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
  427. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
  428. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
  429. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
  430. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
  431. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
  432. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
  433. package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
  434. package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
  435. package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
  436. package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
  437. package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
  438. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
  439. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
  440. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
  441. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
  442. package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
  443. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
  444. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
  445. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
  446. package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
  447. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
  448. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
  449. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
  450. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
  451. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
  452. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
  453. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
  454. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  455. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
  456. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
  457. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
  458. package/.agents/skills/pr-ops/README.md +3 -0
  459. package/.agents/skills/pr-ops/SKILL.md +109 -0
  460. package/.agents/skills/ratatoskr-a2a/README.md +3 -0
  461. package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
  462. package/.agents/skills/rules-check-drift/SKILL.md +81 -0
  463. package/.agents/skills/saga-bearings/README.md +3 -0
  464. package/.agents/skills/saga-bearings/SKILL.md +26 -0
  465. package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
  466. package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
  467. package/.agents/skills/saga-bearings/assets/recap.md +56 -0
  468. package/.agents/skills/syn-recovery/README.md +3 -0
  469. package/.agents/skills/syn-recovery/SKILL.md +54 -0
  470. package/.agents/skills/tyr-check/README.md +3 -0
  471. package/.agents/skills/tyr-check/SKILL.md +218 -0
  472. package/.agents/skills/urdh-hold/README.md +3 -0
  473. package/.agents/skills/urdh-hold/SKILL.md +25 -0
  474. package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
  475. package/.agents/skills/urdh-hold/assets/hold.md +54 -0
  476. package/.agents/skills/vor-diagnostics/README.md +3 -0
  477. package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
  478. package/.agents/skills/ymir-host/README.md +3 -0
  479. package/.agents/skills/ymir-host/SKILL.md +34 -0
  480. package/.agents/skills/ymir-host/assets/desktop.md +36 -0
  481. package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
  482. package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
  483. package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
  484. package/.agents/skills/ymir-host/assets/update.md +64 -0
  485. package/.agents/tests/assets/board-render-harness.mjs +117 -0
  486. package/.agents/tests/cmux-test-safety.sh +51 -0
  487. package/.agents/tests/fixtures.sh +291 -0
  488. package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
  489. package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
  490. package/.agents/tests/fm-afk-launch.test.sh +961 -0
  491. package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
  492. package/.agents/tests/fm-afk-return.test.sh +283 -0
  493. package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
  494. package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
  495. package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
  496. package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
  497. package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
  498. package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
  499. package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
  500. package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
  501. package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
  502. package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
  503. package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
  504. package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
  505. package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
  506. package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
  507. package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
  508. package/.agents/tests/fm-backend-orca.test.sh +1357 -0
  509. package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
  510. package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
  511. package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
  512. package/.agents/tests/fm-backend.test.sh +1142 -0
  513. package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
  514. package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
  515. package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
  516. package/.agents/tests/fm-bearings-board.test.sh +414 -0
  517. package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
  518. package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
  519. package/.agents/tests/fm-bootstrap.test.sh +1178 -0
  520. package/.agents/tests/fm-branch-supervision.test.sh +552 -0
  521. package/.agents/tests/fm-brief.test.sh +785 -0
  522. package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
  523. package/.agents/tests/fm-busy-state.test.sh +465 -0
  524. package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
  525. package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
  526. package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
  527. package/.agents/tests/fm-check-unregister.test.sh +198 -0
  528. package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
  529. package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
  530. package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
  531. package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
  532. package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
  533. package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
  534. package/.agents/tests/fm-composer-ghost.test.sh +716 -0
  535. package/.agents/tests/fm-composer-lib.test.sh +666 -0
  536. package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
  537. package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
  538. package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
  539. package/.agents/tests/fm-control.test.sh +910 -0
  540. package/.agents/tests/fm-crew-state.test.sh +1610 -0
  541. package/.agents/tests/fm-cursor-harness.test.sh +404 -0
  542. package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
  543. package/.agents/tests/fm-cursor-primary.test.sh +708 -0
  544. package/.agents/tests/fm-daemon.test.sh +2740 -0
  545. package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
  546. package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
  547. package/.agents/tests/fm-extension-binding.test.sh +2187 -0
  548. package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
  549. package/.agents/tests/fm-fleet-sync.test.sh +721 -0
  550. package/.agents/tests/fm-gate-refuse.test.sh +356 -0
  551. package/.agents/tests/fm-gitignore-config.test.sh +89 -0
  552. package/.agents/tests/fm-gotmp.test.sh +230 -0
  553. package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
  554. package/.agents/tests/fm-grok-harness.test.sh +117 -0
  555. package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
  556. package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
  557. package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
  558. package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
  559. package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
  560. package/.agents/tests/fm-herdr-lab.test.sh +243 -0
  561. package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
  562. package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
  563. package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
  564. package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
  565. package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
  566. package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
  567. package/.agents/tests/fm-kimi-harness.test.sh +687 -0
  568. package/.agents/tests/fm-lint-workflows.test.sh +532 -0
  569. package/.agents/tests/fm-lint.test.sh +1023 -0
  570. package/.agents/tests/fm-muse-harness.test.sh +932 -0
  571. package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
  572. package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
  573. package/.agents/tests/fm-on.test.sh +507 -0
  574. package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
  575. package/.agents/tests/fm-operational-input.test.sh +160 -0
  576. package/.agents/tests/fm-peek-remote.test.sh +110 -0
  577. package/.agents/tests/fm-pending-reply.test.sh +1288 -0
  578. package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
  579. package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
  580. package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
  581. package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
  582. package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
  583. package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
  584. package/.agents/tests/fm-pr-merge.test.sh +2135 -0
  585. package/.agents/tests/fm-procevent-quota.test.sh +225 -0
  586. package/.agents/tests/fm-procevent-when.test.sh +406 -0
  587. package/.agents/tests/fm-procevent.test.sh +1745 -0
  588. package/.agents/tests/fm-project-origin.test.sh +110 -0
  589. package/.agents/tests/fm-public-followup.test.sh +2272 -0
  590. package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
  591. package/.agents/tests/fm-quota-choose.test.sh +626 -0
  592. package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
  593. package/.agents/tests/fm-remote-doctor.test.sh +625 -0
  594. package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
  595. package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
  596. package/.agents/tests/fm-remote-job.test.sh +768 -0
  597. package/.agents/tests/fm-remote-reply.test.sh +520 -0
  598. package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
  599. package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
  600. package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
  601. package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
  602. package/.agents/tests/fm-review-diff.test.sh +176 -0
  603. package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
  604. package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
  605. package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
  606. package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
  607. package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
  608. package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
  609. package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
  610. package/.agents/tests/fm-send-inbox.test.sh +352 -0
  611. package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
  612. package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
  613. package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
  614. package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
  615. package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
  616. package/.agents/tests/fm-send-settle.test.sh +147 -0
  617. package/.agents/tests/fm-send-strict.test.sh +241 -0
  618. package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
  619. package/.agents/tests/fm-session-start.test.sh +2515 -0
  620. package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
  621. package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
  622. package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
  623. package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
  624. package/.agents/tests/fm-spawn-batch.test.sh +148 -0
  625. package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
  626. package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
  627. package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
  628. package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
  629. package/.agents/tests/fm-startup-network.test.sh +712 -0
  630. package/.agents/tests/fm-stow-cascade.test.sh +370 -0
  631. package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
  632. package/.agents/tests/fm-supervision-events.test.sh +156 -0
  633. package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
  634. package/.agents/tests/fm-tangle-guard.test.sh +277 -0
  635. package/.agents/tests/fm-task-delivery.test.sh +420 -0
  636. package/.agents/tests/fm-task-inbox.test.sh +513 -0
  637. package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
  638. package/.agents/tests/fm-teardown.test.sh +2665 -0
  639. package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
  640. package/.agents/tests/fm-test-fixtures.test.sh +132 -0
  641. package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
  642. package/.agents/tests/fm-test-run.test.sh +1196 -0
  643. package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
  644. package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
  645. package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
  646. package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
  647. package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
  648. package/.agents/tests/fm-transition-lib.test.sh +54 -0
  649. package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
  650. package/.agents/tests/fm-update.test.sh +304 -0
  651. package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
  652. package/.agents/tests/fm-voice-relay.test.sh +4741 -0
  653. package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
  654. package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
  655. package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
  656. package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
  657. package/.agents/tests/fm-wake-queue.test.sh +1224 -0
  658. package/.agents/tests/fm-watch-arm.test.sh +821 -0
  659. package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
  660. package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
  661. package/.agents/tests/fm-watch-triage.test.sh +3889 -0
  662. package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
  663. package/.agents/tests/fm-x-mode.test.sh +3042 -0
  664. package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
  665. package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
  666. package/.agents/tests/herdr-test-safety.sh +42 -0
  667. package/.agents/tests/lib.sh +315 -0
  668. package/.agents/tests/remote-herdr-fixture.sh +125 -0
  669. package/.agents/tests/secondmate-helpers.sh +193 -0
  670. package/.agents/tests/smoke.test.sh +93 -0
  671. package/.agents/tests/wake-helpers.sh +338 -0
  672. package/.agents/tests/zellij-test-safety.sh +60 -0
  673. package/.agents/tools/README.md +10 -0
  674. package/.agents/tools/bin/tasks-cli +267 -0
  675. package/.agents/tools/tasks-cli.ts +216 -0
  676. package/AGENTS.md +454 -0
  677. package/CHANGELOG.md +881 -0
  678. package/LICENSE +1 -1
  679. package/NOTICE +63 -0
  680. package/README.md +113 -39
  681. package/RULES/01-domains.md +51 -0
  682. package/RULES/02-agents.md +78 -0
  683. package/RULES/03-houses.md +61 -0
  684. package/RULES/04-hoard.md +65 -0
  685. package/RULES/05-platforms.md +67 -0
  686. package/RULES/06-append-only.md +58 -0
  687. package/RULES/07-config.md +45 -0
  688. package/RULES/08-delivery-gate.md +75 -0
  689. package/RULES/README.md +20 -0
  690. package/bin/README.md +107 -0
  691. package/bin/a2a-mcp.sh +98 -0
  692. package/bin/a2a-serve.py +103 -0
  693. package/bin/a2a-serve.sh +8 -0
  694. package/bin/a2a-talk.sh +50 -0
  695. package/bin/a2abridge-ensure.sh +108 -0
  696. package/bin/agent-run.sh +43 -0
  697. package/bin/agents-config.sh +233 -0
  698. package/bin/apodex-smoke-test.sh +92 -0
  699. package/bin/bifrost-bridge.sh +112 -0
  700. package/bin/bifrost-ingress.sh +77 -0
  701. package/bin/bootstrap-macos.sh +107 -0
  702. package/bin/bootstrap-windows.ps1 +112 -0
  703. package/bin/branch-guard.sh +64 -0
  704. package/bin/brokk +73 -0
  705. package/bin/brokk-classify-lib.sh +1770 -0
  706. package/bin/brokk-lease-lib.sh +222 -0
  707. package/bin/brokk-lease.sh +193 -0
  708. package/bin/brokk-lint.sh +65 -0
  709. package/bin/brokk-send.sh +38 -0
  710. package/bin/brokk-update.sh +9 -0
  711. package/bin/brokk-wake-grant.sh +127 -0
  712. package/bin/brokk-wake-lib.sh +1849 -0
  713. package/bin/changelog-guard.sh +165 -0
  714. package/bin/crash-sense.sh +160 -0
  715. package/bin/desktop-place.sh +224 -0
  716. package/bin/docs-guard.sh +39 -0
  717. package/bin/editor-place.sh +207 -0
  718. package/bin/eindri-acclaim.sh +34 -0
  719. package/bin/eindri-control.sh +28 -0
  720. package/bin/eindri-role.sh +83 -0
  721. package/bin/eindri-seen.sh +41 -0
  722. package/bin/eindri-send.sh +15 -0
  723. package/bin/eindri-start.sh +85 -0
  724. package/bin/eindri-watch.sh +57 -0
  725. package/bin/einherjar-spawn.sh +576 -0
  726. package/bin/eir-doctor.sh +112 -0
  727. package/bin/erindi-brief.sh +271 -0
  728. package/bin/fleet-apply.sh +68 -0
  729. package/bin/github-deploy.sh +81 -0
  730. package/bin/gjallarhorn-expose.sh +102 -0
  731. package/bin/gjallarhorn-notify.sh +58 -0
  732. package/bin/gjallarhorn-purge.sh +54 -0
  733. package/bin/gjallarhorn-tunnel.sh +72 -0
  734. package/bin/gleipnir-lock-lib.sh +284 -0
  735. package/bin/groa-update.sh +122 -0
  736. package/bin/hall-snapshot.sh +92 -0
  737. package/bin/hamr-harness.sh +246 -0
  738. package/bin/heimdall-ssh-keys.sh +374 -0
  739. package/bin/herdr-agents.py +44 -0
  740. package/bin/herdr-ensure.sh +104 -0
  741. package/bin/herdr-run.sh +431 -0
  742. package/bin/hermes-ensure.sh +88 -0
  743. package/bin/hoard-lib.sh +25 -0
  744. package/bin/hodd.sh +78 -0
  745. package/bin/host-sense.sh +199 -0
  746. package/bin/huginn-research-worker.sh +108 -0
  747. package/bin/local-model-lock.sh +51 -0
  748. package/bin/mimir-bridge.py +226 -0
  749. package/bin/mimir-bridge.sh +87 -0
  750. package/bin/mimir-ingest.sh +131 -0
  751. package/bin/mimir.sh +113 -0
  752. package/bin/mjollnir-webhook.sh +59 -0
  753. package/bin/mjollnir.sh +85 -0
  754. package/bin/model-hardware.sh +82 -0
  755. package/bin/model-resolve.sh +147 -0
  756. package/bin/models-detect.sh +178 -0
  757. package/bin/nornir-cron-start.sh +162 -0
  758. package/bin/nornir-job-daily-briefing.sh +191 -0
  759. package/bin/nornir-job-git-sync.sh +152 -0
  760. package/bin/nornir-job-memory-housekeeping.sh +114 -0
  761. package/bin/nornir-job-observer.sh +166 -0
  762. package/bin/nornir-job-skillopt-sleep.sh +76 -0
  763. package/bin/omarchy-hook-install.sh +60 -0
  764. package/bin/omarchy-install.sh +148 -0
  765. package/bin/omarchy-plugins.sh +174 -0
  766. package/bin/omarchy-sense.sh +149 -0
  767. package/bin/perm-guard.sh +30 -0
  768. package/bin/pi-agent.sh +68 -0
  769. package/bin/pi-ensure.sh +77 -0
  770. package/bin/pi-local.sh +37 -0
  771. package/bin/pi-seat.sh +63 -0
  772. package/bin/prereq-ensure.sh +166 -0
  773. package/bin/private-guard.sh +38 -0
  774. package/bin/project-git.sh +55 -0
  775. package/bin/public-guard.sh +70 -0
  776. package/bin/ratatoskr.sh +49 -0
  777. package/bin/realm-lib.sh +32 -0
  778. package/bin/repo-scrub.sh +62 -0
  779. package/bin/rodd-operational-input.sh +250 -0
  780. package/bin/runes-append.sh +198 -0
  781. package/bin/saga-session-start.sh +172 -0
  782. package/bin/saga-sessionstart-run.sh +79 -0
  783. package/bin/saga-wake-drain.sh +41 -0
  784. package/bin/secret-guard.sh +63 -0
  785. package/bin/sessrumnir-ensure.sh +100 -0
  786. package/bin/sessrumnir-sync.sh +237 -0
  787. package/bin/sessrumnir.sh +91 -0
  788. package/bin/skillopt-setup.sh +76 -0
  789. package/bin/skuld-branch-outcome.sh +203 -0
  790. package/bin/skuld-branch-prompt.sh +101 -0
  791. package/bin/smidja-bootstrap.sh +90 -0
  792. package/bin/smidja-observe.sh +86 -0
  793. package/bin/syn-arm-pretool-check.sh +57 -0
  794. package/bin/syn-asset-pretool-check.sh +76 -0
  795. package/bin/syn-cd-pretool-check.sh +28 -0
  796. package/bin/syn-guard-pretool-check.sh +83 -0
  797. package/bin/syn-turnend-guard.sh +36 -0
  798. package/bin/syn-watch-arm.sh +99 -0
  799. package/bin/tailscale-sync.sh +190 -0
  800. package/bin/telegram-bot.sh +69 -0
  801. package/bin/toolchain.sh +74 -0
  802. package/bin/utgard.sh +111 -0
  803. package/bin/valhalla.sh +115 -0
  804. package/bin/valknut-load.sh +260 -0
  805. package/bin/vor-crew-state.sh +124 -0
  806. package/bin/wedge-notify.sh +47 -0
  807. package/bin/workspace-provision.sh +80 -0
  808. package/bin/workspace-rag.sh +150 -0
  809. package/bin/wyrd-db.sh +47 -0
  810. package/bin/yggdrasil.sh +152 -0
  811. package/bin/ymir-edit.sh +145 -0
  812. package/bin/ymir-install.sh +641 -0
  813. package/bin/ymir-invite.sh +93 -0
  814. package/bin/ymir-isolation.sh +88 -0
  815. package/bin/ymir-migrate.sh +61 -0
  816. package/bin/ymir-platform.sh +348 -0
  817. package/bin/ymir-say.sh +97 -0
  818. package/bin/ymir-setup-auth.sh +108 -0
  819. package/bin/ymir-validate.sh +173 -0
  820. package/bin/ymir.js +74 -16
  821. package/docs/Architecture.md +339 -0
  822. package/docs/design.md +351 -0
  823. package/docs/installations/README.md +63 -0
  824. package/docs/installations/ymir-home.md +93 -0
  825. package/docs/integration.md +105 -0
  826. package/docs/lore.md +1056 -0
  827. package/docs/research/agent-harnesses.md +290 -0
  828. package/docs/runbooks/README.md +19 -0
  829. package/docs/runbooks/a2a.md +91 -0
  830. package/docs/runbooks/agent-permissions.md +99 -0
  831. package/docs/runbooks/agents.md +57 -0
  832. package/docs/runbooks/models.md +69 -0
  833. package/docs/runbooks/realm-onboarding.md +60 -0
  834. package/docs/runbooks/secrets-and-hoard.md +60 -0
  835. package/docs/runbooks/updates-and-migrations.md +110 -0
  836. package/docs/runbooks/worktrees-isolation.md +72 -0
  837. package/docs/session-start.md +105 -0
  838. package/docs/skillopt-integration.md +321 -0
  839. package/docs/sync-system.md +66 -0
  840. package/docs/workspaces.md +104 -0
  841. package/docs/ymir-rut.md +260 -0
  842. package/package.json +44 -14
  843. package/packaging/build.sh +74 -0
  844. package/packaging/macos/Ymir Installer.command +43 -0
  845. package/packaging/windows/ymir-setup.nsi +74 -0
  846. package/scripts/README.md +22 -0
  847. package/scripts/electron.sh +210 -0
  848. package/scripts/start.sh +167 -0
  849. package/scripts/stop.sh +113 -0
  850. package/install.sh +0 -35
@@ -0,0 +1,1463 @@
1
+ #!/usr/bin/env bash
2
+ # fm-fleet-snapshot.sh - read-only structured fleet snapshot.
3
+ #
4
+ # Output contract: `--json` prints one object with schema
5
+ # `fm-fleet-snapshot.v1`.
6
+ # The command is read-only: it does not acquire the session lock, drain wakes,
7
+ # arm watchers, mutate backlog state, or write reports.
8
+ #
9
+ # Top-level fields:
10
+ # schema: stable schema id.
11
+ # generated: UTC observation time for this fresh command execution.
12
+ # fm_home: resolved operational home.
13
+ # roots: resolved root/config/data/state/projects directories.
14
+ # backlog: {path,present,records[]} where records are ordered as written in
15
+ # data/backlog.md and cover In flight, Queued, and Done.
16
+ # Canonical tasks-axi rows are structured; free-form non-empty lines in
17
+ # those sections are preserved as unstructured records.
18
+ # Structured rows preserve captain-hold metadata such as hold_kind,
19
+ # hold_reason, and hold_until when tasks-axi emits it. They also carry
20
+ # normalized current_role, requires_child_metadata, blocked_by_ids,
21
+ # unresolved_blocker_ids, captain_actionable, and deferred_marker fields.
22
+ # Repeated blocker tokens remain ordered; a blocker resolves only when its
23
+ # structured record is Done, and missing ids stay open.
24
+ # captain_actionable means "waiting on the captain now": queued, held for
25
+ # the captain, unblocked, and due (no hold_until, or hold_until at or
26
+ # before the observation date, matching tasks-axi's own date-gate rule).
27
+ # There is no separate decision type: any captain-held task is the same
28
+ # primitive, whatever kind its row carries.
29
+ # deferred_marker is a presentation hint only: the row's hold reason or
30
+ # body carries an explicit SUPERSEDED / NOT REQUIRED / DEFERRED marker.
31
+ # It never changes captain_actionable; renderers may use it to keep
32
+ # prose-deferred rows out of default views.
33
+ # tasks[]: one row per state/<id>.meta, sorted by id.
34
+ # current_state is parsed from bin/fm-crew-state.sh <id> and preserves
35
+ # state, source, detail, and raw line separately.
36
+ # paths.status_log.last_event is historical wake-event data only, never
37
+ # current state.
38
+ # hints.open_decisions is the keyed open-decision set returned by
39
+ # fm-classify-lib.sh's authoritative status_open_decisions fold and reconciled
40
+ # against current_state; hints.pending_decision and hints.blocked_event are
41
+ # booleans derived from that set.
42
+ # endpoint.exists is the cheap backend endpoint-presence read.
43
+ # endpoint.agent_alive is populated for secondmates only, where it is useful
44
+ # return-channel supervision data; other tasks use "not_checked".
45
+ # scout_reports[]: present data/<id>/report.md pointers.
46
+ # main_inventory: {valid,reason,orphan_in_flight[],unstructured_current_count} -
47
+ # main-home current-inventory checks shared with secondmate_home_summary_json
48
+ # (orphan structured in-flight ids with no state/<id>.meta, and unstructured
49
+ # current backlog rows). Does not invent live tasks; meta remains truth for
50
+ # workers. Bearings maps failures into omitted[] disclosure (and a Charted
51
+ # Next gate line) rather than silent empty Underway.
52
+ # secondmate_current: {records[],total,shown,truncated} - bounded current summaries
53
+ # for registered secondmates, selected from validated structured state inside
54
+ # each home with explicit provenance, freshness, endpoint evidence, and unknown
55
+ # failure reasons. Parent status and bounded terminal evidence are historical,
56
+ # untrusted supplements only and never override readable structured-home facts.
57
+ # Each structured-home record carries active_children, decisions_open, holds,
58
+ # queued, landed, endpoints, counts, and omitted. Every successfully sampled
59
+ # home also carries reconcile_inventory independently of projection trust.
60
+ # Actionable captain holds
61
+ # appear in decisions_open; blocked captain holds remain queued with metadata.
62
+ # secondmate_landed: {records[],truncated[],unreadable[],partial[]} - the
63
+ # compatibility landed-work roll-up derived from secondmate_current. Readable
64
+ # structured homes are partial, not unreadable, when an unavailable child state
65
+ # or a backlog-vs-metadata inventory mismatch makes their summary incomplete;
66
+ # they retain independently trustworthy structured surfaces. An inventory
67
+ # mismatch also keeps the home's own current classification, which only an
68
+ # unavailable child state or an untrustworthy backlog collapses to unknown.
69
+ # secondmate_guidance: return-channel action note for renderers and bearings.
70
+ #
71
+ # Compatibility: JSON is the primary machine-readable surface.
72
+ # Human views must render this output instead of parsing state files again.
73
+ set -u
74
+
75
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
76
+ FM_ROOT="${FM_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}"
77
+ FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$FM_ROOT}}"
78
+ STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}"
79
+ DATA="${FM_DATA_OVERRIDE:-$FM_HOME/data}"
80
+ CONFIG="${FM_CONFIG_OVERRIDE:-$FM_HOME/config}"
81
+ PROJECTS="${FM_PROJECTS_OVERRIDE:-$FM_HOME/projects}"
82
+ BACKLOG="$DATA/backlog.md"
83
+ SNAPSHOT_NOW=${FM_SNAPSHOT_NOW:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}
84
+ if [ -n "${FM_SNAPSHOT_NOW_EPOCH:-}" ]; then
85
+ SNAPSHOT_EPOCH=$FM_SNAPSHOT_NOW_EPOCH
86
+ else
87
+ SNAPSHOT_EPOCH=$(date -u -j -f '%Y-%m-%dT%H:%M:%SZ' "$SNAPSHOT_NOW" +%s 2>/dev/null \
88
+ || date -u -d "$SNAPSHOT_NOW" +%s 2>/dev/null \
89
+ || date +%s)
90
+ fi
91
+ case "$SNAPSHOT_EPOCH" in ''|*[!0-9]*) SNAPSHOT_EPOCH=$(date +%s) ;; esac
92
+ # The observation date gates captain-hold deferral: a `hold-until` date still in
93
+ # the future keeps a captain hold out of captain_actionable until it is due
94
+ # (tasks-axi's own contract: the hold is inactive on and after that date).
95
+ SNAPSHOT_TODAY=${SNAPSHOT_NOW%%T*}
96
+ case "$SNAPSHOT_TODAY" in
97
+ [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]) : ;;
98
+ *) SNAPSHOT_TODAY=$(date -u +%Y-%m-%d) ;;
99
+ esac
100
+
101
+ # Cross-home bounds are explicit so one broken or unexpectedly large home cannot
102
+ # hang or explode the parent snapshot.
103
+ FM_SNAPSHOT_SECONDMATES=${FM_SNAPSHOT_SECONDMATES:-20}
104
+ FM_SNAPSHOT_SECONDMATE_TIMEOUT=${FM_SNAPSHOT_SECONDMATE_TIMEOUT:-8}
105
+ FM_SNAPSHOT_CREW_STATE_TIMEOUT=${FM_SNAPSHOT_CREW_STATE_TIMEOUT:-10}
106
+ FM_SNAPSHOT_SECONDMATE_MAX_BYTES=${FM_SNAPSHOT_SECONDMATE_MAX_BYTES:-262144}
107
+ FM_SNAPSHOT_SECONDMATE_CHILDREN=${FM_SNAPSHOT_SECONDMATE_CHILDREN:-20}
108
+ FM_SNAPSHOT_SECONDMATE_QUEUED=${FM_SNAPSHOT_SECONDMATE_QUEUED:-20}
109
+ FM_SNAPSHOT_SECONDMATE_DECISIONS=${FM_SNAPSHOT_SECONDMATE_DECISIONS:-20}
110
+ FM_SNAPSHOT_TERMINAL_LINES=${FM_SNAPSHOT_TERMINAL_LINES:-8}
111
+ FM_SNAPSHOT_TERMINAL_BYTES=${FM_SNAPSHOT_TERMINAL_BYTES:-4096}
112
+ FM_SNAPSHOT_TERMINAL_TIMEOUT=${FM_SNAPSHOT_TERMINAL_TIMEOUT:-2}
113
+ FM_SNAPSHOT_PARENT_ACTIVITY_LINES=${FM_SNAPSHOT_PARENT_ACTIVITY_LINES:-256}
114
+ FM_SNAPSHOT_PARENT_ACTIVITY_BYTES=${FM_SNAPSHOT_PARENT_ACTIVITY_BYTES:-65536}
115
+ FM_SNAPSHOT_PARENT_ACTIVITIES=${FM_SNAPSHOT_PARENT_ACTIVITIES:-20}
116
+ FM_SNAPSHOT_PARENT_ACTIVITY_TIMEOUT=${FM_SNAPSHOT_PARENT_ACTIVITY_TIMEOUT:-2}
117
+ FM_SNAPSHOT_REGISTRY_LINES=${FM_SNAPSHOT_REGISTRY_LINES:-256}
118
+ FM_SNAPSHOT_REGISTRY_BYTES=${FM_SNAPSHOT_REGISTRY_BYTES:-65536}
119
+ FM_SNAPSHOT_REGISTRY_RECORDS=${FM_SNAPSHOT_REGISTRY_RECORDS:-40}
120
+ FM_SNAPSHOT_REGISTRY_TIMEOUT=${FM_SNAPSHOT_REGISTRY_TIMEOUT:-2}
121
+ validate_positive_bound() { # <name> <value>
122
+ case "$2" in
123
+ ''|*[!0-9]*|0)
124
+ printf 'fm-fleet-snapshot: %s must be a positive integer\n' "$1" >&2
125
+ exit 2
126
+ ;;
127
+ esac
128
+ }
129
+ case "$FM_SNAPSHOT_SECONDMATES" in
130
+ ''|*[!0-9]*)
131
+ echo "fm-fleet-snapshot: FM_SNAPSHOT_SECONDMATES must be a non-negative integer" >&2
132
+ exit 2
133
+ ;;
134
+ esac
135
+ validate_positive_bound FM_SNAPSHOT_SECONDMATE_TIMEOUT "$FM_SNAPSHOT_SECONDMATE_TIMEOUT"
136
+ validate_positive_bound FM_SNAPSHOT_CREW_STATE_TIMEOUT "$FM_SNAPSHOT_CREW_STATE_TIMEOUT"
137
+ validate_positive_bound FM_SNAPSHOT_SECONDMATE_MAX_BYTES "$FM_SNAPSHOT_SECONDMATE_MAX_BYTES"
138
+ validate_positive_bound FM_SNAPSHOT_SECONDMATE_CHILDREN "$FM_SNAPSHOT_SECONDMATE_CHILDREN"
139
+ validate_positive_bound FM_SNAPSHOT_SECONDMATE_QUEUED "$FM_SNAPSHOT_SECONDMATE_QUEUED"
140
+ validate_positive_bound FM_SNAPSHOT_SECONDMATE_DECISIONS "$FM_SNAPSHOT_SECONDMATE_DECISIONS"
141
+ validate_positive_bound FM_SNAPSHOT_TERMINAL_LINES "$FM_SNAPSHOT_TERMINAL_LINES"
142
+ validate_positive_bound FM_SNAPSHOT_TERMINAL_BYTES "$FM_SNAPSHOT_TERMINAL_BYTES"
143
+ validate_positive_bound FM_SNAPSHOT_TERMINAL_TIMEOUT "$FM_SNAPSHOT_TERMINAL_TIMEOUT"
144
+ validate_positive_bound FM_SNAPSHOT_PARENT_ACTIVITY_LINES "$FM_SNAPSHOT_PARENT_ACTIVITY_LINES"
145
+ validate_positive_bound FM_SNAPSHOT_PARENT_ACTIVITY_BYTES "$FM_SNAPSHOT_PARENT_ACTIVITY_BYTES"
146
+ validate_positive_bound FM_SNAPSHOT_PARENT_ACTIVITIES "$FM_SNAPSHOT_PARENT_ACTIVITIES"
147
+ validate_positive_bound FM_SNAPSHOT_PARENT_ACTIVITY_TIMEOUT "$FM_SNAPSHOT_PARENT_ACTIVITY_TIMEOUT"
148
+ validate_positive_bound FM_SNAPSHOT_REGISTRY_LINES "$FM_SNAPSHOT_REGISTRY_LINES"
149
+ validate_positive_bound FM_SNAPSHOT_REGISTRY_BYTES "$FM_SNAPSHOT_REGISTRY_BYTES"
150
+ validate_positive_bound FM_SNAPSHOT_REGISTRY_RECORDS "$FM_SNAPSHOT_REGISTRY_RECORDS"
151
+ validate_positive_bound FM_SNAPSHOT_REGISTRY_TIMEOUT "$FM_SNAPSHOT_REGISTRY_TIMEOUT"
152
+
153
+ # shellcheck source=bin/fm-backend.sh
154
+ # shellcheck disable=SC1091
155
+ . "$SCRIPT_DIR/fm-backend.sh"
156
+ # shellcheck source=bin/fm-classify-lib.sh
157
+ # shellcheck disable=SC1091
158
+ . "$SCRIPT_DIR/fm-classify-lib.sh"
159
+ # shellcheck source=bin/fm-ff-lib.sh
160
+ # shellcheck disable=SC1091
161
+ . "$SCRIPT_DIR/fm-ff-lib.sh" # validate_secondmate_home: shared seeded-home boundary checks
162
+ # shellcheck source=bin/fm-timeout-lib.sh
163
+ # shellcheck disable=SC1091
164
+ . "$SCRIPT_DIR/fm-timeout-lib.sh" # fm_run_timed: the shared hard bound
165
+
166
+ usage() {
167
+ cat <<'EOF'
168
+ usage: fm-fleet-snapshot.sh --json
169
+ fm-fleet-snapshot.sh --secondmate-home-summary
170
+
171
+ Print a read-only structured snapshot of the firstmate fleet.
172
+ JSON is the stable machine-readable output contract.
173
+
174
+ --secondmate-home-summary emits the bounded structured summary used after a
175
+ validated registered-home handoff. It is local-only, skips nested secondmate
176
+ aggregation, includes generated_epoch for freshness arithmetic, and marks
177
+ inventory contradictions or unavailable child state invalid.
178
+ Its invalidity object names the normalized failure kind and affected ids.
179
+ Actionable tasks-axi captain holds appear as decisions_open and stay visible in
180
+ queued with hold_reason, hold_kind, hold_until, deferred_marker, and plural
181
+ blocker fields for downstream projections. A captain hold is actionable only
182
+ when every blocker is Done and any hold-until date has arrived.
183
+ Cross-home reads use FM_SNAPSHOT_SECONDMATES (default 20, 0 lifts the count
184
+ bound), FM_SNAPSHOT_SECONDMATE_TIMEOUT, and FM_SNAPSHOT_SECONDMATE_MAX_BYTES.
185
+ Each per-task current-state read is bounded by FM_SNAPSHOT_CREW_STATE_TIMEOUT
186
+ (default 10 seconds), so one unreachable remote secondmate host cannot extend
187
+ the snapshot without limit; a read that hits the bound reports state unknown.
188
+ Terminal contradiction evidence uses
189
+ FM_SNAPSHOT_TERMINAL_LINES, FM_SNAPSHOT_TERMINAL_BYTES, and
190
+ FM_SNAPSHOT_TERMINAL_TIMEOUT and never becomes canonical current state.
191
+ Parent activity evidence uses FM_SNAPSHOT_PARENT_ACTIVITY_LINES,
192
+ FM_SNAPSHOT_PARENT_ACTIVITY_BYTES, FM_SNAPSHOT_PARENT_ACTIVITIES, and
193
+ FM_SNAPSHOT_PARENT_ACTIVITY_TIMEOUT, with truncation disclosed in the result.
194
+ The registered secondmate table uses FM_SNAPSHOT_REGISTRY_LINES,
195
+ FM_SNAPSHOT_REGISTRY_BYTES, FM_SNAPSHOT_REGISTRY_RECORDS, and
196
+ FM_SNAPSHOT_REGISTRY_TIMEOUT, with unavailability and truncation disclosed.
197
+ EOF
198
+ }
199
+
200
+ OUTPUT_MODE=json
201
+ case "${1:---json}" in
202
+ --json) ;;
203
+ --secondmate-home-summary) OUTPUT_MODE=secondmate-home-summary ;;
204
+ -h|--help) usage; exit 0 ;;
205
+ *) usage >&2; exit 2 ;;
206
+ esac
207
+
208
+ command -v jq >/dev/null 2>&1 || { echo "fm-fleet-snapshot: jq not found" >&2; exit 1; }
209
+
210
+ bool_json() {
211
+ if [ "$1" = 1 ]; then printf 'true'; else printf 'false'; fi
212
+ }
213
+
214
+ path_present_json() { # <path>
215
+ local present=0
216
+ [ -e "$1" ] && present=1
217
+ jq -n --arg path "$1" --argjson present "$(bool_json "$present")" \
218
+ '{path:$path,present:$present}'
219
+ }
220
+
221
+ meta_value() { # <meta-file> <key>
222
+ fm_meta_get "$1" "$2"
223
+ }
224
+
225
+ last_nonempty_line() { # <file>
226
+ [ -f "$1" ] || return 1
227
+ grep -v '^[[:space:]]*$' "$1" 2>/dev/null | tail -1
228
+ }
229
+
230
+ # A crew-state read is bounded like every other cross-home read here. For a
231
+ # remote secondmate fm-crew-state.sh reaches its host over ssh, and ssh's own
232
+ # dead-peer detection deliberately never kills a slow-but-alive remote command,
233
+ # so without this bound one unreachable or slow host extends the whole snapshot
234
+ # without limit - and this snapshot is also the producer behind the repeatedly
235
+ # published home ledger. A read that hits the bound is indistinguishable from
236
+ # the already-handled unreadable case: empty output folds to state unknown.
237
+ crew_state_json() { # <id>
238
+ local id=$1 raw rest state source detail sep
239
+ raw=$(
240
+ fm_run_timed "$FM_SNAPSHOT_CREW_STATE_TIMEOUT" \
241
+ env FM_ROOT_OVERRIDE="$FM_ROOT" \
242
+ FM_HOME="$FM_HOME" \
243
+ FM_STATE_OVERRIDE="$STATE" \
244
+ FM_DATA_OVERRIDE="$DATA" \
245
+ FM_PROJECTS_OVERRIDE="$PROJECTS" \
246
+ FM_CONFIG_OVERRIDE="$CONFIG" \
247
+ "$SCRIPT_DIR/fm-crew-state.sh" "$id" 2>/dev/null || true
248
+ )
249
+ raw=$(printf '%s\n' "$raw" | head -1)
250
+ sep=' · '
251
+ state=unknown
252
+ source=none
253
+ detail=
254
+ case "$raw" in
255
+ state:\ *"$sep"source:\ *)
256
+ rest=${raw#state: }
257
+ state=${rest%%"$sep"source: *}
258
+ rest=${rest#*"$sep"source: }
259
+ case "$rest" in
260
+ *"$sep"*) source=${rest%%"$sep"*}; detail=${rest#*"$sep"} ;;
261
+ *) source=$rest ;;
262
+ esac
263
+ ;;
264
+ esac
265
+ jq -n --arg raw "$raw" --arg state "$state" --arg source "$source" --arg detail "$detail" \
266
+ '{state:$state,source:$source,detail:$detail,raw:$raw}'
267
+ }
268
+
269
+ status_event_json() { # <status-log>
270
+ local log=$1 present=0 raw='' verb='' note=''
271
+ if [ -f "$log" ]; then
272
+ present=1
273
+ raw=$(last_nonempty_line "$log" || true)
274
+ verb=$(status_line_verb "$raw")
275
+ note=$(status_line_note "$raw")
276
+ fi
277
+ jq -n \
278
+ --arg path "$log" \
279
+ --arg raw "$raw" \
280
+ --arg verb "$verb" \
281
+ --arg note "$note" \
282
+ --argjson present "$(bool_json "$present")" \
283
+ '{path:$path,present:$present,kind:"event_history",last_event:{state:$verb,note:$note,raw:$raw}}'
284
+ }
285
+
286
+ first_pr_url_in_file() { # <file>
287
+ [ -f "$1" ] || return 1
288
+ grep -Eo 'https?://[^[:space:])"]+/pull/[0-9]+' "$1" 2>/dev/null | head -1
289
+ }
290
+
291
+ backlog_json() { # [<backlog-path>] - defaults to this home's $BACKLOG
292
+ local backlog=${1:-$BACKLOG}
293
+ if [ ! -f "$backlog" ]; then
294
+ jq -n --arg path "$backlog" '{path:$path,present:false,records:[]}'
295
+ return 0
296
+ fi
297
+
298
+ # shellcheck disable=SC2094
299
+ jq -Rn --arg path "$backlog" --arg today "$SNAPSHOT_TODAY" '
300
+ def trim: gsub("^[[:space:]]+|[[:space:]]+$"; "");
301
+ def section_state:
302
+ if . == "In flight" then "in_flight"
303
+ elif . == "Queued" then "queued"
304
+ elif . == "Done" then "done"
305
+ else null end;
306
+ def cap($rest; $re):
307
+ (((($rest | capture($re)?) // {}) | .v) // null) as $v
308
+ | if $v == null then null else ($v | trim) end;
309
+ def metadata($rest; $key):
310
+ cap($rest; ".*(?:\\(|,[[:space:]]*)" + $key + ":[[:space:]]*(?<v>[^,)]*)");
311
+ def metadata_word($rest; $key):
312
+ cap($rest; ".*(?:\\(|,[[:space:]]*)" + $key + "[[:space:]]+(?<v>[^,)]*)");
313
+ def url_pattern: "https?://[^[:space:])\"<>]+";
314
+ def wrapped_url_pattern: "<?" + url_pattern + ">?";
315
+ def links($rest): [$rest | scan(url_pattern)];
316
+ def strip_trailing_metadata:
317
+ reduce range(0; 20) as $_ (.;
318
+ sub("[[:space:]]*\\([[:space:]]*(?:(?:repo|kind|priority|hold|hold-kind|hold-until):[[:space:]]*[^)]*|(?:since|merged|reported|done)[[:space:]]+[^)]*)[[:space:]]*\\)[[:space:]]*$"; ""));
319
+ def strip_title_artifacts:
320
+ sub("[[:space:]]+-[[:space:]]+data/[^[:space:])]+/report\\.md$"; "")
321
+ | sub("[[:space:]]+data/[^[:space:])]+/report\\.md$"; "")
322
+ | sub("[[:space:]]+-[[:space:]]+local main$"; "")
323
+ | sub("[[:space:]]+local main$"; "")
324
+ | sub("[[:space:]]+-[[:space:]]*$"; "");
325
+ def clean_title:
326
+ strip_trailing_metadata
327
+ | strip_title_artifacts
328
+ | gsub("[[:space:]]+"; " ")
329
+ | trim;
330
+ def title_of($rest):
331
+ $rest
332
+ | gsub(wrapped_url_pattern; "")
333
+ | sub("[[:space:]]*blocked-by:[[:space:]]+[^[:space:])]+[[:space:]]+-[[:space:]]+.*$"; "")
334
+ | gsub("[[:space:]]*blocked-by:[[:space:]]+[^[:space:]]+"; "")
335
+ | clean_title;
336
+ def blocked_by_ids($rest):
337
+ [ $rest | scan("blocked-by:[[:space:]]+(?<id>[^[:space:])]+)") | .[0] ]
338
+ | reduce .[] as $id ([]; if index($id) == null then . + [$id] else . end);
339
+ def blocked_reason($rest):
340
+ cap($rest; ".*blocked-by:[[:space:]]*[^[:space:])]+[[:space:]]+-[[:space:]]*(?<v>.*)$") as $reason
341
+ | if $reason == null then null
342
+ else ($reason | clean_title | if . == "" then null else . end)
343
+ end;
344
+ def local_note($rest):
345
+ cap(($rest | strip_trailing_metadata); ".*(?:^|[[:space:]]+-[[:space:]]+|[[:space:]])(?<v>local main)$");
346
+ def completion($rest):
347
+ (metadata_word($rest; "merged")) as $merged
348
+ | (metadata_word($rest; "reported")) as $reported
349
+ | (metadata_word($rest; "done")) as $done
350
+ | if $merged != null then {verb:"merged",date:$merged}
351
+ elif $reported != null then {verb:"reported",date:$reported}
352
+ elif $done != null then {verb:"done",date:$done}
353
+ else {verb:null,date:null} end;
354
+ def row_match($line):
355
+ (($line | capture("^[-*][[:space:]]+\\[(?<check>[ xX])\\][[:space:]]+(?<id>[^[:space:]]+)[[:space:]]+-[[:space:]]+(?<rest>.*)$")?) //
356
+ (($line | capture("^[-*][[:space:]]+\\*\\*(?<id>[^*]+)\\*\\*[[:space:]]+-[[:space:]]+(?<rest>.*)$")?)
357
+ | if . == null then null else . + {check:" "} end));
358
+ def structured_row($line):
359
+ ($line | test("^[-*][[:space:]]+\\[[ xX]\\][[:space:]]+[^[:space:]]+[[:space:]]+-[[:space:]]+"))
360
+ or ($line | test("^[-*][[:space:]]+\\*\\*[^*]+\\*\\*[[:space:]]+-[[:space:]]+"));
361
+ def parse_row($line; $section; $order):
362
+ row_match($line) as $m
363
+ | if $m == null then
364
+ {order:$order,state:$section,structured:false,id:null,raw:$line,body_lines:[],body_excerpt:null}
365
+ else
366
+ ($m.rest) as $rest
367
+ | {order:$order,
368
+ state:$section,
369
+ structured:true,
370
+ id:($m.id | trim),
371
+ checked:($m.check | test("[xX]")),
372
+ title:title_of($rest),
373
+ repo:metadata($rest; "repo"),
374
+ kind:metadata($rest; "kind"),
375
+ priority:metadata($rest; "priority"),
376
+ hold_reason:metadata($rest; "hold"),
377
+ hold_kind:metadata($rest; "hold-kind"),
378
+ hold_until:metadata($rest; "hold-until"),
379
+ blocked_by:cap($rest; ".*blocked-by:[[:space:]]*(?<v>[^[:space:])]+).*"),
380
+ blocked_by_ids:blocked_by_ids($rest),
381
+ blocked_reason:blocked_reason($rest),
382
+ since:metadata_word($rest; "since"),
383
+ merged:metadata_word($rest; "merged"),
384
+ reported:metadata_word($rest; "reported"),
385
+ done:metadata_word($rest; "done"),
386
+ completion:completion($rest),
387
+ links:links($rest),
388
+ pr_url:((links($rest) | map(select(test("/pull/[0-9]+"))) | .[0]) // null),
389
+ report_path:cap($rest; ".*(?<v>data/[^[:space:])]+/report\\.md).*"),
390
+ local_note:local_note($rest),
391
+ raw:$line,
392
+ body_lines:[],
393
+ body_excerpt:null}
394
+ end;
395
+ reduce inputs as $line
396
+ ({path:$path,present:true,records:[],section:null,order:0};
397
+ if ($line | test("^##[[:space:]]+")) then
398
+ .section = (($line | sub("^##[[:space:]]+";"") | trim) | section_state)
399
+ elif .section == null or ($line | trim) == "" then
400
+ .
401
+ elif structured_row($line) then
402
+ .order += 1
403
+ | .records += [parse_row($line; .section; .order)]
404
+ elif ((.records | length) > 0 and (.records[-1].structured == true) and ($line | test("^[[:space:]]+"))) then
405
+ ($line | trim) as $body
406
+ | if $body == "" then .
407
+ else .records[-1].body_lines += [$body] end
408
+ else
409
+ .order += 1
410
+ | .records += [{order:.order,state:.section,structured:false,id:null,raw:$line,body_lines:[],body_excerpt:null}]
411
+ end)
412
+ | .records |= map(
413
+ if (.body_lines | length) > 0 then
414
+ .body_excerpt = ((.body_lines | join(" "))[:240])
415
+ else . end)
416
+ | .records as $records
417
+ | (reduce ($records[] | select(.structured)) as $record ({};
418
+ .[$record.id] = ((.[$record.id] // true) and ($record.state == "done")))) as $resolved_ids
419
+ | .records |= map(
420
+ if .structured then
421
+ . as $record
422
+ | .unresolved_blocker_ids = [
423
+ $record.blocked_by_ids[] as $blocker
424
+ | select($resolved_ids[$blocker] != true)
425
+ | $blocker
426
+ ]
427
+ | .current_role =
428
+ (if .state == "in_flight" and .hold_reason != null and .hold_kind != null then "held"
429
+ elif .state == "in_flight" and .kind == "program" then "program"
430
+ elif .state == "in_flight" then "worker"
431
+ elif .state == "queued" then "queued"
432
+ else "done" end)
433
+ | .requires_child_metadata = (.current_role == "worker")
434
+ | .captain_actionable =
435
+ (.state == "queued" and .hold_kind == "captain"
436
+ and .hold_reason != null and (.unresolved_blocker_ids | length) == 0
437
+ and (.hold_until == null or .hold_until <= $today))
438
+ | .deferred_marker =
439
+ ((((.hold_reason // "") + " " + (.body_excerpt // ""))
440
+ | test("SUPERSEDED|NOT REQUIRED|NOT-REQUIRED|DEFERRED"; "i")))
441
+ else . end)
442
+ | del(.section,.order)
443
+ ' < "$backlog"
444
+ }
445
+
446
+ task_json_lines() {
447
+ local meta id kind harness mode yolo project worktree home projects spawn_gen backend target status_log report_path
448
+ local remote_host remote_root remote_state remote_rc remote_home_present
449
+ local pr pr_source event_json current_json endpoint_exists agent_alive meta_json status_json report_json worktree_json home_json
450
+ local last_event_raw current_state current_source pending_decision blocked_event report_present=0 pr_from_status
451
+ local open_decisions_tsv open_decisions_json
452
+
453
+ for meta in "$STATE"/*.meta; do
454
+ [ -e "$meta" ] || continue
455
+ id=$(basename "$meta" .meta)
456
+ kind=$(meta_value "$meta" kind)
457
+ [ -n "$kind" ] || kind=ship
458
+ harness=$(meta_value "$meta" harness)
459
+ mode=$(meta_value "$meta" mode)
460
+ yolo=$(meta_value "$meta" yolo)
461
+ project=$(meta_value "$meta" project)
462
+ worktree=$(meta_value "$meta" worktree)
463
+ home=$(meta_value "$meta" home)
464
+ projects=$(meta_value "$meta" projects)
465
+ spawn_gen=$(meta_value "$meta" spawn_gen)
466
+ remote_host=$(meta_value "$meta" remote_host)
467
+ remote_root=$(meta_value "$meta" remote_root)
468
+ remote_home_present=null
469
+ if [ -n "$remote_host" ]; then
470
+ backend=$(meta_value "$meta" remote_backend)
471
+ [ -n "$backend" ] || backend=unknown
472
+ target=$(meta_value "$meta" remote_target)
473
+ else
474
+ backend=$(fm_backend_of_meta "$meta")
475
+ target=$(fm_backend_target_of_meta "$meta")
476
+ fi
477
+ status_log="$STATE/$id.status"
478
+ report_path="$DATA/$id/report.md"
479
+ pr=$(meta_value "$meta" pr)
480
+ pr_source=meta
481
+ if [ -z "$pr" ]; then
482
+ pr_from_status=$(first_pr_url_in_file "$status_log" || true)
483
+ pr=$pr_from_status
484
+ pr_source=status_event
485
+ fi
486
+ if [ -z "$pr" ]; then
487
+ pr_source=absent
488
+ fi
489
+
490
+ current_json=$(crew_state_json "$id")
491
+ event_json=$(status_event_json "$status_log")
492
+ last_event_raw=$(printf '%s' "$event_json" | jq -r '.last_event.raw // ""')
493
+ current_state=$(printf '%s' "$current_json" | jq -r '.state // ""')
494
+ current_source=$(printf '%s' "$current_json" | jq -r '.source // ""')
495
+
496
+ # Durable keyed open-decision set: fold the WHOLE status stream
497
+ # (fm-classify-lib.sh's status_open_decisions) so a later unrelated event can
498
+ # never mask a still-open captain decision. The set is derived purely from the
499
+ # keyed fold - never from report bodies or decision-like prose - and then
500
+ # reconciled against the crew LIFECYCLE, which only clears a stale decision the
501
+ # crew has provably moved past. Two lifecycle signals clear it, neither of which
502
+ # reads any report content:
503
+ # - a live activity read (run-step or busy pane) that is working/done, so a
504
+ # crew that resumed past a gate is not still reported as parked; and
505
+ # - a TERMINAL done/failed state on a single-owner task (scout or ship), whose
506
+ # deliverable is its report or PR, so a COMPLETED scout surfaces only as a
507
+ # report POINTER, never as a reopened pending decision.
508
+ # Secondmates are excluded from lifecycle clearing: they are persistent and
509
+ # multiplex many concerns onto one stream, so activity on one concern must
510
+ # never clear another concern's keyed decision. A parked/blocked state, or a
511
+ # non-authoritative status-log/none read on a still-live task, keeps the fold's
512
+ # open decision surfacing.
513
+ open_decisions_tsv=$(status_open_decisions "$status_log")
514
+ if [ "$kind" != secondmate ] && \
515
+ { { { [ "$current_source" = run-step ] || [ "$current_source" = pane ]; } \
516
+ && [ "$current_state" != parked ] && [ "$current_state" != blocked ]; } \
517
+ || { [ "$current_state" = "done" ] || [ "$current_state" = "failed" ]; }; }; then
518
+ open_decisions_tsv=""
519
+ fi
520
+ open_decisions_json=$(printf '%s' "$open_decisions_tsv" | jq -R -s '
521
+ [ splits("\n") | select(length > 0)
522
+ | (capture("^(?<key>[^\t]*)\t(?<verb>[^\t]*)\t(?<summary>.*)$")?)
523
+ | select(. != null) ]')
524
+ pending_decision=$(printf '%s' "$open_decisions_json" | jq 'if any(.[]; .verb == "needs-decision") then 1 else 0 end')
525
+ blocked_event=$(printf '%s' "$open_decisions_json" | jq 'if any(.[]; .verb == "blocked") then 1 else 0 end')
526
+
527
+ endpoint_exists=null
528
+ agent_alive=not_checked
529
+ if [ -n "$remote_host" ]; then
530
+ if remote_state=$(fm_run_timed "$FM_SNAPSHOT_SECONDMATE_TIMEOUT" \
531
+ "$SCRIPT_DIR/fm-on.sh" "$id" fm-remote-secondmate-control.sh state "$id" < /dev/null 2>/dev/null); then
532
+ remote_rc=0
533
+ else
534
+ remote_rc=$?
535
+ fi
536
+ if [ "$remote_rc" -eq 0 ]; then
537
+ remote_home_present=true
538
+ remote_state=$(printf '%s\n' "$remote_state" | tail -1)
539
+ case "$remote_state" in
540
+ alive) endpoint_exists=true; agent_alive=alive ;;
541
+ dead) endpoint_exists=true; agent_alive=dead ;;
542
+ missing) endpoint_exists=false; agent_alive=dead ;;
543
+ *) endpoint_exists=null; agent_alive=unknown ;;
544
+ esac
545
+ else
546
+ endpoint_exists=null
547
+ agent_alive=unknown
548
+ fi
549
+ else
550
+ if [ -n "$target" ]; then
551
+ if fm_backend_target_exists "$backend" "$target" "fm-$id" >/dev/null 2>&1; then
552
+ endpoint_exists=true
553
+ else
554
+ endpoint_exists=false
555
+ fi
556
+ fi
557
+ if [ "$kind" = secondmate ] && [ -n "$target" ]; then
558
+ agent_alive=$(fm_backend_agent_alive "$backend" "$target" 2>/dev/null || printf unknown)
559
+ fi
560
+ fi
561
+
562
+ [ -f "$report_path" ] && report_present=1 || report_present=0
563
+ meta_json=$(path_present_json "$meta")
564
+ status_json=$event_json
565
+ report_json=$(path_present_json "$report_path")
566
+ if [ -n "$worktree" ]; then worktree_json=$(path_present_json "$worktree"); else worktree_json=$(jq -n '{path:null,present:false}'); fi
567
+ if [ -n "$home" ] && [ -n "$remote_host" ]; then
568
+ home_json=$(jq -n --arg path "$home" --argjson present "$remote_home_present" '{path:$path,present:$present}')
569
+ elif [ -n "$home" ]; then
570
+ home_json=$(path_present_json "$home")
571
+ else
572
+ home_json=$(jq -n '{path:null,present:false}')
573
+ fi
574
+
575
+ jq -n \
576
+ --arg id "$id" \
577
+ --arg kind "$kind" \
578
+ --arg harness "$harness" \
579
+ --arg mode "$mode" \
580
+ --arg yolo "$yolo" \
581
+ --arg project "$project" \
582
+ --arg worktree "$worktree" \
583
+ --arg home "$home" \
584
+ --arg projects "$projects" \
585
+ --arg spawn_gen "$spawn_gen" \
586
+ --arg backend "$backend" \
587
+ --arg target "$target" \
588
+ --arg remote_host "$remote_host" \
589
+ --arg remote_root "$remote_root" \
590
+ --arg pr "$pr" \
591
+ --arg pr_source "$pr_source" \
592
+ --arg agent_alive "$agent_alive" \
593
+ --arg observed_at "$SNAPSHOT_NOW" \
594
+ --arg last_event_raw "$last_event_raw" \
595
+ --argjson current_state "$current_json" \
596
+ --argjson meta_path "$meta_json" \
597
+ --argjson status_log "$status_json" \
598
+ --argjson report "$report_json" \
599
+ --argjson worktree_path "$worktree_json" \
600
+ --argjson home_path "$home_json" \
601
+ --argjson endpoint_exists "$endpoint_exists" \
602
+ --argjson open_decisions "$open_decisions_json" \
603
+ --argjson pending_decision "$(bool_json "$pending_decision")" \
604
+ --argjson blocked_event "$(bool_json "$blocked_event")" \
605
+ --argjson report_present "$(bool_json "$report_present")" \
606
+ '{
607
+ id:$id,
608
+ kind:$kind,
609
+ harness:($harness // ""),
610
+ mode:($mode // ""),
611
+ yolo:($yolo // ""),
612
+ project:($project // ""),
613
+ spawn_gen:($spawn_gen | if . == "" then null else . end),
614
+ backend:$backend,
615
+ remote:(if $remote_host == "" then null else {host:$remote_host,root:$remote_root} end),
616
+ paths:{
617
+ meta:$meta_path,
618
+ status_log:$status_log,
619
+ worktree:$worktree_path,
620
+ home:$home_path,
621
+ report:$report
622
+ },
623
+ secondmate_projects:($projects | if . == "" then [] else split(",") | map(gsub("^[[:space:]]+|[[:space:]]+$"; "")) | map(select(. != "")) end),
624
+ current_state:($current_state + {observed_at:$observed_at,freshness:"fresh"}),
625
+ endpoint:{target:($target | if . == "" then null else . end),exists:$endpoint_exists,agent_alive:$agent_alive,
626
+ status:(if $endpoint_exists == false then "absent"
627
+ elif $agent_alive == "alive" or $agent_alive == "dead" then $agent_alive
628
+ else "unknown" end),
629
+ observed_at:$observed_at,freshness:"fresh"},
630
+ pr:{url:($pr | if . == "" then null else . end),source:$pr_source},
631
+ hints:{
632
+ pending_decision:$pending_decision,
633
+ blocked_event:$blocked_event,
634
+ open_decisions:$open_decisions,
635
+ scout_report_present:$report_present,
636
+ last_event_text:$last_event_raw
637
+ },
638
+ actions:(
639
+ if $kind == "secondmate" then
640
+ {send:"bin/fm-send.sh fm-\($id) \u0027<request>\u0027",
641
+ watch:"read status/doc return channel; do not routinely fm-peek a secondmate for answers",
642
+ return_channel_note:"Secondmate answers come back through status/doc paths after a marked fm-send request."}
643
+ else
644
+ {watch:"bin/fm-peek.sh fm-\($id)",
645
+ steer:"bin/fm-send.sh fm-\($id) \u0027<instruction>\u0027",
646
+ return_channel_note:null}
647
+ end)
648
+ }'
649
+ done | jq -s 'sort_by(.id)'
650
+ }
651
+
652
+ # Main-home current-inventory validity: same orphan / unstructured-current checks
653
+ # used by secondmate_home_summary_json, without inventing live task rows.
654
+ # Meta inventory remains the sole source of live workers; this object only
655
+ # discloses backlog↔task inconsistency for renderers (Bearings omitted/gates).
656
+ main_inventory_json() { # <backlog-json> <tasks-json>
657
+ jq -n \
658
+ --argjson backlog "$1" \
659
+ --argjson tasks "$2" '
660
+ ([ $backlog.records[]?
661
+ | select((.state == "in_flight" or .state == "queued") and (.structured | not)) ]) as $unstructured_current
662
+ | ([ $backlog.records[]?
663
+ | select(.state == "in_flight" and .structured and .requires_child_metadata) ]) as $owned_in_flight
664
+ | ([ $owned_in_flight[]
665
+ | select(.id as $id | [$tasks[].id] | index($id) | not)
666
+ | .id ]) as $orphan_in_flight
667
+ | (($unstructured_current | length) == 0
668
+ and ($orphan_in_flight | length) == 0) as $valid
669
+ | (if ($unstructured_current | length) > 0 then "unstructured current backlog row"
670
+ elif ($orphan_in_flight | length) > 0 then "in-flight backlog item has no child metadata"
671
+ else null end) as $reason
672
+ | {
673
+ valid:$valid,
674
+ reason:$reason,
675
+ orphan_in_flight:$orphan_in_flight,
676
+ unstructured_current_count:($unstructured_current | length)
677
+ }'
678
+ }
679
+
680
+ # Project one home's canonical structured inventory into the bounded shape a
681
+ # validated parent read needs.
682
+ # This mode never reads parent events or terminal text and never aggregates
683
+ # nested secondmates.
684
+ secondmate_home_summary_json() { # <backlog-json> <tasks-json>
685
+ jq -n \
686
+ --arg generated "$SNAPSHOT_NOW" \
687
+ --argjson generated_epoch "$SNAPSHOT_EPOCH" \
688
+ --arg home "$FM_HOME" \
689
+ --argjson child_n "$FM_SNAPSHOT_SECONDMATE_CHILDREN" \
690
+ --argjson queued_n "$FM_SNAPSHOT_SECONDMATE_QUEUED" \
691
+ --argjson decisions_n "$FM_SNAPSHOT_SECONDMATE_DECISIONS" \
692
+ --argjson landed_n "$FM_SNAPSHOT_SECONDMATE_LANDED_PER_HOME" \
693
+ --argjson backlog "$1" \
694
+ --argjson tasks "$2" '
695
+ def trunc($n):
696
+ tostring | gsub("\\s+"; " ")
697
+ | if length > $n then .[:$n] + "…" else . end;
698
+ ([ $backlog.records[]?
699
+ | select((.state == "in_flight" or .state == "queued") and (.structured | not)) ]) as $unstructured_current
700
+ | ([ $backlog.records[]? | select(.state == "in_flight" and .structured) ]) as $owned_in_flight
701
+ | ([ $backlog.records[]?
702
+ | select(.structured and
703
+ (.state == "queued" or
704
+ (.state == "in_flight" and .current_role == "held"
705
+ and (.id as $id
706
+ | any($tasks[]; .id == $id and .current_state.state == "working") | not)))) ]) as $queued_all
707
+ | ([ $queued_all[]
708
+ | select(.captain_actionable == true)
709
+ | {id,key:.id,verb:"captain-hold",summary:(.title | trunc(160)),
710
+ reason:(.hold_reason | trunc(160)),
711
+ hold_until:(.hold_until // null),
712
+ deferred_marker:(.deferred_marker // false),source:"backlog"} ]) as $captain_holds_all
713
+ | ([ $backlog.records[]? | select(.state == "done" and .structured and .hold_kind != "captain")
714
+ | {id:(.id | trunc(120)),title:(.title | trunc(120)),
715
+ pr_url:((.pr_url // null) | if . == null then null else trunc(500) end),
716
+ report_path:((.report_path // null) | if . == null then null else trunc(500) end),
717
+ local_note:((.local_note // null) | if . == null then null else trunc(120) end),completion} ]
718
+ | sort_by([(.completion.date // ""), .id]) | reverse) as $landed_all
719
+ | ([ $tasks[] | select(.current_state.state == "unknown") ]) as $unknown_children
720
+ | ([ $owned_in_flight[]
721
+ | select(.requires_child_metadata)
722
+ | select(.id as $id | [$tasks[].id] | index($id) | not) ]) as $orphan_in_flight
723
+ | ([ $tasks[]
724
+ | select(.id as $id | [$owned_in_flight[].id] | index($id) | not)
725
+ | {id,state:.current_state.state} ]) as $unowned_children
726
+ | ([ $owned_in_flight[] as $work
727
+ | $tasks[]
728
+ | select(.id == $work.id and (.current_state.state == "done" or .current_state.state == "failed"))
729
+ | {id,state:.current_state.state} ]) as $terminal_in_flight
730
+ | ([if $backlog.present != true then
731
+ {kind:"missing_backlog",ids:[],reason:"missing structured backlog"}
732
+ else empty end,
733
+ if ($unstructured_current | length) > 0 then
734
+ {kind:"unstructured_current",ids:[],reason:"unstructured current backlog row"}
735
+ else empty end,
736
+ if ($orphan_in_flight | length) > 0 then
737
+ {kind:"orphan_in_flight",ids:($orphan_in_flight | map(.id)),
738
+ reason:("in-flight backlog item has no child metadata: " + ($orphan_in_flight | map(.id) | join(", ")))}
739
+ else empty end,
740
+ if ($unowned_children | length) > 0 then
741
+ {kind:"unowned_current",ids:($unowned_children | map(.id)),
742
+ reason:("live child state has no in-flight backlog item: " +
743
+ ($unowned_children | map(.id + "=" + .state) | join(", ")))}
744
+ else empty end,
745
+ if ($terminal_in_flight | length) > 0 then
746
+ {kind:"terminal_in_flight",ids:($terminal_in_flight | map(.id)),
747
+ reason:("in-flight backlog item has terminal child state: " +
748
+ ($terminal_in_flight | map(.id + "=" + .state) | join(", ")))}
749
+ else empty end]) as $strict_invalidities
750
+ | ([ $owned_in_flight[] as $work
751
+ | select($work.current_role != "program")
752
+ | $tasks[]
753
+ | select(.id == $work.id and .current_state.state == "working")
754
+ | {id,kind,state:.current_state.state,source:.current_state.source,
755
+ doing:((.current_state.detail // "") | trunc(120))} ]) as $active_all
756
+ | ($captain_holds_all
757
+ + ([ $tasks[] as $t | ($t.hints.open_decisions // [])[]
758
+ | {id:$t.id,key,verb,summary:(.summary | trunc(160)),reason:null,source:"status"} ])) as $decisions_all
759
+ | ([ $queued_all[]
760
+ | select((.unresolved_blocker_ids | length) > 0 or (.hold_reason != null and .hold_kind != null))
761
+ | {id:(.id | trunc(120)),title:(.title | trunc(90)),
762
+ blocked_by:((.unresolved_blocker_ids | join(",")) | if . == "" then null else trunc(120) end),
763
+ blocked_by_ids:(.blocked_by_ids | map(trunc(120))),
764
+ unresolved_blocker_ids:(.unresolved_blocker_ids | map(trunc(120))),
765
+ reason:((.hold_reason // .blocked_reason // "blocked") | trunc(120)),source:"backlog"} ]
766
+ + [ $owned_in_flight[] as $work
767
+ | $tasks[]
768
+ | select(.id == $work.id and (.current_state.state == "parked" or .current_state.state == "paused" or .current_state.state == "blocked"))
769
+ | select(($work.hold_reason != null and $work.hold_kind != null) | not)
770
+ | {id,title:((.backlog.title // .id) | trunc(90)),blocked_by:null,
771
+ blocked_by_ids:[],unresolved_blocker_ids:[],
772
+ reason:((.current_state.detail // .current_state.state) | trunc(120)),source:"child-state"} ]) as $holds_all
773
+ | ($backlog.present == true
774
+ and ($unstructured_current | length) == 0
775
+ and ($unknown_children | length) == 0
776
+ and ($orphan_in_flight | length) == 0
777
+ and ($unowned_children | length) == 0
778
+ and ($terminal_in_flight | length) == 0) as $valid
779
+ | (if ($strict_invalidities | length) > 0 then $strict_invalidities[0].reason
780
+ elif ($unknown_children | length) > 0 then
781
+ "child current state unavailable: " + ($unknown_children | map(.id) | join(", "))
782
+ else null end) as $reason
783
+ | (if ($strict_invalidities | length) > 0 then $strict_invalidities[0] | del(.reason)
784
+ elif ($unknown_children | length) > 0 then {kind:"child_current_unavailable",ids:($unknown_children | map(.id))}
785
+ else {kind:null,ids:[]} end) as $invalidity
786
+ | (if ($valid | not)
787
+ and (($unknown_children | length) > 0
788
+ or (["orphan_in_flight","unowned_current","terminal_in_flight"]
789
+ | index($invalidity.kind) | not))
790
+ then "unknown"
791
+ elif any($decisions_all[]; .verb == "needs-decision" or .verb == "captain-hold") then "captain_decision"
792
+ elif ($active_all | length) > 0 then "active_child_work"
793
+ elif ($holds_all | length) > 0 then "externally_held"
794
+ else "no_active_work" end) as $state
795
+ | {
796
+ schema:"fm-secondmate-home-summary.v1",
797
+ generated:$generated,
798
+ generated_epoch:$generated_epoch,
799
+ home:$home,
800
+ valid:$valid,
801
+ reason:$reason,
802
+ invalidity:$invalidity,
803
+ state:$state,
804
+ active_children:$active_all[:$child_n],
805
+ decisions_open:$decisions_all[:$decisions_n],
806
+ holds:$holds_all[:$queued_n],
807
+ queued:([$queued_all[] | {id:(.id | trunc(120)),title:(.title | trunc(120)),
808
+ blocked_by:((.blocked_by // null) | if . == null then null else trunc(120) end),
809
+ blocked_by_ids:((.blocked_by_ids // []) | map(trunc(120))),
810
+ unresolved_blocker_ids:((.unresolved_blocker_ids // []) | map(trunc(120))),
811
+ blocked_reason:((.blocked_reason // null) | if . == null then null else trunc(160) end),
812
+ hold_reason:((.hold_reason // null) | if . == null then null else trunc(160) end),
813
+ hold_kind:((.hold_kind // null) | if . == null then null else trunc(40) end),
814
+ hold_until:((.hold_until // null) | if . == null then null else trunc(40) end),
815
+ deferred_marker:(.deferred_marker // false),
816
+ captain_actionable:(.captain_actionable // false),
817
+ repo:((.repo // null) | if . == null then null else trunc(120) end),
818
+ kind:((.kind // null) | if . == null then null else trunc(40) end)}][:$queued_n]),
819
+ landed:(if $landed_n == 0 then $landed_all else $landed_all[:$landed_n] end),
820
+ endpoints:([$tasks[] | {id,state:.current_state.state,source:.current_state.source,
821
+ endpoint:(.endpoint + {target:((.endpoint.target // null) | if . == null then null else trunc(240) end)})}][:$child_n]),
822
+ counts:{
823
+ active_children:($active_all | length),
824
+ decisions_open:($decisions_all | length),
825
+ holds:($holds_all | length),
826
+ queued:($queued_all | length),
827
+ landed:($landed_all | length),
828
+ endpoints:($tasks | length)
829
+ },
830
+ omitted:[
831
+ (if ($active_all | length) > $child_n then {surface:"active_children",count:(($active_all | length) - $child_n)} else empty end),
832
+ (if ($decisions_all | length) > $decisions_n then {surface:"decisions_open",count:(($decisions_all | length) - $decisions_n)} else empty end),
833
+ (if ($queued_all | length) > $queued_n then {surface:"queued",count:(($queued_all | length) - $queued_n)} else empty end),
834
+ (if ($tasks | length) > $child_n then {surface:"endpoints",count:(($tasks | length) - $child_n)} else empty end),
835
+ (if $landed_n > 0 and ($landed_all | length) > $landed_n then {surface:"landed",count:(($landed_all | length) - $landed_n)} else empty end)
836
+ ]
837
+ }'
838
+ }
839
+
840
+ # Current registered-secondmate aggregation.
841
+ # The validated home summary is canonical.
842
+ # Parent status and bounded terminal capture remain untrusted supplemental evidence
843
+ # with explicit provenance, and can only produce a contradiction or unknown fallback.
844
+ FM_SNAPSHOT_SECONDMATE_LANDED_PER_HOME=${FM_SNAPSHOT_SECONDMATE_LANDED_PER_HOME:-10}
845
+ case "$FM_SNAPSHOT_SECONDMATE_LANDED_PER_HOME" in ''|*[!0-9]*) FM_SNAPSHOT_SECONDMATE_LANDED_PER_HOME=10 ;; esac
846
+
847
+ # GNU stat treats -f as a filesystem-report command, so a BSD-first fallback can
848
+ # pollute arithmetic input before failing. Select the platform syntax once.
849
+ if [ "$(uname 2>/dev/null || true)" = Darwin ]; then
850
+ SNAPSHOT_STAT_STYLE=bsd
851
+ file_mtime_epoch() { stat -f '%m' "$1" 2>/dev/null || true; }
852
+ file_mode_octal() { stat -f '%Lp' "$1" 2>/dev/null || true; }
853
+ else
854
+ SNAPSHOT_STAT_STYLE=gnu
855
+ file_mtime_epoch() { stat -c '%Y' "$1" 2>/dev/null || true; }
856
+ file_mode_octal() { stat -c '%a' "$1" 2>/dev/null || true; }
857
+ fi
858
+
859
+ registry_secondmates_json() {
860
+ local reg="$DATA/secondmates.md" out rc reason mode script parse_filter output_filter
861
+ if [ ! -f "$reg" ]; then
862
+ jq -n --arg path "$reg" --arg observed "$SNAPSHOT_NOW" \
863
+ '{present:false,available:true,complete:true,reason:null,provenance:"registered-table",path:$path,freshness:{status:"fresh",observed_at:$observed},records:[],input_truncated:false,records_truncated:false,reasons:[],lines_in_window:0,records_in_window:0}'
864
+ return 0
865
+ fi
866
+ mode=$(file_mode_octal "$reg")
867
+ if [ -z "$mode" ] || [ $((8#$mode & 0444)) -eq 0 ]; then
868
+ jq -n --arg path "$reg" --arg observed "$SNAPSHOT_NOW" \
869
+ --arg reason "registered secondmate table is unreadable" \
870
+ '{present:true,available:false,complete:false,reason:$reason,provenance:"registered-table",path:$path,freshness:{status:"unavailable",observed_at:$observed},records:[],input_truncated:false,records_truncated:false,reasons:[$reason],lines_in_window:0,records_in_window:0}'
871
+ return 0
872
+ fi
873
+ script=$(cat <<'BASH'
874
+ f=$1
875
+ max_lines=$2
876
+ max_bytes=$3
877
+ max_records=$4
878
+ path=$5
879
+ observed=$6
880
+ parse_filter=$7
881
+ output_filter=$8
882
+ content=$(LC_ALL=C head -c "$((max_bytes + 1))" "$f" || exit 3; printf "\036") || exit 3
883
+ content=${content%$'\036'}
884
+ bytes=$(printf "%s" "$content" | LC_ALL=C wc -c | tr -d " ")
885
+ byte_truncated=false
886
+ if [ "$bytes" -gt "$max_bytes" ]; then
887
+ byte_truncated=true
888
+ content=$(printf "%s" "$content" | LC_ALL=C head -c "$max_bytes")
889
+ complete=${content%$'\n'*}
890
+ if [ "$complete" != "$content" ]; then
891
+ content=$complete
892
+ else
893
+ content=
894
+ fi
895
+ fi
896
+ if [ -n "$content" ]; then
897
+ lines=$(printf "%s\n" "$content" | awk "END {print NR}")
898
+ else
899
+ lines=0
900
+ fi
901
+ line_truncated=false
902
+ if [ "$lines" -gt "$max_lines" ]; then line_truncated=true; fi
903
+ window=$(printf "%s\n" "$content" | LC_ALL=C head -n "$max_lines") || exit 3
904
+ if [ -n "$window" ]; then
905
+ lines_in_window=$(printf "%s\n" "$window" | awk "END {print NR}")
906
+ else
907
+ lines_in_window=0
908
+ fi
909
+ records=$(printf "%s\n" "$window" | jq -Rn "$parse_filter") || exit 3
910
+ records_in_window=$(printf "%s" "$records" | jq "length") || exit 3
911
+ records_truncated=false
912
+ if [ "$records_in_window" -gt "$max_records" ]; then records_truncated=true; fi
913
+ printf "%s" "$records" | jq \
914
+ --arg path "$path" --arg observed "$observed" \
915
+ --argjson byte_truncated "$byte_truncated" \
916
+ --argjson line_truncated "$line_truncated" \
917
+ --argjson records_truncated "$records_truncated" \
918
+ --argjson lines_in_window "$lines_in_window" \
919
+ --argjson records_in_window "$records_in_window" \
920
+ --argjson max_records "$max_records" "$output_filter"
921
+ BASH
922
+ )
923
+ parse_filter=$(cat <<'JQ'
924
+ [ inputs
925
+ | select(startswith("- "))
926
+ | (capture("^- (?<id>[^[:space:]]+)")?) as $id
927
+ | select($id != null)
928
+ | ([capture("^.*\\(host:[[:space:]]*(?<host>[^;)]*);[[:space:]]*root:[[:space:]]*(?<root>[^;)]*);[[:space:]]*home:[[:space:]]*(?<home>[^;)]*);[[:space:]]*scope:[[:space:]]*.*;[[:space:]]*projects:[[:space:]]*[^;)]*;[[:space:]]*added[[:space:]]+[0-9]{4}-[0-9]{2}-[0-9]{2}\\)[[:space:]]*$")?][0] // null) as $remote
929
+ | ([capture("^.*\\(home:[[:space:]]*(?<home>[^;)]*);[[:space:]]*scope:[[:space:]]*.*;[[:space:]]*projects:[[:space:]]*[^;)]*;[[:space:]]*added[[:space:]]+[0-9]{4}-[0-9]{2}-[0-9]{2}\\)[[:space:]]*$")?][0] // null) as $local
930
+ | ($local // $remote) as $route
931
+ | (($local == null) and ($remote != null)) as $is_remote
932
+ | {id:$id.id,home:($route.home // null),host:(if $is_remote then $remote.host else null end),root:(if $is_remote then $remote.root else null end),
933
+ remote:$is_remote,registered:true,
934
+ registry_error:(if $route == null or ($route.home | length) == 0 then "registry entry has no home" else null end)} ]
935
+ | group_by(.id)
936
+ | map(if length > 1 then .[0] + {registry_error:"duplicate secondmate id in registry"} else .[0] end)
937
+ JQ
938
+ )
939
+ output_filter=$(cat <<'JQ'
940
+ {present:true,available:true,reason:null,provenance:"registered-table",path:$path,
941
+ freshness:{status:"fresh",observed_at:$observed},
942
+ records:(if length > $max_records then .[:$max_records] else . end),
943
+ input_truncated:($byte_truncated or $line_truncated),records_truncated:$records_truncated,
944
+ complete:(($byte_truncated or $line_truncated or $records_truncated) | not),
945
+ reasons:[
946
+ (if $byte_truncated then "byte_limit" else empty end),
947
+ (if $line_truncated then "line_limit" else empty end),
948
+ (if $records_truncated then "record_limit" else empty end)
949
+ ],lines_in_window:$lines_in_window,records_in_window:$records_in_window}
950
+ JQ
951
+ )
952
+ out=$(fm_run_timed "$FM_SNAPSHOT_REGISTRY_TIMEOUT" bash -c "$script" \
953
+ fm-secondmate-registry "$reg" "$FM_SNAPSHOT_REGISTRY_LINES" \
954
+ "$FM_SNAPSHOT_REGISTRY_BYTES" "$FM_SNAPSHOT_REGISTRY_RECORDS" "$reg" "$SNAPSHOT_NOW" \
955
+ "$parse_filter" "$output_filter" 2>/dev/null)
956
+ rc=$?
957
+ if [ "$rc" -eq 0 ] && printf '%s' "$out" | jq -e '
958
+ .available == true and (.records | type) == "array"
959
+ ' >/dev/null 2>&1; then
960
+ printf '%s' "$out"
961
+ return 0
962
+ fi
963
+ [ "$rc" -eq 124 ] && reason="registered secondmate table read timed out" \
964
+ || reason="registered secondmate table is unreadable"
965
+ jq -n --arg path "$reg" --arg observed "$SNAPSHOT_NOW" --arg reason "$reason" \
966
+ '{present:true,available:false,complete:false,reason:$reason,provenance:"registered-table",path:$path,freshness:{status:"unavailable",observed_at:$observed},records:[],input_truncated:false,records_truncated:false,reasons:[$reason],lines_in_window:0,records_in_window:0}'
967
+ }
968
+
969
+ bounded_parent_activities_json() { # <status-file>
970
+ local f=$1 out rc reason script
971
+ if [ ! -f "$f" ]; then
972
+ jq -n '{records:[],available:true,input_truncated:false,retained_truncated:false,reasons:[],lines_in_window:0,records_in_window:0}'
973
+ return 0
974
+ fi
975
+ script=$(cat <<'BASH'
976
+ classify=$1
977
+ f=$2
978
+ max_lines=$3
979
+ max_bytes=$4
980
+ max_records=$5
981
+ stat_style=$6
982
+ . "$classify"
983
+ if [ "$stat_style" = bsd ]; then
984
+ size=$(stat -f "%z" "$f" 2>/dev/null) || exit 3
985
+ else
986
+ size=$(stat -c "%s" "$f" 2>/dev/null) || exit 3
987
+ fi
988
+ content=$(LC_ALL=C tail -c "$max_bytes" "$f") || exit 3
989
+ byte_truncated=false
990
+ if [ "$size" -gt "$max_bytes" ]; then
991
+ byte_truncated=true
992
+ complete=${content#*$'\n'}
993
+ if [ "$complete" != "$content" ]; then
994
+ content=$complete
995
+ else
996
+ content=
997
+ fi
998
+ fi
999
+ if [ -n "$content" ]; then
1000
+ lines_in_chunk=$(printf "%s\n" "$content" | awk "END {print NR}")
1001
+ else
1002
+ lines_in_chunk=0
1003
+ fi
1004
+ line_truncated=false
1005
+ if [ "$lines_in_chunk" -gt "$max_lines" ]; then line_truncated=true; fi
1006
+ window=$(printf "%s\n" "$content" | LC_ALL=C tail -n "$max_lines") || exit 3
1007
+ if [ -n "$window" ]; then
1008
+ lines_in_window=$(printf "%s\n" "$window" | awk "END {print NR}")
1009
+ else
1010
+ lines_in_window=0
1011
+ fi
1012
+ records=$(printf "%s\n" "$window" | status_open_activities - \
1013
+ | jq -R -s '[splits("\n") | select(length > 0)
1014
+ | (capture("^(?<key>[^\t]*)\t(?<verb>[^\t]*)\t(?<summary>.*)$")?)
1015
+ | select(. != null)]') || exit 3
1016
+ records_in_window=$(printf "%s" "$records" | jq "length") || exit 3
1017
+ retained_truncated=false
1018
+ if [ "$records_in_window" -gt "$max_records" ]; then retained_truncated=true; fi
1019
+ printf "%s" "$records" | jq \
1020
+ --argjson byte_truncated "$byte_truncated" \
1021
+ --argjson line_truncated "$line_truncated" \
1022
+ --argjson retained_truncated "$retained_truncated" \
1023
+ --argjson lines_in_window "$lines_in_window" \
1024
+ --argjson records_in_window "$records_in_window" \
1025
+ --argjson max_records "$max_records" '
1026
+ {records:(if length > $max_records then .[-$max_records:] else . end),
1027
+ available:true,
1028
+ input_truncated:($byte_truncated or $line_truncated),
1029
+ retained_truncated:$retained_truncated,
1030
+ reasons:[
1031
+ (if $byte_truncated then "byte_limit" else empty end),
1032
+ (if $line_truncated then "line_limit" else empty end),
1033
+ (if $retained_truncated then "activity_limit" else empty end)
1034
+ ],
1035
+ lines_in_window:$lines_in_window,
1036
+ records_in_window:$records_in_window}'
1037
+ BASH
1038
+ )
1039
+ out=$(fm_run_timed "$FM_SNAPSHOT_PARENT_ACTIVITY_TIMEOUT" bash -c "$script" \
1040
+ fm-parent-activities "$SCRIPT_DIR/fm-classify-lib.sh" "$f" \
1041
+ "$FM_SNAPSHOT_PARENT_ACTIVITY_LINES" "$FM_SNAPSHOT_PARENT_ACTIVITY_BYTES" \
1042
+ "$FM_SNAPSHOT_PARENT_ACTIVITIES" "$SNAPSHOT_STAT_STYLE" 2>/dev/null)
1043
+ rc=$?
1044
+ if [ "$rc" -eq 0 ] && printf '%s' "$out" | jq -e '
1045
+ (.records | type) == "array" and (.available | type) == "boolean"
1046
+ ' >/dev/null 2>&1; then
1047
+ printf '%s' "$out"
1048
+ return 0
1049
+ fi
1050
+ [ "$rc" -eq 124 ] && reason="timeout" || reason="read_failed"
1051
+ jq -n --arg reason "$reason" \
1052
+ '{records:[],available:false,input_truncated:false,retained_truncated:false,reasons:[$reason],lines_in_window:0,records_in_window:0}'
1053
+ }
1054
+
1055
+ terminal_evidence_json() { # <parent-task-json> <event-note> <evidence-contradicts>
1056
+ local task=$1 note=$2 evidence_contradicts=$3 backend target exists expected out rc clean bytes lines seen=false contradiction=false reason='' remote_host
1057
+ backend=$(printf '%s' "$task" | jq -r '.backend // ""')
1058
+ target=$(printf '%s' "$task" | jq -r '.endpoint.target // ""')
1059
+ exists=$(printf '%s' "$task" | jq -r '.endpoint.exists // "unknown"')
1060
+ remote_host=$(printf '%s' "$task" | jq -r '.remote.host // ""')
1061
+ if [ -n "$remote_host" ]; then
1062
+ jq -n --arg observed "$SNAPSHOT_NOW" --arg reason "remote terminal evidence is not collected by the primary" \
1063
+ '{provenance:"remote-direct-report-terminal",trust:"untrusted-supplement",captured:false,observed_at:$observed,freshness:"not-collected",reason:$reason,lines:0,bytes:0,event_note_seen:false,contradiction:false}'
1064
+ return 0
1065
+ fi
1066
+ expected=$(printf '%s' "$task" | jq -r '"fm-" + (.id // "")')
1067
+ if [ -z "$target" ] || [ "$exists" = false ]; then
1068
+ [ "$exists" = false ] && reason="recorded endpoint is absent" || reason="no recorded endpoint"
1069
+ jq -n --arg observed "$SNAPSHOT_NOW" --arg reason "$reason" \
1070
+ '{provenance:"parent-direct-report-terminal",trust:"untrusted-supplement",captured:false,observed_at:$observed,freshness:"unknown",reason:$reason,lines:0,bytes:0,event_note_seen:false,contradiction:false}'
1071
+ return 0
1072
+ fi
1073
+ # shellcheck disable=SC2016 # Positional parameters expand inside the child bash, not here.
1074
+ out=$(fm_run_timed "$FM_SNAPSHOT_TERMINAL_TIMEOUT" bash -c \
1075
+ '. "$1"; fm_backend_capture "$2" "$3" "$4" "$5" | LC_ALL=C head -c "$6"; rc=${PIPESTATUS[0]}; [ "$rc" -eq 141 ] && rc=0; exit "$rc"' \
1076
+ fm-terminal-capture "$SCRIPT_DIR/fm-backend.sh" "$backend" "$target" "$FM_SNAPSHOT_TERMINAL_LINES" "$expected" "$FM_SNAPSHOT_TERMINAL_BYTES" 2>/dev/null)
1077
+ rc=$?
1078
+ if [ "$rc" -ne 0 ]; then
1079
+ [ "$rc" -eq 124 ] && reason="terminal capture timed out" || reason="terminal capture unavailable"
1080
+ jq -n --arg observed "$SNAPSHOT_NOW" --arg reason "$reason" \
1081
+ '{provenance:"parent-direct-report-terminal",trust:"untrusted-supplement",captured:false,observed_at:$observed,freshness:"unknown",reason:$reason,lines:0,bytes:0,event_note_seen:false,contradiction:false}'
1082
+ return 0
1083
+ fi
1084
+ clean=$(printf '%s' "$out" | tail -n "$FM_SNAPSHOT_TERMINAL_LINES" | LC_ALL=C head -c "$FM_SNAPSHOT_TERMINAL_BYTES")
1085
+ if command -v perl >/dev/null 2>&1; then
1086
+ clean=$(printf '%s' "$clean" | perl -pe 's/\e\[[0-?]*[ -\/]*[@-~]//g; s/[^\x09\x0A\x0D\x20-\x7E]//g')
1087
+ else
1088
+ clean=$(printf '%s' "$clean" | LC_ALL=C tr -cd '\11\12\15\40-\176')
1089
+ fi
1090
+ bytes=$(printf '%s' "$clean" | LC_ALL=C wc -c | tr -d ' ')
1091
+ if [ -n "$clean" ]; then
1092
+ lines=$(printf '%s\n' "$clean" | wc -l | tr -d ' ')
1093
+ else
1094
+ lines=0
1095
+ fi
1096
+ if [ -n "$note" ]; then
1097
+ case "$clean" in *"$note"*) seen=true ;; esac
1098
+ fi
1099
+ if [ "$seen" = true ] && [ "$evidence_contradicts" = true ]; then contradiction=true; fi
1100
+ jq -n \
1101
+ --arg observed "$SNAPSHOT_NOW" \
1102
+ --argjson lines "$lines" \
1103
+ --argjson bytes "$bytes" \
1104
+ --argjson seen "$seen" \
1105
+ --argjson contradiction "$contradiction" \
1106
+ '{provenance:"parent-direct-report-terminal",trust:"untrusted-supplement",captured:true,observed_at:$observed,freshness:"fresh",reason:null,lines:$lines,bytes:$bytes,event_note_seen:$seen,contradiction:$contradiction}'
1107
+ }
1108
+
1109
+ parent_evidence_reconciliation_json() { # <summary-json> <activities-json> <decisions-json>
1110
+ jq -n --argjson summary "$1" --argjson activities "$2" --argjson decisions "$3" '
1111
+ def keyed: . != null and . != "" and . != "default";
1112
+ def result($e; $matches; $complete; $surface):
1113
+ $e + {
1114
+ verdict:(if ($e.key | keyed | not) then "inconclusive"
1115
+ elif ($matches | length) > 0 then "corroborates"
1116
+ elif $complete then "contradicts"
1117
+ else "inconclusive" end),
1118
+ compared_to:$surface,
1119
+ matched:(if ($e.key | keyed) then ($matches[0] // null) else null end)
1120
+ };
1121
+ ([ $activities[] as $e
1122
+ | if $e.verb == "working" then
1123
+ ([ $summary.active_children[]
1124
+ | select(if ($e.key | keyed) then .id == $e.key else true end)
1125
+ | {surface:"active_children",id,key:null,verb:"working"}]) as $matches
1126
+ | result($e; $matches;
1127
+ $summary.counts.active_children == ($summary.active_children | length);
1128
+ "active_children")
1129
+ elif $e.verb == "paused" then
1130
+ ([ $summary.holds[]
1131
+ | select(if ($e.key | keyed) then .id == $e.key or .blocked_by == $e.key else true end)
1132
+ | {surface:"holds",id,key:(.blocked_by // null),verb:"paused"}]) as $matches
1133
+ | result($e; $matches;
1134
+ $summary.counts.holds == ($summary.holds | length);
1135
+ "holds")
1136
+ else
1137
+ $e + {verdict:"inconclusive",compared_to:null,matched:null}
1138
+ end ]) as $activity_results
1139
+ | ([ $decisions[] as $e
1140
+ | if $e.verb == "needs-decision" then
1141
+ ([ $summary.decisions_open[]
1142
+ | select(.verb == "needs-decision")
1143
+ | select(if ($e.key | keyed) then .key == $e.key else true end)
1144
+ | {surface:"decisions_open",id,key,verb}]) as $matches
1145
+ | result($e; $matches;
1146
+ $summary.counts.decisions_open == ($summary.decisions_open | length);
1147
+ "decisions_open")
1148
+ elif $e.verb == "blocked" then
1149
+ ([ $summary.decisions_open[]
1150
+ | select(.verb == "blocked")
1151
+ | select(if ($e.key | keyed) then .key == $e.key or .id == $e.key else true end)
1152
+ | {surface:"decisions_open",id,key,verb}]
1153
+ + [ $summary.holds[]
1154
+ | select(if ($e.key | keyed) then .id == $e.key or .blocked_by == $e.key else true end)
1155
+ | {surface:"holds",id,key:(.blocked_by // null),verb:"blocked"}]) as $matches
1156
+ | result($e; $matches;
1157
+ ($summary.counts.decisions_open == ($summary.decisions_open | length)
1158
+ and $summary.counts.holds == ($summary.holds | length));
1159
+ "decisions_open_or_holds")
1160
+ else
1161
+ $e + {verdict:"inconclusive",compared_to:null,matched:null}
1162
+ end ]) as $decision_results
1163
+ | {provenance:"parent-status-keyed-fold",trust:"untrusted-supplement",
1164
+ activities:$activity_results,decisions:$decision_results,
1165
+ contradiction:any(($activity_results + $decision_results)[]; .verdict == "contradicts"),
1166
+ inconclusive:any(($activity_results + $decision_results)[]; .verdict == "inconclusive")}'
1167
+ }
1168
+
1169
+ secondmate_current_json() { # <parent-tasks-json>
1170
+ local tasks=$1 registry union rows total_registered total shown truncated
1171
+ local row id home host remote registered registry_error task sampled_spawn_gen status_file event_raw event_note event_epoch event_age
1172
+ local activity_scan activities decisions reconciliation provenance freshness reason summary summary_rc summary_bytes summary_sampled summary_valid summary_reason summary_invalidity state current_reason terminal terminal_contradiction contradiction
1173
+ local records='[]' seen_homes=''
1174
+ registry=$(registry_secondmates_json) || return 1
1175
+ union=$(jq -n --argjson registry "$registry" --argjson tasks "$tasks" '
1176
+ ($registry.records // []) as $registered
1177
+ | (($registered | map(.id)) // []) as $registered_ids
1178
+ | ([ $registered[] as $r
1179
+ | $r + {parent_task:([$tasks[] | select(.id == $r.id)][0] // null)} ]
1180
+ + [ $tasks[] | select(.kind == "secondmate") as $t
1181
+ | select(($registered_ids | index($t.id)) == null)
1182
+ | {id:$t.id,home:($t.paths.home.path // null),
1183
+ registered:(if $registry.complete == true then false else null end),
1184
+ registry_error:(if $registry.complete == true
1185
+ then "secondmate metadata is not registered"
1186
+ else "secondmate registration is unknown because the registry read is incomplete or unavailable" end),
1187
+ parent_task:$t} ])
1188
+ | sort_by(.id)
1189
+ | {registry:$registry,records:.}') || return 1
1190
+ total_registered=$(printf '%s' "$union" | jq '[.records[] | select(.registered)] | length')
1191
+ total=$(printf '%s' "$union" | jq '.records | length')
1192
+ rows=$(printf '%s' "$union" | jq -c --argjson cap "$FM_SNAPSHOT_SECONDMATES" '(if $cap == 0 then .records else .records[:$cap] end)[]')
1193
+ shown=$(printf '%s\n' "$rows" | grep -c . || true)
1194
+ truncated=$((total - shown))
1195
+
1196
+ while IFS= read -r row; do
1197
+ [ -n "$row" ] || continue
1198
+ id=$(printf '%s' "$row" | jq -r '.id')
1199
+ home=$(printf '%s' "$row" | jq -r '.home // ""')
1200
+ host=$(printf '%s' "$row" | jq -r '.host // ""')
1201
+ remote=$(printf '%s' "$row" | jq -r '.remote // false')
1202
+ registered=$(printf '%s' "$row" | jq -r '.registered')
1203
+ registry_error=$(printf '%s' "$row" | jq -r '.registry_error // ""')
1204
+ task=$(printf '%s' "$row" | jq -c '.parent_task // {}')
1205
+ sampled_spawn_gen=$(printf '%s' "$task" | jq -r '.spawn_gen // ""')
1206
+ status_file=$(printf '%s' "$task" | jq -r '.paths.status_log.path // ""')
1207
+ event_raw=$(printf '%s' "$task" | jq -r '.paths.status_log.last_event.raw // ""')
1208
+ event_note=$(printf '%s' "$task" | jq -r '.paths.status_log.last_event.note // ""')
1209
+ activity_scan=$(bounded_parent_activities_json "$status_file")
1210
+ activities=$(printf '%s' "$activity_scan" | jq -c '.records')
1211
+ decisions=$(printf '%s' "$task" | jq -c '.hints.open_decisions // []')
1212
+ event_epoch=$(file_mtime_epoch "$status_file")
1213
+ event_age=null
1214
+ if [ -n "$event_epoch" ]; then
1215
+ event_age=$((SNAPSHOT_EPOCH - event_epoch))
1216
+ [ "$event_age" -lt 0 ] && event_age=0
1217
+ fi
1218
+
1219
+ reason=$registry_error
1220
+ summary='{}'
1221
+ summary_sampled=false
1222
+ summary_valid=false
1223
+ if [ -z "$reason" ] && [ -z "$home" ]; then reason="no recorded secondmate home"; fi
1224
+ if [ -z "$reason" ]; then
1225
+ case "$home" in
1226
+ /*) : ;;
1227
+ *) reason="invalid home: registered path is not absolute" ;;
1228
+ esac
1229
+ fi
1230
+ if [ -z "$reason" ]; then
1231
+ if [ "$remote" = true ]; then
1232
+ [ -n "$host" ] || reason="invalid remote route: missing SSH host"
1233
+ case " $seen_homes " in
1234
+ *" $host:$home "*) reason="invalid home: duplicate resolved remote route" ;;
1235
+ *) seen_homes="$seen_homes $host:$home" ;;
1236
+ esac
1237
+ elif ! validate_secondmate_home "$id" "$home" 2>/dev/null; then
1238
+ reason="invalid home: $VALIDATION_ERROR"
1239
+ else
1240
+ home=$VALIDATED_HOME
1241
+ case " $seen_homes " in
1242
+ *" local:$home "*) reason="invalid home: duplicate resolved home route" ;;
1243
+ *) seen_homes="$seen_homes local:$home" ;;
1244
+ esac
1245
+ fi
1246
+ fi
1247
+ if [ -z "$reason" ]; then
1248
+ if [ "$remote" = true ]; then
1249
+ summary=$(fm_run_timed "$FM_SNAPSHOT_SECONDMATE_TIMEOUT" \
1250
+ "$SCRIPT_DIR/fm-on.sh" "$id" fm-fleet-snapshot.sh --secondmate-home-summary < /dev/null 2>/dev/null)
1251
+ summary_rc=$?
1252
+ else
1253
+ summary=$(fm_run_timed "$FM_SNAPSHOT_SECONDMATE_TIMEOUT" env \
1254
+ FM_ROOT_OVERRIDE="$FM_ROOT" \
1255
+ FM_HOME="$home" \
1256
+ FM_STATE_OVERRIDE="$home/state" \
1257
+ FM_DATA_OVERRIDE="$home/data" \
1258
+ FM_CONFIG_OVERRIDE="$home/config" \
1259
+ FM_PROJECTS_OVERRIDE="$home/projects" \
1260
+ FM_SNAPSHOT_NOW="$SNAPSHOT_NOW" \
1261
+ FM_SNAPSHOT_NOW_EPOCH="$SNAPSHOT_EPOCH" \
1262
+ FM_SNAPSHOT_SECONDMATE_CHILDREN="$FM_SNAPSHOT_SECONDMATE_CHILDREN" \
1263
+ FM_SNAPSHOT_SECONDMATE_QUEUED="$FM_SNAPSHOT_SECONDMATE_QUEUED" \
1264
+ FM_SNAPSHOT_SECONDMATE_DECISIONS="$FM_SNAPSHOT_SECONDMATE_DECISIONS" \
1265
+ FM_SNAPSHOT_SECONDMATE_LANDED_PER_HOME="$FM_SNAPSHOT_SECONDMATE_LANDED_PER_HOME" \
1266
+ "$SCRIPT_DIR/fm-fleet-snapshot.sh" --secondmate-home-summary 2>/dev/null)
1267
+ summary_rc=$?
1268
+ fi
1269
+ if [ "$summary_rc" -ne 0 ]; then
1270
+ summary='{}'
1271
+ [ "$summary_rc" -eq 124 ] && reason="structured home snapshot timed out" || reason="structured home snapshot failed"
1272
+ else
1273
+ summary_bytes=$(printf '%s' "$summary" | LC_ALL=C wc -c | tr -d ' ')
1274
+ if [ "$summary_bytes" -gt "$FM_SNAPSHOT_SECONDMATE_MAX_BYTES" ]; then
1275
+ reason="structured home snapshot exceeded byte limit"
1276
+ elif ! printf '%s' "$summary" | jq -e --arg home "$home" --arg generated "$SNAPSHOT_NOW" --argjson remote "$remote" '
1277
+ .schema == "fm-secondmate-home-summary.v1" and .home == $home
1278
+ and (($remote == true) or .generated == $generated)
1279
+ and (.valid | type) == "boolean" and (.state | type) == "string"
1280
+ and (.invalidity | type) == "object" and (.invalidity.ids | type) == "array"
1281
+ and (.active_children | type) == "array" and (.decisions_open | type) == "array"
1282
+ and (.holds | type) == "array" and (.queued | type) == "array"
1283
+ and (.landed | type) == "array" and (.endpoints | type) == "array"
1284
+ and (.counts | type) == "object" and (.omitted | type) == "array"
1285
+ ' >/dev/null 2>&1; then
1286
+ reason="structured home snapshot was malformed or stale"
1287
+ else
1288
+ summary_sampled=true
1289
+ summary_valid=$(printf '%s' "$summary" | jq -r '.valid')
1290
+ if [ "$summary_valid" != true ]; then
1291
+ summary_reason=$(printf '%s' "$summary" | jq -r '.reason // "unknown reason"')
1292
+ summary_invalidity=$(printf '%s' "$summary" | jq -r '.invalidity.kind // "unknown"')
1293
+ case "$summary_invalidity" in
1294
+ child_current_unavailable|orphan_in_flight|unowned_current|terminal_in_flight) : ;;
1295
+ *) reason="structured home state invalid: $summary_reason" ;;
1296
+ esac
1297
+ fi
1298
+ fi
1299
+ fi
1300
+ fi
1301
+
1302
+ if [ -z "$reason" ]; then
1303
+ state=$(printf '%s' "$summary" | jq -r '.state')
1304
+ current_reason=
1305
+ if [ "$summary_valid" != true ]; then
1306
+ current_reason="structured home state invalid: $(printf '%s' "$summary" | jq -r '.reason // "unknown reason"')"
1307
+ fi
1308
+ reconciliation=$(parent_evidence_reconciliation_json "$summary" "$activities" "$decisions")
1309
+ contradiction=$(printf '%s' "$reconciliation" | jq -r '.contradiction')
1310
+ terminal_contradiction=$(printf '%s' "$reconciliation" | jq -r --arg note "$event_note" '
1311
+ any(.activities[]; .verdict == "contradicts" and .summary == $note)')
1312
+ if [ "$terminal_contradiction" = true ]; then
1313
+ terminal=$(terminal_evidence_json "$task" "$event_note" true)
1314
+ else
1315
+ terminal=$(jq -n --arg observed "$SNAPSHOT_NOW" \
1316
+ '{provenance:"parent-direct-report-terminal",trust:"untrusted-supplement",captured:false,observed_at:$observed,freshness:"not-collected",reason:"no useful contradiction check",lines:0,bytes:0,event_note_seen:false,contradiction:false}')
1317
+ fi
1318
+ if printf '%s' "$terminal" | jq -e '.contradiction == true' >/dev/null; then contradiction=true; fi
1319
+ record=$(jq -n \
1320
+ --arg id "$id" --arg home "$home" --arg host "$host" --argjson remote "$remote" --arg state "$state" --arg current_reason "$current_reason" --arg observed "$SNAPSHOT_NOW" \
1321
+ --arg spawn_gen "$sampled_spawn_gen" \
1322
+ --argjson registered "$registered" --argjson summary "$summary" --argjson summary_valid "$summary_valid" --argjson decisions "$decisions" \
1323
+ --argjson activities "$activities" --argjson activity_scan "$activity_scan" \
1324
+ --argjson reconciliation "$reconciliation" --argjson terminal "$terminal" --argjson contradiction "$contradiction" \
1325
+ --arg event_raw "$event_raw" --arg event_note "$event_note" --argjson event_age "$event_age" '
1326
+ {id:$id,home:$home,host:($host | if . == "" then null else . end),remote:$remote,registered:$registered,
1327
+ spawn_gen:($spawn_gen | if . == "" then null else . end),
1328
+ current:{state:$state,reason:($current_reason | if . == "" then null else . end)},invalidity:$summary.invalidity,
1329
+ reconcile_inventory:$summary.invalidity,
1330
+ provenance:{selected:"structured-home",structured_home:$home,summary_valid:$summary_valid,
1331
+ trust:(if $summary_valid then "complete" else "partial-structured" end),parent_event_role:"historical-only"},
1332
+ freshness:{status:"fresh",observed_at:$observed,age_seconds:0},
1333
+ active_children:$summary.active_children,
1334
+ decisions_open:$summary.decisions_open,holds:$summary.holds,queued:$summary.queued,
1335
+ landed:$summary.landed,endpoints:$summary.endpoints,counts:$summary.counts,omitted:$summary.omitted,
1336
+ parent_event:{raw:$event_raw,note:$event_note,age_seconds:$event_age,open_activities:$activities,open_decisions:$decisions,activity_scan:$activity_scan,reconciliation:$reconciliation},
1337
+ terminal_evidence:$terminal,contradiction:$contradiction}')
1338
+ else
1339
+ if [ -n "$event_raw" ]; then
1340
+ provenance='parent-event-fallback'
1341
+ freshness=historical-event
1342
+ else
1343
+ provenance=unknown
1344
+ freshness=unknown
1345
+ fi
1346
+ if [ -n "$event_raw" ]; then
1347
+ terminal=$(terminal_evidence_json "$task" "$event_note" false)
1348
+ else
1349
+ terminal=$(jq -n --arg observed "$SNAPSHOT_NOW" \
1350
+ '{provenance:"parent-direct-report-terminal",trust:"untrusted-supplement",captured:false,observed_at:$observed,freshness:"not-collected",reason:"no parent event to compare",lines:0,bytes:0,event_note_seen:false,contradiction:false}')
1351
+ fi
1352
+ record=$(jq -n \
1353
+ --arg id "$id" --arg home "$home" --arg host "$host" --argjson remote "$remote" --arg reason "$reason" --arg observed "$SNAPSHOT_NOW" \
1354
+ --arg spawn_gen "$sampled_spawn_gen" \
1355
+ --arg provenance "$provenance" --arg freshness "$freshness" --arg event_raw "$event_raw" --arg event_note "$event_note" \
1356
+ --argjson registered "$registered" --argjson event_age "$event_age" --argjson activities "$activities" --argjson activity_scan "$activity_scan" \
1357
+ --argjson decisions "$decisions" --argjson terminal "$terminal" --argjson summary "$summary" --argjson summary_sampled "$summary_sampled" '
1358
+ {id:$id,home:($home | if . == "" then null else . end),host:($host | if . == "" then null else . end),remote:$remote,registered:$registered,
1359
+ spawn_gen:($spawn_gen | if . == "" then null else . end),
1360
+ current:{state:"unknown",reason:$reason},invalidity:null,
1361
+ reconcile_inventory:(if $summary_sampled then $summary.invalidity else null end),
1362
+ provenance:{selected:$provenance,structured_home:($home | if . == "" then null else . end),parent_event_role:"fallback-only-not-current"},
1363
+ freshness:{status:$freshness,observed_at:$observed,age_seconds:$event_age},
1364
+ active_children:[],decisions_open:[],holds:[],queued:[],landed:[],endpoints:[],counts:{active_children:0,decisions_open:0,holds:0,queued:0,landed:0,endpoints:0},omitted:[],
1365
+ parent_event:{raw:$event_raw,note:$event_note,age_seconds:$event_age,open_activities:$activities,open_decisions:$decisions,activity_scan:$activity_scan},
1366
+ terminal_evidence:$terminal,contradiction:false}')
1367
+ fi
1368
+ records=$(jq -n --argjson records "$records" --argjson record "$record" '$records + [$record]')
1369
+ done <<EOF
1370
+ $rows
1371
+ EOF
1372
+ jq -n \
1373
+ --argjson registry "$(printf '%s' "$union" | jq '.registry')" \
1374
+ --argjson records "$records" \
1375
+ --argjson total_registered "$total_registered" \
1376
+ --argjson total "$total" \
1377
+ --argjson shown "$shown" \
1378
+ --argjson truncated "$truncated" \
1379
+ '{registry:$registry,records:$records,total_registered:$total_registered,total:$total,shown:$shown,truncated:$truncated}'
1380
+ }
1381
+
1382
+ secondmate_landed_from_current_json() { # <secondmate-current-json>
1383
+ jq -n --argjson current "$1" '
1384
+ {records:[ $current.records[]
1385
+ | select(.provenance.selected == "structured-home") as $mate
1386
+ | $mate.landed[]
1387
+ | . + {home:$mate.home,home_id:$mate.id}],
1388
+ truncated:[ $current.records[]
1389
+ | select(.provenance.selected == "structured-home" and (.counts.landed > (.landed | length)))
1390
+ | .home],
1391
+ unreadable:[ $current.records[]
1392
+ | select(.current.state == "unknown" and .provenance.selected != "structured-home")
1393
+ | .home // ("<" + .id + ": unavailable>")],
1394
+ partial:[ $current.records[]
1395
+ | select(.provenance.selected == "structured-home" and .provenance.trust == "partial-structured")
1396
+ | .home // ("<" + .id + ": partial>")]}
1397
+ | .records |= sort_by([(.completion.date // ""), .id]) | .records |= reverse'
1398
+ }
1399
+
1400
+ scout_report_lines() {
1401
+ local report id
1402
+ if [ ! -d "$DATA" ]; then
1403
+ jq -n '[]'
1404
+ return 0
1405
+ fi
1406
+ LC_ALL=C find "$DATA" -mindepth 2 -maxdepth 2 -type f -name report.md -print \
1407
+ | sort \
1408
+ | while IFS= read -r report; do
1409
+ id=$(basename "$(dirname "$report")")
1410
+ jq -n --arg id "$id" --arg path "$report" '{id:$id,path:$path}'
1411
+ done \
1412
+ | jq -s 'sort_by(.id)'
1413
+ }
1414
+
1415
+ BACKLOG_JSON=$(backlog_json) || { echo "fm-fleet-snapshot: backlog read failed" >&2; exit 1; }
1416
+ TASKS_JSON=$(task_json_lines) || { echo "fm-fleet-snapshot: task snapshot failed" >&2; exit 1; }
1417
+
1418
+ if [ "$OUTPUT_MODE" = secondmate-home-summary ]; then
1419
+ secondmate_home_summary_json "$BACKLOG_JSON" "$TASKS_JSON" \
1420
+ || { echo "fm-fleet-snapshot: secondmate home summary failed" >&2; exit 1; }
1421
+ exit 0
1422
+ fi
1423
+
1424
+ SCOUT_REPORTS_JSON=$(scout_report_lines)
1425
+ MAIN_INVENTORY_JSON=$(main_inventory_json "$BACKLOG_JSON" "$TASKS_JSON") \
1426
+ || { echo "fm-fleet-snapshot: main inventory summary failed" >&2; exit 1; }
1427
+ SECONDMATE_CURRENT_JSON=$(secondmate_current_json "$TASKS_JSON") \
1428
+ || { echo "fm-fleet-snapshot: registered secondmate aggregation failed" >&2; exit 1; }
1429
+ SECONDMATE_LANDED_JSON=$(secondmate_landed_from_current_json "$SECONDMATE_CURRENT_JSON") \
1430
+ || { echo "fm-fleet-snapshot: secondmate landed projection failed" >&2; exit 1; }
1431
+
1432
+ jq -n \
1433
+ --arg generated "$SNAPSHOT_NOW" \
1434
+ --arg fm_home "$FM_HOME" \
1435
+ --arg fm_root "$FM_ROOT" \
1436
+ --arg state "$STATE" \
1437
+ --arg data "$DATA" \
1438
+ --arg config "$CONFIG" \
1439
+ --arg projects "$PROJECTS" \
1440
+ --argjson backlog "$BACKLOG_JSON" \
1441
+ --argjson tasks "$TASKS_JSON" \
1442
+ --argjson main_inventory "$MAIN_INVENTORY_JSON" \
1443
+ --argjson scout_reports "$SCOUT_REPORTS_JSON" \
1444
+ --argjson secondmate_current "$SECONDMATE_CURRENT_JSON" \
1445
+ --argjson secondmate_landed "$SECONDMATE_LANDED_JSON" \
1446
+ 'def backlog_by_id($id): ($backlog.records[]? | select(.structured == true and .id == $id) | .) // null;
1447
+ def task_by_id($id): ($tasks[]? | select(.id == $id) | .) // null;
1448
+ def report_kind($id): (task_by_id($id).kind // backlog_by_id($id).kind // "scout");
1449
+ {
1450
+ schema:"fm-fleet-snapshot.v1",
1451
+ generated:$generated,
1452
+ fm_home:$fm_home,
1453
+ roots:{fm_root:$fm_root,state:$state,data:$data,config:$config,projects:$projects},
1454
+ backlog:$backlog,
1455
+ tasks:($tasks | map(. + {backlog:backlog_by_id(.id)})),
1456
+ main_inventory:$main_inventory,
1457
+ scout_reports:($scout_reports | map(. + {kind:report_kind(.id)})),
1458
+ secondmate_current:$secondmate_current,
1459
+ secondmate_landed:$secondmate_landed,
1460
+ secondmate_guidance:{
1461
+ note:"For kind=secondmate, bearings selects validated structured state from that registered home; parent events and bounded terminal evidence are fallback-only supplements and never current-state authority."
1462
+ }
1463
+ }'