@ouro.bot/cli 0.1.0-alpha.56 → 0.1.0-alpha.561

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 (396) hide show
  1. package/README.md +127 -23
  2. package/RepairGuide.ouro/agent.json +5 -0
  3. package/RepairGuide.ouro/psyche/IDENTITY.md +19 -0
  4. package/RepairGuide.ouro/psyche/SOUL.md +55 -0
  5. package/RepairGuide.ouro/skills/diagnose-broken-remote.md +63 -0
  6. package/RepairGuide.ouro/skills/diagnose-stacked-typed-issues.md +35 -0
  7. package/RepairGuide.ouro/skills/diagnose-sync-blocked.md +54 -0
  8. package/RepairGuide.ouro/skills/diagnose-vault-expired.md +60 -0
  9. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +4 -2
  10. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +2 -2
  11. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +1 -1
  12. package/changelog.json +3604 -0
  13. package/dist/arc/attention-types.js +8 -0
  14. package/dist/arc/cares.js +140 -0
  15. package/dist/arc/episodes.js +117 -0
  16. package/dist/arc/intentions.js +133 -0
  17. package/dist/arc/json-store.js +117 -0
  18. package/dist/arc/obligations.js +237 -0
  19. package/dist/arc/packets.js +193 -0
  20. package/dist/arc/presence.js +185 -0
  21. package/dist/arc/task-lifecycle.js +65 -0
  22. package/dist/heart/active-work.js +837 -26
  23. package/dist/heart/agent-entry.js +58 -3
  24. package/dist/heart/attachments/image-normalize.js +194 -0
  25. package/dist/heart/attachments/materialize.js +97 -0
  26. package/dist/heart/attachments/originals.js +88 -0
  27. package/dist/heart/attachments/render.js +29 -0
  28. package/dist/heart/attachments/sources/adapter.js +2 -0
  29. package/dist/heart/attachments/sources/bluebubbles.js +156 -0
  30. package/dist/heart/attachments/sources/cli-local-file.js +78 -0
  31. package/dist/heart/attachments/sources/index.js +16 -0
  32. package/dist/heart/attachments/store.js +103 -0
  33. package/dist/heart/attachments/types.js +93 -0
  34. package/dist/heart/auth/auth-flow.js +479 -0
  35. package/dist/heart/background-operations.js +281 -0
  36. package/dist/heart/bundle-state.js +168 -0
  37. package/dist/heart/commitments.js +111 -0
  38. package/dist/heart/config-registry.js +322 -0
  39. package/dist/heart/config.js +114 -118
  40. package/dist/heart/core.js +913 -246
  41. package/dist/heart/cross-chat-delivery.js +3 -18
  42. package/dist/heart/daemon/agent-config-check.js +419 -0
  43. package/dist/heart/daemon/agent-discovery.js +102 -3
  44. package/dist/heart/daemon/agent-service.js +522 -0
  45. package/dist/heart/daemon/agentic-repair.js +547 -0
  46. package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
  47. package/dist/heart/daemon/boot-sync-probe.js +197 -0
  48. package/dist/heart/daemon/cadence.js +70 -0
  49. package/dist/heart/daemon/cli-defaults.js +776 -0
  50. package/dist/heart/daemon/cli-exec.js +7457 -0
  51. package/dist/heart/daemon/cli-help.js +498 -0
  52. package/dist/heart/daemon/cli-parse.js +1592 -0
  53. package/dist/heart/daemon/cli-render-doctor.js +57 -0
  54. package/dist/heart/daemon/cli-render.js +763 -0
  55. package/dist/heart/daemon/cli-types.js +8 -0
  56. package/dist/heart/daemon/connect-bay.js +323 -0
  57. package/dist/heart/daemon/daemon-cli.js +29 -1698
  58. package/dist/heart/daemon/daemon-entry.js +387 -2
  59. package/dist/heart/daemon/daemon-health.js +176 -0
  60. package/dist/heart/daemon/daemon-rollup.js +57 -0
  61. package/dist/heart/daemon/daemon-runtime-sync.js +88 -13
  62. package/dist/heart/daemon/daemon-tombstone.js +236 -0
  63. package/dist/heart/daemon/daemon.js +796 -71
  64. package/dist/heart/daemon/dns-workflow.js +394 -0
  65. package/dist/heart/daemon/doctor-types.js +8 -0
  66. package/dist/heart/daemon/doctor.js +826 -0
  67. package/dist/heart/daemon/health-monitor.js +122 -1
  68. package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
  69. package/dist/heart/daemon/hooks/bundle-meta.js +115 -1
  70. package/dist/heart/daemon/http-health-probe.js +80 -0
  71. package/dist/heart/daemon/human-command-screens.js +234 -0
  72. package/dist/heart/daemon/human-readiness.js +114 -0
  73. package/dist/heart/daemon/inner-status.js +89 -0
  74. package/dist/heart/daemon/interactive-repair.js +394 -0
  75. package/dist/heart/daemon/launchd.js +37 -8
  76. package/dist/heart/daemon/log-tailer.js +82 -12
  77. package/dist/heart/daemon/logs-prune.js +110 -0
  78. package/dist/heart/daemon/mcp-canary.js +297 -0
  79. package/dist/heart/daemon/message-router.js +2 -2
  80. package/dist/heart/daemon/os-cron-deps.js +135 -0
  81. package/dist/heart/daemon/os-cron.js +14 -12
  82. package/dist/heart/daemon/ouro-bot-entry.js +4 -2
  83. package/dist/heart/daemon/ouro-entry.js +3 -1
  84. package/dist/heart/daemon/process-manager.js +375 -33
  85. package/dist/heart/daemon/provider-discovery.js +137 -0
  86. package/dist/heart/daemon/provider-ping-progress.js +83 -0
  87. package/dist/heart/daemon/pulse.js +475 -0
  88. package/dist/heart/daemon/readiness-repair.js +365 -0
  89. package/dist/heart/daemon/run-hooks.js +2 -0
  90. package/dist/heart/daemon/runtime-logging.js +67 -16
  91. package/dist/heart/daemon/runtime-metadata.js +3 -31
  92. package/dist/heart/daemon/safe-mode.js +161 -0
  93. package/dist/heart/daemon/sense-manager.js +389 -38
  94. package/dist/heart/daemon/session-id-resolver.js +131 -0
  95. package/dist/heart/daemon/skill-management-installer.js +94 -0
  96. package/dist/heart/daemon/socket-client.js +158 -11
  97. package/dist/heart/daemon/stale-bundle-prune.js +96 -0
  98. package/dist/heart/daemon/startup-tui.js +330 -0
  99. package/dist/heart/daemon/task-scheduler.js +3 -25
  100. package/dist/heart/daemon/terminal-ui.js +499 -0
  101. package/dist/heart/daemon/thoughts.js +162 -17
  102. package/dist/heart/daemon/up-progress.js +366 -0
  103. package/dist/heart/daemon/vault-items.js +56 -0
  104. package/dist/heart/delegation.js +1 -1
  105. package/dist/heart/habits/habit-migration.js +189 -0
  106. package/dist/heart/habits/habit-parser.js +140 -0
  107. package/dist/heart/habits/habit-runtime-state.js +100 -0
  108. package/dist/heart/habits/habit-scheduler.js +372 -0
  109. package/dist/heart/{daemon → hatch}/hatch-flow.js +32 -56
  110. package/dist/heart/{daemon → hatch}/hatch-specialist.js +6 -8
  111. package/dist/heart/{daemon → hatch}/specialist-prompt.js +12 -9
  112. package/dist/heart/{daemon → hatch}/specialist-tools.js +35 -12
  113. package/dist/heart/identity.js +203 -57
  114. package/dist/heart/kept-notes.js +357 -0
  115. package/dist/heart/kicks.js +1 -1
  116. package/dist/heart/machine-identity.js +161 -0
  117. package/dist/heart/mail-import-discovery.js +353 -0
  118. package/dist/heart/mailbox/mailbox-http-hooks.js +66 -0
  119. package/dist/heart/mailbox/mailbox-http-response.js +7 -0
  120. package/dist/heart/mailbox/mailbox-http-routes.js +246 -0
  121. package/dist/heart/mailbox/mailbox-http-static.js +103 -0
  122. package/dist/heart/mailbox/mailbox-http-transport.js +116 -0
  123. package/dist/heart/mailbox/mailbox-http.js +99 -0
  124. package/dist/heart/mailbox/mailbox-read.js +31 -0
  125. package/dist/heart/mailbox/mailbox-types.js +27 -0
  126. package/dist/heart/mailbox/mailbox-view.js +195 -0
  127. package/dist/heart/mailbox/readers/agent-machine.js +382 -0
  128. package/dist/heart/mailbox/readers/continuity-readers.js +338 -0
  129. package/dist/heart/mailbox/readers/mail.js +362 -0
  130. package/dist/heart/mailbox/readers/runtime-readers.js +651 -0
  131. package/dist/heart/mailbox/readers/sessions.js +232 -0
  132. package/dist/heart/mailbox/readers/shared.js +111 -0
  133. package/dist/heart/mcp/mcp-server.js +683 -0
  134. package/dist/heart/migrate-config.js +100 -0
  135. package/dist/heart/model-capabilities.js +19 -0
  136. package/dist/heart/platform.js +81 -0
  137. package/dist/heart/provider-attempt.js +134 -0
  138. package/dist/heart/provider-binding-resolver.js +267 -0
  139. package/dist/heart/provider-credentials.js +425 -0
  140. package/dist/heart/provider-failover.js +301 -0
  141. package/dist/heart/provider-models.js +81 -0
  142. package/dist/heart/provider-ping.js +262 -0
  143. package/dist/heart/provider-readiness-cache.js +40 -0
  144. package/dist/heart/provider-visibility.js +188 -0
  145. package/dist/heart/providers/anthropic-token.js +131 -0
  146. package/dist/heart/providers/anthropic.js +139 -52
  147. package/dist/heart/providers/azure.js +97 -13
  148. package/dist/heart/providers/error-classification.js +127 -0
  149. package/dist/heart/providers/github-copilot.js +145 -0
  150. package/dist/heart/providers/minimax-vlm.js +189 -0
  151. package/dist/heart/providers/minimax.js +26 -8
  152. package/dist/heart/providers/openai-codex.js +55 -40
  153. package/dist/heart/runtime-capability-check.js +170 -0
  154. package/dist/heart/runtime-credentials.js +367 -0
  155. package/dist/heart/runtime-cwd.js +87 -0
  156. package/dist/heart/sense-truth.js +13 -4
  157. package/dist/heart/session-activity.js +43 -22
  158. package/dist/heart/session-events.js +1149 -0
  159. package/dist/heart/session-playback-cli-main.js +5 -0
  160. package/dist/heart/session-playback-cli.js +36 -0
  161. package/dist/heart/session-playback.js +231 -0
  162. package/dist/heart/session-stats-cli-main.js +5 -0
  163. package/dist/heart/session-stats.js +182 -0
  164. package/dist/heart/session-transcript.js +243 -0
  165. package/dist/heart/start-of-turn-packet.js +345 -0
  166. package/dist/heart/streaming.js +44 -27
  167. package/dist/heart/sync-classification.js +176 -0
  168. package/dist/heart/sync.js +449 -0
  169. package/dist/heart/target-resolution.js +9 -5
  170. package/dist/heart/tempo.js +93 -0
  171. package/dist/heart/temporal-view.js +41 -0
  172. package/dist/heart/timeouts.js +101 -0
  173. package/dist/heart/tool-activity-callbacks.js +59 -0
  174. package/dist/heart/tool-description.js +139 -0
  175. package/dist/heart/tool-friction.js +55 -0
  176. package/dist/heart/tool-loop.js +200 -0
  177. package/dist/heart/turn-context.js +389 -0
  178. package/dist/heart/{daemon → versioning}/ouro-bot-global-installer.js +6 -5
  179. package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
  180. package/dist/heart/versioning/ouro-path-installer.js +426 -0
  181. package/dist/heart/versioning/ouro-version-manager.js +295 -0
  182. package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
  183. package/dist/heart/{daemon → versioning}/update-checker.js +6 -1
  184. package/dist/heart/{daemon → versioning}/update-hooks.js +63 -59
  185. package/dist/mailbox-ui/assets/index-B-461hes.js +61 -0
  186. package/dist/mailbox-ui/assets/index-BPr5vNuM.css +1 -0
  187. package/dist/mailbox-ui/index.html +15 -0
  188. package/dist/mailroom/attention.js +167 -0
  189. package/dist/mailroom/autonomy.js +209 -0
  190. package/dist/mailroom/blob-store.js +674 -0
  191. package/dist/mailroom/body-cache.js +61 -0
  192. package/dist/mailroom/core.js +720 -0
  193. package/dist/mailroom/entry.js +160 -0
  194. package/dist/mailroom/file-store.js +430 -0
  195. package/dist/mailroom/mbox-import.js +383 -0
  196. package/dist/mailroom/outbound.js +380 -0
  197. package/dist/mailroom/policy.js +263 -0
  198. package/dist/mailroom/reader.js +233 -0
  199. package/dist/mailroom/search-cache.js +256 -0
  200. package/dist/mailroom/search-relevance.js +319 -0
  201. package/dist/mailroom/smtp-ingress.js +176 -0
  202. package/dist/mailroom/source-state.js +176 -0
  203. package/dist/mailroom/thread.js +109 -0
  204. package/dist/mailroom/travel-extract.js +89 -0
  205. package/dist/mind/bundle-manifest.js +7 -1
  206. package/dist/mind/context.js +165 -101
  207. package/dist/mind/diary-integrity.js +60 -0
  208. package/dist/mind/{memory.js → diary.js} +62 -75
  209. package/dist/mind/embedding-provider.js +60 -0
  210. package/dist/mind/file-state.js +179 -0
  211. package/dist/mind/friends/channel.js +39 -0
  212. package/dist/mind/friends/resolver.js +54 -2
  213. package/dist/mind/friends/store-file.js +39 -3
  214. package/dist/mind/friends/types.js +2 -2
  215. package/dist/mind/journal-index.js +161 -0
  216. package/dist/mind/note-search.js +268 -0
  217. package/dist/mind/obligation-steering.js +221 -0
  218. package/dist/mind/pending.js +4 -0
  219. package/dist/mind/prompt-refresh.js +3 -2
  220. package/dist/mind/prompt.js +1011 -123
  221. package/dist/mind/provenance-trust.js +26 -0
  222. package/dist/mind/scrutiny.js +173 -0
  223. package/dist/nerves/cli-logging.js +7 -1
  224. package/dist/nerves/coverage/audit-rules.js +15 -6
  225. package/dist/nerves/coverage/audit.js +28 -2
  226. package/dist/nerves/coverage/cli.js +1 -1
  227. package/dist/nerves/coverage/contract.js +5 -5
  228. package/dist/nerves/coverage/file-completeness.js +129 -5
  229. package/dist/nerves/coverage/run-artifacts.js +1 -1
  230. package/dist/nerves/event-buffer.js +111 -0
  231. package/dist/nerves/index.js +224 -4
  232. package/dist/nerves/observation.js +20 -0
  233. package/dist/nerves/redact.js +79 -0
  234. package/dist/nerves/review/cli-main.js +5 -0
  235. package/dist/nerves/review/cli.js +156 -0
  236. package/dist/nerves/review/core.js +152 -0
  237. package/dist/nerves/runtime.js +5 -1
  238. package/dist/repertoire/ado-client.js +15 -56
  239. package/dist/repertoire/ado-semantic.js +11 -10
  240. package/dist/repertoire/api-client.js +97 -0
  241. package/dist/repertoire/bitwarden-store.js +963 -0
  242. package/dist/repertoire/bundle-templates.js +72 -0
  243. package/dist/repertoire/bw-installer.js +180 -0
  244. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  245. package/dist/repertoire/coding/context-pack.js +330 -0
  246. package/dist/repertoire/coding/feedback.js +197 -30
  247. package/dist/repertoire/coding/manager.js +158 -9
  248. package/dist/repertoire/coding/spawner.js +55 -9
  249. package/dist/repertoire/coding/tools.js +170 -7
  250. package/dist/repertoire/commerce-errors.js +109 -0
  251. package/dist/repertoire/commerce-self-test.js +156 -0
  252. package/dist/repertoire/credential-access.js +178 -0
  253. package/dist/repertoire/duffel-client.js +185 -0
  254. package/dist/repertoire/github-client.js +14 -55
  255. package/dist/repertoire/graph-client.js +11 -52
  256. package/dist/repertoire/guardrails.js +396 -0
  257. package/dist/repertoire/mcp-client.js +295 -0
  258. package/dist/repertoire/mcp-manager.js +362 -0
  259. package/dist/repertoire/mcp-tools.js +63 -0
  260. package/dist/repertoire/shell-sessions.js +133 -0
  261. package/dist/repertoire/skills.js +15 -24
  262. package/dist/repertoire/stripe-client.js +131 -0
  263. package/dist/repertoire/tasks/board.js +31 -5
  264. package/dist/repertoire/tasks/fix.js +182 -0
  265. package/dist/repertoire/tasks/index.js +16 -4
  266. package/dist/repertoire/tasks/lifecycle.js +2 -2
  267. package/dist/repertoire/tasks/parser.js +3 -2
  268. package/dist/repertoire/tasks/scanner.js +194 -37
  269. package/dist/repertoire/tasks/transitions.js +16 -78
  270. package/dist/repertoire/tool-results.js +29 -0
  271. package/dist/repertoire/tools-attachments.js +317 -0
  272. package/dist/repertoire/tools-base.js +47 -1075
  273. package/dist/repertoire/tools-bluebubbles.js +1 -0
  274. package/dist/repertoire/tools-bridge.js +142 -0
  275. package/dist/repertoire/tools-bundle.js +984 -0
  276. package/dist/repertoire/tools-config.js +185 -0
  277. package/dist/repertoire/tools-continuity.js +248 -0
  278. package/dist/repertoire/tools-credential.js +381 -0
  279. package/dist/repertoire/tools-files.js +342 -0
  280. package/dist/repertoire/tools-flight.js +224 -0
  281. package/dist/repertoire/tools-flow.js +119 -0
  282. package/dist/repertoire/tools-github.js +1 -7
  283. package/dist/repertoire/tools-mail.js +1857 -0
  284. package/dist/repertoire/tools-notes.js +421 -0
  285. package/dist/repertoire/tools-session.js +750 -0
  286. package/dist/repertoire/tools-shell.js +120 -0
  287. package/dist/repertoire/tools-stripe.js +180 -0
  288. package/dist/repertoire/tools-surface.js +243 -0
  289. package/dist/repertoire/tools-teams.js +9 -39
  290. package/dist/repertoire/tools-travel.js +125 -0
  291. package/dist/repertoire/tools-trip.js +604 -0
  292. package/dist/repertoire/tools-user-profile.js +144 -0
  293. package/dist/repertoire/tools-vault.js +40 -0
  294. package/dist/repertoire/tools.js +108 -100
  295. package/dist/repertoire/travel-api-client.js +360 -0
  296. package/dist/repertoire/user-profile.js +131 -0
  297. package/dist/repertoire/vault-setup.js +246 -0
  298. package/dist/repertoire/vault-unlock.js +594 -0
  299. package/dist/scripts/claude-code-hook.js +41 -0
  300. package/dist/scripts/claude-code-stop-hook.js +47 -0
  301. package/dist/senses/attention-queue.js +116 -0
  302. package/dist/senses/bluebubbles/active-turns.js +216 -0
  303. package/dist/senses/bluebubbles/attachment-cache.js +53 -0
  304. package/dist/senses/bluebubbles/attachment-download.js +137 -0
  305. package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +219 -18
  306. package/dist/senses/bluebubbles/entry.js +77 -0
  307. package/dist/senses/{bluebubbles-inbound-log.js → bluebubbles/inbound-log.js} +20 -3
  308. package/dist/senses/bluebubbles/index.js +2305 -0
  309. package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -70
  310. package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +33 -12
  311. package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +3 -3
  312. package/dist/senses/bluebubbles/processed-log.js +133 -0
  313. package/dist/senses/bluebubbles/replay.js +137 -0
  314. package/dist/senses/{bluebubbles-runtime-state.js → bluebubbles/runtime-state.js} +30 -2
  315. package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
  316. package/dist/senses/cli/bracketed-paste.js +82 -0
  317. package/dist/senses/cli/image-paste.js +287 -0
  318. package/dist/senses/cli/image-ref-navigation.js +75 -0
  319. package/dist/senses/cli/ink-app.js +156 -0
  320. package/dist/senses/cli/inline-diff.js +64 -0
  321. package/dist/senses/cli/input-keys.js +174 -0
  322. package/dist/senses/cli/kill-ring.js +86 -0
  323. package/dist/senses/cli/message-list.js +51 -0
  324. package/dist/senses/cli/ouro-tui.js +607 -0
  325. package/dist/senses/cli/spinner-imperative.js +135 -0
  326. package/dist/senses/cli/spinner.js +101 -0
  327. package/dist/senses/cli/status-line.js +60 -0
  328. package/dist/senses/cli/streaming-markdown.js +526 -0
  329. package/dist/senses/cli/tool-display.js +85 -0
  330. package/dist/senses/cli/tool-render.js +85 -0
  331. package/dist/senses/cli/tui-store.js +240 -0
  332. package/dist/senses/cli/virtual-list.js +35 -0
  333. package/dist/senses/cli-entry.js +60 -8
  334. package/dist/senses/cli-layout.js +187 -0
  335. package/dist/senses/cli.js +520 -209
  336. package/dist/senses/commands.js +66 -3
  337. package/dist/senses/habit-turn-message.js +108 -0
  338. package/dist/senses/inner-dialog-worker.js +175 -21
  339. package/dist/senses/inner-dialog.js +330 -27
  340. package/dist/senses/mail-entry.js +66 -0
  341. package/dist/senses/mail.js +379 -0
  342. package/dist/senses/pipeline.js +549 -181
  343. package/dist/senses/proactive-content-guard.js +51 -0
  344. package/dist/senses/shared-turn.js +251 -0
  345. package/dist/senses/surface-tool.js +68 -0
  346. package/dist/senses/teams-entry.js +60 -8
  347. package/dist/senses/teams.js +387 -98
  348. package/dist/senses/trust-gate.js +100 -5
  349. package/dist/senses/voice/audio-routing.js +119 -0
  350. package/dist/senses/voice/elevenlabs.js +178 -0
  351. package/dist/senses/voice/golden-path.js +116 -0
  352. package/dist/senses/voice/index.js +26 -0
  353. package/dist/senses/voice/meeting.js +113 -0
  354. package/dist/senses/voice/playback.js +139 -0
  355. package/dist/senses/voice/transcript.js +70 -0
  356. package/dist/senses/voice/turn.js +85 -0
  357. package/dist/senses/voice/types.js +2 -0
  358. package/dist/senses/voice/whisper.js +161 -0
  359. package/dist/senses/voice-entry.js +80 -0
  360. package/dist/trips/core.js +138 -0
  361. package/dist/trips/store.js +146 -0
  362. package/package.json +38 -7
  363. package/skills/agent-commerce.md +106 -0
  364. package/skills/browser-navigation.md +117 -0
  365. package/skills/commerce-setup-guide.md +116 -0
  366. package/skills/commerce-setup.md +84 -0
  367. package/skills/configure-dev-tools.md +101 -0
  368. package/skills/travel-planning.md +138 -0
  369. package/dist/heart/daemon/auth-flow.js +0 -351
  370. package/dist/heart/daemon/ouro-path-installer.js +0 -178
  371. package/dist/heart/daemon/subagent-installer.js +0 -166
  372. package/dist/heart/session-recall.js +0 -116
  373. package/dist/mind/associative-recall.js +0 -209
  374. package/dist/senses/bluebubbles-entry.js +0 -13
  375. package/dist/senses/bluebubbles.js +0 -1177
  376. package/dist/senses/debug-activity.js +0 -148
  377. package/subagents/README.md +0 -86
  378. package/subagents/work-doer.md +0 -237
  379. package/subagents/work-merger.md +0 -618
  380. package/subagents/work-planner.md +0 -390
  381. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
  382. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
  383. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
  384. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
  385. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
  386. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
  387. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
  388. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
  389. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
  390. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
  391. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
  392. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
  393. /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
  394. /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
  395. /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
  396. /package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +0 -0
