@ouro.bot/cli 0.1.0-alpha.99 → 0.1.0-alpha.991

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 (777) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -23
  3. package/RepairGuide.ouro/agent.json +5 -0
  4. package/RepairGuide.ouro/psyche/IDENTITY.md +19 -0
  5. package/RepairGuide.ouro/psyche/SOUL.md +55 -0
  6. package/RepairGuide.ouro/skills/diagnose-broken-remote.md +63 -0
  7. package/RepairGuide.ouro/skills/diagnose-stacked-typed-issues.md +35 -0
  8. package/RepairGuide.ouro/skills/diagnose-sync-blocked.md +54 -0
  9. package/RepairGuide.ouro/skills/diagnose-vault-expired.md +60 -0
  10. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +4 -2
  11. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +2 -2
  12. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +1 -1
  13. package/assets/bluebubbles-host +264 -0
  14. package/assets/sanctuary-host-launcher.sh +16 -0
  15. package/changelog.json +6257 -16
  16. package/deploy/unraid/Dockerfile +71 -0
  17. package/deploy/unraid/INSTALL.md +64 -0
  18. package/deploy/unraid/README.txt +3032 -0
  19. package/deploy/unraid/audit-container-spec.sh +4 -0
  20. package/deploy/unraid/container-runtime.json +1 -0
  21. package/deploy/unraid/credit-gateway.md +95 -0
  22. package/deploy/unraid/docker-man-template-transaction.mjs +727 -0
  23. package/deploy/unraid/docker-man-template-xml.cjs +105 -0
  24. package/deploy/unraid/github_known_hosts +3 -0
  25. package/deploy/unraid/install.sh +274 -0
  26. package/deploy/unraid/migrate-sanctuary-bundle.mjs +63 -0
  27. package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
  28. package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
  29. package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
  30. package/deploy/unraid/ouro-events/install-usenet-guard.sh +435 -0
  31. package/deploy/unraid/ouro-events/usenet-health.sh +176 -0
  32. package/deploy/unraid/ouro-sync-ssh +98 -0
  33. package/deploy/unraid/ouro-sync.sudoers +3 -0
  34. package/deploy/unraid/sanctuary-acceptance-adapter.sh +43 -0
  35. package/deploy/unraid/sanctuary-acceptance-contract.json +198 -0
  36. package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
  37. package/deploy/unraid/sanctuary-authority-installation.json +36 -0
  38. package/deploy/unraid/sanctuary-authority-service.sh +30 -0
  39. package/deploy/unraid/sanctuary-butler-upgrade.mjs +1523 -0
  40. package/deploy/unraid/sanctuary-deployment-target.mjs +710 -0
  41. package/deploy/unraid/sanctuary-replay-gate.mjs +2467 -0
  42. package/deploy/unraid/sanctuary-safe-rename.py +24 -0
  43. package/deploy/unraid/sanctuary-sync-cutover.sh +1039 -0
  44. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +2088 -0
  45. package/deploy/unraid/sanctuary-unit16-run.sh +570 -0
  46. package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
  47. package/deploy/unraid/sanctuary.ouro/agent.json +67 -0
  48. package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
  49. package/deploy/unraid/sanctuary.ouro/awaiting/house-care-sweep.md +17 -0
  50. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
  51. package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
  52. package/deploy/unraid/sanctuary.ouro/mcp/media-mcp.mjs +2255 -0
  53. package/deploy/unraid/sanctuary.ouro/product/PRODUCT.md +13 -0
  54. package/deploy/unraid/sanctuary.ouro/provider-readiness.json +11 -0
  55. package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +5 -0
  56. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +7 -0
  57. package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +7 -0
  58. package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +15 -0
  59. package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +3 -0
  60. package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
  61. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +234 -0
  62. package/deploy/unraid/sanctuary.xml +30 -0
  63. package/dist/a2a/card.js +57 -0
  64. package/dist/a2a/client.js +290 -0
  65. package/dist/a2a/config.js +50 -0
  66. package/dist/a2a/delegated-command-grants.js +93 -0
  67. package/dist/a2a/delegated-command.js +197 -0
  68. package/dist/a2a/delegation-stores.js +56 -0
  69. package/dist/a2a/did-resolution.js +205 -0
  70. package/dist/a2a/escalation-grants.js +84 -0
  71. package/dist/a2a/escalation-wiring.js +60 -0
  72. package/dist/a2a/identity.js +258 -0
  73. package/dist/a2a/inbound-share.js +131 -0
  74. package/dist/a2a/mission-result-wire.js +265 -0
  75. package/dist/a2a/onboarding.js +133 -0
  76. package/dist/a2a/operator-trust.js +399 -0
  77. package/dist/a2a/outbox-client.js +59 -0
  78. package/dist/a2a/outbox-store.js +188 -0
  79. package/dist/a2a/outbox-wire.js +106 -0
  80. package/dist/a2a/pin-store.js +167 -0
  81. package/dist/a2a/refusals.js +53 -0
  82. package/dist/a2a/relay-client.js +106 -0
  83. package/dist/a2a/relay-config-vault.js +64 -0
  84. package/dist/a2a/relay-contacts.js +120 -0
  85. package/dist/a2a/relay-inbox.js +360 -0
  86. package/dist/a2a/relay-retirements.js +89 -0
  87. package/dist/a2a/replay-grant-holders.js +33 -0
  88. package/dist/a2a/replay-harness.js +188 -0
  89. package/dist/a2a/resolution-proof.js +53 -0
  90. package/dist/a2a/sealed-chat.js +114 -0
  91. package/dist/a2a/seen-ledger.js +214 -0
  92. package/dist/a2a/server.js +808 -0
  93. package/dist/a2a/task-store.js +69 -0
  94. package/dist/a2a/transport.js +100 -0
  95. package/dist/a2a/trust-commands.js +203 -0
  96. package/dist/a2a/trusted-files.js +185 -0
  97. package/dist/a2a/types.js +3 -0
  98. package/dist/arc/attention-types.js +8 -0
  99. package/dist/arc/cares.js +360 -0
  100. package/dist/arc/episodes.js +118 -0
  101. package/dist/arc/evolution.js +490 -0
  102. package/dist/arc/flight-recorder.js +1034 -0
  103. package/dist/arc/intentions.js +134 -0
  104. package/dist/arc/json-store.js +117 -0
  105. package/dist/arc/obligations.js +386 -0
  106. package/dist/arc/packets.js +336 -0
  107. package/dist/arc/presence.js +185 -0
  108. package/dist/arc/task-lifecycle.js +57 -0
  109. package/dist/commerce/store.js +755 -0
  110. package/dist/commerce/types.js +3 -0
  111. package/dist/gateway/keys.js +117 -0
  112. package/dist/gateway/server.js +429 -0
  113. package/dist/heart/active-work.js +798 -40
  114. package/dist/heart/agent-entry.js +252 -3
  115. package/dist/heart/approval-files.js +113 -0
  116. package/dist/heart/approval-store.js +960 -0
  117. package/dist/heart/attachments/image-normalize.js +201 -0
  118. package/dist/heart/attachments/materialize.js +97 -0
  119. package/dist/heart/attachments/originals.js +88 -0
  120. package/dist/heart/attachments/render.js +29 -0
  121. package/dist/heart/attachments/sources/bluebubbles.js +156 -0
  122. package/dist/heart/attachments/sources/cli-local-file.js +78 -0
  123. package/dist/heart/attachments/sources/index.js +18 -0
  124. package/dist/heart/attachments/sources/telegram.js +71 -0
  125. package/dist/heart/attachments/store.js +132 -0
  126. package/dist/heart/attachments/types.js +93 -0
  127. package/dist/heart/auth/auth-flow.js +511 -0
  128. package/dist/heart/autonomy-budget.js +478 -0
  129. package/dist/heart/awaiting/a2a-await-delivery.js +109 -0
  130. package/dist/heart/awaiting/await-alert.js +152 -0
  131. package/dist/heart/awaiting/await-expiry.js +143 -0
  132. package/dist/heart/awaiting/await-loader.js +91 -0
  133. package/dist/heart/awaiting/await-parser.js +152 -0
  134. package/dist/heart/awaiting/await-runtime-state.js +130 -0
  135. package/dist/heart/awaiting/await-scheduler.js +391 -0
  136. package/dist/heart/background-operations.js +281 -0
  137. package/dist/heart/bluebubbles-host-account.js +78 -0
  138. package/dist/heart/bridges/manager.js +137 -17
  139. package/dist/heart/bridges/store.js +14 -2
  140. package/dist/heart/bundle-state.js +168 -0
  141. package/dist/heart/commitments.js +44 -18
  142. package/dist/heart/config-registry.js +331 -0
  143. package/dist/heart/config.js +179 -131
  144. package/dist/heart/context-loss-gauntlet.js +387 -0
  145. package/dist/heart/context-loss-sentinel.js +1204 -0
  146. package/dist/heart/core.js +2378 -586
  147. package/dist/heart/cross-chat-delivery.js +9 -22
  148. package/dist/heart/daemon/agent-config-check.js +474 -0
  149. package/dist/heart/daemon/agent-discovery.js +287 -3
  150. package/dist/heart/daemon/agent-home.js +355 -0
  151. package/dist/heart/daemon/agent-service.js +542 -0
  152. package/dist/heart/daemon/agentic-repair.js +547 -0
  153. package/dist/heart/daemon/await-private-wake.js +59 -0
  154. package/dist/heart/daemon/bluebubbles-cli-integration.js +71 -0
  155. package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
  156. package/dist/heart/daemon/bluebubbles-host-protocol.js +433 -0
  157. package/dist/heart/daemon/bluebubbles-host.js +273 -0
  158. package/dist/heart/daemon/boot-sync-probe.js +223 -0
  159. package/dist/heart/daemon/cadence.js +180 -0
  160. package/dist/heart/daemon/cli-arg-redaction.js +26 -0
  161. package/dist/heart/daemon/cli-defaults.js +881 -0
  162. package/dist/heart/daemon/cli-desk.js +331 -0
  163. package/dist/heart/daemon/cli-exec.js +9339 -0
  164. package/dist/heart/daemon/cli-help.js +766 -0
  165. package/dist/heart/daemon/cli-parse.js +3343 -0
  166. package/dist/heart/daemon/cli-render-doctor.js +57 -0
  167. package/dist/heart/daemon/cli-render.js +819 -0
  168. package/dist/heart/daemon/cli-types.js +8 -0
  169. package/dist/heart/daemon/connect-bay.js +323 -0
  170. package/dist/heart/daemon/container-credential-bootstrap.js +273 -0
  171. package/dist/heart/daemon/container-healthcheck.js +112 -0
  172. package/dist/heart/daemon/container-runtime.js +97 -0
  173. package/dist/heart/daemon/container-spec-auditor-main.js +187 -0
  174. package/dist/heart/daemon/container-spec-auditor.js +257 -0
  175. package/dist/heart/daemon/daemon-bootstrap-startup.js +115 -0
  176. package/dist/heart/daemon/daemon-cli.js +29 -2373
  177. package/dist/heart/daemon/daemon-entry.js +975 -11
  178. package/dist/heart/daemon/daemon-health.js +186 -0
  179. package/dist/heart/daemon/daemon-rollup.js +57 -0
  180. package/dist/heart/daemon/daemon-runtime-sync.js +88 -13
  181. package/dist/heart/daemon/daemon-tombstone.js +236 -0
  182. package/dist/heart/daemon/daemon.js +2110 -104
  183. package/dist/heart/daemon/desk-admin.js +420 -0
  184. package/dist/heart/daemon/dns-workflow.js +394 -0
  185. package/dist/heart/daemon/doctor-types.js +8 -0
  186. package/dist/heart/daemon/doctor.js +1766 -0
  187. package/dist/heart/daemon/freshness.js +345 -0
  188. package/dist/heart/daemon/habit-private-wake.js +61 -0
  189. package/dist/heart/daemon/health-monitor.js +154 -11
  190. package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
  191. package/dist/heart/daemon/hooks/bundle-meta.js +135 -1
  192. package/dist/heart/daemon/http-health-probe.js +80 -0
  193. package/dist/heart/daemon/human-command-screens.js +234 -0
  194. package/dist/heart/daemon/human-readiness.js +114 -0
  195. package/dist/heart/daemon/inner-status.js +78 -0
  196. package/dist/heart/daemon/interactive-repair.js +394 -0
  197. package/dist/heart/daemon/launchd.js +37 -8
  198. package/dist/heart/daemon/log-tailer.js +79 -10
  199. package/dist/heart/daemon/logs-prune.js +132 -0
  200. package/dist/heart/daemon/mcp-canary.js +524 -0
  201. package/dist/heart/daemon/message-router.js +65 -5
  202. package/dist/heart/daemon/migrate-to-desk.js +848 -0
  203. package/dist/heart/daemon/os-cron-deps.js +135 -0
  204. package/dist/heart/daemon/os-cron.js +22 -14
  205. package/dist/heart/daemon/ouro-bot-entry.js +6 -3
  206. package/dist/heart/daemon/ouro-entry.js +9 -3
  207. package/dist/heart/daemon/plugin-cli.js +432 -0
  208. package/dist/heart/daemon/process-manager.js +760 -61
  209. package/dist/heart/daemon/provider-discovery.js +137 -0
  210. package/dist/heart/daemon/provider-ping-progress.js +83 -0
  211. package/dist/heart/daemon/prunable-bundle.js +144 -0
  212. package/dist/heart/daemon/pulse.js +511 -0
  213. package/dist/heart/daemon/readiness-repair.js +365 -0
  214. package/dist/heart/daemon/run-hooks.js +2 -0
  215. package/dist/heart/daemon/runtime-logging.js +47 -14
  216. package/dist/heart/daemon/runtime-metadata.js +2 -30
  217. package/dist/heart/daemon/runtime-mode.js +13 -2
  218. package/dist/heart/daemon/safe-mode.js +161 -0
  219. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +2662 -0
  220. package/dist/heart/daemon/sanctuary-acceptance-harness.js +1832 -0
  221. package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
  222. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +956 -0
  223. package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
  224. package/dist/heart/daemon/sanctuary-authority-epoch.js +291 -0
  225. package/dist/heart/daemon/sanctuary-authority-installation.js +161 -0
  226. package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
  227. package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +1332 -0
  228. package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
  229. package/dist/heart/daemon/sanctuary-bundle-migration.js +974 -0
  230. package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
  231. package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +493 -0
  232. package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
  233. package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
  234. package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
  235. package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
  236. package/dist/heart/daemon/sanctuary-package-management.js +113 -0
  237. package/dist/heart/daemon/sanctuary-scheduler-liveness.js +227 -0
  238. package/dist/heart/daemon/sanctuary-scheduler-origin.js +132 -0
  239. package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +475 -0
  240. package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
  241. package/dist/heart/daemon/sanctuary-telegram-authority-service.js +676 -0
  242. package/dist/heart/daemon/sense-manager.js +786 -48
  243. package/dist/heart/daemon/session-id-resolver.js +131 -0
  244. package/dist/heart/daemon/skill-management-installer.js +1 -1
  245. package/dist/heart/daemon/socket-client.js +180 -12
  246. package/dist/heart/daemon/stale-bundle-prune.js +113 -0
  247. package/dist/heart/daemon/startup-tui.js +339 -0
  248. package/dist/heart/daemon/supercronic-supervisor.js +282 -0
  249. package/dist/heart/daemon/task-scheduler.js +117 -39
  250. package/dist/heart/daemon/terminal-ui.js +503 -0
  251. package/dist/heart/daemon/thoughts.js +138 -54
  252. package/dist/heart/daemon/up-progress.js +366 -0
  253. package/dist/heart/daemon/vault-items.js +56 -0
  254. package/dist/heart/delegation.js +1 -4
  255. package/dist/heart/external-events/router.js +1531 -0
  256. package/dist/heart/failure-reports.js +336 -0
  257. package/dist/heart/file-lock.js +109 -0
  258. package/dist/heart/frontend-approval-runtime.js +506 -0
  259. package/dist/heart/frontend-journal.js +215 -0
  260. package/dist/heart/frontend-session-service.js +271 -0
  261. package/dist/heart/frontend-socket-client.js +173 -0
  262. package/dist/heart/frontend-socket.js +434 -0
  263. package/dist/heart/frontend-turn-errors.js +20 -0
  264. package/dist/heart/habits/habit-cancel.js +810 -0
  265. package/dist/heart/habits/habit-lifecycle.js +1327 -0
  266. package/dist/heart/habits/habit-migration.js +194 -0
  267. package/dist/heart/habits/habit-parser.js +278 -0
  268. package/dist/heart/habits/habit-runtime-state.js +93 -0
  269. package/dist/heart/habits/habit-scheduler.js +653 -0
  270. package/dist/heart/habits/habit-session-summary.js +318 -0
  271. package/dist/heart/habits/habit-session.js +636 -0
  272. package/dist/heart/{daemon → hatch}/hatch-flow.js +55 -62
  273. package/dist/heart/{daemon → hatch}/hatch-specialist.js +6 -8
  274. package/dist/heart/{daemon → hatch}/specialist-prompt.js +12 -9
  275. package/dist/heart/{daemon → hatch}/specialist-tools.js +53 -18
  276. package/dist/heart/identity.js +239 -59
  277. package/dist/heart/kept-notes.js +289 -0
  278. package/dist/heart/kicks.js +1 -1
  279. package/dist/heart/machine-identity.js +175 -0
  280. package/dist/heart/mail-import-discovery.js +356 -0
  281. package/dist/heart/mailbox/mailbox-http-hooks.js +99 -0
  282. package/dist/heart/mailbox/mailbox-http-response.js +7 -0
  283. package/dist/heart/mailbox/mailbox-http-routes.js +406 -0
  284. package/dist/heart/mailbox/mailbox-http-static.js +103 -0
  285. package/dist/heart/mailbox/mailbox-http-transport.js +272 -0
  286. package/dist/heart/mailbox/mailbox-http.js +99 -0
  287. package/dist/heart/mailbox/mailbox-read.js +41 -0
  288. package/dist/heart/mailbox/mailbox-supervision.js +131 -0
  289. package/dist/heart/mailbox/mailbox-types.js +27 -0
  290. package/dist/heart/mailbox/mailbox-view.js +197 -0
  291. package/dist/heart/mailbox/readers/agent-machine.js +428 -0
  292. package/dist/heart/mailbox/readers/continuity-readers.js +329 -0
  293. package/dist/heart/mailbox/readers/mail.js +375 -0
  294. package/dist/heart/mailbox/readers/runtime-readers.js +823 -0
  295. package/dist/heart/mailbox/readers/sessions.js +231 -0
  296. package/dist/heart/mailbox/readers/shared.js +111 -0
  297. package/dist/heart/mcp/mcp-server.js +760 -0
  298. package/dist/heart/migrate-config.js +100 -0
  299. package/dist/heart/model-capabilities.js +83 -16
  300. package/dist/heart/orientation-frame.js +376 -0
  301. package/dist/heart/platform.js +81 -0
  302. package/dist/heart/primitives.js +1 -1
  303. package/dist/heart/private-runtime/decision-renderer.js +158 -0
  304. package/dist/heart/private-runtime/index.js +18 -0
  305. package/dist/heart/private-runtime/ledger.js +221 -0
  306. package/dist/heart/private-runtime/policy.js +260 -0
  307. package/dist/heart/private-runtime/types.js +2 -0
  308. package/dist/heart/progress-story.js +1 -1
  309. package/dist/heart/provider-attempt.js +137 -0
  310. package/dist/heart/provider-binding-resolver.js +273 -0
  311. package/dist/heart/provider-credentials.js +449 -0
  312. package/dist/heart/provider-failover.js +311 -0
  313. package/dist/heart/provider-models.js +96 -0
  314. package/dist/heart/provider-ping.js +266 -0
  315. package/dist/heart/provider-readiness-cache.js +119 -0
  316. package/dist/heart/provider-visibility.js +188 -0
  317. package/dist/heart/providers/anthropic-token.js +131 -0
  318. package/dist/heart/providers/anthropic.js +248 -67
  319. package/dist/heart/providers/azure.js +23 -11
  320. package/dist/heart/providers/error-classification.js +129 -0
  321. package/dist/heart/providers/github-copilot.js +38 -39
  322. package/dist/heart/providers/minimax-vlm.js +189 -0
  323. package/dist/heart/providers/minimax.js +26 -8
  324. package/dist/heart/providers/openai-codex-token.js +349 -0
  325. package/dist/heart/providers/openai-codex.js +55 -40
  326. package/dist/heart/providers/openai-compatible.js +177 -0
  327. package/dist/heart/reach-channel.js +85 -0
  328. package/dist/heart/reply-style.js +52 -0
  329. package/dist/heart/run-ledger.js +247 -0
  330. package/dist/heart/runtime-capability-check.js +170 -0
  331. package/dist/heart/runtime-credentials.js +608 -0
  332. package/dist/heart/runtime-cwd.js +87 -0
  333. package/dist/heart/sense-truth.js +24 -4
  334. package/dist/heart/session-activity.js +94 -24
  335. package/dist/heart/session-events.js +1521 -0
  336. package/dist/heart/session-playback-cli-main.js +5 -0
  337. package/dist/heart/session-playback-cli.js +36 -0
  338. package/dist/heart/session-playback.js +231 -0
  339. package/dist/heart/session-redaction-repair-cli-main.js +31 -0
  340. package/dist/heart/session-redaction-repair.js +552 -0
  341. package/dist/heart/session-stats-cli-main.js +5 -0
  342. package/dist/heart/session-stats.js +183 -0
  343. package/dist/heart/session-transcript.js +133 -0
  344. package/dist/heart/shelf-claims.js +78 -0
  345. package/dist/heart/shepherd/custody-types.js +2 -0
  346. package/dist/heart/shepherd/mandates.js +352 -0
  347. package/dist/heart/shepherd/operation-commands.js +19 -0
  348. package/dist/heart/shepherd/operations.js +135 -0
  349. package/dist/heart/shepherd/supervision.js +114 -0
  350. package/dist/heart/shepherd/types.js +2 -0
  351. package/dist/heart/shepherd-config.js +123 -0
  352. package/dist/heart/source-grounding.js +313 -0
  353. package/dist/heart/start-of-turn-packet.js +372 -0
  354. package/dist/heart/steward-policy.js +656 -0
  355. package/dist/heart/streaming.js +631 -207
  356. package/dist/heart/structured-output.js +196 -0
  357. package/dist/heart/sync-classification.js +176 -0
  358. package/dist/heart/sync-health.js +152 -0
  359. package/dist/heart/sync-lock.js +431 -0
  360. package/dist/heart/sync.js +1579 -0
  361. package/dist/heart/target-resolution.js +11 -7
  362. package/dist/heart/tempo.js +93 -0
  363. package/dist/heart/temporal-view.js +41 -0
  364. package/dist/heart/timeouts.js +101 -0
  365. package/dist/heart/tool-activity-callbacks.js +59 -0
  366. package/dist/heart/tool-approval.js +382 -0
  367. package/dist/heart/tool-description.js +155 -0
  368. package/dist/heart/tool-friction.js +55 -0
  369. package/dist/heart/tool-loop.js +206 -0
  370. package/dist/heart/tool-output-budget.js +41 -0
  371. package/dist/heart/turn-context.js +496 -0
  372. package/dist/heart/turn-execution-lease.js +30 -0
  373. package/dist/heart/{daemon → versioning}/ouro-bot-global-installer.js +6 -5
  374. package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +3 -2
  375. package/dist/heart/versioning/ouro-path-installer.js +432 -0
  376. package/dist/heart/versioning/ouro-recovery-launcher.js +76 -0
  377. package/dist/heart/versioning/ouro-version-manager.js +432 -0
  378. package/dist/heart/versioning/release-sources.js +173 -0
  379. package/dist/heart/versioning/self-update-defaults.js +294 -0
  380. package/dist/heart/versioning/self-update.js +196 -0
  381. package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
  382. package/dist/heart/{daemon → versioning}/update-checker.js +6 -1
  383. package/dist/heart/versioning/update-hooks.js +154 -0
  384. package/dist/heart/versioning/update-state.js +126 -0
  385. package/dist/heart/versioning/updater-launchd.js +150 -0
  386. package/dist/heart/versioning/version-intent.js +114 -0
  387. package/dist/heart/work-card.js +380 -0
  388. package/dist/mailbox-ui/assets/index-Dbska1tZ.js +1 -0
  389. package/dist/mailbox-ui/assets/index-DsiKaPyH.css +1 -0
  390. package/dist/mailbox-ui/assets/vendor-CcN1XpQ9.js +61 -0
  391. package/dist/mailbox-ui/index.html +16 -0
  392. package/dist/mailroom/attention.js +167 -0
  393. package/dist/mailroom/autonomy.js +209 -0
  394. package/dist/mailroom/blob-store.js +889 -0
  395. package/dist/mailroom/body-cache.js +61 -0
  396. package/dist/mailroom/cache-sync-cli.js +58 -0
  397. package/dist/mailroom/core.js +803 -0
  398. package/dist/mailroom/entry.js +160 -0
  399. package/dist/mailroom/file-store.js +568 -0
  400. package/dist/mailroom/hosted-cache-sync.js +384 -0
  401. package/dist/mailroom/mbox-import.js +393 -0
  402. package/dist/mailroom/migration.js +164 -0
  403. package/dist/mailroom/outbound.js +380 -0
  404. package/dist/mailroom/policy.js +263 -0
  405. package/dist/mailroom/reader.js +292 -0
  406. package/dist/mailroom/search-cache.js +513 -0
  407. package/dist/mailroom/search-relevance.js +319 -0
  408. package/dist/mailroom/smtp-ingress.js +176 -0
  409. package/dist/mailroom/source-state.js +176 -0
  410. package/dist/mailroom/thread.js +109 -0
  411. package/dist/mailroom/travel-extract.js +89 -0
  412. package/dist/mind/bundle-manifest.js +25 -2
  413. package/dist/mind/context.js +328 -139
  414. package/dist/mind/desk-section.js +362 -0
  415. package/dist/mind/diary-integrity.js +60 -0
  416. package/dist/mind/{memory.js → diary.js} +68 -77
  417. package/dist/mind/embedding-provider.js +60 -0
  418. package/dist/mind/file-state.js +179 -0
  419. package/dist/mind/friend-link-codes.js +209 -0
  420. package/dist/mind/friend-merge.js +336 -0
  421. package/dist/mind/house-names.js +43 -0
  422. package/dist/mind/{associative-recall.js → note-search.js} +47 -58
  423. package/dist/mind/obligation-steering.js +199 -3
  424. package/dist/mind/outbound-record.js +168 -0
  425. package/dist/mind/pending.js +36 -10
  426. package/dist/mind/prompt-budget.js +479 -0
  427. package/dist/mind/prompt-refresh.js +3 -2
  428. package/dist/mind/prompt.js +1222 -203
  429. package/dist/mind/provenance-trust.js +26 -0
  430. package/dist/mind/record-paths.js +312 -0
  431. package/dist/mind/scrutiny.js +173 -0
  432. package/dist/mind/session-transaction.js +622 -0
  433. package/dist/nerves/cli-logging.js +7 -1
  434. package/dist/nerves/coverage/audit-rules.js +15 -6
  435. package/dist/nerves/coverage/audit.js +28 -2
  436. package/dist/nerves/coverage/cli.js +1 -1
  437. package/dist/nerves/coverage/contract.js +5 -5
  438. package/dist/nerves/coverage/file-completeness.js +143 -5
  439. package/dist/nerves/coverage/run-artifacts.js +55 -35
  440. package/dist/nerves/coverage/source-scanner.js +4 -4
  441. package/dist/nerves/event-buffer.js +111 -0
  442. package/dist/nerves/index.js +293 -9
  443. package/dist/nerves/observation.js +20 -0
  444. package/dist/nerves/redact.js +79 -0
  445. package/dist/nerves/review/cli-main.js +5 -0
  446. package/dist/nerves/review/cli.js +156 -0
  447. package/dist/nerves/review/core.js +159 -0
  448. package/dist/nerves/runtime.js +17 -1
  449. package/dist/repertoire/ado-client.js +15 -56
  450. package/dist/repertoire/ado-semantic.js +16 -10
  451. package/dist/repertoire/api-client.js +97 -0
  452. package/dist/repertoire/bitwarden-store.js +1199 -0
  453. package/dist/repertoire/bundle-templates.js +71 -0
  454. package/dist/repertoire/bw-installer.js +231 -0
  455. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  456. package/dist/repertoire/coding/context-pack.js +331 -0
  457. package/dist/repertoire/coding/feedback.js +146 -34
  458. package/dist/repertoire/coding/index.js +15 -3
  459. package/dist/repertoire/coding/manager.js +159 -10
  460. package/dist/repertoire/coding/spawner.js +40 -11
  461. package/dist/repertoire/coding/tools.js +182 -18
  462. package/dist/repertoire/coding/workbench-client.js +272 -0
  463. package/dist/repertoire/coding/workbench-manager.js +346 -0
  464. package/dist/repertoire/commerce-errors.js +109 -0
  465. package/dist/repertoire/commerce-self-test.js +156 -0
  466. package/dist/repertoire/credential-access.js +183 -0
  467. package/dist/repertoire/desk/classifier.js +362 -0
  468. package/dist/repertoire/duffel-client.js +185 -0
  469. package/dist/repertoire/github-client.js +14 -55
  470. package/dist/repertoire/graph-client.js +11 -52
  471. package/dist/repertoire/guardrails.js +1480 -22
  472. package/dist/repertoire/house-sweep.js +471 -0
  473. package/dist/repertoire/mcp-client.js +58 -5
  474. package/dist/repertoire/mcp-manager.js +427 -139
  475. package/dist/repertoire/mcp-tools.js +132 -0
  476. package/dist/repertoire/mcp-write-guard.js +46 -0
  477. package/dist/repertoire/plugin-mcp.js +175 -0
  478. package/dist/repertoire/plugins.js +253 -0
  479. package/dist/repertoire/relationship-authorization.js +322 -0
  480. package/dist/repertoire/sanctuary-container-policy.js +50 -0
  481. package/dist/repertoire/shell-sessions.js +134 -0
  482. package/dist/repertoire/skills.js +48 -4
  483. package/dist/repertoire/stripe-client.js +131 -0
  484. package/dist/repertoire/tool-arguments.js +182 -0
  485. package/dist/repertoire/tool-results.js +29 -0
  486. package/dist/repertoire/tools-a2a.js +742 -0
  487. package/dist/repertoire/tools-attachments.js +322 -0
  488. package/dist/repertoire/tools-awaiting.js +897 -0
  489. package/dist/repertoire/tools-base.js +83 -1168
  490. package/dist/repertoire/tools-bluebubbles.js +2 -0
  491. package/dist/repertoire/tools-bridge.js +144 -0
  492. package/dist/repertoire/tools-bundle.js +917 -0
  493. package/dist/repertoire/tools-commerce.js +253 -0
  494. package/dist/repertoire/tools-config.js +186 -0
  495. package/dist/repertoire/tools-continuity.js +588 -0
  496. package/dist/repertoire/tools-credential.js +383 -0
  497. package/dist/repertoire/tools-escalation.js +84 -0
  498. package/dist/repertoire/tools-evolution.js +527 -0
  499. package/dist/repertoire/tools-files.js +362 -0
  500. package/dist/repertoire/tools-flight.js +290 -0
  501. package/dist/repertoire/tools-flow.js +119 -0
  502. package/dist/repertoire/tools-friend-link.js +57 -0
  503. package/dist/repertoire/tools-github.js +3 -8
  504. package/dist/repertoire/tools-habits.js +103 -0
  505. package/dist/repertoire/tools-house-care.js +174 -0
  506. package/dist/repertoire/tools-mail.js +1828 -0
  507. package/dist/repertoire/tools-notes.js +501 -0
  508. package/dist/repertoire/tools-obligations.js +182 -0
  509. package/dist/repertoire/tools-orientation.js +31 -0
  510. package/dist/repertoire/tools-record.js +482 -0
  511. package/dist/repertoire/tools-rsvp.js +139 -0
  512. package/dist/repertoire/tools-runtime.js +238 -0
  513. package/dist/repertoire/tools-sanctuary-host.js +202 -0
  514. package/dist/repertoire/tools-session.js +1141 -0
  515. package/dist/repertoire/tools-shell.js +154 -0
  516. package/dist/repertoire/tools-shepherd-guidance.js +171 -0
  517. package/dist/repertoire/tools-shepherd.js +325 -0
  518. package/dist/repertoire/tools-steward-policy.js +147 -0
  519. package/dist/repertoire/tools-stripe.js +224 -0
  520. package/dist/repertoire/tools-surface.js +369 -0
  521. package/dist/repertoire/tools-teams.js +50 -39
  522. package/dist/repertoire/tools-telegram-contacts.js +37 -0
  523. package/dist/repertoire/tools-travel.js +125 -0
  524. package/dist/repertoire/tools-trip.js +982 -0
  525. package/dist/repertoire/tools-unraid.js +556 -0
  526. package/dist/repertoire/tools-user-profile.js +146 -0
  527. package/dist/repertoire/tools-vault.js +40 -0
  528. package/dist/repertoire/tools-voice-local.js +198 -0
  529. package/dist/repertoire/tools-voice.js +145 -0
  530. package/dist/repertoire/tools.js +523 -156
  531. package/dist/repertoire/travel-api-client.js +360 -0
  532. package/dist/repertoire/unraid-client.js +155 -0
  533. package/dist/repertoire/unraid-restart.js +339 -0
  534. package/dist/repertoire/user-profile.js +131 -0
  535. package/dist/repertoire/vault-setup.js +246 -0
  536. package/dist/repertoire/vault-unlock.js +686 -0
  537. package/dist/rsvp/aisleplanner-client.js +354 -0
  538. package/dist/rsvp/cli.js +936 -0
  539. package/dist/rsvp/config.js +327 -0
  540. package/dist/rsvp/cutover.js +453 -0
  541. package/dist/rsvp/diagnostics.js +232 -0
  542. package/dist/rsvp/diff-renderer.js +176 -0
  543. package/dist/rsvp/habit-policy.js +110 -0
  544. package/dist/rsvp/habit-stage.js +123 -0
  545. package/dist/rsvp/incident-bundle.js +97 -0
  546. package/dist/rsvp/migration.js +204 -0
  547. package/dist/rsvp/native-habit-runner.js +630 -0
  548. package/dist/rsvp/outbound-state.js +628 -0
  549. package/dist/rsvp/query.js +118 -0
  550. package/dist/rsvp/replay.js +233 -0
  551. package/dist/rsvp/snapshot.js +232 -0
  552. package/dist/rsvp/spend-ledger.js +175 -0
  553. package/dist/scripts/claude-code-hook.js +41 -0
  554. package/dist/scripts/claude-code-stop-hook.js +47 -0
  555. package/dist/senses/a2a-entry.js +140 -0
  556. package/dist/senses/acp-server.js +386 -0
  557. package/dist/senses/attention-queue.js +186 -0
  558. package/dist/senses/await-turn-message.js +65 -0
  559. package/dist/senses/bluebubbles/active-turns.js +216 -0
  560. package/dist/senses/bluebubbles/attachment-cache.js +53 -0
  561. package/dist/senses/bluebubbles/attachment-download.js +137 -0
  562. package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +393 -43
  563. package/dist/senses/bluebubbles/context-packet.js +207 -0
  564. package/dist/senses/bluebubbles/context-smoke.js +144 -0
  565. package/dist/senses/bluebubbles/entry.js +90 -0
  566. package/dist/senses/bluebubbles/host-account-verifier.js +88 -0
  567. package/dist/senses/{bluebubbles-inbound-log.js → bluebubbles/inbound-log.js} +21 -3
  568. package/dist/senses/bluebubbles/index.js +4054 -0
  569. package/dist/senses/bluebubbles/latest-turn.js +311 -0
  570. package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -71
  571. package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +159 -28
  572. package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +24 -13
  573. package/dist/senses/bluebubbles/outbound-state.js +335 -0
  574. package/dist/senses/bluebubbles/processed-log.js +133 -0
  575. package/dist/senses/bluebubbles/reaction-policy.js +47 -0
  576. package/dist/senses/bluebubbles/replay.js +284 -0
  577. package/dist/senses/{bluebubbles-runtime-state.js → bluebubbles/runtime-state.js} +30 -2
  578. package/dist/senses/bluebubbles/semantic-receipts.js +1986 -0
  579. package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
  580. package/dist/senses/bluebubbles/webhook-registration.js +257 -0
  581. package/dist/senses/bluebubbles-meta-guard.js +39 -0
  582. package/dist/senses/cli/bracketed-paste.js +82 -0
  583. package/dist/senses/cli/image-paste.js +287 -0
  584. package/dist/senses/cli/image-ref-navigation.js +75 -0
  585. package/dist/senses/cli/ink-app.js +156 -0
  586. package/dist/senses/cli/inline-diff.js +64 -0
  587. package/dist/senses/cli/input-keys.js +174 -0
  588. package/dist/senses/cli/kill-ring.js +86 -0
  589. package/dist/senses/cli/message-list.js +51 -0
  590. package/dist/senses/cli/ouro-tui.js +607 -0
  591. package/dist/senses/cli/spinner-imperative.js +135 -0
  592. package/dist/senses/cli/spinner.js +101 -0
  593. package/dist/senses/cli/status-line.js +60 -0
  594. package/dist/senses/cli/streaming-markdown.js +526 -0
  595. package/dist/senses/cli/tool-display.js +85 -0
  596. package/dist/senses/cli/tool-render.js +85 -0
  597. package/dist/senses/cli/tui-store.js +241 -0
  598. package/dist/senses/cli/virtual-list.js +35 -0
  599. package/dist/senses/cli-entry.js +60 -8
  600. package/dist/senses/cli.js +570 -309
  601. package/dist/senses/commands.js +66 -3
  602. package/dist/senses/context-packet-ledger.js +371 -0
  603. package/dist/senses/context-packets.js +154 -0
  604. package/dist/senses/habit-lifecycle-guard.js +24 -0
  605. package/dist/senses/habit-turn-message.js +160 -0
  606. package/dist/senses/ingress-evidence.js +2 -0
  607. package/dist/senses/mail-entry.js +66 -0
  608. package/dist/senses/mail.js +524 -0
  609. package/dist/senses/pipeline.js +1192 -204
  610. package/dist/senses/private-runtime-worker.js +857 -0
  611. package/dist/senses/private-runtime.js +1622 -0
  612. package/dist/senses/proactive-content-guard.js +51 -0
  613. package/dist/senses/root-host-approval-port.js +345 -0
  614. package/dist/senses/root-host-approval-runtime.js +393 -0
  615. package/dist/senses/sanctuary-authority-resident.js +102 -0
  616. package/dist/senses/sanctuary-download-credit-presentation.js +31 -0
  617. package/dist/senses/sanctuary-full-visibility-contract.js +302 -0
  618. package/dist/senses/sanctuary-grounding.js +118 -0
  619. package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
  620. package/dist/senses/sanctuary-health-acceptance-probe.js +786 -0
  621. package/dist/senses/sanctuary-health-runner.js +106 -0
  622. package/dist/senses/sanctuary-health.js +442 -0
  623. package/dist/senses/sanctuary-house.js +97 -0
  624. package/dist/senses/sanctuary-install-state-contract.js +169 -0
  625. package/dist/senses/sanctuary-interactive-control.js +410 -0
  626. package/dist/senses/sanctuary-media-catalog-contract.js +268 -0
  627. package/dist/senses/sanctuary-media-optimization.js +517 -0
  628. package/dist/senses/sanctuary-runtime.js +390 -0
  629. package/dist/senses/sanctuary-sab.js +85 -0
  630. package/dist/senses/sanctuary-storage-optimization-contract.js +32 -0
  631. package/dist/senses/shared-turn.js +832 -0
  632. package/dist/senses/shepherd/agent-presentation.js +88 -0
  633. package/dist/senses/shepherd/attention-page.js +166 -0
  634. package/dist/senses/shepherd/attention-sidebar.js +76 -0
  635. package/dist/senses/shepherd/attention.js +295 -0
  636. package/dist/senses/shepherd/checkin-config.js +76 -0
  637. package/dist/senses/shepherd/checkin-store.js +386 -0
  638. package/dist/senses/shepherd/client.js +196 -0
  639. package/dist/senses/shepherd/cmux.js +483 -0
  640. package/dist/senses/shepherd/composer.js +336 -0
  641. package/dist/senses/shepherd/copilot-app.js +648 -0
  642. package/dist/senses/shepherd/feedback-reply.js +108 -0
  643. package/dist/senses/shepherd/feedback-source.js +20 -0
  644. package/dist/senses/shepherd/feedback-store.js +331 -0
  645. package/dist/senses/shepherd/feedback-submit.js +182 -0
  646. package/dist/senses/shepherd/feedback-worker.js +208 -0
  647. package/dist/senses/shepherd/guidance.js +273 -0
  648. package/dist/senses/shepherd/herdr.js +427 -0
  649. package/dist/senses/shepherd/host.js +17 -0
  650. package/dist/senses/shepherd/human-decision-guard.js +29 -0
  651. package/dist/senses/shepherd/judge-evaluation-command.js +183 -0
  652. package/dist/senses/shepherd/judge-evaluation-entry.js +13 -0
  653. package/dist/senses/shepherd/judge-evaluation.js +155 -0
  654. package/dist/senses/shepherd/judge.js +210 -0
  655. package/dist/senses/shepherd/notice-outbox.js +262 -0
  656. package/dist/senses/shepherd/observation-publisher.js +320 -0
  657. package/dist/senses/shepherd/operations-defaults.js +86 -0
  658. package/dist/senses/shepherd/operations.js +159 -0
  659. package/dist/senses/shepherd/owner-notice.js +489 -0
  660. package/dist/senses/shepherd/prompt-state.js +60 -0
  661. package/dist/senses/shepherd/recovery.js +543 -0
  662. package/dist/senses/shepherd/redact.js +66 -0
  663. package/dist/senses/shepherd/returns.js +79 -0
  664. package/dist/senses/shepherd/sense.js +849 -0
  665. package/dist/senses/shepherd/shepherd-judge-corpus-v1.json +170 -0
  666. package/dist/senses/shepherd/surface.js +123 -0
  667. package/dist/senses/shepherd/target-binding.js +13 -0
  668. package/dist/senses/shepherd/terminal-process.js +147 -0
  669. package/dist/senses/shepherd-entry.js +98 -0
  670. package/dist/senses/surface-tool.js +109 -0
  671. package/dist/senses/teams-entry.js +68 -8
  672. package/dist/senses/teams-image.js +119 -0
  673. package/dist/senses/teams.js +998 -337
  674. package/dist/senses/telegram-admission.js +718 -0
  675. package/dist/senses/telegram-approval-runtime.js +793 -0
  676. package/dist/senses/telegram-attachments.js +186 -0
  677. package/dist/senses/telegram-audit-ledger.js +220 -0
  678. package/dist/senses/telegram-authority-transport.js +231 -0
  679. package/dist/senses/telegram-client.js +1755 -0
  680. package/dist/senses/telegram-effect-adapter.js +904 -0
  681. package/dist/senses/telegram-entry.js +83 -0
  682. package/dist/senses/telegram-handoff.js +154 -0
  683. package/dist/senses/telegram.js +2474 -0
  684. package/dist/senses/trust-gate.js +104 -9
  685. package/dist/senses/voice/audio-playback.js +237 -0
  686. package/dist/senses/voice/audio-routing.js +119 -0
  687. package/dist/senses/voice/call-auth.js +250 -0
  688. package/dist/senses/voice/elevenlabs.js +202 -0
  689. package/dist/senses/voice/floor-control.js +431 -0
  690. package/dist/senses/voice/floor-controller.js +115 -0
  691. package/dist/senses/voice/golden-path.js +116 -0
  692. package/dist/senses/voice/index.js +29 -0
  693. package/dist/senses/voice/local-audio-control.js +344 -0
  694. package/dist/senses/voice/local-audio-devices.js +516 -0
  695. package/dist/senses/voice/local-audio-disclosure.js +174 -0
  696. package/dist/senses/voice/local-audio-identity.js +103 -0
  697. package/dist/senses/voice/local-audio-join.js +325 -0
  698. package/dist/senses/voice/local-audio-mute.js +199 -0
  699. package/dist/senses/voice/local-audio-notify.js +124 -0
  700. package/dist/senses/voice/local-audio-prompts.js +116 -0
  701. package/dist/senses/voice/local-audio-transport.js +427 -0
  702. package/dist/senses/voice/local-audio.js +281 -0
  703. package/dist/senses/voice/local-media-stream.js +145 -0
  704. package/dist/senses/voice/meeting.js +113 -0
  705. package/dist/senses/voice/outbound.js +190 -0
  706. package/dist/senses/voice/phone.js +33 -0
  707. package/dist/senses/voice/playback.js +139 -0
  708. package/dist/senses/voice/process-guards.js +51 -0
  709. package/dist/senses/voice/realtime-eval.js +496 -0
  710. package/dist/senses/voice/realtime-tool-schema.js +195 -0
  711. package/dist/senses/voice/realtime-trace.js +531 -0
  712. package/dist/senses/voice/transcript.js +70 -0
  713. package/dist/senses/voice/turn.js +191 -0
  714. package/dist/senses/voice/twilio-phone-runtime.js +836 -0
  715. package/dist/senses/voice/twilio-phone.js +5454 -0
  716. package/dist/senses/voice/types.js +2 -0
  717. package/dist/senses/voice/whisper.js +161 -0
  718. package/dist/senses/voice-entry.js +83 -0
  719. package/dist/senses/voice-realtime-eval-command.js +99 -0
  720. package/dist/senses/voice-realtime-eval-entry.js +21 -0
  721. package/dist/senses/voice-twilio-entry.js +87 -0
  722. package/dist/trips/core.js +138 -0
  723. package/dist/trips/store.js +265 -0
  724. package/dist/util/frontmatter.js +69 -0
  725. package/npm-shrinkwrap.json +8342 -0
  726. package/package.json +76 -13
  727. package/skills/agent-commerce.md +113 -0
  728. package/skills/browser-navigation.md +117 -0
  729. package/skills/commerce-setup-guide.md +116 -0
  730. package/skills/commerce-setup.md +84 -0
  731. package/skills/configure-dev-tools.md +99 -0
  732. package/skills/travel-planning.md +138 -0
  733. package/dist/heart/daemon/auth-flow.js +0 -430
  734. package/dist/heart/daemon/ouro-path-installer.js +0 -260
  735. package/dist/heart/daemon/ouro-version-manager.js +0 -171
  736. package/dist/heart/daemon/update-hooks.js +0 -138
  737. package/dist/heart/obligations.js +0 -191
  738. package/dist/heart/safe-workspace.js +0 -381
  739. package/dist/heart/session-recall.js +0 -116
  740. package/dist/mind/friends/channel.js +0 -84
  741. package/dist/mind/friends/group-context.js +0 -144
  742. package/dist/mind/friends/resolver.js +0 -84
  743. package/dist/mind/friends/store-file.js +0 -190
  744. package/dist/mind/friends/store.js +0 -4
  745. package/dist/mind/friends/tokens.js +0 -26
  746. package/dist/mind/friends/trust-explanation.js +0 -74
  747. package/dist/mind/friends/types.js +0 -29
  748. package/dist/repertoire/tasks/board.js +0 -134
  749. package/dist/repertoire/tasks/index.js +0 -224
  750. package/dist/repertoire/tasks/lifecycle.js +0 -80
  751. package/dist/repertoire/tasks/middleware.js +0 -65
  752. package/dist/repertoire/tasks/parser.js +0 -173
  753. package/dist/repertoire/tasks/scanner.js +0 -132
  754. package/dist/repertoire/tasks/transitions.js +0 -144
  755. package/dist/senses/bluebubbles-entry.js +0 -13
  756. package/dist/senses/bluebubbles.js +0 -1181
  757. package/dist/senses/debug-activity.js +0 -154
  758. package/dist/senses/inner-dialog-worker.js +0 -90
  759. package/dist/senses/inner-dialog.js +0 -525
  760. package/subagents/README.md +0 -7
  761. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
  762. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
  763. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
  764. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
  765. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
  766. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
  767. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
  768. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
  769. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
  770. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
  771. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
  772. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
  773. /package/dist/{repertoire/tasks/types.js → heart/attachments/sources/adapter.js} +0 -0
  774. /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
  775. /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
  776. /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
  777. /package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +0 -0