@@ -0,0 +1,984 @@
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.bundleToolDefinitions = void 0;
37
+ exports.__getConfirmationTokenStore = __getConfirmationTokenStore;
38
+ exports.isFirstPushToRemote = isFirstPushToRemote;
39
+ /**
40
+ * Agent-callable bundle management tools.
41
+ *
42
+ * These tools let the agent fix its own git state — initializing the
43
+ * bundle as a repo, adding a remote, making the first commit, pushing,
44
+ * and recovering from push rejections. The `bundleState` field in the
45
+ * start-of-turn packet (PR 5) points the agent at these tools whenever
46
+ * it detects an unresolved git issue.
47
+ *
48
+ * ## Security boundaries
49
+ *
50
+ * Every tool computes `bundleRoot = getAgentRoot()` once and refuses to
51
+ * touch anything outside that directory. Path arguments are validated
52
+ * via `path.resolve(bundleRoot, input)` + a prefix check — if the
53
+ * resolved path escapes the bundle, the handler emits a `*_refused`
54
+ * nerves event and returns `{ ok: false, error: "refused: ..." }`
55
+ * without executing the git operation.
56
+ *
57
+ * ## Destructive-op refusal pattern (Directive B)
58
+ *
59
+ * Tools that could lose work (bundle_init_git on an existing repo,
60
+ * bundle_add_remote on a configured remote, bundle_pull_rebase with
61
+ * dirty tree, etc.) refuse by default and require an explicit `force`
62
+ * flag — mirroring Claude Code's `ExitWorktreeTool` safety pattern. The
63
+ * refusal surface is the LLM's responsibility to bridge with the human:
64
+ * when a tool refuses, the agent should ask the user for permission
65
+ * before retrying with `force: true`.
66
+ *
67
+ * ## Enumeration, not recursive delete (Directive A)
68
+ *
69
+ * No tool uses recursive rmSync or shells out to destructive shell delete
70
+ * commands. `bundle_do_first_commit` stages files
71
+ * via `git add -- <file1> <file2> ...` with explicit enumeration even
72
+ * when the caller omits the files list — the default path internally
73
+ * calls `bundle_list_first_commit` and stages each entry individually.
74
+ */
75
+ const child_process_1 = require("child_process");
76
+ const crypto = __importStar(require("crypto"));
77
+ const fs = __importStar(require("fs"));
78
+ const path = __importStar(require("path"));
79
+ const runtime_1 = require("../nerves/runtime");
80
+ const identity_1 = require("../heart/identity");
81
+ const bundle_state_1 = require("../heart/bundle-state");
82
+ const bundle_templates_1 = require("./bundle-templates");
83
+ function gitExec(bundleRoot, args, timeoutMs = 10000) {
84
+ try {
85
+ const stdout = (0, child_process_1.execFileSync)("git", args, {
86
+ cwd: bundleRoot,
87
+ stdio: "pipe",
88
+ timeout: timeoutMs,
89
+ }).toString();
90
+ return { stdout, stderr: "", code: 0 };
91
+ }
92
+ catch (err) {
93
+ /* v8 ignore start -- defensive fallback branches on the err shape are hard to exercise without mocking; real git failures populate all three fields @preserve */
94
+ const anyErr = err;
95
+ return {
96
+ stdout: anyErr.stdout?.toString() ?? "",
97
+ stderr: anyErr.stderr?.toString() ?? anyErr.message ?? String(err),
98
+ code: anyErr.status ?? 1,
99
+ };
100
+ /* v8 ignore stop */
101
+ }
102
+ }
103
+ function isGitRepo(bundleRoot) {
104
+ return fs.existsSync(path.join(bundleRoot, ".git"));
105
+ }
106
+ function hasHead(bundleRoot) {
107
+ const result = gitExec(bundleRoot, ["rev-parse", "HEAD"]);
108
+ return result.code === 0;
109
+ }
110
+ function getRemoteUrl(bundleRoot, name) {
111
+ const result = gitExec(bundleRoot, ["remote", "get-url", name]);
112
+ if (result.code !== 0)
113
+ return undefined;
114
+ /* v8 ignore next -- empty stdout from `git remote get-url` on a configured remote doesn't happen in practice @preserve */
115
+ return result.stdout.trim() || undefined;
116
+ }
117
+ function listRemotes(bundleRoot) {
118
+ const result = gitExec(bundleRoot, ["remote"]);
119
+ /* v8 ignore next -- `git remote` on an initialized repo always exits 0 @preserve */
120
+ if (result.code !== 0)
121
+ return [];
122
+ return result.stdout.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
123
+ }
124
+ function assertInsideBundle(bundleRoot, rel) {
125
+ // path.resolve against an absolute input keeps it absolute — which may
126
+ // escape the bundle. Force-join then normalize to catch those cases.
127
+ const joined = path.resolve(bundleRoot, rel);
128
+ const normalized = path.normalize(joined);
129
+ /* v8 ignore next -- normalized === bundleRoot happens when rel is "" or "." — guarded by the empty-string check at the caller @preserve */
130
+ if (normalized === bundleRoot)
131
+ return { ok: true, resolved: normalized };
132
+ if (!normalized.startsWith(bundleRoot + path.sep)) {
133
+ return { ok: false, error: `refused: path outside bundle root: ${rel}` };
134
+ }
135
+ return { ok: true, resolved: normalized };
136
+ }
137
+ function json(obj) {
138
+ return JSON.stringify(obj);
139
+ }
140
+ // ─── tool: bundle_check_sync_status ────────────────────────────────────
141
+ const checkSyncStatusHandler = () => {
142
+ const bundleRoot = (0, identity_1.getAgentRoot)();
143
+ (0, runtime_1.emitNervesEvent)({
144
+ component: "repertoire",
145
+ event: "repertoire.bundle_check_sync_status_start",
146
+ message: "checking bundle sync status",
147
+ meta: { bundleRoot },
148
+ });
149
+ const gitRepo = isGitRepo(bundleRoot);
150
+ let hasRemote = false;
151
+ let remoteUrl;
152
+ let dirtyFileCount = 0;
153
+ let firstCommitExists = false;
154
+ let ahead = 0;
155
+ let behind = 0;
156
+ if (gitRepo) {
157
+ const remotes = listRemotes(bundleRoot);
158
+ hasRemote = remotes.length > 0;
159
+ if (hasRemote) {
160
+ remoteUrl = getRemoteUrl(bundleRoot, remotes[0]);
161
+ }
162
+ firstCommitExists = hasHead(bundleRoot);
163
+ const status = gitExec(bundleRoot, ["status", "--porcelain"]);
164
+ /* v8 ignore next -- git status --porcelain only fails on a corrupt repo @preserve */
165
+ if (status.code === 0) {
166
+ dirtyFileCount = status.stdout
167
+ .split("\n")
168
+ .map((l) => l.trim())
169
+ .filter((l) => l.length > 0).length;
170
+ }
171
+ /* v8 ignore start -- upstream tracking ahead/behind requires a live remote, not practical to cover in unit tests @preserve */
172
+ if (hasRemote && firstCommitExists) {
173
+ // Best-effort ahead/behind from git rev-list if an upstream is tracked.
174
+ const counts = gitExec(bundleRoot, ["rev-list", "--left-right", "--count", "HEAD...@{upstream}"]);
175
+ if (counts.code === 0) {
176
+ const [a, b] = counts.stdout.trim().split("\t").map((n) => parseInt(n, 10));
177
+ if (!Number.isNaN(a) && !Number.isNaN(b)) {
178
+ ahead = a;
179
+ behind = b;
180
+ }
181
+ }
182
+ }
183
+ /* v8 ignore stop */
184
+ }
185
+ const pendingSyncExists = fs.existsSync(path.join(bundleRoot, "state", "pending-sync.json"));
186
+ const bundleStateIssues = (0, bundle_state_1.detectBundleState)(bundleRoot);
187
+ const result = {
188
+ ok: true,
189
+ isGitRepo: gitRepo,
190
+ hasRemote,
191
+ remoteUrl: remoteUrl ?? null,
192
+ dirtyFileCount,
193
+ firstCommitExists,
194
+ ahead,
195
+ behind,
196
+ pendingSyncExists,
197
+ bundleStateIssues,
198
+ };
199
+ (0, runtime_1.emitNervesEvent)({
200
+ component: "repertoire",
201
+ event: "repertoire.bundle_check_sync_status_end",
202
+ message: "bundle sync status checked",
203
+ meta: { bundleRoot, ...result },
204
+ });
205
+ return json(result);
206
+ };
207
+ // ─── tool: bundle_init_git ─────────────────────────────────────────────
208
+ const initGitHandler = (args) => {
209
+ const bundleRoot = (0, identity_1.getAgentRoot)();
210
+ const force = args.force === "true" || args.force === true;
211
+ (0, runtime_1.emitNervesEvent)({
212
+ component: "repertoire",
213
+ event: "repertoire.bundle_init_git_start",
214
+ message: "initializing bundle git repo",
215
+ meta: { bundleRoot, force },
216
+ });
217
+ const alreadyInit = isGitRepo(bundleRoot);
218
+ if (alreadyInit && !force) {
219
+ (0, runtime_1.emitNervesEvent)({
220
+ level: "warn",
221
+ component: "repertoire",
222
+ event: "repertoire.bundle_init_git_refused",
223
+ message: "bundle_init_git refused: already initialized",
224
+ meta: { bundleRoot },
225
+ });
226
+ (0, runtime_1.emitNervesEvent)({
227
+ component: "repertoire",
228
+ event: "repertoire.bundle_init_git_end",
229
+ message: "bundle_init_git refused",
230
+ meta: { bundleRoot, refused: true },
231
+ });
232
+ return json({
233
+ ok: false,
234
+ error: "bundle already has a .git directory — pass force: true to re-init",
235
+ alreadyInit: true,
236
+ });
237
+ }
238
+ if (!alreadyInit) {
239
+ const init = gitExec(bundleRoot, ["init", "--initial-branch=main"]);
240
+ /* v8 ignore start -- git init failure requires a broken git binary or a permissions edge case that's not practical to cover in unit tests @preserve */
241
+ if (init.code !== 0) {
242
+ (0, runtime_1.emitNervesEvent)({
243
+ level: "error",
244
+ component: "repertoire",
245
+ event: "repertoire.bundle_init_git_error",
246
+ message: "git init failed",
247
+ meta: { bundleRoot, stderr: init.stderr },
248
+ });
249
+ (0, runtime_1.emitNervesEvent)({
250
+ component: "repertoire",
251
+ event: "repertoire.bundle_init_git_end",
252
+ message: "git init failed",
253
+ meta: { bundleRoot, ok: false },
254
+ });
255
+ return json({ ok: false, error: `git init failed: ${init.stderr}` });
256
+ }
257
+ /* v8 ignore stop */
258
+ }
259
+ // Write the full .gitignore template if one doesn't already exist.
260
+ // See BUNDLE_GITIGNORE_TEMPLATE's design philosophy: functional blocks
261
+ // only (credentials, state, noise, build artifacts); PII is handled
262
+ // separately by bundle_first_push_review at first-push time.
263
+ const gitignorePath = path.join(bundleRoot, ".gitignore");
264
+ let gitignoreWritten = false;
265
+ if (!fs.existsSync(gitignorePath)) {
266
+ fs.writeFileSync(gitignorePath, bundle_templates_1.BUNDLE_GITIGNORE_TEMPLATE, "utf-8");
267
+ gitignoreWritten = true;
268
+ }
269
+ const result = { ok: true, alreadyInit, gitignoreWritten };
270
+ (0, runtime_1.emitNervesEvent)({
271
+ component: "repertoire",
272
+ event: "repertoire.bundle_init_git_end",
273
+ message: "bundle git initialized",
274
+ meta: { bundleRoot, ...result },
275
+ });
276
+ return json(result);
277
+ };
278
+ // ─── tool: bundle_add_remote ───────────────────────────────────────────
279
+ const REMOTE_URL_PATTERN = /^(https?:\/\/|git@[^\s:]+:)[^\s]+$/;
280
+ const addRemoteHandler = (args) => {
281
+ const bundleRoot = (0, identity_1.getAgentRoot)();
282
+ const url = typeof args.url === "string" ? args.url.trim() : "";
283
+ const name = typeof args.name === "string" && args.name.length > 0 ? args.name : "origin";
284
+ const force = args.force === "true" || args.force === true;
285
+ (0, runtime_1.emitNervesEvent)({
286
+ component: "repertoire",
287
+ event: "repertoire.bundle_add_remote_start",
288
+ message: "adding git remote to bundle",
289
+ meta: { bundleRoot, name, url, force },
290
+ });
291
+ const finish = (ok, payload) => {
292
+ (0, runtime_1.emitNervesEvent)({
293
+ component: "repertoire",
294
+ event: "repertoire.bundle_add_remote_end",
295
+ message: "bundle_add_remote finished",
296
+ meta: { bundleRoot, ok, ...payload },
297
+ });
298
+ return json({ ok, ...payload });
299
+ };
300
+ if (!isGitRepo(bundleRoot)) {
301
+ return finish(false, { error: "bundle is not a git repo — run bundle_init_git first" });
302
+ }
303
+ if (!REMOTE_URL_PATTERN.test(url)) {
304
+ (0, runtime_1.emitNervesEvent)({
305
+ level: "warn",
306
+ component: "repertoire",
307
+ event: "repertoire.bundle_add_remote_refused",
308
+ message: "bundle_add_remote refused: invalid url",
309
+ meta: { bundleRoot, url },
310
+ });
311
+ return finish(false, { error: `invalid remote url: ${url || "(empty)"}` });
312
+ }
313
+ const existing = getRemoteUrl(bundleRoot, name);
314
+ if (existing && !force) {
315
+ (0, runtime_1.emitNervesEvent)({
316
+ level: "warn",
317
+ component: "repertoire",
318
+ event: "repertoire.bundle_add_remote_refused",
319
+ message: "bundle_add_remote refused: remote already exists",
320
+ meta: { bundleRoot, name, existing },
321
+ });
322
+ return finish(false, {
323
+ error: `remote "${name}" already points to ${existing} — pass force: true to overwrite`,
324
+ previousUrl: existing,
325
+ });
326
+ }
327
+ if (existing) {
328
+ const setUrl = gitExec(bundleRoot, ["remote", "set-url", name, url]);
329
+ /* v8 ignore next 3 -- git remote set-url failure requires a permissions/git-state edge case @preserve */
330
+ if (setUrl.code !== 0) {
331
+ return finish(false, { error: `git remote set-url failed: ${setUrl.stderr}` });
332
+ }
333
+ }
334
+ else {
335
+ const add = gitExec(bundleRoot, ["remote", "add", name, url]);
336
+ /* v8 ignore next 3 -- git remote add failure requires a permissions/git-state edge case @preserve */
337
+ if (add.code !== 0) {
338
+ return finish(false, { error: `git remote add failed: ${add.stderr}` });
339
+ }
340
+ }
341
+ return finish(true, { name, url, previousUrl: existing ?? null });
342
+ };
343
+ const listFirstCommitHandler = () => {
344
+ const bundleRoot = (0, identity_1.getAgentRoot)();
345
+ (0, runtime_1.emitNervesEvent)({
346
+ component: "repertoire",
347
+ event: "repertoire.bundle_list_first_commit_start",
348
+ message: "listing first-commit candidates",
349
+ meta: { bundleRoot },
350
+ });
351
+ const finish = (ok, payload) => {
352
+ (0, runtime_1.emitNervesEvent)({
353
+ component: "repertoire",
354
+ event: "repertoire.bundle_list_first_commit_end",
355
+ message: "bundle_list_first_commit finished",
356
+ meta: { bundleRoot, ok, ...payload },
357
+ });
358
+ return json({ ok, ...payload });
359
+ };
360
+ if (!isGitRepo(bundleRoot)) {
361
+ return finish(false, { error: "bundle is not a git repo — run bundle_init_git first" });
362
+ }
363
+ if (hasHead(bundleRoot)) {
364
+ return finish(false, { error: "bundle already has commits — bundle_list_first_commit only applies before the first commit" });
365
+ }
366
+ const ls = gitExec(bundleRoot, ["ls-files", "-o", "--exclude-standard"]);
367
+ /* v8 ignore next 3 -- git ls-files failure requires a corrupt repo @preserve */
368
+ if (ls.code !== 0) {
369
+ return finish(false, { error: `git ls-files failed: ${ls.stderr}` });
370
+ }
371
+ const files = ls.stdout
372
+ .split("\n")
373
+ .map((l) => l.trim())
374
+ .filter((l) => l.length > 0);
375
+ const groups = {};
376
+ let totalFiles = 0;
377
+ let totalBytes = 0;
378
+ for (const relFile of files) {
379
+ const absPath = path.join(bundleRoot, relFile);
380
+ let size = 0;
381
+ try {
382
+ size = fs.statSync(absPath).size;
383
+ /* v8 ignore start -- statSync failure on a git-listed file is a race condition we can't reliably reproduce @preserve */
384
+ }
385
+ catch {
386
+ // File listed by git but not readable — skip silently
387
+ continue;
388
+ }
389
+ /* v8 ignore stop */
390
+ const topDir = relFile.includes(path.sep) ? relFile.split(path.sep)[0] : "(root)";
391
+ if (!groups[topDir]) {
392
+ groups[topDir] = { files: [], totalBytes: 0, fileCount: 0 };
393
+ }
394
+ groups[topDir].files.push({ path: relFile, size });
395
+ groups[topDir].totalBytes += size;
396
+ groups[topDir].fileCount += 1;
397
+ totalFiles += 1;
398
+ totalBytes += size;
399
+ }
400
+ return finish(true, { groups, totalFiles, totalBytes });
401
+ };
402
+ // ─── tool: bundle_do_first_commit ──────────────────────────────────────
403
+ const doFirstCommitHandler = (args) => {
404
+ const bundleRoot = (0, identity_1.getAgentRoot)();
405
+ const rawFiles = args.files;
406
+ const hasExplicitFiles = rawFiles !== undefined;
407
+ const message = typeof args.message === "string" && args.message.length > 0
408
+ ? args.message
409
+ : "initial: import pre-sync bundle state";
410
+ (0, runtime_1.emitNervesEvent)({
411
+ component: "repertoire",
412
+ event: "repertoire.bundle_do_first_commit_start",
413
+ message: "performing first commit",
414
+ meta: { bundleRoot, hasExplicitFiles },
415
+ });
416
+ const finish = (ok, payload) => {
417
+ (0, runtime_1.emitNervesEvent)({
418
+ component: "repertoire",
419
+ event: "repertoire.bundle_do_first_commit_end",
420
+ message: "bundle_do_first_commit finished",
421
+ meta: { bundleRoot, ok, ...payload },
422
+ });
423
+ return json({ ok, ...payload });
424
+ };
425
+ if (!isGitRepo(bundleRoot)) {
426
+ return finish(false, { error: "bundle is not a git repo — run bundle_init_git first" });
427
+ }
428
+ if (hasHead(bundleRoot)) {
429
+ return finish(false, { error: "bundle already has commits — first commit already exists" });
430
+ }
431
+ // Resolve the file list — either explicit or the default set from ls-files.
432
+ let filesToStage;
433
+ if (hasExplicitFiles) {
434
+ if (!Array.isArray(rawFiles)) {
435
+ return finish(false, { error: "files must be an array of relative paths" });
436
+ }
437
+ if (rawFiles.length === 0) {
438
+ // Directive A: explicit enumeration required. Empty list is a refusal.
439
+ (0, runtime_1.emitNervesEvent)({
440
+ level: "warn",
441
+ component: "repertoire",
442
+ event: "repertoire.bundle_do_first_commit_refused",
443
+ message: "bundle_do_first_commit refused: empty file list",
444
+ meta: { bundleRoot },
445
+ });
446
+ return finish(false, { error: "refused: files array must be non-empty — pass explicit file paths or omit the argument to stage everything" });
447
+ }
448
+ filesToStage = rawFiles;
449
+ }
450
+ else {
451
+ const ls = gitExec(bundleRoot, ["ls-files", "-o", "--exclude-standard"]);
452
+ /* v8 ignore next 3 -- git ls-files failure requires a corrupt repo @preserve */
453
+ if (ls.code !== 0) {
454
+ return finish(false, { error: `git ls-files failed: ${ls.stderr}` });
455
+ }
456
+ filesToStage = ls.stdout
457
+ .split("\n")
458
+ .map((l) => l.trim())
459
+ .filter((l) => l.length > 0);
460
+ }
461
+ /* v8 ignore next 3 -- empty filesToStage on default path means ls-files returned zero entries, covered by the empty-bundle test path elsewhere @preserve */
462
+ if (filesToStage.length === 0) {
463
+ return finish(false, { error: "no files to commit" });
464
+ }
465
+ // Security boundary: every file must resolve inside bundleRoot.
466
+ for (const file of filesToStage) {
467
+ /* v8 ignore next 3 -- defensive non-string entry check: LLM tool call args are JSON-parsed, a non-string would already fail JSON schema validation upstream @preserve */
468
+ if (typeof file !== "string" || file.length === 0) {
469
+ return finish(false, { error: `invalid file entry: ${JSON.stringify(file)}` });
470
+ }
471
+ const check = assertInsideBundle(bundleRoot, file);
472
+ if (!check.ok) {
473
+ (0, runtime_1.emitNervesEvent)({
474
+ level: "warn",
475
+ component: "repertoire",
476
+ event: "repertoire.bundle_do_first_commit_refused",
477
+ message: "bundle_do_first_commit refused: path outside bundle root",
478
+ meta: { bundleRoot, file },
479
+ });
480
+ return finish(false, { error: check.error });
481
+ }
482
+ }
483
+ // Stage via `git add -- <file1> <file2> ...` — explicit enumeration.
484
+ const add = gitExec(bundleRoot, ["add", "--", ...filesToStage]);
485
+ /* v8 ignore next 3 -- git add failure on a valid file list requires a corrupt repo or race @preserve */
486
+ if (add.code !== 0) {
487
+ return finish(false, { error: `git add failed: ${add.stderr}` });
488
+ }
489
+ const commit = gitExec(bundleRoot, ["commit", "-m", message]);
490
+ /* v8 ignore next 3 -- git commit failure requires a hook-reject or missing user.email, which createTmpBundle pre-configures @preserve */
491
+ if (commit.code !== 0) {
492
+ return finish(false, { error: `git commit failed: ${commit.stderr}` });
493
+ }
494
+ const rev = gitExec(bundleRoot, ["rev-parse", "HEAD"]);
495
+ const commitSha = rev.stdout.trim();
496
+ return finish(true, { commitSha, fileCount: filesToStage.length, message });
497
+ };
498
+ const CONFIRMATION_TOKEN_TTL_MS = 15 * 60 * 1000;
499
+ const _confirmationTokens = new Map();
500
+ function pruneExpiredTokens(now) {
501
+ for (const [token, entry] of _confirmationTokens) {
502
+ if (now - entry.createdAt > CONFIRMATION_TOKEN_TTL_MS) {
503
+ _confirmationTokens.delete(token);
504
+ }
505
+ }
506
+ }
507
+ /** Test hook: lets unit tests inspect + clear the token store. */
508
+ function __getConfirmationTokenStore() {
509
+ return _confirmationTokens;
510
+ }
511
+ function countFilesInDir(bundleRoot, relDir) {
512
+ // Use `git ls-files --others --exclude-standard -- <dir>` to honor .gitignore.
513
+ const result = gitExec(bundleRoot, ["ls-files", "--others", "--exclude-standard", "--", relDir]);
514
+ /* v8 ignore next -- git ls-files failure requires a corrupt repo @preserve */
515
+ if (result.code !== 0)
516
+ return 0;
517
+ return result.stdout.split("\n").filter((l) => l.trim().length > 0).length;
518
+ }
519
+ function parseGitHubUrl(url) {
520
+ // https://github.com/owner/repo(.git)
521
+ const httpsMatch = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(\.git)?$/);
522
+ if (httpsMatch)
523
+ return { owner: httpsMatch[1], repo: httpsMatch[2] };
524
+ // git@github.com:owner/repo(.git)
525
+ const sshMatch = url.match(/^git@github\.com:([^/]+)\/([^/]+?)(\.git)?$/);
526
+ if (sshMatch)
527
+ return { owner: sshMatch[1], repo: sshMatch[2] };
528
+ return null;
529
+ }
530
+ async function checkGitHubVisibility(parsed, fetchFn) {
531
+ const controller = new AbortController();
532
+ /* v8 ignore next -- 5-second timeout abort only fires if fetch takes longer than the test runner allows; the abort path is defensive @preserve */
533
+ const timer = setTimeout(() => controller.abort(), 5000);
534
+ try {
535
+ const res = await fetchFn(`https://api.github.com/repos/${parsed.owner}/${parsed.repo}`, {
536
+ signal: controller.signal,
537
+ headers: { Accept: "application/vnd.github+json" },
538
+ });
539
+ if (!res.ok)
540
+ return "unknown";
541
+ const data = (await res.json());
542
+ if (data.private === true)
543
+ return "private";
544
+ if (data.private === false)
545
+ return "public";
546
+ /* v8 ignore next -- malformed GitHub API response (private field neither true nor false) is not practical to provoke @preserve */
547
+ return "unknown";
548
+ }
549
+ catch {
550
+ return "unknown";
551
+ }
552
+ finally {
553
+ clearTimeout(timer);
554
+ }
555
+ }
556
+ function makeFirstPushReviewHandler(deps = {}) {
557
+ return async () => {
558
+ const bundleRoot = (0, identity_1.getAgentRoot)();
559
+ const fetchFn = deps.fetch ?? globalThis.fetch;
560
+ const now = deps.now ?? Date.now;
561
+ (0, runtime_1.emitNervesEvent)({
562
+ component: "repertoire",
563
+ event: "repertoire.bundle_first_push_review_start",
564
+ message: "reviewing bundle for first push",
565
+ meta: { bundleRoot },
566
+ });
567
+ const finish = (ok, payload) => {
568
+ (0, runtime_1.emitNervesEvent)({
569
+ component: "repertoire",
570
+ event: "repertoire.bundle_first_push_review_end",
571
+ message: "bundle_first_push_review finished",
572
+ meta: { bundleRoot, ok, ...payload },
573
+ });
574
+ return json({ ok, ...payload });
575
+ };
576
+ if (!isGitRepo(bundleRoot)) {
577
+ return finish(false, { error: "bundle is not a git repo — run bundle_init_git first" });
578
+ }
579
+ const remotes = listRemotes(bundleRoot);
580
+ if (remotes.length === 0) {
581
+ return finish(false, { error: "no remote configured — run bundle_add_remote first" });
582
+ }
583
+ const remoteName = remotes[0];
584
+ const remoteUrl = getRemoteUrl(bundleRoot, remoteName);
585
+ /* v8 ignore next 3 -- listRemotes returned a name but get-url failed; only possible under a git race @preserve */
586
+ if (!remoteUrl) {
587
+ return finish(false, { error: "could not resolve remote url" });
588
+ }
589
+ // Enumerate PII directory counts
590
+ const piiCounts = {};
591
+ let totalPiiRecords = 0;
592
+ for (const dir of bundle_templates_1.PII_BUNDLE_DIRECTORIES) {
593
+ const dirPath = path.join(bundleRoot, dir);
594
+ if (!fs.existsSync(dirPath))
595
+ continue;
596
+ const count = countFilesInDir(bundleRoot, dir);
597
+ if (count > 0) {
598
+ piiCounts[dir] = count;
599
+ totalPiiRecords += count;
600
+ }
601
+ }
602
+ // GitHub visibility probe
603
+ const parsedGitHub = parseGitHubUrl(remoteUrl);
604
+ let warningLevel = "generic";
605
+ if (parsedGitHub) {
606
+ const visibility = await checkGitHubVisibility(parsedGitHub, fetchFn);
607
+ if (visibility === "public")
608
+ warningLevel = "public_github";
609
+ else if (visibility === "private")
610
+ warningLevel = "private_github";
611
+ }
612
+ // Build first-person warning text
613
+ const piiSummary = Object.entries(piiCounts)
614
+ .map(([dir, count]) => `${count} ${dir} record${count === 1 ? "" : "s"}`)
615
+ .join(", ");
616
+ const piiClause = piiSummary.length > 0
617
+ ? `my bundle contains personal data: ${piiSummary} (${totalPiiRecords} records total)`
618
+ : "my bundle has no PII directories populated yet";
619
+ let visibilityClause;
620
+ if (warningLevel === "public_github") {
621
+ visibilityClause = `⚠️ ${remoteUrl} is a PUBLIC GitHub repo — anything i push will be visible to anyone. are you SURE you want to push PII to a public repo?`;
622
+ }
623
+ else if (warningLevel === "private_github") {
624
+ visibilityClause = `${remoteUrl} is a PRIVATE GitHub repo. still, confirm you want to push this data there.`;
625
+ }
626
+ else {
627
+ visibilityClause = `${remoteUrl} — i can't verify this remote's visibility. confirm the repo is private before i push PII.`;
628
+ }
629
+ const warningText = `${piiClause}. ${visibilityClause}`;
630
+ // Issue and store the confirmation token
631
+ const token = crypto.randomUUID();
632
+ const currentTime = now();
633
+ pruneExpiredTokens(currentTime);
634
+ _confirmationTokens.set(token, { bundleRoot, createdAt: currentTime });
635
+ return finish(true, {
636
+ warningLevel,
637
+ remoteUrl,
638
+ piiCounts,
639
+ totalPiiRecords,
640
+ warningText,
641
+ confirmRequired: true,
642
+ confirmationToken: token,
643
+ });
644
+ };
645
+ }
646
+ const firstPushReviewHandler = makeFirstPushReviewHandler();
647
+ // ─── tool: bundle_push ─────────────────────────────────────────────────
648
+ /* v8 ignore start -- push error classification branches require mocking git stderr from each failure mode; covered by a single network-failure integration test in the suite @preserve */
649
+ function classifyPushError(stderr) {
650
+ const lower = stderr.toLowerCase();
651
+ if (lower.includes("rejected") || lower.includes("non-fast-forward") || lower.includes("fetch first"))
652
+ return "rejected";
653
+ if (lower.includes("could not resolve") || lower.includes("network") || lower.includes("connection") || lower.includes("timeout"))
654
+ return "network";
655
+ if (lower.includes("authentication") || lower.includes("permission denied") || lower.includes("unauthorized") || lower.includes("403"))
656
+ return "auth";
657
+ return "unknown";
658
+ }
659
+ /* v8 ignore stop */
660
+ /**
661
+ * Detect whether this is a first push to the remote.
662
+ *
663
+ * Returns true if the remote branch does not yet exist (bundle has local
664
+ * commits but has never been pushed). Also returns true — conservatively —
665
+ * when git fails to probe the remote (network unreachable, git error).
666
+ * The token requirement is a security gate; if we can't verify the remote
667
+ * state, we assume the worst and force the agent to get confirmation. An
668
+ * unreachable remote should NOT be a bypass vector.
669
+ */
670
+ /**
671
+ * Exported for direct unit testing in bundle-push-first-push.test.ts.
672
+ * The integration tests use unreachable remotes so they always hit the
673
+ * network-failure branch. The unit tests mock child_process to exercise
674
+ * the successful-probe branches (empty stdout = first push, non-empty
675
+ * = subsequent push, symbolic-ref failure = conservative true).
676
+ */
677
+ function isFirstPushToRemote(bundleRoot, remote) {
678
+ const branchResult = gitExec(bundleRoot, ["symbolic-ref", "--short", "HEAD"]);
679
+ if (branchResult.code !== 0)
680
+ return true;
681
+ const branch = branchResult.stdout.trim();
682
+ const lsRemote = gitExec(bundleRoot, ["ls-remote", "--heads", remote, branch], 10000);
683
+ if (lsRemote.code !== 0)
684
+ return true;
685
+ return lsRemote.stdout.trim().length === 0;
686
+ }
687
+ function makePushHandler(deps = {}) {
688
+ return (args) => {
689
+ const bundleRoot = (0, identity_1.getAgentRoot)();
690
+ const remote = typeof args.remote === "string" && args.remote.length > 0 ? args.remote : "origin";
691
+ const confirmationToken = typeof args.confirmation_token === "string"
692
+ ? args.confirmation_token
693
+ : undefined;
694
+ const now = deps.now ?? Date.now;
695
+ (0, runtime_1.emitNervesEvent)({
696
+ component: "repertoire",
697
+ event: "repertoire.bundle_push_start",
698
+ message: "pushing bundle to remote",
699
+ meta: { bundleRoot, remote },
700
+ });
701
+ const finish = (ok, payload) => {
702
+ (0, runtime_1.emitNervesEvent)({
703
+ component: "repertoire",
704
+ event: "repertoire.bundle_push_end",
705
+ message: "bundle_push finished",
706
+ meta: { bundleRoot, ok, ...payload },
707
+ });
708
+ return json({ ok, ...payload });
709
+ };
710
+ if (!isGitRepo(bundleRoot)) {
711
+ return finish(false, { error: "bundle is not a git repo" });
712
+ }
713
+ const remotes = listRemotes(bundleRoot);
714
+ if (!remotes.includes(remote)) {
715
+ return finish(false, { error: `remote "${remote}" not configured — run bundle_add_remote first` });
716
+ }
717
+ if (!hasHead(bundleRoot)) {
718
+ return finish(false, { error: "bundle has no commits — run bundle_do_first_commit first" });
719
+ }
720
+ // Directive D: first push to a remote requires the agent to have
721
+ // previously called bundle_first_push_review and obtained a
722
+ // confirmation token. This forces a PII-review gate between
723
+ // "bundle built locally" and "bundle contents on the internet".
724
+ const isFirstPush = isFirstPushToRemote(bundleRoot, remote);
725
+ /* v8 ignore next -- the !isFirstPush branch (subsequent push path) requires a reachable remote to test end-to-end; the path itself falls through to the git push below which is covered by the network-failure test @preserve */
726
+ if (isFirstPush) {
727
+ if (!confirmationToken) {
728
+ (0, runtime_1.emitNervesEvent)({
729
+ level: "warn",
730
+ component: "repertoire",
731
+ event: "repertoire.bundle_push_refused",
732
+ message: "bundle_push refused: first push requires confirmation token",
733
+ meta: { bundleRoot },
734
+ });
735
+ return finish(false, {
736
+ error: "refused: first push requires a confirmation token — call bundle_first_push_review, show the warning to the user, and pass the returned confirmationToken to bundle_push",
737
+ kind: "confirmation_required",
738
+ });
739
+ }
740
+ const currentTime = now();
741
+ pruneExpiredTokens(currentTime);
742
+ const entry = _confirmationTokens.get(confirmationToken);
743
+ if (!entry) {
744
+ (0, runtime_1.emitNervesEvent)({
745
+ level: "warn",
746
+ component: "repertoire",
747
+ event: "repertoire.bundle_push_refused",
748
+ message: "bundle_push refused: confirmation token invalid or expired",
749
+ meta: { bundleRoot },
750
+ });
751
+ return finish(false, {
752
+ error: "refused: confirmation token invalid or expired — call bundle_first_push_review again",
753
+ kind: "confirmation_required",
754
+ });
755
+ }
756
+ if (entry.bundleRoot !== bundleRoot) {
757
+ (0, runtime_1.emitNervesEvent)({
758
+ level: "warn",
759
+ component: "repertoire",
760
+ event: "repertoire.bundle_push_refused",
761
+ message: "bundle_push refused: confirmation token bound to a different bundle",
762
+ meta: { bundleRoot, tokenBundleRoot: entry.bundleRoot },
763
+ });
764
+ return finish(false, {
765
+ error: "refused: confirmation token was issued for a different bundle",
766
+ kind: "confirmation_required",
767
+ });
768
+ }
769
+ (0, runtime_1.emitNervesEvent)({
770
+ component: "repertoire",
771
+ event: "repertoire.bundle_push_first_push_confirmed",
772
+ message: "first push confirmed by valid token",
773
+ meta: { bundleRoot },
774
+ });
775
+ // One-shot: consume the token on successful validation.
776
+ _confirmationTokens.delete(confirmationToken);
777
+ }
778
+ const push = gitExec(bundleRoot, ["push", remote, "HEAD"], 30000);
779
+ /* v8 ignore start -- push success branch requires a reachable remote, not practical to cover in unit tests; failure branch covered by network-failure test @preserve */
780
+ if (push.code === 0) {
781
+ return finish(true, { remote, firstPush: isFirstPush });
782
+ }
783
+ /* v8 ignore stop */
784
+ const kind = classifyPushError(push.stderr);
785
+ return finish(false, { error: push.stderr.trim(), kind });
786
+ };
787
+ }
788
+ const pushHandler = makePushHandler();
789
+ // ─── tool: bundle_pull_rebase ──────────────────────────────────────────
790
+ const pullRebaseHandler = (args) => {
791
+ const bundleRoot = (0, identity_1.getAgentRoot)();
792
+ const remote = typeof args.remote === "string" && args.remote.length > 0 ? args.remote : "origin";
793
+ const discardChanges = args.discard_changes === "true" || args.discard_changes === true;
794
+ (0, runtime_1.emitNervesEvent)({
795
+ component: "repertoire",
796
+ event: "repertoire.bundle_pull_rebase_start",
797
+ message: "pulling with rebase",
798
+ meta: { bundleRoot, remote, discardChanges },
799
+ });
800
+ const finish = (ok, payload) => {
801
+ (0, runtime_1.emitNervesEvent)({
802
+ component: "repertoire",
803
+ event: "repertoire.bundle_pull_rebase_end",
804
+ message: "bundle_pull_rebase finished",
805
+ meta: { bundleRoot, ok, ...payload },
806
+ });
807
+ return json({ ok, ...payload });
808
+ };
809
+ if (!isGitRepo(bundleRoot)) {
810
+ return finish(false, { error: "bundle is not a git repo" });
811
+ }
812
+ const remotes = listRemotes(bundleRoot);
813
+ if (!remotes.includes(remote)) {
814
+ return finish(false, { error: `remote "${remote}" not configured` });
815
+ }
816
+ // Uncommitted changes: refuse unless discard_changes was explicitly set.
817
+ // Even with discard_changes, we stash+rebase+pop rather than nuke — the
818
+ // user can manually `git stash drop` if they really want to discard.
819
+ const status = gitExec(bundleRoot, ["status", "--porcelain"]);
820
+ const isDirty = status.stdout.trim().length > 0;
821
+ const stashed = { value: false };
822
+ if (isDirty) {
823
+ if (!discardChanges) {
824
+ (0, runtime_1.emitNervesEvent)({
825
+ level: "warn",
826
+ component: "repertoire",
827
+ event: "repertoire.bundle_pull_rebase_refused",
828
+ message: "bundle_pull_rebase refused: uncommitted changes",
829
+ meta: { bundleRoot },
830
+ });
831
+ return finish(false, { error: "refused: bundle has uncommitted changes — commit them or pass discard_changes: true (stash+rebase+pop)" });
832
+ }
833
+ /* v8 ignore start -- discardChanges=true stash path exercised in a dirty-tree test below but full rebase+pop requires reachable remote @preserve */
834
+ const stash = gitExec(bundleRoot, ["stash", "push", "-u", "-m", "bundle_pull_rebase stash"]);
835
+ if (stash.code !== 0) {
836
+ return finish(false, { error: `git stash failed: ${stash.stderr}` });
837
+ }
838
+ stashed.value = true;
839
+ /* v8 ignore stop */
840
+ }
841
+ const rebase = gitExec(bundleRoot, ["pull", "--rebase", remote], 30000);
842
+ /* v8 ignore start -- rebase success branch requires a reachable remote @preserve */
843
+ if (rebase.code === 0) {
844
+ if (stashed.value) {
845
+ const pop = gitExec(bundleRoot, ["stash", "pop"]);
846
+ if (pop.code !== 0) {
847
+ return finish(false, { error: `git stash pop failed after successful rebase: ${pop.stderr}`, kind: "stash_conflict" });
848
+ }
849
+ }
850
+ return finish(true, { remote, stashed: stashed.value });
851
+ }
852
+ /* v8 ignore stop */
853
+ // Capture conflict files if the rebase left us mid-conflict.
854
+ const conflictStatus = gitExec(bundleRoot, ["status", "--porcelain=v1"]);
855
+ const conflictFiles = [];
856
+ for (const line of conflictStatus.stdout.split("\n")) {
857
+ /* v8 ignore next -- conflict marker extraction requires a real merge conflict, covered indirectly via the network-failure test which returns empty conflictStatus @preserve */
858
+ if (/^(UU|AA|DD|AU|UA|DU|UD) /.test(line))
859
+ conflictFiles.push(line.slice(3).trim());
860
+ }
861
+ return finish(false, { error: rebase.stderr.trim(), kind: "conflict", conflictFiles });
862
+ };
863
+ // ─── registry ──────────────────────────────────────────────────────────
864
+ exports.bundleToolDefinitions = [
865
+ {
866
+ tool: {
867
+ type: "function",
868
+ function: {
869
+ name: "bundle_check_sync_status",
870
+ description: "Check the git sync status of my bundle. Returns whether my bundle is a git repo, whether it has a remote, the remote URL, the dirty file count, ahead/behind counts, and the structured bundleStateIssues array.",
871
+ parameters: { type: "object", properties: {} },
872
+ },
873
+ },
874
+ handler: checkSyncStatusHandler,
875
+ },
876
+ {
877
+ tool: {
878
+ type: "function",
879
+ function: {
880
+ name: "bundle_init_git",
881
+ description: "Initialize my bundle as a git repo. Refuses if a .git directory already exists unless I pass force: true. Also writes a minimal .gitignore that excludes state/. Safe to run — does not touch any existing files.",
882
+ parameters: {
883
+ type: "object",
884
+ properties: {
885
+ force: { type: "boolean", description: "Re-run git init even if .git already exists. Default false." },
886
+ },
887
+ },
888
+ },
889
+ },
890
+ handler: initGitHandler,
891
+ },
892
+ {
893
+ tool: {
894
+ type: "function",
895
+ function: {
896
+ name: "bundle_add_remote",
897
+ description: "Add a git remote to my bundle. Accepts https or git@ URLs. Refuses if the named remote already exists unless I pass force: true. On force, updates the URL via git remote set-url.",
898
+ parameters: {
899
+ type: "object",
900
+ properties: {
901
+ url: { type: "string", description: "The remote URL (https://... or git@...:...)." },
902
+ name: { type: "string", description: "Remote name. Defaults to 'origin'." },
903
+ force: { type: "boolean", description: "Overwrite an existing remote. Default false." },
904
+ },
905
+ required: ["url"],
906
+ },
907
+ },
908
+ },
909
+ handler: addRemoteHandler,
910
+ },
911
+ {
912
+ tool: {
913
+ type: "function",
914
+ function: {
915
+ name: "bundle_list_first_commit",
916
+ description: "List all untracked files in my bundle (honoring .gitignore), grouped by top-level directory with per-file sizes. Refuses if the bundle is not a git repo or already has commits. Use this before bundle_do_first_commit to review what would be committed.",
917
+ parameters: { type: "object", properties: {} },
918
+ },
919
+ },
920
+ handler: listFirstCommitHandler,
921
+ },
922
+ {
923
+ tool: {
924
+ type: "function",
925
+ function: {
926
+ name: "bundle_do_first_commit",
927
+ description: "Make the first commit in my bundle. If I pass an explicit files array, stages only those files. If I omit files, stages everything bundle_list_first_commit would return. Refuses on empty array, on paths outside the bundle root, or if HEAD already exists.",
928
+ parameters: {
929
+ type: "object",
930
+ properties: {
931
+ files: { type: "array", items: { type: "string" }, description: "Optional explicit list of relative paths to stage. Omit to stage everything." },
932
+ message: { type: "string", description: "Commit message. Defaults to 'initial: import pre-sync bundle state'." },
933
+ },
934
+ },
935
+ },
936
+ },
937
+ handler: doFirstCommitHandler,
938
+ },
939
+ {
940
+ tool: {
941
+ type: "function",
942
+ function: {
943
+ name: "bundle_first_push_review",
944
+ description: "Review my bundle for PII exposure before the first push to a new remote. Enumerates PII-bearing directories (friends, diary, journal, etc.) with per-directory counts, probes the remote URL for GitHub public/private visibility, and returns a first-person warning text I must show the human plus a confirmationToken I must pass to bundle_push on first push. Required before the first push to any new remote.",
945
+ parameters: { type: "object", properties: {} },
946
+ },
947
+ },
948
+ handler: firstPushReviewHandler,
949
+ },
950
+ {
951
+ tool: {
952
+ type: "function",
953
+ function: {
954
+ name: "bundle_push",
955
+ description: "Push my bundle's HEAD to the configured remote. On first push to a new remote, requires a confirmation_token from bundle_first_push_review (Directive D: human must acknowledge PII exposure before the bundle goes over the wire). Subsequent pushes ignore the token. Returns a structured error with kind: 'rejected' | 'network' | 'auth' | 'unknown' | 'confirmation_required' on failure. Does NOT auto-rebase on rejection — use bundle_pull_rebase explicitly when needed.",
956
+ parameters: {
957
+ type: "object",
958
+ properties: {
959
+ remote: { type: "string", description: "Remote name. Defaults to 'origin'." },
960
+ confirmation_token: { type: "string", description: "Opaque token from bundle_first_push_review. Required on the first push to a new remote; ignored on subsequent pushes." },
961
+ },
962
+ },
963
+ },
964
+ },
965
+ handler: pushHandler,
966
+ },
967
+ {
968
+ tool: {
969
+ type: "function",
970
+ function: {
971
+ name: "bundle_pull_rebase",
972
+ description: "Pull from the remote with --rebase. Refuses on uncommitted changes unless I pass discard_changes: true (stash + rebase + pop, NOT a hard discard). On conflict, returns conflictFiles so I can walk the user through resolution.",
973
+ parameters: {
974
+ type: "object",
975
+ properties: {
976
+ remote: { type: "string", description: "Remote name. Defaults to 'origin'." },
977
+ discard_changes: { type: "boolean", description: "Stash + rebase + pop even if the working tree is dirty. Default false." },
978
+ },
979
+ },
980
+ },
981
+ },
982
+ handler: pullRebaseHandler,
983
+ },
984
+ ];