@@ -0,0 +1,1579 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.NOT_A_REPO_ERROR = exports.STALE_INDEX_LOCK_MS = exports.SYNC_KILL_PROBE_MS = exports.SYNC_PAUSE_MAX_MS = exports.__testHooks = exports.PRE_TURN_LOCK_WAIT_MS = exports.OWNER_WINS_MESSAGE = exports.SYNC_SSH_COMMAND = exports.SYNC_SECRET_DIR = exports.SYNC_KILL_GRACE_MS = exports.SYNC_PUSH_TIMEOUT_MS = exports.SYNC_FETCH_TIMEOUT_MS = exports.SYNC_GIT_TIMEOUT_MS = exports.SYNC_GIT_MAX_BUFFER_BYTES = exports.SYNC_FILE_SIZE_LIMIT_BYTES = exports.FIRST_PUSH_HELD_NOTICE = void 0;
37
+ exports.syncSecretDir = syncSecretDir;
38
+ exports.pendingSyncPath = pendingSyncPath;
39
+ exports.skippedSyncFilesPath = skippedSyncFilesPath;
40
+ exports.syncPausedPath = syncPausedPath;
41
+ exports.firstPushHeldPath = firstPushHeldPath;
42
+ exports.agentNameFromRoot = agentNameFromRoot;
43
+ exports.syncNetworkEnv = syncNetworkEnv;
44
+ exports.syncGroupExists = syncGroupExists;
45
+ exports.abortInFlightSync = abortInFlightSync;
46
+ exports.clearPendingSync = clearPendingSync;
47
+ exports.setSyncOwnerNotifier = setSyncOwnerNotifier;
48
+ exports.tellOwner = tellOwner;
49
+ exports.commitLocalChanges = commitLocalChanges;
50
+ exports.integrateFetched = integrateFetched;
51
+ exports.fetchRemote = fetchRemote;
52
+ exports.listRemoteRefs = listRemoteRefs;
53
+ exports.pushAsync = pushAsync;
54
+ exports.syncPausedUntil = syncPausedUntil;
55
+ exports.syncTrackingRef = syncTrackingRef;
56
+ exports.holdSyncLock = holdSyncLock;
57
+ exports.pushHeadToBranch = pushHeadToBranch;
58
+ exports.probeSync = probeSync;
59
+ exports.preTurnPull = preTurnPull;
60
+ exports.preTurnPullAsync = preTurnPullAsync;
61
+ exports.postTurnPush = postTurnPush;
62
+ exports.syncNow = syncNow;
63
+ const child_process_1 = require("child_process");
64
+ const crypto_1 = require("crypto");
65
+ const fs = __importStar(require("fs"));
66
+ const path = __importStar(require("path"));
67
+ const runtime_1 = require("../nerves/runtime");
68
+ const sync_health_1 = require("./sync-health");
69
+ const sync_lock_1 = require("./sync-lock");
70
+ /** What sync tells the owner, once, when it holds off a first push to an empty remote. */
71
+ exports.FIRST_PUSH_HELD_NOTICE = "My bundle's GitHub repo is empty. Before I push to it for the first time, please look over what would go up and tell me yes.";
72
+ /**
73
+ * Sync never commits a file larger than this. GitHub rejects any push that
74
+ * contains a file over 100 MiB and warns above 50 MiB; one such file in an
75
+ * unpushed commit blocks every later push from the bundle. The guard leaves
76
+ * the file in the working tree, uncommitted, and records it.
77
+ */
78
+ exports.SYNC_FILE_SIZE_LIMIT_BYTES = 50 * 1024 * 1024;
79
+ /**
80
+ * Output buffer for git commands whose output grows with the number of
81
+ * changed files. Node's 1 MiB default made `git status` fail with ENOBUFS
82
+ * once a bundle held about 30,000 changed files, which stopped sync.
83
+ */
84
+ exports.SYNC_GIT_MAX_BUFFER_BYTES = 256 * 1024 * 1024;
85
+ /**
86
+ * Timeout for sync's local git steps (status, add, commit, merge). `git add -A`
87
+ * took 17 s on a bundle with 30,000 changed files, past the old 10 s limit.
88
+ */
89
+ exports.SYNC_GIT_TIMEOUT_MS = 120_000;
90
+ /** Network steps run as detached children, killed by process group at these limits. */
91
+ exports.SYNC_FETCH_TIMEOUT_MS = 10_000;
92
+ exports.SYNC_PUSH_TIMEOUT_MS = 60_000;
93
+ /**
94
+ * Between the SIGTERM and the SIGKILL of a timed-out network step. In the
95
+ * container ssh runs as ouro-sync, which the agent's uid cannot signal; sudo
96
+ * can, and relays the SIGTERM to it.
97
+ */
98
+ exports.SYNC_KILL_GRACE_MS = 2_000;
99
+ /**
100
+ * The container's read-only mount for the sync deploy key. The key is owned by
101
+ * ouro-sync (10002) mode 0400, so the agent's uid cannot read it.
102
+ */
103
+ exports.SYNC_SECRET_DIR = "/run/ouro-sync";
104
+ /**
105
+ * git's ssh in the container: the root-owned wrapper, run as ouro-sync, is the
106
+ * only thing that reads the key. It accepts only git's call for one GitHub repo.
107
+ */
108
+ exports.SYNC_SSH_COMMAND = "sudo -n -u ouro-sync /usr/local/libexec/ouro-sync-ssh";
109
+ /** The deploy key directory: SYNC_SECRET_DIR, unless a test points it elsewhere. */
110
+ function syncSecretDir() {
111
+ return exports.__testHooks.syncSecretDir ?? exports.SYNC_SECRET_DIR;
112
+ }
113
+ /** Message of the merge commit in which the owner's psyche edits win. */
114
+ exports.OWNER_WINS_MESSAGE = "sync: owner's psyche edit wins";
115
+ /**
116
+ * How long `preTurnPull` waits for the bundle lock before skipping. A turn
117
+ * skips rather than queues: the next turn integrates anyway.
118
+ */
119
+ exports.PRE_TURN_LOCK_WAIT_MS = 5_000;
120
+ /**
121
+ * Test seams.
122
+ * @internal Not part of the public API; production code must not set these.
123
+ */
124
+ exports.__testHooks = {};
125
+ /** A pause marker longer than this is garbage: a crashed pauser must not stop sync for good. */
126
+ exports.SYNC_PAUSE_MAX_MS = 24 * 60 * 60 * 1000;
127
+ function pendingSyncPath(agentRoot) {
128
+ return path.join(agentRoot, "state", "pending-sync.json");
129
+ }
130
+ function skippedSyncFilesPath(agentRoot) {
131
+ return path.join(agentRoot, "state", "sync-skipped-files.json");
132
+ }
133
+ function syncPausedPath(agentRoot) {
134
+ return path.join(agentRoot, "state", "sync-paused");
135
+ }
136
+ /** Present once the owner has been told a first push is held, so they are told only once. */
137
+ function firstPushHeldPath(agentRoot) {
138
+ return path.join(agentRoot, "state", "sync-first-push-held.json");
139
+ }
140
+ function syncConflictStatePath(agentRoot) {
141
+ return path.join(agentRoot, "state", "sync-conflict.json");
142
+ }
143
+ function agentNameFromRoot(agentRoot) {
144
+ return path.basename(agentRoot).replace(/\.ouro$/, "");
145
+ }
146
+ function branchOf(config) {
147
+ return config.branch ?? "main";
148
+ }
149
+ function errorText(err) {
150
+ return err instanceof Error ? err.message : String(err);
151
+ }
152
+ /**
153
+ * Flags on every call. The identity: a container or an empty HOME has no
154
+ * gitconfig. The rest: the owner's global gitconfig on the Mac must not
155
+ * stash, replay recorded resolutions, run hooks or demand signatures.
156
+ */
157
+ function identityArgs(agentName) {
158
+ return [
159
+ "-c", `user.name=${agentName}`,
160
+ "-c", `user.email=${agentName}@ouro.local`,
161
+ "-c", "commit.gpgsign=false",
162
+ "-c", "merge.autoStash=false",
163
+ "-c", "rerere.enabled=false",
164
+ "-c", "core.hooksPath=/dev/null",
165
+ "-c", "merge.verifySignatures=false",
166
+ "-c", "core.fsmonitor=false",
167
+ ...(exports.__testHooks.extraGitConfig ?? []),
168
+ ];
169
+ }
170
+ /** git's messages are matched in English, so the locale is pinned. */
171
+ function gitEnv(extra = {}) {
172
+ return { ...process.env, GIT_TERMINAL_PROMPT: "0", LC_ALL: "C", LANG: "C", ...extra };
173
+ }
174
+ /**
175
+ * The environment of a fetch or push. When the container has the key mount,
176
+ * git's ssh is the sync wrapper under sudo. `GIT_SSH_VARIANT=ssh` stops git
177
+ * probing an unknown "sudo" with `-G`. Without the mount (the Mac, or before
178
+ * the cutover) ssh is left to the host's own setup.
179
+ */
180
+ function syncNetworkEnv() {
181
+ if (!fs.existsSync(syncSecretDir()))
182
+ return gitEnv();
183
+ return gitEnv({ GIT_SSH_COMMAND: exports.SYNC_SSH_COMMAND, GIT_SSH_VARIANT: "ssh" });
184
+ }
185
+ function runGit(agentRoot, agentName, args, env, input) {
186
+ const run = (0, child_process_1.spawnSync)("git", [...identityArgs(agentName), ...args], {
187
+ cwd: agentRoot,
188
+ env: gitEnv(env),
189
+ input,
190
+ stdio: [input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
191
+ timeout: exports.SYNC_GIT_TIMEOUT_MS,
192
+ maxBuffer: exports.SYNC_GIT_MAX_BUFFER_BYTES,
193
+ });
194
+ /* v8 ignore next -- spawn failures (ENOENT, timeout, ENOBUFS) need a broken host @preserve */
195
+ if (run.error)
196
+ return { status: null, stdout: "", stderr: run.error.message };
197
+ return { status: run.status, stdout: run.stdout.toString(), stderr: run.stderr.toString() };
198
+ }
199
+ function gitFailure(args, run) {
200
+ return new Error(`git ${args[0]} failed: ${(run.stderr || run.stdout).trim()}`);
201
+ }
202
+ /** Run git and return stdout; throw with git's message on a non-zero exit. */
203
+ function git(agentRoot, agentName, args, env, input) {
204
+ const run = runGit(agentRoot, agentName, args, env, input);
205
+ if (run.status !== 0)
206
+ throw gitFailure(args, run);
207
+ return run.stdout;
208
+ }
209
+ /**
210
+ * Run a git command over a list of paths, passed NUL-separated on stdin and
211
+ * matched literally. A list in argv dies with E2BIG past about 1 MB.
212
+ */
213
+ function gitOnPaths(agentRoot, agentName, args, paths) {
214
+ return git(agentRoot, agentName, ["--literal-pathspecs", ...args, "--pathspec-from-file=-", "--pathspec-file-nul"], undefined, paths.join("\0"));
215
+ }
216
+ /**
217
+ * A network git call. It runs detached, in its own process group, so a timeout
218
+ * or an abort ends git and everything it started (ssh, a credential helper):
219
+ * a lone `kill` of git leaves ssh holding the pipes open. The group gets
220
+ * SIGTERM, which sudo relays to the ssh it runs, then SIGKILL after a grace.
221
+ */
222
+ function runGitAsync(agentRoot, agentName, args, timeoutMs, signal) {
223
+ return new Promise((resolve) => {
224
+ const child = (0, child_process_1.spawn)("git", [...identityArgs(agentName), ...args], {
225
+ cwd: agentRoot,
226
+ env: syncNetworkEnv(),
227
+ stdio: ["ignore", "pipe", "pipe"],
228
+ detached: true,
229
+ });
230
+ let stdout = "";
231
+ let stderr = "";
232
+ let timedOut = false;
233
+ let aborted = false;
234
+ let settled = false;
235
+ const pid = child.pid;
236
+ inFlightChildren.add(pid);
237
+ child.stdout.on("data", (chunk) => { stdout += chunk.toString(); });
238
+ child.stderr.on("data", (chunk) => { stderr += chunk.toString(); });
239
+ const killGroup = () => killSyncGroup(pid);
240
+ const onAbort = () => {
241
+ aborted = true;
242
+ killGroup();
243
+ };
244
+ const timer = setTimeout(() => {
245
+ timedOut = true;
246
+ killGroup();
247
+ }, timeoutMs);
248
+ signal?.addEventListener("abort", onAbort, { once: true });
249
+ const finish = (status, extra = "") => {
250
+ /* v8 ignore next -- only a spawn error followed by close settles twice @preserve */
251
+ if (settled)
252
+ return;
253
+ settled = true;
254
+ inFlightChildren.delete(pid);
255
+ clearTimeout(timer);
256
+ signal?.removeEventListener("abort", onAbort);
257
+ resolve({ status, stdout, stderr: extra + stderr, timedOut, aborted });
258
+ };
259
+ /* v8 ignore next -- spawning git itself failing needs a host without git @preserve */
260
+ child.on("error", (err) => finish(null, `${err.message}\n`));
261
+ child.on("close", (code) => finish(code));
262
+ if (signal?.aborted)
263
+ onAbort();
264
+ });
265
+ }
266
+ function signalSyncGroup(pid, name) {
267
+ try {
268
+ process.kill(-pid, name);
269
+ }
270
+ catch {
271
+ // The group is already gone.
272
+ }
273
+ }
274
+ /** How often a killed group is probed during the grace, so its timers stop once it is gone. */
275
+ exports.SYNC_KILL_PROBE_MS = 50;
276
+ /**
277
+ * Whether anything is left in process group `pid`. EPERM means a member this
278
+ * uid cannot signal (the ouro-sync ssh) is still there.
279
+ */
280
+ function syncGroupExists(pid, kill = process.kill.bind(process)) {
281
+ try {
282
+ kill(-pid, 0);
283
+ return true;
284
+ }
285
+ catch (error) {
286
+ return error.code === "EPERM";
287
+ }
288
+ }
289
+ /**
290
+ * SIGTERM to a network git child's process group, then SIGKILL after the
291
+ * grace. sudo relays only the SIGTERM to the ouro-sync ssh, which the agent's
292
+ * uid cannot signal. The timers are not tied to git closing: whatever in the
293
+ * group outlived the SIGTERM still gets the SIGKILL. They stop as soon as a
294
+ * probe finds the group gone, and the SIGKILL goes only to a group that still
295
+ * exists, so a reused group id is never signalled.
296
+ */
297
+ function killSyncGroup(pid) {
298
+ signalSyncGroup(pid, "SIGTERM");
299
+ const probe = setInterval(() => {
300
+ if (syncGroupExists(pid))
301
+ return;
302
+ clearInterval(probe);
303
+ clearTimeout(kill);
304
+ }, exports.__testHooks.killProbeMs ?? exports.SYNC_KILL_PROBE_MS);
305
+ probe.unref();
306
+ const kill = setTimeout(() => {
307
+ clearInterval(probe);
308
+ if (syncGroupExists(pid))
309
+ signalSyncGroup(pid, "SIGKILL");
310
+ }, exports.__testHooks.killGraceMs ?? exports.SYNC_KILL_GRACE_MS);
311
+ kill.unref();
312
+ }
313
+ // ---------- termination ----------
314
+ /** Process groups of the network git children now running. */
315
+ const inFlightChildren = new Set();
316
+ /** Sync locks this module holds, wrapped by `trackLock`. */
317
+ const heldLocks = new Set();
318
+ const TERMINATION_SIGNALS = ["SIGTERM", "SIGINT"];
319
+ const terminationListeners = {
320
+ SIGTERM: () => onTerminationSignal("SIGTERM"),
321
+ SIGINT: () => onTerminationSignal("SIGINT"),
322
+ };
323
+ let guardInstalled = false;
324
+ /**
325
+ * Kill every network git child's process group and release every sync lock
326
+ * this process holds. The daemon's SIGTERM handler calls it; so do the guard
327
+ * listeners below, for processes (the agent worker) with no handler of their own.
328
+ */
329
+ function abortInFlightSync() {
330
+ const children = inFlightChildren.size;
331
+ const locks = heldLocks.size;
332
+ if (children === 0 && locks === 0)
333
+ return;
334
+ for (const pid of inFlightChildren)
335
+ killSyncGroup(pid);
336
+ inFlightChildren.clear();
337
+ for (const handle of [...heldLocks])
338
+ handle.release();
339
+ (0, runtime_1.emitNervesEvent)({
340
+ level: "warn",
341
+ component: "heart",
342
+ event: "heart.sync_aborted",
343
+ message: "sync: killed in-flight git and released the bundle lock",
344
+ meta: { children, locks },
345
+ });
346
+ }
347
+ /**
348
+ * The guard is prepended, so it runs before every other listener for the
349
+ * signal, `once` wrappers included, and sees them all still registered. It
350
+ * kills sync's git children and releases the locks synchronously, then steps
351
+ * aside: another component's own shutdown runs to completion. Only when no
352
+ * other listener exists does it re-raise, restoring the default exit.
353
+ */
354
+ function onTerminationSignal(signal) {
355
+ const others = process.rawListeners(signal).filter((listener) => listener !== terminationListeners[signal]);
356
+ abortInFlightSync();
357
+ removeTerminationGuard();
358
+ /* v8 ignore next -- re-raising ends the process; the SIGTERM child-process test covers it @preserve */
359
+ if (others.length === 0)
360
+ process.kill(process.pid, signal);
361
+ }
362
+ function installTerminationGuard() {
363
+ if (guardInstalled)
364
+ return;
365
+ guardInstalled = true;
366
+ for (const signal of TERMINATION_SIGNALS)
367
+ process.prependListener(signal, terminationListeners[signal]);
368
+ process.on("exit", abortInFlightSync);
369
+ }
370
+ function removeTerminationGuard() {
371
+ if (!guardInstalled)
372
+ return;
373
+ guardInstalled = false;
374
+ for (const signal of TERMINATION_SIGNALS)
375
+ process.removeListener(signal, terminationListeners[signal]);
376
+ process.removeListener("exit", abortInFlightSync);
377
+ }
378
+ /** While any lock is held, a signal or an exit kills sync's children and releases the lock. */
379
+ function trackLock(handle) {
380
+ let released = false;
381
+ const tracked = {
382
+ release() {
383
+ if (released)
384
+ return;
385
+ released = true;
386
+ heldLocks.delete(tracked);
387
+ try {
388
+ handle.release();
389
+ }
390
+ finally {
391
+ if (heldLocks.size === 0)
392
+ removeTerminationGuard();
393
+ }
394
+ },
395
+ };
396
+ heldLocks.add(tracked);
397
+ installTerminationGuard();
398
+ return tracked;
399
+ }
400
+ function describeAsyncFailure(step, run, timeoutMs) {
401
+ if (run.timedOut)
402
+ return `git ${step} timed out after ${timeoutMs} ms`;
403
+ if (run.aborted)
404
+ return `git ${step} aborted`;
405
+ return `git ${step} failed: ${run.stderr.trim()}`;
406
+ }
407
+ function hasRemote(agentRoot, agentName, config) {
408
+ return git(agentRoot, agentName, ["remote"]).split("\n").map((line) => line.trim()).includes(config.remote);
409
+ }
410
+ function remoteTrackingRef(config) {
411
+ return `refs/remotes/${config.remote}/${branchOf(config)}`;
412
+ }
413
+ function refExists(agentRoot, agentName, ref) {
414
+ return runGit(agentRoot, agentName, ["rev-parse", "--verify", "--quiet", ref]).status === 0;
415
+ }
416
+ /** Commits on HEAD that the last fetched remote branch lacks. An unborn HEAD has none. */
417
+ function commitsAhead(agentRoot, agentName, config) {
418
+ if (!refExists(agentRoot, agentName, "HEAD"))
419
+ return 0;
420
+ const ref = remoteTrackingRef(config);
421
+ const range = refExists(agentRoot, agentName, ref) ? `${ref}..HEAD` : "HEAD";
422
+ return Number.parseInt(git(agentRoot, agentName, ["rev-list", "--count", range]).trim(), 10);
423
+ }
424
+ function unmergedPaths(agentRoot, agentName) {
425
+ return git(agentRoot, agentName, ["diff", "--name-only", "--diff-filter=U", "-z"]).split("\0").filter((p) => p.length > 0);
426
+ }
427
+ function gitDirEntry(agentRoot, name) {
428
+ return path.join(agentRoot, ".git", name);
429
+ }
430
+ // ---------- persistent records ----------
431
+ function readPreviousFailure(agentRoot) {
432
+ try {
433
+ return (0, sync_health_1.parseBundleSyncFailure)(JSON.parse(fs.readFileSync(pendingSyncPath(agentRoot), "utf-8")));
434
+ }
435
+ catch {
436
+ return null;
437
+ }
438
+ }
439
+ /** The repair ref the pending record names, if any. */
440
+ function pendingRepairRef(agentRoot) {
441
+ try {
442
+ const record = JSON.parse(fs.readFileSync(pendingSyncPath(agentRoot), "utf-8"));
443
+ return typeof record?.repairRef === "string" ? record.repairRef : null;
444
+ }
445
+ catch {
446
+ return null;
447
+ }
448
+ }
449
+ function writePendingSync(agentRoot, error, classification, conflictFiles, repairRef) {
450
+ const previous = readPreviousFailure(agentRoot);
451
+ const failedAt = new Date().toISOString();
452
+ const previousFirst = previous && previous.firstFailedAt !== "unknown" ? previous.firstFailedAt : null;
453
+ fs.mkdirSync(path.join(agentRoot, "state"), { recursive: true });
454
+ const record = {
455
+ error,
456
+ failedAt,
457
+ classification,
458
+ conflictFiles,
459
+ firstFailedAt: previousFirst ?? failedAt,
460
+ consecutiveFailures: (previous?.consecutiveFailures ?? 0) + 1,
461
+ ...(repairRef === undefined ? {} : { repairRef }),
462
+ };
463
+ fs.writeFileSync(pendingSyncPath(agentRoot), JSON.stringify(record, null, 2), "utf-8");
464
+ }
465
+ function recordPendingSync(agentRoot, error, classification, conflictFiles, repairRef) {
466
+ // An incomplete restore outranks later failures: it names the ref holding the agent's work.
467
+ if (classification !== "repair_restore_incomplete" && readPreviousFailure(agentRoot)?.classification === "repair_restore_incomplete")
468
+ return;
469
+ try {
470
+ writePendingSync(agentRoot, error, classification, conflictFiles, repairRef);
471
+ }
472
+ catch {
473
+ // Best effort: the caller's nerves event still records the failure.
474
+ }
475
+ }
476
+ /**
477
+ * Remove `state/pending-sync.json` after a sync that reached the remote (or
478
+ * committed, for a local-only bundle). The record otherwise stays, so the
479
+ * start-of-turn packet, `ouro status` and the daemon health check keep
480
+ * reporting the failure until it is fixed.
481
+ */
482
+ function clearPendingSync(agentRoot) {
483
+ try {
484
+ fs.rmSync(pendingSyncPath(agentRoot), { force: true });
485
+ }
486
+ catch {
487
+ // Best effort: a stale record is reported again and cleared next time.
488
+ }
489
+ }
490
+ /**
491
+ * Clear the pending record after a sync step succeeded, unless it reports an
492
+ * incomplete restore: that one stays until a person clears it
493
+ * (`clearPendingSync`), since the agent's work is only in the named ref.
494
+ */
495
+ function settlePendingSync(agentRoot) {
496
+ if (readPreviousFailure(agentRoot)?.classification === "repair_restore_incomplete")
497
+ return;
498
+ clearPendingSync(agentRoot);
499
+ }
500
+ function writeSkippedSyncFiles(agentRoot, files) {
501
+ try {
502
+ if (files.length === 0) {
503
+ fs.rmSync(skippedSyncFilesPath(agentRoot), { force: true });
504
+ return;
505
+ }
506
+ fs.mkdirSync(path.join(agentRoot, "state"), { recursive: true });
507
+ const record = {
508
+ updatedAt: new Date().toISOString(),
509
+ limitBytes: exports.SYNC_FILE_SIZE_LIMIT_BYTES,
510
+ files,
511
+ };
512
+ fs.writeFileSync(skippedSyncFilesPath(agentRoot), JSON.stringify(record, null, 2), "utf-8");
513
+ }
514
+ catch {
515
+ // Best effort: the nerves event emitted by the caller names the files.
516
+ }
517
+ }
518
+ function readConflictState(agentRoot) {
519
+ try {
520
+ const parsed = JSON.parse(fs.readFileSync(syncConflictStatePath(agentRoot), "utf-8"));
521
+ return typeof parsed.hash === "string" ? parsed : null;
522
+ }
523
+ catch {
524
+ return null;
525
+ }
526
+ }
527
+ function writeConflictState(agentRoot, state) {
528
+ try {
529
+ fs.mkdirSync(path.join(agentRoot, "state"), { recursive: true });
530
+ fs.writeFileSync(syncConflictStatePath(agentRoot), JSON.stringify(state, null, 2), "utf-8");
531
+ }
532
+ catch {
533
+ // Best effort: without the record the owner may be told twice.
534
+ }
535
+ }
536
+ function clearConflictState(agentRoot) {
537
+ try {
538
+ fs.rmSync(syncConflictStatePath(agentRoot), { force: true });
539
+ }
540
+ catch {
541
+ // Best effort: a stale record only suppresses a repeat notice.
542
+ }
543
+ }
544
+ // ---------- owner notices ----------
545
+ let ownerNotifier = null;
546
+ /**
547
+ * Replace how sync tells the owner. Null restores the default: Telegram when
548
+ * the agent has it; otherwise the notice goes to the agent's private-runtime
549
+ * pending queue, and the agent tells the owner through whatever sense it has.
550
+ */
551
+ function setSyncOwnerNotifier(notifier) {
552
+ ownerNotifier = notifier;
553
+ }
554
+ async function defaultOwnerNotifier(agentRoot, agentName, notice) {
555
+ try {
556
+ const { sendTelegramOwnerNotice } = await Promise.resolve().then(() => __importStar(require("../senses/telegram")));
557
+ await sendTelegramOwnerNotice(agentName, { ...notice, signal: AbortSignal.timeout(30_000) });
558
+ return;
559
+ }
560
+ catch (err) {
561
+ // Not every agent has Telegram: queue it once, for the agent to relay.
562
+ (0, runtime_1.emitNervesEvent)({
563
+ component: "heart",
564
+ event: "heart.sync_owner_notice_telegram_skipped",
565
+ message: "sync: Telegram did not send the owner notice; queued it instead",
566
+ meta: { agentRoot, noticeId: notice.noticeId, error: errorText(err) },
567
+ });
568
+ }
569
+ const { getPrivateRuntimePendingDir, queuePendingMessageOnce } = await Promise.resolve().then(() => __importStar(require("../mind/pending")));
570
+ queuePendingMessageOnce(getPrivateRuntimePendingDir(agentName, agentRoot), {
571
+ from: agentName,
572
+ friendId: "self",
573
+ channel: "inner",
574
+ key: "dialog",
575
+ content: `${notice.text}\n\ni should tell my owner.`,
576
+ timestamp: Date.now(),
577
+ packetId: notice.noticeId,
578
+ });
579
+ }
580
+ /** Fire and forget: a turn never waits on a notice. */
581
+ function tellOwner(agentRoot, agentName, notice) {
582
+ (0, runtime_1.emitNervesEvent)({
583
+ component: "heart",
584
+ event: "heart.sync_owner_notice",
585
+ message: "sync: telling the owner",
586
+ meta: { agentRoot, noticeId: notice.noticeId },
587
+ });
588
+ const send = ownerNotifier ?? ((name, body) => defaultOwnerNotifier(agentRoot, name, body));
589
+ void Promise.resolve()
590
+ .then(() => send(agentName, notice))
591
+ .catch((err) => {
592
+ (0, runtime_1.emitNervesEvent)({
593
+ level: "warn",
594
+ component: "heart",
595
+ event: "heart.sync_owner_notice_error",
596
+ message: "sync: could not tell the owner",
597
+ meta: { agentRoot, noticeId: notice.noticeId, error: errorText(err) },
598
+ });
599
+ });
600
+ }
601
+ /** Read the pause marker without changing anything. */
602
+ function readPauseMarker(agentRoot) {
603
+ let raw;
604
+ try {
605
+ raw = fs.readFileSync(syncPausedPath(agentRoot), "utf-8");
606
+ }
607
+ catch {
608
+ return { kind: "none" };
609
+ }
610
+ let marker;
611
+ try {
612
+ marker = JSON.parse(raw);
613
+ }
614
+ catch {
615
+ marker = null;
616
+ }
617
+ const expiresAt = marker?.expiresAt;
618
+ const expiresMs = typeof expiresAt === "string" ? Date.parse(expiresAt) : Number.NaN;
619
+ const startedMs = typeof marker?.startedAt === "string" ? Date.parse(marker.startedAt) : Number.NaN;
620
+ const now = Date.now();
621
+ const longestEnd = (Number.isFinite(startedMs) ? startedMs : now) + exports.SYNC_PAUSE_MAX_MS;
622
+ if (Number.isFinite(expiresMs) && expiresMs > now && expiresMs <= longestEnd)
623
+ return { kind: "active", expiresAt: expiresAt };
624
+ return { kind: "garbage", expiresAt };
625
+ }
626
+ function activePause(agentRoot) {
627
+ const markerPath = syncPausedPath(agentRoot);
628
+ const state = readPauseMarker(agentRoot);
629
+ if (state.kind === "none")
630
+ return null;
631
+ if (state.kind === "active")
632
+ return state.expiresAt;
633
+ const expiresAt = state.expiresAt;
634
+ try {
635
+ fs.rmSync(markerPath, { force: true });
636
+ }
637
+ catch (err) {
638
+ (0, runtime_1.emitNervesEvent)({
639
+ level: "warn",
640
+ component: "heart",
641
+ event: "heart.sync_pause_clear_failed",
642
+ message: "sync: could not remove an expired or unreadable pause marker",
643
+ meta: { agentRoot, error: errorText(err) },
644
+ });
645
+ return null;
646
+ }
647
+ (0, runtime_1.emitNervesEvent)({
648
+ component: "heart",
649
+ event: "heart.sync_pause_expired",
650
+ message: "sync: removed an expired or unreadable pause marker",
651
+ meta: { agentRoot, expiresAt: typeof expiresAt === "string" ? expiresAt : null },
652
+ });
653
+ return null;
654
+ }
655
+ function pausedResult(agentRoot, step) {
656
+ const until = activePause(agentRoot);
657
+ if (until === null)
658
+ return null;
659
+ (0, runtime_1.emitNervesEvent)({
660
+ component: "heart",
661
+ event: "heart.sync_paused",
662
+ message: `${step} skipped: sync is paused`,
663
+ meta: { agentRoot, expiresAt: until },
664
+ });
665
+ return { ok: true, paused: true, pausedUntil: until };
666
+ }
667
+ /** Parse `git diff --cached --name-status -z` output. */
668
+ function parseStagedChanges(output) {
669
+ const tokens = output.split("\0").filter((entry) => entry.length > 0);
670
+ const changes = [];
671
+ for (let index = 0; index < tokens.length;) {
672
+ const status = tokens[index];
673
+ if (status.startsWith("R") || status.startsWith("C")) {
674
+ changes.push({ status, fromPath: tokens[index + 1], path: tokens[index + 2] });
675
+ index += 3;
676
+ }
677
+ else {
678
+ changes.push({ status, path: tokens[index + 1] });
679
+ index += 2;
680
+ }
681
+ }
682
+ return changes;
683
+ }
684
+ /**
685
+ * Unstage every staged file larger than `SYNC_FILE_SIZE_LIMIT_BYTES`. The
686
+ * file stays in the working tree and the previous committed version, if any,
687
+ * stays in the index, so nothing is deleted on other machines. For a rename,
688
+ * the deletion of the old path is unstaged too; otherwise the remote would
689
+ * lose the old path while never receiving the new one.
690
+ */
691
+ function unstageOversizedFiles(agentRoot, agentName) {
692
+ const changes = parseStagedChanges(git(agentRoot, agentName, ["diff", "--cached", "--name-status", "-M", "-z"]));
693
+ const oversized = [];
694
+ const unstage = [];
695
+ for (const change of changes) {
696
+ if (change.status.startsWith("D"))
697
+ continue;
698
+ let bytes;
699
+ try {
700
+ bytes = fs.lstatSync(path.join(agentRoot, change.path)).size;
701
+ }
702
+ catch {
703
+ /* v8 ignore next -- the file was removed between `git add` and this stat @preserve */
704
+ continue;
705
+ }
706
+ if (bytes <= exports.SYNC_FILE_SIZE_LIMIT_BYTES)
707
+ continue;
708
+ oversized.push({ path: change.path, bytes });
709
+ unstage.push(change.path);
710
+ if (change.status.startsWith("R") && change.fromPath)
711
+ unstage.push(change.fromPath);
712
+ }
713
+ if (oversized.length === 0)
714
+ return oversized;
715
+ // An unborn HEAD has nothing to reset to; `rm --cached` drops the new entry instead.
716
+ if (refExists(agentRoot, agentName, "HEAD"))
717
+ gitOnPaths(agentRoot, agentName, ["reset", "-q"], unstage);
718
+ else
719
+ gitOnPaths(agentRoot, agentName, ["rm", "-q", "--cached"], unstage);
720
+ (0, runtime_1.emitNervesEvent)({
721
+ level: "warn",
722
+ component: "heart",
723
+ event: "heart.sync_oversized_files_skipped",
724
+ message: "sync: left files over the sync size limit uncommitted",
725
+ meta: { agentRoot, limitBytes: exports.SYNC_FILE_SIZE_LIMIT_BYTES, files: oversized },
726
+ });
727
+ return oversized;
728
+ }
729
+ function hasStagedChanges(agentRoot, agentName) {
730
+ return runGit(agentRoot, agentName, ["diff", "--cached", "--quiet"]).status !== 0;
731
+ }
732
+ /**
733
+ * Git holds `.git/index.lock` for milliseconds to seconds, and every local git
734
+ * call in this module has a timeout well under this. A lock older than this was
735
+ * left behind by a git process that died mid-write; while it exists every
736
+ * `git add` fails, which silently stopped one bundle's sync for three months.
737
+ */
738
+ exports.STALE_INDEX_LOCK_MS = 10 * 60 * 1000;
739
+ // Only an empty lock is removed: git writes the new index into the lock right
740
+ // after creating it, while a live `git commit` waiting in an editor holds a
741
+ // full, unchanging lock that can be older than the threshold. When `.git` is a
742
+ // gitdir file (worktree), statSync fails and the lock is left alone.
743
+ function clearStaleIndexLock(agentRoot) {
744
+ const lockPath = gitDirEntry(agentRoot, "index.lock");
745
+ let ageMs;
746
+ try {
747
+ const stat = fs.statSync(lockPath);
748
+ if (stat.size !== 0)
749
+ return;
750
+ ageMs = Date.now() - stat.mtimeMs;
751
+ }
752
+ catch {
753
+ return;
754
+ }
755
+ if (ageMs < exports.STALE_INDEX_LOCK_MS)
756
+ return;
757
+ try {
758
+ fs.rmSync(lockPath, { force: true });
759
+ }
760
+ catch (err) {
761
+ (0, runtime_1.emitNervesEvent)({
762
+ level: "warn",
763
+ component: "heart",
764
+ event: "heart.sync_stale_lock_clear_failed",
765
+ message: "sync: could not remove stale git index.lock",
766
+ meta: { agentRoot, lockPath, error: errorText(err) },
767
+ });
768
+ return;
769
+ }
770
+ (0, runtime_1.emitNervesEvent)({
771
+ level: "warn",
772
+ component: "heart",
773
+ event: "heart.sync_stale_lock_cleared",
774
+ message: "sync: removed stale git index.lock",
775
+ meta: { agentRoot, lockPath, ageMs },
776
+ });
777
+ }
778
+ /**
779
+ * Commit every change in the bundle except files over the size limit, as the
780
+ * agent. Synchronous, so no turn's file writes interleave with it. Throws with
781
+ * git's message when git fails; never commits unmerged paths (conflict markers).
782
+ */
783
+ function commitLocalChanges(agentRoot, agentName, message) {
784
+ const status = git(agentRoot, agentName, ["status", "--porcelain"]).trim();
785
+ if (status.length === 0) {
786
+ // Nothing to commit, so nothing is skipped for size any more.
787
+ writeSkippedSyncFiles(agentRoot, []);
788
+ return { committed: false, skipped: [] };
789
+ }
790
+ const unmerged = unmergedPaths(agentRoot, agentName);
791
+ if (unmerged.length > 0)
792
+ throw new Error(`unmerged paths in the index: ${unmerged.join(", ")}`);
793
+ clearStaleIndexLock(agentRoot);
794
+ git(agentRoot, agentName, ["add", "-A"]);
795
+ const skipped = unstageOversizedFiles(agentRoot, agentName);
796
+ writeSkippedSyncFiles(agentRoot, skipped);
797
+ if (!hasStagedChanges(agentRoot, agentName))
798
+ return { committed: false, skipped };
799
+ // -q: without it git prints a create/delete line per file, which overran
800
+ // the output buffer on a 30,000-file commit after the commit had landed.
801
+ git(agentRoot, agentName, ["commit", "-q", "-m", message]);
802
+ return { committed: true, skipped };
803
+ }
804
+ // ---------- integrate ----------
805
+ /**
806
+ * Check whether the bundle is initialized as a git repo. The error reaches the
807
+ * agent's start-of-turn packet as a sync warning.
808
+ */
809
+ exports.NOT_A_REPO_ERROR = "my bundle is not a git repository";
810
+ function ensureGitRepo(agentRoot, step) {
811
+ if (fs.existsSync(path.join(agentRoot, ".git")))
812
+ return { ok: true };
813
+ (0, runtime_1.emitNervesEvent)({
814
+ level: "warn",
815
+ component: "heart",
816
+ event: "heart.sync_not_a_repo",
817
+ message: `${step} failed: bundle is not a git repo`,
818
+ meta: { agentRoot },
819
+ });
820
+ return { ok: false, error: exports.NOT_A_REPO_ERROR };
821
+ }
822
+ const REPAIR_REF_PREFIX = "refs/ouro-sync/repair/";
823
+ /** Repair refs kept regardless of age. */
824
+ const REPAIR_REFS_KEPT = 10;
825
+ /** Repair refs younger than this are kept regardless of count. */
826
+ const REPAIR_REF_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
827
+ /** Repair refs, newest first (names are ISO timestamps, so they sort by time). */
828
+ function listRepairRefs(agentRoot, agentName) {
829
+ return git(agentRoot, agentName, ["for-each-ref", "--sort=-refname", "--format=%(refname) %(tree) %(committerdate:unix)", REPAIR_REF_PREFIX])
830
+ .split("\n")
831
+ .filter((line) => line.length > 0)
832
+ .map((line) => {
833
+ const [name, tree, seconds] = line.split(" ");
834
+ return { name, tree, committedMs: Number(seconds) * 1000 };
835
+ });
836
+ }
837
+ /** Drop repair refs past the newest ten that are 30 days old, except one the pending record names. */
838
+ function pruneRepairRefs(agentRoot, agentName, refs) {
839
+ const protectedRef = pendingRepairRef(agentRoot);
840
+ const now = Date.now();
841
+ const doomed = refs.filter((ref, i) => i >= REPAIR_REFS_KEPT && now - ref.committedMs >= REPAIR_REF_MAX_AGE_MS && ref.name !== protectedRef);
842
+ if (doomed.length === 0)
843
+ return;
844
+ git(agentRoot, agentName, ["update-ref", "--stdin"], undefined, doomed.map((ref) => `delete ${ref.name}\n`).join(""));
845
+ }
846
+ /**
847
+ * Commit the whole working tree, as it is now, to `refs/ouro-sync/repair/<ts>`
848
+ * without touching the real index. Its parent is the current HEAD, which for a
849
+ * stranded rebase is the detached HEAD, so commits made there stay reachable.
850
+ * When the newest repair ref already holds this tree, that ref is reused.
851
+ */
852
+ function snapshotWorkingTree(agentRoot, agentName) {
853
+ const indexFile = gitDirEntry(agentRoot, `ouro-sync-repair-${process.pid}.index`);
854
+ const env = { GIT_INDEX_FILE: indexFile };
855
+ let tree;
856
+ try {
857
+ // Seed from HEAD so tracked files that match an ignore rule are kept.
858
+ git(agentRoot, agentName, ["read-tree", "HEAD"], env);
859
+ git(agentRoot, agentName, ["add", "-A"], env);
860
+ tree = git(agentRoot, agentName, ["write-tree"], env).trim();
861
+ }
862
+ finally {
863
+ fs.rmSync(indexFile, { force: true });
864
+ }
865
+ const existing = listRepairRefs(agentRoot, agentName);
866
+ if (existing[0]?.tree === tree)
867
+ return existing[0].name;
868
+ const commit = git(agentRoot, agentName, ["commit-tree", tree, "-p", "HEAD", "-m", "sync: repair snapshot"]).trim();
869
+ // A refname may not contain ":".
870
+ const ref = `${REPAIR_REF_PREFIX}${new Date().toISOString().replace(/:/g, "-")}`;
871
+ git(agentRoot, agentName, ["update-ref", ref, commit]);
872
+ pruneRepairRefs(agentRoot, agentName, [{ name: ref, tree, committedMs: Date.now() }, ...existing]);
873
+ return ref;
874
+ }
875
+ /** `merge --abort`, else `reset --merge`, else `reset --hard HEAD` (the snapshot restores the rest). */
876
+ function abortMergeThoroughly(agentRoot, agentName) {
877
+ if (runGit(agentRoot, agentName, exports.__testHooks.mergeAbortArgs ?? ["merge", "--abort"]).status === 0)
878
+ return "merge --abort";
879
+ if (runGit(agentRoot, agentName, ["reset", "-q", "--merge"]).status === 0)
880
+ return "reset --merge";
881
+ git(agentRoot, agentName, ["reset", "-q", "--hard", "HEAD"]);
882
+ return "reset --hard";
883
+ }
884
+ /** Paths whose content in the snapshot differs from HEAD. */
885
+ function pathsChangedInSnapshot(agentRoot, agentName, ref) {
886
+ return git(agentRoot, agentName, ["diff", "--name-only", "--no-renames", "-z", "HEAD", ref])
887
+ .split("\0")
888
+ .filter((p) => p.length > 0);
889
+ }
890
+ const refTimestamp = (ref) => ref.slice(REPAIR_REF_PREFIX.length);
891
+ /**
892
+ * Abort a rebase or merge left in progress, keeping the work done since. The
893
+ * working tree is snapshotted first; after the abort, every path the operation
894
+ * did not conflict on is restored from it. Conflicted paths keep HEAD's
895
+ * version; edits to them stay in the snapshot ref, which the owner is told about.
896
+ *
897
+ * Returns null when there was nothing to repair or the repair completed, else
898
+ * the failure to report. A failed snapshot touches nothing, so the stranded
899
+ * state stays exactly as found for a later attempt.
900
+ */
901
+ function repairInterrupted(agentRoot, agentName) {
902
+ const rebase = fs.existsSync(gitDirEntry(agentRoot, "rebase-merge")) || fs.existsSync(gitDirEntry(agentRoot, "rebase-apply"));
903
+ const merge = fs.existsSync(gitDirEntry(agentRoot, "MERGE_HEAD"));
904
+ if (!rebase && !merge)
905
+ return null;
906
+ const operation = rebase ? "rebase" : "merge";
907
+ const conflicted = unmergedPaths(agentRoot, agentName);
908
+ let ref;
909
+ try {
910
+ ref = snapshotWorkingTree(agentRoot, agentName);
911
+ }
912
+ catch (err) {
913
+ return reportSnapshotFailure(agentRoot, agentName, operation, errorText(err));
914
+ }
915
+ let abortedWith = "rebase --abort";
916
+ if (rebase)
917
+ git(agentRoot, agentName, ["rebase", "--abort"]);
918
+ else
919
+ abortedWith = abortMergeThoroughly(agentRoot, agentName);
920
+ const changed = pathsChangedInSnapshot(agentRoot, agentName, ref);
921
+ const skip = new Set(conflicted);
922
+ const restore = changed.filter((p) => !skip.has(p));
923
+ const kept = changed.filter((p) => skip.has(p));
924
+ try {
925
+ if (restore.length > 0)
926
+ gitOnPaths(agentRoot, agentName, ["restore", `--source=${ref}`, "--worktree"], restore);
927
+ }
928
+ catch (err) {
929
+ return reportRestoreIncomplete(agentRoot, agentName, operation, ref, errorText(err));
930
+ }
931
+ (0, runtime_1.emitNervesEvent)({
932
+ level: "warn",
933
+ component: "heart",
934
+ event: "heart.sync_repaired",
935
+ message: "sync: aborted a rebase or merge left in progress",
936
+ meta: { agentRoot, rebase, merge, ref, conflicted, kept, abortedWith },
937
+ });
938
+ if (kept.length === 0)
939
+ return null;
940
+ recordPendingSync(agentRoot, `an interrupted ${operation} was aborted; edits to ${kept.join(", ")} are kept in ${ref}`, "unknown", kept, ref);
941
+ tellOwner(agentRoot, agentName, {
942
+ noticeId: `sync-repair:${agentName}:${refTimestamp(ref)}`,
943
+ text: `Sync found an interrupted ${operation} in ${agentName}'s bundle and aborted it. Edits to ${kept.join(", ")} made since are kept in ${ref}.`,
944
+ });
945
+ return null;
946
+ }
947
+ function reportSnapshotFailure(agentRoot, agentName, operation, gitError) {
948
+ const error = `an interrupted ${operation} was left in place: the working tree could not be saved first (${gitError})`;
949
+ const previous = readPreviousFailure(agentRoot);
950
+ const repeat = previous?.classification === "repair_snapshot_failed" && previous.error === error;
951
+ recordPendingSync(agentRoot, error, "repair_snapshot_failed", []);
952
+ (0, runtime_1.emitNervesEvent)({
953
+ level: "warn",
954
+ component: "heart",
955
+ event: "heart.sync_repair_snapshot_failed",
956
+ message: "sync: could not snapshot before repair; nothing was changed",
957
+ meta: { agentRoot, operation, error: gitError },
958
+ });
959
+ if (!repeat) {
960
+ tellOwner(agentRoot, agentName, {
961
+ noticeId: `sync-repair-snapshot:${agentName}:${(0, crypto_1.createHash)("sha256").update(error).digest("hex").slice(0, 16)}`,
962
+ text: `Sync found an interrupted ${operation} in ${agentName}'s bundle but could not save the working tree first, so it left everything as it is and stopped syncing. git said: ${gitError}`,
963
+ });
964
+ }
965
+ return { ok: false, error };
966
+ }
967
+ function reportRestoreIncomplete(agentRoot, agentName, operation, ref, gitError) {
968
+ const error = `an interrupted ${operation} was aborted but not every file was put back; the full working tree is in ${ref} (${gitError})`;
969
+ recordPendingSync(agentRoot, error, "repair_restore_incomplete", [], ref);
970
+ (0, runtime_1.emitNervesEvent)({
971
+ level: "warn",
972
+ component: "heart",
973
+ event: "heart.sync_repair_restore_incomplete",
974
+ message: "sync: repair could not restore every file from its snapshot",
975
+ meta: { agentRoot, operation, ref, error: gitError },
976
+ });
977
+ tellOwner(agentRoot, agentName, {
978
+ noticeId: `sync-repair-restore:${agentName}:${refTimestamp(ref)}`,
979
+ text: `Sync aborted an interrupted ${operation} in ${agentName}'s bundle but could not put every file back. The working tree as it was is in ${ref}; git said: ${gitError}`,
980
+ });
981
+ return { ok: false, error };
982
+ }
983
+ function abortMergeIfAny(agentRoot, agentName) {
984
+ if (fs.existsSync(gitDirEntry(agentRoot, "MERGE_HEAD")))
985
+ runGit(agentRoot, agentName, ["merge", "--abort"]);
986
+ }
987
+ /**
988
+ * Redo a merge whose conflicts are all in `psyche/` so the owner's conflicting
989
+ * hunks win and the agent's other hunks stay. Delete/modify conflicts take the
990
+ * owner's side too: removed where the owner deleted, the owner's file where the
991
+ * owner modified.
992
+ */
993
+ function mergeOwnerWins(agentRoot, agentName, ref, paths) {
994
+ git(agentRoot, agentName, ["merge", "--abort"]);
995
+ const merge = runGit(agentRoot, agentName, ["merge", "--no-edit", "--ff", "-X", "theirs", "-m", exports.OWNER_WINS_MESSAGE, ref]);
996
+ if (merge.status !== 0) {
997
+ const remaining = unmergedPaths(agentRoot, agentName);
998
+ if (remaining.length === 0)
999
+ throw gitFailure(["merge"], merge);
1000
+ for (const file of remaining) {
1001
+ const stages = git(agentRoot, agentName, ["--literal-pathspecs", "ls-files", "-u", "--", file]);
1002
+ const theirsHasIt = stages.split("\n").some((line) => /^\d+ [0-9a-f]+ 3\t/.test(line));
1003
+ if (theirsHasIt) {
1004
+ git(agentRoot, agentName, ["--literal-pathspecs", "checkout", "--theirs", "--", file]);
1005
+ git(agentRoot, agentName, ["--literal-pathspecs", "add", "--", file]);
1006
+ }
1007
+ else {
1008
+ git(agentRoot, agentName, ["--literal-pathspecs", "rm", "-q", "--", file]);
1009
+ }
1010
+ }
1011
+ git(agentRoot, agentName, ["commit", "-q", "-m", exports.OWNER_WINS_MESSAGE]);
1012
+ }
1013
+ (0, runtime_1.emitNervesEvent)({
1014
+ component: "heart",
1015
+ event: "heart.sync_owner_wins",
1016
+ message: "sync: merged with the owner's psyche edits winning conflicts",
1017
+ meta: { agentRoot, paths },
1018
+ });
1019
+ }
1020
+ function conflictHash(paths) {
1021
+ return (0, crypto_1.createHash)("sha256").update(paths.join("\0")).digest("hex").slice(0, 16);
1022
+ }
1023
+ function handleNonPsycheConflict(agentRoot, config, agentName, paths) {
1024
+ git(agentRoot, agentName, ["merge", "--abort"]);
1025
+ const sorted = [...paths].sort();
1026
+ const hash = conflictHash(sorted);
1027
+ const error = `merge with ${config.remote}/${branchOf(config)} conflicts in ${sorted.join(", ")}; the merge was aborted and local commits are kept`;
1028
+ recordPendingSync(agentRoot, error, "pull_rebase_conflict", sorted);
1029
+ (0, runtime_1.emitNervesEvent)({
1030
+ level: "warn",
1031
+ component: "heart",
1032
+ event: "heart.sync_conflict",
1033
+ message: "sync: merge conflict outside psyche/, merge aborted",
1034
+ meta: { agentRoot, conflictFiles: sorted, hash },
1035
+ });
1036
+ const previous = readConflictState(agentRoot);
1037
+ if (previous?.hash === hash)
1038
+ return { result: { ok: false, error }, conflictBranch: previous };
1039
+ writeConflictState(agentRoot, { hash, paths: sorted });
1040
+ tellOwner(agentRoot, agentName, {
1041
+ noticeId: `sync-conflict:${agentName}:${hash}`,
1042
+ text: `Bundle sync for ${agentName} stopped: ${sorted.join(", ")} changed on both sides. My changes stay committed locally until the conflict is merged.`,
1043
+ });
1044
+ return { result: { ok: false, error } };
1045
+ }
1046
+ /** Record a successful integrate: a resolved conflict is over, and a synced bundle has nothing pending. */
1047
+ function settleIntegrated(agentRoot, config, agentName) {
1048
+ clearConflictState(agentRoot);
1049
+ const pending = readPreviousFailure(agentRoot);
1050
+ if (!pending)
1051
+ return;
1052
+ if (pending.classification === "pull_rebase_conflict" || commitsAhead(agentRoot, agentName, config) === 0)
1053
+ settlePendingSync(agentRoot);
1054
+ }
1055
+ function integrateCore(agentRoot, config, agentName, commitMessage = "sync: pre-turn update") {
1056
+ // Outside the try: its catch aborts a merge, which must never run before a snapshot.
1057
+ const repairFailure = repairInterrupted(agentRoot, agentName);
1058
+ if (repairFailure)
1059
+ return { result: repairFailure };
1060
+ try {
1061
+ commitLocalChanges(agentRoot, agentName, commitMessage);
1062
+ const ref = remoteTrackingRef(config);
1063
+ if (!refExists(agentRoot, agentName, ref)) {
1064
+ settleIntegrated(agentRoot, config, agentName);
1065
+ return { result: { ok: true } };
1066
+ }
1067
+ const shortRef = `${config.remote}/${branchOf(config)}`;
1068
+ const merge = runGit(agentRoot, agentName, ["merge", "--no-edit", "--ff", shortRef]);
1069
+ if (merge.status !== 0) {
1070
+ const paths = unmergedPaths(agentRoot, agentName);
1071
+ if (paths.length === 0) {
1072
+ // Refused before touching anything ("would be overwritten"): no conflict state.
1073
+ abortMergeIfAny(agentRoot, agentName);
1074
+ const error = gitFailure(["merge"], merge).message;
1075
+ recordPendingSync(agentRoot, error, "unknown", []);
1076
+ (0, runtime_1.emitNervesEvent)({
1077
+ level: "warn",
1078
+ component: "heart",
1079
+ event: "heart.sync_merge_failed",
1080
+ message: "sync: merge refused",
1081
+ meta: { agentRoot, error },
1082
+ });
1083
+ return { result: { ok: false, error } };
1084
+ }
1085
+ if (paths.some((p) => !p.startsWith("psyche/")))
1086
+ return handleNonPsycheConflict(agentRoot, config, agentName, paths);
1087
+ mergeOwnerWins(agentRoot, agentName, shortRef, paths);
1088
+ }
1089
+ settleIntegrated(agentRoot, config, agentName);
1090
+ return { result: { ok: true } };
1091
+ }
1092
+ catch (err) {
1093
+ abortMergeIfAny(agentRoot, agentName);
1094
+ const error = errorText(err);
1095
+ recordPendingSync(agentRoot, error, "unknown", []);
1096
+ (0, runtime_1.emitNervesEvent)({
1097
+ level: "warn",
1098
+ component: "heart",
1099
+ event: "heart.sync_merge_failed",
1100
+ message: "sync: integrate failed",
1101
+ meta: { agentRoot, error },
1102
+ });
1103
+ return { result: { ok: false, error } };
1104
+ }
1105
+ }
1106
+ /**
1107
+ * Merge the fetched remote branch into the bundle. Synchronous. The caller
1108
+ * holds the bundle lock. Steps: repair an interrupted rebase or merge, commit
1109
+ * local changes, then merge `<remote>/<branch>` (fast-forward when possible).
1110
+ * Conflicts only in `psyche/` go to the owner; any other conflict aborts the
1111
+ * merge and is recorded in `state/pending-sync.json`.
1112
+ */
1113
+ function integrateFetched(agentRoot, config, agentName) {
1114
+ return integrateCore(agentRoot, config, agentName).result;
1115
+ }
1116
+ // ---------- network steps ----------
1117
+ async function fetchUnlocked(agentRoot, config, agentName, signal) {
1118
+ const branch = branchOf(config);
1119
+ const timeoutMs = exports.__testHooks.fetchTimeoutMs ?? exports.SYNC_FETCH_TIMEOUT_MS;
1120
+ const run = await runGitAsync(agentRoot, agentName, ["fetch", "--no-tags", config.remote, `+refs/heads/${branch}:${remoteTrackingRef(config)}`], timeoutMs, signal);
1121
+ // An empty remote has no branch yet; integrate then only commits.
1122
+ if (run.status === 0 || /couldn't find remote ref/i.test(run.stderr))
1123
+ return { ok: true };
1124
+ const error = describeAsyncFailure("fetch", run, timeoutMs);
1125
+ (0, runtime_1.emitNervesEvent)({
1126
+ level: "warn",
1127
+ component: "heart",
1128
+ event: "heart.sync_fetch_error",
1129
+ message: "sync: fetch failed",
1130
+ meta: { agentRoot, remote: config.remote, branch, error },
1131
+ });
1132
+ return { ok: false, error };
1133
+ }
1134
+ /** `git fetch <remote> <branch>` as a detached child, killed by process group at 10 s or on abort. The caller holds the lock. */
1135
+ function fetchRemote(agentRoot, config, signal) {
1136
+ return fetchUnlocked(agentRoot, config, agentNameFromRoot(agentRoot), signal);
1137
+ }
1138
+ /**
1139
+ * `git ls-remote <remote> [patterns]` the way sync's own fetch and push reach
1140
+ * the remote: the same flags and environment (in the container, the ouro-sync
1141
+ * ssh), detached and killed by process group at the fetch timeout. `refs` are
1142
+ * the ref names listed; an empty remote lists none.
1143
+ */
1144
+ async function listRemoteRefs(agentRoot, remote, patterns = [], signal) {
1145
+ const timeoutMs = exports.__testHooks.fetchTimeoutMs ?? exports.SYNC_FETCH_TIMEOUT_MS;
1146
+ const run = await runGitAsync(agentRoot, agentNameFromRoot(agentRoot), ["ls-remote", remote, ...patterns], timeoutMs, signal);
1147
+ if (run.status !== 0)
1148
+ return { ok: false, error: describeAsyncFailure("ls-remote", run, timeoutMs) };
1149
+ const refs = run.stdout.split("\n").map((line) => line.split("\t")[1]).filter((ref) => ref !== undefined && ref.length > 0);
1150
+ return { ok: true, refs };
1151
+ }
1152
+ /**
1153
+ * Spec R3.10: sync never makes the first push to an empty remote (one with no
1154
+ * refs) without the owner's yes. A remote branch fetched before means the
1155
+ * remote has history, and no network is needed. Otherwise the remote is asked;
1156
+ * when it is empty and no yes came with this push, the push is held and the
1157
+ * owner is told once. A remote that cannot be asked is not pushed to.
1158
+ */
1159
+ async function holdFirstPush(agentRoot, config, agentName, options, signal) {
1160
+ if (options.firstPushApproved === true || refExists(agentRoot, agentName, remoteTrackingRef(config)))
1161
+ return null;
1162
+ const listed = await listRemoteRefs(agentRoot, config.remote, [], signal);
1163
+ if (!listed.ok)
1164
+ return { ok: false, error: `could not check whether ${config.remote} is empty before pushing to it: ${listed.error}` };
1165
+ if (listed.refs.length > 0)
1166
+ return null;
1167
+ (0, runtime_1.emitNervesEvent)({
1168
+ component: "heart",
1169
+ event: "heart.sync_first_push_held",
1170
+ message: "sync: remote is empty; holding the first push for the owner's yes",
1171
+ meta: { agentRoot, remote: config.remote },
1172
+ });
1173
+ if (!fs.existsSync(firstPushHeldPath(agentRoot))) {
1174
+ fs.mkdirSync(path.dirname(firstPushHeldPath(agentRoot)), { recursive: true });
1175
+ fs.writeFileSync(firstPushHeldPath(agentRoot), `${JSON.stringify({ remote: config.remote, at: new Date().toISOString() })}\n`);
1176
+ tellOwner(agentRoot, agentName, { noticeId: `sync-first-push-held:${agentName}`, text: exports.FIRST_PUSH_HELD_NOTICE });
1177
+ }
1178
+ return { ok: false, firstPushHeld: true, error: `${config.remote} is empty, so the first push waits for the owner's yes (bundle_first_push_review)` };
1179
+ }
1180
+ async function pushUnlocked(agentRoot, config, agentName, signal, options = {}) {
1181
+ try {
1182
+ if (!hasRemote(agentRoot, agentName, config)) {
1183
+ settlePendingSync(agentRoot);
1184
+ (0, runtime_1.emitNervesEvent)({
1185
+ component: "heart",
1186
+ event: "heart.sync_push_end",
1187
+ message: "sync: committed locally, no remote configured",
1188
+ meta: { agentRoot },
1189
+ });
1190
+ return { ok: true };
1191
+ }
1192
+ const held = await holdFirstPush(agentRoot, config, agentName, options, signal);
1193
+ if (held)
1194
+ return held;
1195
+ const branch = branchOf(config);
1196
+ const timeoutMs = exports.__testHooks.pushTimeoutMs ?? exports.SYNC_PUSH_TIMEOUT_MS;
1197
+ const run = await runGitAsync(agentRoot, agentName, ["push", config.remote, `HEAD:refs/heads/${branch}`], timeoutMs, signal);
1198
+ if (run.status === 0) {
1199
+ settlePendingSync(agentRoot);
1200
+ fs.rmSync(firstPushHeldPath(agentRoot), { force: true });
1201
+ (0, runtime_1.emitNervesEvent)({
1202
+ component: "heart",
1203
+ event: "heart.sync_push_end",
1204
+ message: "sync: push complete",
1205
+ meta: { agentRoot, remote: config.remote, branch },
1206
+ });
1207
+ return { ok: true };
1208
+ }
1209
+ const rejected = !run.timedOut && !run.aborted && /\[rejected\]|non-fast-forward|fetch first/.test(run.stderr);
1210
+ const error = describeAsyncFailure("push", run, timeoutMs);
1211
+ const classification = rejected ? "push_rejected" : "unknown";
1212
+ recordPendingSync(agentRoot, error, classification, []);
1213
+ (0, runtime_1.emitNervesEvent)({
1214
+ level: "warn",
1215
+ component: "heart",
1216
+ event: "heart.sync_push_error",
1217
+ message: `sync: push failed: ${classification}`,
1218
+ meta: { agentRoot, error, classification },
1219
+ });
1220
+ return { ok: false, error };
1221
+ }
1222
+ catch (err) {
1223
+ const error = errorText(err);
1224
+ recordPendingSync(agentRoot, error, "unknown", []);
1225
+ (0, runtime_1.emitNervesEvent)({
1226
+ level: "warn",
1227
+ component: "heart",
1228
+ event: "heart.sync_push_error",
1229
+ message: "sync: push failed",
1230
+ meta: { agentRoot, error },
1231
+ });
1232
+ return { ok: false, error };
1233
+ }
1234
+ }
1235
+ /**
1236
+ * `git push <remote> HEAD:<branch>` as a detached child, killed by process
1237
+ * group at 60 s. It takes the lock the caller acquired before committing and
1238
+ * releases it when the push ends, so nothing runs between commit and push. A
1239
+ * rejection is recorded as `push_rejected`; the next integrate merges and the
1240
+ * push after it succeeds. Never throws.
1241
+ */
1242
+ async function pushAsync(agentRoot, config, handle, signal) {
1243
+ try {
1244
+ return await pushUnlocked(agentRoot, config, agentNameFromRoot(agentRoot), signal);
1245
+ }
1246
+ finally {
1247
+ handle.release();
1248
+ }
1249
+ }
1250
+ // ---------- building blocks for other bundle writers (ouro move, the home check) ----------
1251
+ /** The active pause's `expiresAt`, or null. Reads the marker and changes nothing. */
1252
+ function syncPausedUntil(agentRoot) {
1253
+ const state = readPauseMarker(agentRoot);
1254
+ return state.kind === "active" ? state.expiresAt : null;
1255
+ }
1256
+ /** `refs/remotes/<remote>/<branch>`, the ref `fetchRemote` updates. */
1257
+ function syncTrackingRef(config) {
1258
+ return remoteTrackingRef(config);
1259
+ }
1260
+ /**
1261
+ * Take the bundle sync lock, waiting up to `waitMs`. While it is held, a
1262
+ * SIGTERM or SIGINT kills sync's git children and releases it, as for sync itself.
1263
+ */
1264
+ async function holdSyncLock(agentRoot, waitMs) {
1265
+ const acquired = waitMs > 0 ? await (0, sync_lock_1.acquireSyncLock)(agentRoot, waitMs) : (0, sync_lock_1.tryAcquireSyncLockSync)(agentRoot);
1266
+ return "busy" in acquired ? acquired : trackLock(acquired);
1267
+ }
1268
+ /**
1269
+ * `git push <remote> HEAD:refs/heads/<branch>` as a detached child, killed by
1270
+ * process group at 60 s. It records nothing in `state/pending-sync.json`: the
1271
+ * caller, which holds the lock, decides what a refusal means.
1272
+ */
1273
+ async function pushHeadToBranch(agentRoot, config, signal) {
1274
+ const timeoutMs = exports.__testHooks.pushTimeoutMs ?? exports.SYNC_PUSH_TIMEOUT_MS;
1275
+ const run = await runGitAsync(agentRoot, agentNameFromRoot(agentRoot), ["push", config.remote, `HEAD:refs/heads/${branchOf(config)}`], timeoutMs, signal);
1276
+ if (run.status === 0)
1277
+ return { ok: true };
1278
+ const rejected = !run.timedOut && !run.aborted && /\[rejected\]|non-fast-forward|fetch first/.test(run.stderr);
1279
+ return { ok: false, rejected, error: describeAsyncFailure("push", run, timeoutMs) };
1280
+ }
1281
+ /** The same conflict recurred: put the agent's side where the owner can merge it from the Mac. */
1282
+ async function pushConflictBranch(agentRoot, config, agentName, state, signal) {
1283
+ const branch = `sync-conflict/${agentName}`;
1284
+ const head = git(agentRoot, agentName, ["rev-parse", "HEAD"]).trim();
1285
+ // Pushed already, and nothing new since: skip the network.
1286
+ if (state.branchPushedHead === head)
1287
+ return;
1288
+ const timeoutMs = exports.__testHooks.pushTimeoutMs ?? exports.SYNC_PUSH_TIMEOUT_MS;
1289
+ const run = await runGitAsync(agentRoot, agentName, ["push", config.remote, `HEAD:refs/heads/${branch}`], timeoutMs, signal);
1290
+ if (run.status !== 0) {
1291
+ (0, runtime_1.emitNervesEvent)({
1292
+ level: "warn",
1293
+ component: "heart",
1294
+ event: "heart.sync_conflict_branch_push_error",
1295
+ message: "sync: could not push the conflict branch",
1296
+ meta: { agentRoot, branch, error: describeAsyncFailure("push", run, timeoutMs) },
1297
+ });
1298
+ return;
1299
+ }
1300
+ (0, runtime_1.emitNervesEvent)({
1301
+ component: "heart",
1302
+ event: "heart.sync_conflict_branch_pushed",
1303
+ message: "sync: pushed the agent's side of a conflict",
1304
+ meta: { agentRoot, branch },
1305
+ });
1306
+ writeConflictState(agentRoot, { ...state, branchNoticeSent: true, branchPushedHead: head });
1307
+ if (state.branchNoticeSent)
1308
+ return;
1309
+ tellOwner(agentRoot, agentName, {
1310
+ noticeId: `sync-conflict-branch:${agentName}:${state.hash}`,
1311
+ text: `My side of the sync conflict in ${state.paths.join(", ")} is on branch ${branch} in ${config.remote}. Merging it into ${branchOf(config)} from the Mac resumes sync.`,
1312
+ });
1313
+ }
1314
+ /** An unborn HEAD (nothing ever committed) has nothing to push. */
1315
+ function pushIfBorn(agentRoot, config, agentName, options) {
1316
+ if (refExists(agentRoot, agentName, "HEAD"))
1317
+ return pushUnlocked(agentRoot, config, agentName, undefined, options);
1318
+ settlePendingSync(agentRoot);
1319
+ return Promise.resolve({ ok: true });
1320
+ }
1321
+ /** Fetch then integrate. The caller holds the lock. */
1322
+ async function pullUnlocked(agentRoot, config, agentName, signal, commitMessage) {
1323
+ let remote;
1324
+ try {
1325
+ remote = hasRemote(agentRoot, agentName, config);
1326
+ }
1327
+ catch (err) {
1328
+ return { ok: false, error: errorText(err) };
1329
+ }
1330
+ if (remote) {
1331
+ const fetched = await fetchUnlocked(agentRoot, config, agentName, signal);
1332
+ if (!fetched.ok)
1333
+ return fetched;
1334
+ }
1335
+ const outcome = integrateCore(agentRoot, config, agentName, commitMessage);
1336
+ if (outcome.conflictBranch)
1337
+ await pushConflictBranch(agentRoot, config, agentName, outcome.conflictBranch, signal);
1338
+ return outcome.result;
1339
+ }
1340
+ /** Commits on each side of HEAD and the last fetched remote branch. */
1341
+ function aheadBehind(agentRoot, agentName, config) {
1342
+ const ahead = commitsAhead(agentRoot, agentName, config);
1343
+ const ref = remoteTrackingRef(config);
1344
+ if (!refExists(agentRoot, agentName, ref))
1345
+ return { ahead, behind: 0 };
1346
+ const range = refExists(agentRoot, agentName, "HEAD") ? `HEAD..${ref}` : ref;
1347
+ return { ahead, behind: Number.parseInt(git(agentRoot, agentName, ["rev-list", "--count", range]).trim(), 10) };
1348
+ }
1349
+ /**
1350
+ * The boot probe: fetch the remote branch and count commits ahead and behind.
1351
+ * It takes the lock without waiting, never merges, commits or pushes, and
1352
+ * writes nothing under `state/` (an expired pause marker is left for the next
1353
+ * sync to remove).
1354
+ */
1355
+ async function probeSync(agentRoot, config, signal) {
1356
+ const agentName = agentNameFromRoot(agentRoot);
1357
+ const pause = readPauseMarker(agentRoot);
1358
+ if (pause.kind === "active")
1359
+ return { ok: true, paused: true, pausedUntil: pause.expiresAt };
1360
+ const repoCheck = ensureGitRepo(agentRoot, "boot probe");
1361
+ if (!repoCheck.ok)
1362
+ return repoCheck;
1363
+ try {
1364
+ const acquired = (0, sync_lock_1.tryAcquireSyncLockSync)(agentRoot);
1365
+ if ("busy" in acquired)
1366
+ return { ok: true, busy: true, holderPid: acquired.holderPid };
1367
+ const lock = trackLock(acquired);
1368
+ try {
1369
+ if (!hasRemote(agentRoot, agentName, config))
1370
+ return { ok: true };
1371
+ const fetched = await fetchUnlocked(agentRoot, config, agentName, signal);
1372
+ if (!fetched.ok)
1373
+ return fetched;
1374
+ return { ok: true, ...aheadBehind(agentRoot, agentName, config) };
1375
+ }
1376
+ finally {
1377
+ lock.release();
1378
+ }
1379
+ }
1380
+ catch (err) {
1381
+ return { ok: false, error: errorText(err) };
1382
+ }
1383
+ }
1384
+ /**
1385
+ * Pre-turn pull: under the bundle lock (waiting up to 5 s, then skipping),
1386
+ * fetch the remote branch and merge it, so the turn reads the owner's latest
1387
+ * edits. With no remote, local changes are committed and the result is ok.
1388
+ */
1389
+ async function preTurnPull(agentRoot, config, options = {}) {
1390
+ const agentName = options.agentName ?? agentNameFromRoot(agentRoot);
1391
+ (0, runtime_1.emitNervesEvent)({
1392
+ component: "heart",
1393
+ event: "heart.sync_pull_start",
1394
+ message: "pre-turn pull starting",
1395
+ meta: { agentRoot, remote: config.remote, branch: branchOf(config) },
1396
+ });
1397
+ const paused = pausedResult(agentRoot, "pre-turn pull");
1398
+ if (paused)
1399
+ return paused;
1400
+ const repoCheck = ensureGitRepo(agentRoot, "pre-turn pull");
1401
+ if (!repoCheck.ok)
1402
+ return repoCheck;
1403
+ if (options.signal?.aborted) {
1404
+ (0, runtime_1.emitNervesEvent)({
1405
+ level: "warn",
1406
+ component: "heart",
1407
+ event: "heart.sync_pull_aborted",
1408
+ message: "pre-turn pull skipped: signal already aborted",
1409
+ meta: { agentRoot },
1410
+ });
1411
+ return { ok: false, error: "aborted before pull started" };
1412
+ }
1413
+ let acquired;
1414
+ try {
1415
+ acquired = await (0, sync_lock_1.acquireSyncLock)(agentRoot, exports.__testHooks.preTurnLockWaitMs ?? exports.PRE_TURN_LOCK_WAIT_MS, options.signal);
1416
+ }
1417
+ catch (err) {
1418
+ return { ok: false, error: `aborted while waiting for the sync lock: ${errorText(err)}` };
1419
+ }
1420
+ if ("busy" in acquired) {
1421
+ (0, runtime_1.emitNervesEvent)({
1422
+ component: "heart",
1423
+ event: "heart.sync_pull_end",
1424
+ message: "pre-turn pull skipped: sync lock busy",
1425
+ meta: { agentRoot, holderPid: acquired.holderPid },
1426
+ });
1427
+ return { ok: true, busy: true, holderPid: acquired.holderPid };
1428
+ }
1429
+ const lock = trackLock(acquired);
1430
+ try {
1431
+ // A pause written while this waited for the lock still wins.
1432
+ const pausedNow = pausedResult(agentRoot, "pre-turn pull");
1433
+ if (pausedNow)
1434
+ return pausedNow;
1435
+ const result = await pullUnlocked(agentRoot, config, agentName, options.signal);
1436
+ if (result.ok) {
1437
+ (0, runtime_1.emitNervesEvent)({ component: "heart", event: "heart.sync_pull_end", message: "pre-turn pull complete", meta: { agentRoot } });
1438
+ }
1439
+ else {
1440
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "heart", event: "heart.sync_pull_error", message: "pre-turn pull failed", meta: { agentRoot, error: result.error } });
1441
+ }
1442
+ return result;
1443
+ }
1444
+ finally {
1445
+ lock.release();
1446
+ }
1447
+ }
1448
+ /** Same as `preTurnPull`, kept for older callers. */
1449
+ function preTurnPullAsync(agentRoot, config, options = {}) {
1450
+ return preTurnPull(agentRoot, config, options);
1451
+ }
1452
+ /**
1453
+ * Post-turn push. Synchronous up to the push: take the lock without waiting
1454
+ * (busy means skip; the next turn commits), commit, then hand the held lock to
1455
+ * `pushAsync`, which is not awaited. `push` in the result is that promise.
1456
+ */
1457
+ function postTurnPush(agentRoot, config, agentName = agentNameFromRoot(agentRoot)) {
1458
+ (0, runtime_1.emitNervesEvent)({
1459
+ component: "heart",
1460
+ event: "heart.sync_push_start",
1461
+ message: "post-turn push starting",
1462
+ meta: { agentRoot, remote: config.remote, branch: branchOf(config) },
1463
+ });
1464
+ const repoCheck = ensureGitRepo(agentRoot, "post-turn push");
1465
+ if (!repoCheck.ok)
1466
+ return repoCheck;
1467
+ const acquired = (0, sync_lock_1.tryAcquireSyncLockSync)(agentRoot);
1468
+ if ("busy" in acquired) {
1469
+ (0, runtime_1.emitNervesEvent)({
1470
+ component: "heart",
1471
+ event: "heart.sync_push_end",
1472
+ message: "post-turn push skipped: sync lock busy",
1473
+ meta: { agentRoot, holderPid: acquired.holderPid },
1474
+ });
1475
+ return { ok: true, busy: true, holderPid: acquired.holderPid };
1476
+ }
1477
+ const lock = trackLock(acquired);
1478
+ let release = true;
1479
+ try {
1480
+ // Checked under the lock, so a pauser that saw the lock free sees no git after it.
1481
+ const paused = pausedResult(agentRoot, "post-turn push");
1482
+ if (paused)
1483
+ return paused;
1484
+ const repairFailure = repairInterrupted(agentRoot, agentName);
1485
+ if (repairFailure)
1486
+ return repairFailure;
1487
+ commitLocalChanges(agentRoot, agentName, "sync: post-turn update");
1488
+ // The turn waits only up to here: the network push, when there is one, runs after it.
1489
+ (0, runtime_1.emitNervesEvent)({ component: "heart", event: "heart.sync_commit_end", message: "post-turn commit done", meta: { agentRoot } });
1490
+ const remote = hasRemote(agentRoot, agentName, config);
1491
+ if (remote && readPreviousFailure(agentRoot)?.classification === "pull_rebase_conflict") {
1492
+ // The push would be rejected until the conflict is merged; the next integrate retries it.
1493
+ (0, runtime_1.emitNervesEvent)({
1494
+ component: "heart",
1495
+ event: "heart.sync_push_end",
1496
+ message: "post-turn push: committed, push waits for an unresolved conflict",
1497
+ meta: { agentRoot },
1498
+ });
1499
+ return { ok: true };
1500
+ }
1501
+ if (remote && commitsAhead(agentRoot, agentName, config) === 0) {
1502
+ settlePendingSync(agentRoot);
1503
+ (0, runtime_1.emitNervesEvent)({
1504
+ component: "heart",
1505
+ event: "heart.sync_push_end",
1506
+ message: "post-turn push: nothing to push",
1507
+ meta: { agentRoot },
1508
+ });
1509
+ return { ok: true };
1510
+ }
1511
+ release = false;
1512
+ return { ok: true, push: pushAsync(agentRoot, config, lock) };
1513
+ }
1514
+ catch (err) {
1515
+ const error = errorText(err);
1516
+ recordPendingSync(agentRoot, error, "unknown", []);
1517
+ (0, runtime_1.emitNervesEvent)({
1518
+ level: "warn",
1519
+ component: "heart",
1520
+ event: "heart.sync_push_error",
1521
+ message: "post-turn push failed",
1522
+ meta: { agentRoot, error },
1523
+ });
1524
+ return { ok: false, error };
1525
+ }
1526
+ finally {
1527
+ if (release)
1528
+ lock.release();
1529
+ }
1530
+ }
1531
+ /**
1532
+ * Manual sync (`ouro sync`, the bundle tools): under one lock, waiting up to
1533
+ * `waitMs`, fetch, integrate and push. Built on the unlocked internals, so it
1534
+ * never waits on its own lock.
1535
+ */
1536
+ async function syncNow(agentRoot, config, agentName, waitMs, options = {}) {
1537
+ (0, runtime_1.emitNervesEvent)({
1538
+ component: "heart",
1539
+ event: "heart.sync_now_start",
1540
+ message: "sync now starting",
1541
+ meta: { agentRoot, remote: config.remote, branch: branchOf(config) },
1542
+ });
1543
+ const paused = pausedResult(agentRoot, "sync now");
1544
+ if (paused)
1545
+ return paused;
1546
+ const repoCheck = ensureGitRepo(agentRoot, "sync now");
1547
+ if (!repoCheck.ok)
1548
+ return repoCheck;
1549
+ const acquired = await (0, sync_lock_1.acquireSyncLock)(agentRoot, waitMs);
1550
+ if ("busy" in acquired) {
1551
+ const error = `bundle sync lock is held${acquired.holderPid === null ? "" : ` by pid ${acquired.holderPid}`}`;
1552
+ (0, runtime_1.emitNervesEvent)({
1553
+ level: "warn",
1554
+ component: "heart",
1555
+ event: "heart.sync_now_error",
1556
+ message: "sync now: lock busy",
1557
+ meta: { agentRoot, error },
1558
+ });
1559
+ return { ok: false, busy: true, holderPid: acquired.holderPid, error };
1560
+ }
1561
+ const lock = trackLock(acquired);
1562
+ try {
1563
+ const pausedNow = pausedResult(agentRoot, "sync now");
1564
+ if (pausedNow)
1565
+ return pausedNow;
1566
+ const pulled = await pullUnlocked(agentRoot, config, agentName, undefined, "sync: update");
1567
+ const result = pulled.ok ? await pushIfBorn(agentRoot, config, agentName, options) : pulled;
1568
+ if (result.ok) {
1569
+ (0, runtime_1.emitNervesEvent)({ component: "heart", event: "heart.sync_now_end", message: "sync now complete", meta: { agentRoot } });
1570
+ }
1571
+ else {
1572
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "heart", event: "heart.sync_now_error", message: "sync now failed", meta: { agentRoot, error: result.error } });
1573
+ }
1574
+ return result;
1575
+ }
1576
+ finally {
1577
+ lock.release();
1578
+ }
1579
+ }