@intent-systems/nexus 2026.1.5-3

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 (827) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/LICENSE +21 -0
  3. package/README-header.png +0 -0
  4. package/README.md +462 -0
  5. package/dist/agents/agent-paths.js +16 -0
  6. package/dist/agents/agent-scope.js +44 -0
  7. package/dist/agents/auth-profiles.js +626 -0
  8. package/dist/agents/bash-process-registry.js +126 -0
  9. package/dist/agents/bash-tools.js +838 -0
  10. package/dist/agents/chutes-oauth.js +47 -0
  11. package/dist/agents/clawdbot-tools.js +62 -0
  12. package/dist/agents/context.js +34 -0
  13. package/dist/agents/defaults.js +6 -0
  14. package/dist/agents/memory-search.js +80 -0
  15. package/dist/agents/model-auth.js +115 -0
  16. package/dist/agents/model-catalog.js +55 -0
  17. package/dist/agents/model-fallback.js +210 -0
  18. package/dist/agents/model-scan.js +263 -0
  19. package/dist/agents/model-selection.js +152 -0
  20. package/dist/agents/models-config.js +171 -0
  21. package/dist/agents/nexus-tools.js +46 -0
  22. package/dist/agents/pi-embedded-block-chunker.js +188 -0
  23. package/dist/agents/pi-embedded-helpers.js +139 -0
  24. package/dist/agents/pi-embedded-runner.js +932 -0
  25. package/dist/agents/pi-embedded-subscribe.js +541 -0
  26. package/dist/agents/pi-embedded-utils.js +20 -0
  27. package/dist/agents/pi-embedded.js +1 -0
  28. package/dist/agents/pi-extensions/compaction-safeguard.js +140 -0
  29. package/dist/agents/pi-tool-definition-adapter.js +17 -0
  30. package/dist/agents/pi-tools.js +510 -0
  31. package/dist/agents/pi-tools.schema.js +358 -0
  32. package/dist/agents/sandbox-paths.js +68 -0
  33. package/dist/agents/sandbox.js +667 -0
  34. package/dist/agents/shell-utils.js +53 -0
  35. package/dist/agents/skill-runner.js +224 -0
  36. package/dist/agents/skill-state.js +164 -0
  37. package/dist/agents/skill-tools.js +191 -0
  38. package/dist/agents/skill-usage.js +43 -0
  39. package/dist/agents/skills-install.js +244 -0
  40. package/dist/agents/skills-status.js +157 -0
  41. package/dist/agents/skills.js +472 -0
  42. package/dist/agents/subagent-registry.js +321 -0
  43. package/dist/agents/subagent-registry.store.js +47 -0
  44. package/dist/agents/system-prompt.js +179 -0
  45. package/dist/agents/timeout.js +26 -0
  46. package/dist/agents/tool-display.js +155 -0
  47. package/dist/agents/tool-display.json +236 -0
  48. package/dist/agents/tool-images.js +138 -0
  49. package/dist/agents/tool-policy.js +87 -0
  50. package/dist/agents/tools/agent-step.js +41 -0
  51. package/dist/agents/tools/browser-tool.js +295 -0
  52. package/dist/agents/tools/canvas-tool.js +193 -0
  53. package/dist/agents/tools/common.js +88 -0
  54. package/dist/agents/tools/cron-tool.js +141 -0
  55. package/dist/agents/tools/discord-actions-guild.js +186 -0
  56. package/dist/agents/tools/discord-actions-messaging.js +313 -0
  57. package/dist/agents/tools/discord-actions-moderation.js +70 -0
  58. package/dist/agents/tools/discord-actions.js +56 -0
  59. package/dist/agents/tools/discord-schema.js +199 -0
  60. package/dist/agents/tools/discord-tool.js +16 -0
  61. package/dist/agents/tools/gateway-tool.js +46 -0
  62. package/dist/agents/tools/gateway.js +28 -0
  63. package/dist/agents/tools/image-tool.js +140 -0
  64. package/dist/agents/tools/memory-tool.js +92 -0
  65. package/dist/agents/tools/nodes-tool.js +413 -0
  66. package/dist/agents/tools/nodes-utils.js +92 -0
  67. package/dist/agents/tools/sessions-announce-target.js +35 -0
  68. package/dist/agents/tools/sessions-helpers.js +88 -0
  69. package/dist/agents/tools/sessions-history-tool.js +137 -0
  70. package/dist/agents/tools/sessions-list-tool.js +196 -0
  71. package/dist/agents/tools/sessions-send-helpers.js +103 -0
  72. package/dist/agents/tools/sessions-send-tool.js +371 -0
  73. package/dist/agents/tools/sessions-spawn-tool.js +319 -0
  74. package/dist/agents/tools/slack-actions.js +129 -0
  75. package/dist/agents/tools/slack-schema.js +59 -0
  76. package/dist/agents/tools/slack-tool.js +16 -0
  77. package/dist/agents/tools/telegram-actions.js +159 -0
  78. package/dist/agents/tools/telegram-schema.js +28 -0
  79. package/dist/agents/tools/telegram-tool.js +16 -0
  80. package/dist/agents/tools/whatsapp-login-tool.js +63 -0
  81. package/dist/agents/usage.js +58 -0
  82. package/dist/agents/workspace.js +264 -0
  83. package/dist/auto-reply/chunk.js +177 -0
  84. package/dist/auto-reply/command-auth.js +44 -0
  85. package/dist/auto-reply/command-detection.js +22 -0
  86. package/dist/auto-reply/envelope.js +30 -0
  87. package/dist/auto-reply/group-activation.js +20 -0
  88. package/dist/auto-reply/heartbeat.js +58 -0
  89. package/dist/auto-reply/model.js +22 -0
  90. package/dist/auto-reply/reply/abort.js +14 -0
  91. package/dist/auto-reply/reply/agent-runner.js +426 -0
  92. package/dist/auto-reply/reply/bash-command.js +314 -0
  93. package/dist/auto-reply/reply/block-streaming.js +34 -0
  94. package/dist/auto-reply/reply/body.js +29 -0
  95. package/dist/auto-reply/reply/commands.js +332 -0
  96. package/dist/auto-reply/reply/directive-handling.js +626 -0
  97. package/dist/auto-reply/reply/directives.js +59 -0
  98. package/dist/auto-reply/reply/dispatch-from-config.js +23 -0
  99. package/dist/auto-reply/reply/followup-runner.js +181 -0
  100. package/dist/auto-reply/reply/groups.js +152 -0
  101. package/dist/auto-reply/reply/mentions.js +64 -0
  102. package/dist/auto-reply/reply/model-selection.js +209 -0
  103. package/dist/auto-reply/reply/queue.js +399 -0
  104. package/dist/auto-reply/reply/reply-dispatcher.js +68 -0
  105. package/dist/auto-reply/reply/reply-tags.js +26 -0
  106. package/dist/auto-reply/reply/session-updates.js +103 -0
  107. package/dist/auto-reply/reply/session.js +169 -0
  108. package/dist/auto-reply/reply/typing.js +125 -0
  109. package/dist/auto-reply/reply.js +655 -0
  110. package/dist/auto-reply/send-policy.js +28 -0
  111. package/dist/auto-reply/status.js +197 -0
  112. package/dist/auto-reply/templating.js +9 -0
  113. package/dist/auto-reply/thinking.js +49 -0
  114. package/dist/auto-reply/tokens.js +2 -0
  115. package/dist/auto-reply/tool-meta.js +74 -0
  116. package/dist/auto-reply/transcription.js +57 -0
  117. package/dist/auto-reply/types.js +1 -0
  118. package/dist/browser/bridge-server.js +37 -0
  119. package/dist/browser/cdp.js +382 -0
  120. package/dist/browser/chrome.js +432 -0
  121. package/dist/browser/client-actions-core.js +67 -0
  122. package/dist/browser/client-actions-observe.js +24 -0
  123. package/dist/browser/client-actions-types.js +1 -0
  124. package/dist/browser/client-actions.js +3 -0
  125. package/dist/browser/client-fetch.js +43 -0
  126. package/dist/browser/client.js +105 -0
  127. package/dist/browser/config.js +155 -0
  128. package/dist/browser/constants.js +5 -0
  129. package/dist/browser/profiles-service.js +124 -0
  130. package/dist/browser/profiles.js +96 -0
  131. package/dist/browser/pw-ai.js +2 -0
  132. package/dist/browser/pw-session.js +144 -0
  133. package/dist/browser/pw-tools-core.js +363 -0
  134. package/dist/browser/routes/agent.js +535 -0
  135. package/dist/browser/routes/basic.js +155 -0
  136. package/dist/browser/routes/index.js +8 -0
  137. package/dist/browser/routes/tabs.js +105 -0
  138. package/dist/browser/routes/utils.js +62 -0
  139. package/dist/browser/screenshot.js +40 -0
  140. package/dist/browser/server-context.js +377 -0
  141. package/dist/browser/server.js +81 -0
  142. package/dist/browser/target-id.js +18 -0
  143. package/dist/browser/trash.js +21 -0
  144. package/dist/canvas-host/a2ui/a2ui.bundle.js +17768 -0
  145. package/dist/canvas-host/a2ui/index.html +246 -0
  146. package/dist/canvas-host/a2ui.js +187 -0
  147. package/dist/canvas-host/server.js +382 -0
  148. package/dist/channel-web.js +8 -0
  149. package/dist/cli/browser-cli-actions-input.js +459 -0
  150. package/dist/cli/browser-cli-actions-observe.js +56 -0
  151. package/dist/cli/browser-cli-examples.js +31 -0
  152. package/dist/cli/browser-cli-inspect.js +97 -0
  153. package/dist/cli/browser-cli-manage.js +286 -0
  154. package/dist/cli/browser-cli-shared.js +1 -0
  155. package/dist/cli/browser-cli.js +26 -0
  156. package/dist/cli/canvas-cli.js +416 -0
  157. package/dist/cli/cloud-cli.js +336 -0
  158. package/dist/cli/credential-cli.js +227 -0
  159. package/dist/cli/cron-cli.js +454 -0
  160. package/dist/cli/deps.js +17 -0
  161. package/dist/cli/dns-cli.js +180 -0
  162. package/dist/cli/gateway-cli.js +665 -0
  163. package/dist/cli/gateway-rpc.js +20 -0
  164. package/dist/cli/hooks-cli.js +135 -0
  165. package/dist/cli/memory-cli.js +101 -0
  166. package/dist/cli/models-cli.js +248 -0
  167. package/dist/cli/nodes-camera.js +57 -0
  168. package/dist/cli/nodes-canvas.js +26 -0
  169. package/dist/cli/nodes-cli.js +946 -0
  170. package/dist/cli/nodes-screen.js +37 -0
  171. package/dist/cli/pairing-cli.js +100 -0
  172. package/dist/cli/parse-duration.js +20 -0
  173. package/dist/cli/plugins-cli.js +158 -0
  174. package/dist/cli/ports.js +97 -0
  175. package/dist/cli/profile.js +81 -0
  176. package/dist/cli/program.js +162 -0
  177. package/dist/cli/prompt.js +19 -0
  178. package/dist/cli/run-main.js +48 -0
  179. package/dist/cli/skills-cli.js +132 -0
  180. package/dist/cli/skills-hub-cli.js +1093 -0
  181. package/dist/cli/telegram-cli.js +56 -0
  182. package/dist/cli/tool-connector-cli.js +118 -0
  183. package/dist/cli/tui-cli.js +35 -0
  184. package/dist/cli/upstream-sync-cli.js +2833 -0
  185. package/dist/cli/usage-cli.js +24 -0
  186. package/dist/cli/wait.js +8 -0
  187. package/dist/commands/agent-via-gateway.js +115 -0
  188. package/dist/commands/agent.js +665 -0
  189. package/dist/commands/antigravity-oauth.js +327 -0
  190. package/dist/commands/auth-choice-options.js +80 -0
  191. package/dist/commands/auth-choice.js +134 -0
  192. package/dist/commands/auth-token.js +31 -0
  193. package/dist/commands/bootstrap-preset.js +214 -0
  194. package/dist/commands/capabilities.js +36 -0
  195. package/dist/commands/chutes-oauth.js +144 -0
  196. package/dist/commands/claude-md.js +137 -0
  197. package/dist/commands/config-view.js +11 -0
  198. package/dist/commands/configure.js +648 -0
  199. package/dist/commands/credential.js +236 -0
  200. package/dist/commands/cursor-rules.js +230 -0
  201. package/dist/commands/doctor-state-migrations.js +358 -0
  202. package/dist/commands/doctor-ui.js +113 -0
  203. package/dist/commands/doctor.js +673 -0
  204. package/dist/commands/health.js +112 -0
  205. package/dist/commands/identity.js +54 -0
  206. package/dist/commands/init.js +167 -0
  207. package/dist/commands/models/aliases.js +85 -0
  208. package/dist/commands/models/fallbacks.js +123 -0
  209. package/dist/commands/models/image-fallbacks.js +123 -0
  210. package/dist/commands/models/list.js +347 -0
  211. package/dist/commands/models/scan.js +271 -0
  212. package/dist/commands/models/set-image.js +27 -0
  213. package/dist/commands/models/set.js +27 -0
  214. package/dist/commands/models/shared.js +73 -0
  215. package/dist/commands/models.js +7 -0
  216. package/dist/commands/onboard-auth.js +101 -0
  217. package/dist/commands/onboard-channels.js +814 -0
  218. package/dist/commands/onboard-eve-identity.js +98 -0
  219. package/dist/commands/onboard-github.js +153 -0
  220. package/dist/commands/onboard-helpers.js +303 -0
  221. package/dist/commands/onboard-interactive.js +17 -0
  222. package/dist/commands/onboard-non-interactive.js +228 -0
  223. package/dist/commands/onboard-providers.js +829 -0
  224. package/dist/commands/onboard-quickstart.js +185 -0
  225. package/dist/commands/onboard-remote.js +120 -0
  226. package/dist/commands/onboard-skills.js +148 -0
  227. package/dist/commands/onboard-types.js +1 -0
  228. package/dist/commands/onboard.js +19 -0
  229. package/dist/commands/openai-codex-model-default.js +38 -0
  230. package/dist/commands/poll.js +64 -0
  231. package/dist/commands/quest.js +27 -0
  232. package/dist/commands/reset.js +61 -0
  233. package/dist/commands/send.js +124 -0
  234. package/dist/commands/sessions-ingest.js +359 -0
  235. package/dist/commands/sessions.js +212 -0
  236. package/dist/commands/setup.js +59 -0
  237. package/dist/commands/signal-install.js +135 -0
  238. package/dist/commands/skills-manifest.js +235 -0
  239. package/dist/commands/status.js +139 -0
  240. package/dist/commands/suggestions.js +54 -0
  241. package/dist/commands/systemd-linger.js +71 -0
  242. package/dist/commands/update.js +16 -0
  243. package/dist/commands/usage-upload.js +27 -0
  244. package/dist/config/config.js +6 -0
  245. package/dist/config/defaults.js +140 -0
  246. package/dist/config/group-policy.js +49 -0
  247. package/dist/config/includes.js +183 -0
  248. package/dist/config/io.js +188 -0
  249. package/dist/config/legacy-migrate.js +13 -0
  250. package/dist/config/legacy.js +425 -0
  251. package/dist/config/paths.js +82 -0
  252. package/dist/config/port-defaults.js +32 -0
  253. package/dist/config/schema.js +173 -0
  254. package/dist/config/sessions.js +611 -0
  255. package/dist/config/talk.js +31 -0
  256. package/dist/config/types.js +1 -0
  257. package/dist/config/validation.js +29 -0
  258. package/dist/config/zod-schema.js +1110 -0
  259. package/dist/control-ui/assets/index-D8Q5AI4D.js +2393 -0
  260. package/dist/control-ui/assets/index-D8Q5AI4D.js.map +1 -0
  261. package/dist/control-ui/assets/index-g06q5Xc3.css +1 -0
  262. package/dist/control-ui/favicon.ico +0 -0
  263. package/dist/control-ui/index.html +16 -0
  264. package/dist/cron/isolated-agent.js +529 -0
  265. package/dist/cron/normalize.js +73 -0
  266. package/dist/cron/parse.js +24 -0
  267. package/dist/cron/run-log.js +72 -0
  268. package/dist/cron/schedule.js +24 -0
  269. package/dist/cron/service.js +471 -0
  270. package/dist/cron/store.js +43 -0
  271. package/dist/cron/types.js +1 -0
  272. package/dist/daemon/constants.js +10 -0
  273. package/dist/daemon/launchd.js +295 -0
  274. package/dist/daemon/legacy.js +63 -0
  275. package/dist/daemon/program-args.js +141 -0
  276. package/dist/daemon/schtasks.js +269 -0
  277. package/dist/daemon/service.js +69 -0
  278. package/dist/daemon/systemd.js +332 -0
  279. package/dist/discord/index.js +2 -0
  280. package/dist/discord/monitor.js +1089 -0
  281. package/dist/discord/probe.js +54 -0
  282. package/dist/discord/send.js +652 -0
  283. package/dist/discord/token.js +8 -0
  284. package/dist/entry.js +16 -0
  285. package/dist/gateway/auth.js +121 -0
  286. package/dist/gateway/call.js +103 -0
  287. package/dist/gateway/chat-attachments.js +41 -0
  288. package/dist/gateway/client.js +180 -0
  289. package/dist/gateway/config-reload.js +274 -0
  290. package/dist/gateway/control-ui.js +184 -0
  291. package/dist/gateway/hooks-mapping.js +282 -0
  292. package/dist/gateway/hooks.js +168 -0
  293. package/dist/gateway/net.js +29 -0
  294. package/dist/gateway/protocol/index.js +62 -0
  295. package/dist/gateway/protocol/schema.js +577 -0
  296. package/dist/gateway/server-bridge-subscriptions.js +93 -0
  297. package/dist/gateway/server-bridge.js +1066 -0
  298. package/dist/gateway/server-browser.js +11 -0
  299. package/dist/gateway/server-channels.js +680 -0
  300. package/dist/gateway/server-chat.js +159 -0
  301. package/dist/gateway/server-constants.js +8 -0
  302. package/dist/gateway/server-discovery.js +62 -0
  303. package/dist/gateway/server-http.js +165 -0
  304. package/dist/gateway/server-methods/agent-job.js +114 -0
  305. package/dist/gateway/server-methods/agent.js +254 -0
  306. package/dist/gateway/server-methods/channels.js +239 -0
  307. package/dist/gateway/server-methods/chat.js +207 -0
  308. package/dist/gateway/server-methods/config.js +50 -0
  309. package/dist/gateway/server-methods/connect.js +6 -0
  310. package/dist/gateway/server-methods/cron.js +89 -0
  311. package/dist/gateway/server-methods/health.js +28 -0
  312. package/dist/gateway/server-methods/models.js +16 -0
  313. package/dist/gateway/server-methods/nodes.js +294 -0
  314. package/dist/gateway/server-methods/providers.js +257 -0
  315. package/dist/gateway/server-methods/send.js +254 -0
  316. package/dist/gateway/server-methods/sessions.js +382 -0
  317. package/dist/gateway/server-methods/skills.js +83 -0
  318. package/dist/gateway/server-methods/system.js +118 -0
  319. package/dist/gateway/server-methods/talk.js +22 -0
  320. package/dist/gateway/server-methods/types.js +1 -0
  321. package/dist/gateway/server-methods/voicewake.js +30 -0
  322. package/dist/gateway/server-methods/web.js +81 -0
  323. package/dist/gateway/server-methods/wizard.js +100 -0
  324. package/dist/gateway/server-methods.js +53 -0
  325. package/dist/gateway/server-providers.js +687 -0
  326. package/dist/gateway/server-shared.js +1 -0
  327. package/dist/gateway/server-utils.js +35 -0
  328. package/dist/gateway/server.js +1478 -0
  329. package/dist/gateway/session-utils.js +355 -0
  330. package/dist/gateway/ws-log.js +343 -0
  331. package/dist/gateway/ws-logging.js +8 -0
  332. package/dist/globals.js +41 -0
  333. package/dist/hooks/gmail-ops.js +236 -0
  334. package/dist/hooks/gmail-setup-utils.js +278 -0
  335. package/dist/hooks/gmail-watcher.js +190 -0
  336. package/dist/hooks/gmail.js +177 -0
  337. package/dist/imessage/client.js +165 -0
  338. package/dist/imessage/index.js +3 -0
  339. package/dist/imessage/monitor.js +365 -0
  340. package/dist/imessage/probe.js +26 -0
  341. package/dist/imessage/send.js +83 -0
  342. package/dist/imessage/targets.js +176 -0
  343. package/dist/index.js +55 -0
  344. package/dist/infra/agent-events.js +46 -0
  345. package/dist/infra/binaries.js +9 -0
  346. package/dist/infra/bonjour-discovery.js +163 -0
  347. package/dist/infra/bonjour.js +200 -0
  348. package/dist/infra/bridge/server.js +564 -0
  349. package/dist/infra/canvas-host-url.js +54 -0
  350. package/dist/infra/channel-summary.js +78 -0
  351. package/dist/infra/control-ui-assets.js +112 -0
  352. package/dist/infra/dotenv.js +15 -0
  353. package/dist/infra/env.js +8 -0
  354. package/dist/infra/errors.js +28 -0
  355. package/dist/infra/event-log.js +251 -0
  356. package/dist/infra/gateway-lock.js +8 -0
  357. package/dist/infra/git-commit.js +91 -0
  358. package/dist/infra/heartbeat-events.js +21 -0
  359. package/dist/infra/heartbeat-runner.js +458 -0
  360. package/dist/infra/heartbeat-wake.js +61 -0
  361. package/dist/infra/is-main.js +37 -0
  362. package/dist/infra/json-file.js +21 -0
  363. package/dist/infra/machine-name.js +40 -0
  364. package/dist/infra/nexus-root.js +56 -0
  365. package/dist/infra/node-pairing.js +212 -0
  366. package/dist/infra/path-env.js +93 -0
  367. package/dist/infra/ports.js +87 -0
  368. package/dist/infra/provider-summary.js +80 -0
  369. package/dist/infra/provider-usage.auth.js +189 -0
  370. package/dist/infra/provider-usage.fetch.claude.js +129 -0
  371. package/dist/infra/provider-usage.fetch.codex.js +62 -0
  372. package/dist/infra/provider-usage.fetch.copilot.js +42 -0
  373. package/dist/infra/provider-usage.fetch.gemini.js +57 -0
  374. package/dist/infra/provider-usage.fetch.js +6 -0
  375. package/dist/infra/provider-usage.fetch.minimax.js +214 -0
  376. package/dist/infra/provider-usage.fetch.shared.js +11 -0
  377. package/dist/infra/provider-usage.fetch.zai.js +62 -0
  378. package/dist/infra/provider-usage.format.js +77 -0
  379. package/dist/infra/provider-usage.js +145 -0
  380. package/dist/infra/provider-usage.load.js +54 -0
  381. package/dist/infra/provider-usage.shared.js +19 -0
  382. package/dist/infra/provider-usage.types.js +1 -0
  383. package/dist/infra/restart.js +29 -0
  384. package/dist/infra/retry.js +16 -0
  385. package/dist/infra/runtime-guard.js +59 -0
  386. package/dist/infra/shell-env.js +88 -0
  387. package/dist/infra/system-events.js +71 -0
  388. package/dist/infra/system-presence.js +217 -0
  389. package/dist/infra/tailnet.js +46 -0
  390. package/dist/infra/tailscale.js +149 -0
  391. package/dist/infra/unhandled-rejections.js +19 -0
  392. package/dist/infra/usage-suggestions.js +241 -0
  393. package/dist/infra/usage-upload.js +290 -0
  394. package/dist/infra/voicewake.js +78 -0
  395. package/dist/infra/widearea-dns.js +123 -0
  396. package/dist/infra/ws.js +13 -0
  397. package/dist/logger.js +52 -0
  398. package/dist/logging.js +506 -0
  399. package/dist/macos/gateway-daemon.js +145 -0
  400. package/dist/macos/relay.js +49 -0
  401. package/dist/media/constants.js +33 -0
  402. package/dist/media/host.js +42 -0
  403. package/dist/media/image-ops.js +119 -0
  404. package/dist/media/mime.js +123 -0
  405. package/dist/media/parse.js +83 -0
  406. package/dist/media/server.js +64 -0
  407. package/dist/media/store.js +139 -0
  408. package/dist/polls.js +43 -0
  409. package/dist/process/command-queue.js +97 -0
  410. package/dist/process/exec.js +75 -0
  411. package/dist/provider-web.js +8 -0
  412. package/dist/providers/github-copilot-auth.js +123 -0
  413. package/dist/providers/github-copilot-models.js +35 -0
  414. package/dist/providers/github-copilot-token.js +11 -0
  415. package/dist/providers/location.js +48 -0
  416. package/dist/providers/web/index.js +2 -0
  417. package/dist/runtime.js +8 -0
  418. package/dist/sessions/level-overrides.js +9 -0
  419. package/dist/sessions/send-policy.js +68 -0
  420. package/dist/signal/client.js +134 -0
  421. package/dist/signal/daemon.js +69 -0
  422. package/dist/signal/index.js +3 -0
  423. package/dist/signal/monitor.js +411 -0
  424. package/dist/signal/probe.js +46 -0
  425. package/dist/signal/send.js +91 -0
  426. package/dist/slack/actions.js +97 -0
  427. package/dist/slack/index.js +5 -0
  428. package/dist/slack/monitor.js +1270 -0
  429. package/dist/slack/probe.js +47 -0
  430. package/dist/slack/send.js +131 -0
  431. package/dist/slack/token.js +10 -0
  432. package/dist/telegram/allowed-updates.js +8 -0
  433. package/dist/telegram/bot.js +724 -0
  434. package/dist/telegram/download.js +34 -0
  435. package/dist/telegram/index.js +4 -0
  436. package/dist/telegram/monitor.js +47 -0
  437. package/dist/telegram/pairing-store.js +77 -0
  438. package/dist/telegram/probe.js +63 -0
  439. package/dist/telegram/proxy.js +9 -0
  440. package/dist/telegram/reaction-level.js +45 -0
  441. package/dist/telegram/send.js +151 -0
  442. package/dist/telegram/sent-message-cache.js +65 -0
  443. package/dist/telegram/token.js +30 -0
  444. package/dist/telegram/update-offset-store.js +61 -0
  445. package/dist/telegram/webhook-set.js +12 -0
  446. package/dist/telegram/webhook.js +56 -0
  447. package/dist/tui/commands.js +87 -0
  448. package/dist/tui/components/assistant-message.js +16 -0
  449. package/dist/tui/components/chat-log.js +92 -0
  450. package/dist/tui/components/custom-editor.js +55 -0
  451. package/dist/tui/components/selectors.js +8 -0
  452. package/dist/tui/components/tool-execution.js +111 -0
  453. package/dist/tui/components/user-message.js +17 -0
  454. package/dist/tui/gateway-chat.js +140 -0
  455. package/dist/tui/theme/theme.js +80 -0
  456. package/dist/tui/tui.js +708 -0
  457. package/dist/utils.js +153 -0
  458. package/dist/version.js +18 -0
  459. package/dist/web/accounts.js +86 -0
  460. package/dist/web/active-listener.js +25 -0
  461. package/dist/web/auto-reply.js +1256 -0
  462. package/dist/web/inbound.js +649 -0
  463. package/dist/web/login-qr.js +230 -0
  464. package/dist/web/login.js +71 -0
  465. package/dist/web/media.js +175 -0
  466. package/dist/web/outbound.js +102 -0
  467. package/dist/web/qr-image.js +97 -0
  468. package/dist/web/reconnect.js +60 -0
  469. package/dist/web/session.js +370 -0
  470. package/dist/wizard/clack-prompter.js +56 -0
  471. package/dist/wizard/onboarding.js +620 -0
  472. package/dist/wizard/prompts.js +6 -0
  473. package/dist/wizard/session.js +203 -0
  474. package/docs/AGENTS.default.md +116 -0
  475. package/docs/CAPABILITIES.md +444 -0
  476. package/docs/CNAME +1 -0
  477. package/docs/NEXUS_CORE_REWRITE_SPEC.md +226 -0
  478. package/docs/RELEASING.md +69 -0
  479. package/docs/_config.yml +53 -0
  480. package/docs/_layouts/default.html +145 -0
  481. package/docs/agent-assisted-install.md +95 -0
  482. package/docs/agent-loop.md +61 -0
  483. package/docs/agent-send.md +21 -0
  484. package/docs/agent.md +108 -0
  485. package/docs/android.md +133 -0
  486. package/docs/architecture.md +114 -0
  487. package/docs/assets/markdown.css +133 -0
  488. package/docs/assets/pixel-lobster.svg +60 -0
  489. package/docs/assets/terminal.css +470 -0
  490. package/docs/assets/theme.js +55 -0
  491. package/docs/audio.md +48 -0
  492. package/docs/automation/nexus-sync.md +371 -0
  493. package/docs/background-process.md +74 -0
  494. package/docs/bash.md +32 -0
  495. package/docs/bedrock.md +71 -0
  496. package/docs/bonjour.md +159 -0
  497. package/docs/browser-linux-troubleshooting.md +114 -0
  498. package/docs/browser.md +293 -0
  499. package/docs/bun.md +56 -0
  500. package/docs/camera.md +152 -0
  501. package/docs/clawd.md +212 -0
  502. package/docs/concepts/usage-tracking.md +29 -0
  503. package/docs/configuration.md +1666 -0
  504. package/docs/control-ui.md +83 -0
  505. package/docs/cron.md +385 -0
  506. package/docs/dashboard.md +17 -0
  507. package/docs/device-models.md +46 -0
  508. package/docs/discord.md +308 -0
  509. package/docs/discovery.md +112 -0
  510. package/docs/docker.md +258 -0
  511. package/docs/docs.json +105 -0
  512. package/docs/doctor.md +68 -0
  513. package/docs/elevated.md +31 -0
  514. package/docs/faq.md +736 -0
  515. package/docs/feature-inventory/overview.md +141 -0
  516. package/docs/feature-inventory/rollout-checklist.md +53 -0
  517. package/docs/feature-inventory/test-matrix.md +87 -0
  518. package/docs/feature-inventory.md +9 -0
  519. package/docs/gateway/configuration-examples.md +221 -0
  520. package/docs/gateway/configuration.md +172 -0
  521. package/docs/gateway/cron.md +61 -0
  522. package/docs/gateway/heartbeat.md +207 -0
  523. package/docs/gateway/pairing.md +109 -0
  524. package/docs/gateway-lock.md +28 -0
  525. package/docs/gateway.md +227 -0
  526. package/docs/gmail-pubsub.md +191 -0
  527. package/docs/grammy.md +27 -0
  528. package/docs/group-messages.md +73 -0
  529. package/docs/groups.md +130 -0
  530. package/docs/health.md +28 -0
  531. package/docs/heartbeat.md +73 -0
  532. package/docs/home-userspace.md +277 -0
  533. package/docs/hubs.md +148 -0
  534. package/docs/images.md +51 -0
  535. package/docs/imessage.md +94 -0
  536. package/docs/index.md +196 -0
  537. package/docs/ios.md +372 -0
  538. package/docs/linux.md +11 -0
  539. package/docs/location-command.md +95 -0
  540. package/docs/location.md +46 -0
  541. package/docs/logging.md +110 -0
  542. package/docs/lore.md +131 -0
  543. package/docs/mac/bun.md +133 -0
  544. package/docs/mac/canvas.md +161 -0
  545. package/docs/mac/child-process.md +72 -0
  546. package/docs/mac/dev-setup.md +81 -0
  547. package/docs/mac/health.md +28 -0
  548. package/docs/mac/icon.md +26 -0
  549. package/docs/mac/logging.md +51 -0
  550. package/docs/mac/menu-bar.md +69 -0
  551. package/docs/mac/peekaboo.md +170 -0
  552. package/docs/mac/permissions.md +40 -0
  553. package/docs/mac/release.md +76 -0
  554. package/docs/mac/remote.md +57 -0
  555. package/docs/mac/signing.md +41 -0
  556. package/docs/mac/skills.md +27 -0
  557. package/docs/mac/voice-overlay.md +52 -0
  558. package/docs/mac/voicewake.md +56 -0
  559. package/docs/mac/webchat.md +27 -0
  560. package/docs/mac/xpc.md +40 -0
  561. package/docs/macos.md +104 -0
  562. package/docs/model-failover.md +75 -0
  563. package/docs/models.md +91 -0
  564. package/docs/multi-agent.md +74 -0
  565. package/docs/nix.md +95 -0
  566. package/docs/nodes.md +157 -0
  567. package/docs/onboarding-config-protocol.md +34 -0
  568. package/docs/onboarding.md +189 -0
  569. package/docs/pairing.md +85 -0
  570. package/docs/plans/cron-add-hardening.md +72 -0
  571. package/docs/plans/group-policy-hardening.md +121 -0
  572. package/docs/poll.md +52 -0
  573. package/docs/prereqs.md +67 -0
  574. package/docs/presence.md +133 -0
  575. package/docs/proposals/model-config.md +147 -0
  576. package/docs/provider-routing.md +25 -0
  577. package/docs/queue.md +78 -0
  578. package/docs/reference/templates/AGENTS.md +164 -0
  579. package/docs/remote-gateway-readme.md +153 -0
  580. package/docs/remote.md +61 -0
  581. package/docs/research/memory.md +227 -0
  582. package/docs/rpc.md +35 -0
  583. package/docs/security.md +200 -0
  584. package/docs/session-ingestion.md +119 -0
  585. package/docs/session-tool.md +154 -0
  586. package/docs/session.md +85 -0
  587. package/docs/sessions.md +8 -0
  588. package/docs/setup.md +131 -0
  589. package/docs/showcase.md +37 -0
  590. package/docs/signal.md +122 -0
  591. package/docs/skills-config.md +58 -0
  592. package/docs/skills.md +153 -0
  593. package/docs/slack.md +221 -0
  594. package/docs/subagents.md +72 -0
  595. package/docs/tailscale.md +71 -0
  596. package/docs/talk.md +79 -0
  597. package/docs/telegram.md +96 -0
  598. package/docs/templates/AGENTS.md +286 -0
  599. package/docs/templates/BOOTSTRAP.md +35 -0
  600. package/docs/templates/IDENTITY.md +17 -0
  601. package/docs/templates/PROFILE.md +14 -0
  602. package/docs/templates/SOUL.md +41 -0
  603. package/docs/templates/TOOLS.md +41 -0
  604. package/docs/templates/USER.md +8 -0
  605. package/docs/test.md +43 -0
  606. package/docs/testing-onboarding-quickstart.md +76 -0
  607. package/docs/testing-philosophy.md +211 -0
  608. package/docs/thinking.md +46 -0
  609. package/docs/timezone.md +40 -0
  610. package/docs/tools.md +346 -0
  611. package/docs/troubleshooting.md +257 -0
  612. package/docs/tui.md +71 -0
  613. package/docs/typebox.md +42 -0
  614. package/docs/updating.md +138 -0
  615. package/docs/usage-cloud-aggregation-spec.md +133 -0
  616. package/docs/usage-suggestions-pipeline.md +126 -0
  617. package/docs/voicewake.md +61 -0
  618. package/docs/web.md +115 -0
  619. package/docs/webchat.md +34 -0
  620. package/docs/webhook.md +132 -0
  621. package/docs/whatsapp-clawd.jpg +0 -0
  622. package/docs/whatsapp.md +170 -0
  623. package/docs/windows.md +11 -0
  624. package/docs/wizard.md +167 -0
  625. package/package.json +209 -0
  626. package/skills/1password/SKILL.md +54 -0
  627. package/skills/1password/docs/setup.md +85 -0
  628. package/skills/1password/docs/troubleshooting.md +63 -0
  629. package/skills/1password/references/cli-examples.md +29 -0
  630. package/skills/1password/references/get-started.md +17 -0
  631. package/skills/agent-browser/SKILL.md +450 -0
  632. package/skills/agent-browser/docs/browser-use-eval.md +95 -0
  633. package/skills/agent-browser/docs/first-tests.md +261 -0
  634. package/skills/agent-browser/docs/wordle-nyt-eval.js +32 -0
  635. package/skills/aix/SKILL.md +93 -0
  636. package/skills/aix/docs/embeddings.md +40 -0
  637. package/skills/aix/docs/setup.md +58 -0
  638. package/skills/aix/docs/troubleshooting.md +41 -0
  639. package/skills/aix/references/sql.md +48 -0
  640. package/skills/apple-notes/SKILL.md +50 -0
  641. package/skills/apple-reminders/SKILL.md +67 -0
  642. package/skills/bear-notes/SKILL.md +79 -0
  643. package/skills/bird/SKILL.md +32 -0
  644. package/skills/bird/docs/auth.md +31 -0
  645. package/skills/bird/docs/troubleshooting.md +31 -0
  646. package/skills/blogwatcher/SKILL.md +46 -0
  647. package/skills/blucli/SKILL.md +27 -0
  648. package/skills/brave-search/SKILL.md +36 -0
  649. package/skills/brave-search/docs/setup.md +40 -0
  650. package/skills/brave-search/docs/troubleshooting.md +37 -0
  651. package/skills/brave-search/docs/usage.md +28 -0
  652. package/skills/brave-search/scripts/content.mjs +53 -0
  653. package/skills/brave-search/scripts/search.mjs +79 -0
  654. package/skills/browser-use-agent-sdk/SKILL.md +90 -0
  655. package/skills/camsnap/SKILL.md +25 -0
  656. package/skills/clawdhub/SKILL.md +53 -0
  657. package/skills/coding-agent/SKILL.md +274 -0
  658. package/skills/comms/SKILL.md +249 -0
  659. package/skills/comms/docs/adapters.md +54 -0
  660. package/skills/comms/docs/setup.md +56 -0
  661. package/skills/comms/docs/troubleshooting.md +44 -0
  662. package/skills/comms/references/schema.md +49 -0
  663. package/skills/computer-use/SKILL.md +204 -0
  664. package/skills/computer-use/docs/open-interpreter.md +26 -0
  665. package/skills/computer-use/docs/peekaboo.md +26 -0
  666. package/skills/computer-use/docs/setup.md +47 -0
  667. package/skills/computer-use/docs/troubleshooting.md +33 -0
  668. package/skills/discord/SKILL.md +370 -0
  669. package/skills/eightctl/SKILL.md +29 -0
  670. package/skills/eve/SKILL.md +215 -0
  671. package/skills/eve/docs/dual-account.md +84 -0
  672. package/skills/eve/docs/intelligence.md +58 -0
  673. package/skills/eve/docs/setup.md +60 -0
  674. package/skills/eve/docs/troubleshooting.md +54 -0
  675. package/skills/eve/scripts/setup-dual-account.sh +125 -0
  676. package/skills/filesystem/SKILL.md +217 -0
  677. package/skills/food-order/SKILL.md +41 -0
  678. package/skills/gemini/SKILL.md +23 -0
  679. package/skills/gh/SKILL.md +22 -0
  680. package/skills/gh/docs/usage.md +41 -0
  681. package/skills/gifgrep/SKILL.md +47 -0
  682. package/skills/github/SKILL.md +26 -0
  683. package/skills/github/docs/setup.md +21 -0
  684. package/skills/github/docs/troubleshooting.md +24 -0
  685. package/skills/gog/SKILL.md +104 -0
  686. package/skills/gog/docs/portability.md +94 -0
  687. package/skills/gog/docs/setup.md +76 -0
  688. package/skills/gog/docs/troubleshooting.md +94 -0
  689. package/skills/gog/scripts/cdp/README.md +90 -0
  690. package/skills/gog/scripts/cdp/add_test_users.py +69 -0
  691. package/skills/gog/scripts/cdp/auth_add_accounts.py +209 -0
  692. package/skills/gog/scripts/cdp/auth_add_accounts_manual.py +206 -0
  693. package/skills/gog/scripts/cdp/create_oauth_client.py +165 -0
  694. package/skills/gog/scripts/cdp/launch_cdp_chrome.sh +58 -0
  695. package/skills/google-oauth/SKILL.md +94 -0
  696. package/skills/goplaces/SKILL.md +30 -0
  697. package/skills/imsg/SKILL.md +25 -0
  698. package/skills/json-render/SKILL.md +154 -0
  699. package/skills/json-render/assets/components/README.md +21 -0
  700. package/skills/json-render/assets/components/catalog.ts +78 -0
  701. package/skills/json-render/assets/components/registry.tsx +172 -0
  702. package/skills/json-render/assets/demo/App.css +397 -0
  703. package/skills/json-render/assets/demo/App.tsx +897 -0
  704. package/skills/json-render/assets/demo/README.md +22 -0
  705. package/skills/json-render/assets/demo/catalog.ts +78 -0
  706. package/skills/json-render/assets/demo/data/nexus-core.json +31 -0
  707. package/skills/json-render/assets/demo/index.css +27 -0
  708. package/skills/json-render/assets/demo/registry.tsx +150 -0
  709. package/skills/json-render/docs/nexus-state-demo.md +84 -0
  710. package/skills/json-render/docs/shadcn-preset.md +33 -0
  711. package/skills/json-render/scripts/create-vite-demo.sh +45 -0
  712. package/skills/json-render/scripts/llm-server/README.md +33 -0
  713. package/skills/json-render/scripts/llm-server/catalog.ts +78 -0
  714. package/skills/json-render/scripts/llm-server/package-lock.json +702 -0
  715. package/skills/json-render/scripts/llm-server/package.json +18 -0
  716. package/skills/json-render/scripts/llm-server/server.ts +285 -0
  717. package/skills/local-places/SERVER_README.md +101 -0
  718. package/skills/local-places/SKILL.md +91 -0
  719. package/skills/local-places/pyproject.toml +27 -0
  720. package/skills/local-places/src/local_places/__init__.py +2 -0
  721. package/skills/local-places/src/local_places/__pycache__/__init__.cpython-314.pyc +0 -0
  722. package/skills/local-places/src/local_places/__pycache__/google_places.cpython-314.pyc +0 -0
  723. package/skills/local-places/src/local_places/__pycache__/main.cpython-314.pyc +0 -0
  724. package/skills/local-places/src/local_places/__pycache__/schemas.cpython-314.pyc +0 -0
  725. package/skills/local-places/src/local_places/google_places.py +314 -0
  726. package/skills/local-places/src/local_places/main.py +65 -0
  727. package/skills/local-places/src/local_places/schemas.py +107 -0
  728. package/skills/mcporter/SKILL.md +38 -0
  729. package/skills/model-usage/SKILL.md +45 -0
  730. package/skills/model-usage/references/codexbar-cli.md +28 -0
  731. package/skills/model-usage/scripts/model_usage.py +310 -0
  732. package/skills/nano-banana-pro/SKILL.md +30 -0
  733. package/skills/nano-banana-pro/scripts/generate_image.py +169 -0
  734. package/skills/nano-pdf/SKILL.md +20 -0
  735. package/skills/nexus-cloud/SKILL.md +53 -0
  736. package/skills/nexus-cloud/docs/security.md +24 -0
  737. package/skills/nexus-cloud/docs/setup.md +51 -0
  738. package/skills/nexus-cloud/docs/troubleshooting.md +28 -0
  739. package/skills/notion/SKILL.md +156 -0
  740. package/skills/obsidian/SKILL.md +55 -0
  741. package/skills/onboarding/SKILL.md +515 -0
  742. package/skills/onboarding/docs/CAPABILITIES.md +444 -0
  743. package/skills/onboarding/docs/CAPABILITY_TAXONOMY.md +608 -0
  744. package/skills/onboarding/docs/CLI_GRAMMAR.md +797 -0
  745. package/skills/onboarding/docs/CLI_GRAMMAR_CREDENTIALS.md +632 -0
  746. package/skills/onboarding/docs/CLI_GRAMMAR_ONBOARDING.md +815 -0
  747. package/skills/onboarding/docs/CLI_GRAMMAR_SKILLS.md +449 -0
  748. package/skills/onboarding/docs/DOCUMENTATION_OVERVIEW.md +290 -0
  749. package/skills/onboarding/docs/ENTITY_MODEL.md +582 -0
  750. package/skills/onboarding/docs/GOAL_STATE_ARCHITECTURE.md +395 -0
  751. package/skills/onboarding/docs/NEXUS_SYSTEM_OVERVIEW.md +476 -0
  752. package/skills/onboarding/docs/SKILLS_HUB_SPEC.md +477 -0
  753. package/skills/onboarding/docs/SKILLS_SPECIFICATION.md +947 -0
  754. package/skills/onboarding/docs/SKILL_GATEWAY_DESIGN.md +702 -0
  755. package/skills/onboarding/docs/SKILL_GATEWAY_PRD.md +278 -0
  756. package/skills/onboarding/docs/SKILL_INVENTORY.md +266 -0
  757. package/skills/onboarding/docs/STATE_ARCHITECTURE.md +547 -0
  758. package/skills/onboarding/docs/TROUBLESHOOTING.md +363 -0
  759. package/skills/onboarding/docs/USER_JOURNEY.md +797 -0
  760. package/skills/onboarding/docs/WOW_MOMENTS.md +232 -0
  761. package/skills/onboarding/docs/agent-apple-id.md +289 -0
  762. package/skills/onboarding/docs/skill-deep-dives/1password.md +367 -0
  763. package/skills/onboarding/docs/skill-deep-dives/TEMPLATE.md +197 -0
  764. package/skills/onboarding/docs/skill-deep-dives/aix.md +498 -0
  765. package/skills/onboarding/docs/skill-deep-dives/bird.md +357 -0
  766. package/skills/onboarding/docs/skill-deep-dives/brave-search.md +601 -0
  767. package/skills/onboarding/docs/skill-deep-dives/comms.md +607 -0
  768. package/skills/onboarding/docs/skill-deep-dives/computer-use.md +599 -0
  769. package/skills/onboarding/docs/skill-deep-dives/cron-and-heartbeat.md +576 -0
  770. package/skills/onboarding/docs/skill-deep-dives/eve.md +711 -0
  771. package/skills/onboarding/docs/skill-deep-dives/github.md +333 -0
  772. package/skills/onboarding/docs/skill-deep-dives/gog.md +640 -0
  773. package/skills/onboarding/docs/skill-deep-dives/homebrew-prereqs.md +785 -0
  774. package/skills/onboarding/docs/skill-deep-dives/nexus-cloud.md +689 -0
  775. package/skills/onboarding/docs/skill-deep-dives/qmd.md +742 -0
  776. package/skills/onboarding/docs/skill-deep-dives/telegram.md +379 -0
  777. package/skills/onboarding/docs/skill-deep-dives/wacli.md +399 -0
  778. package/skills/onboarding/docs/skill-deep-dives/weather.md +513 -0
  779. package/skills/onboarding/scripts/ralph/prd.json +215 -0
  780. package/skills/onboarding/scripts/ralph/progress.txt +99 -0
  781. package/skills/onboarding/scripts/ralph/prompt.md +87 -0
  782. package/skills/onboarding/scripts/ralph/ralph.log +84 -0
  783. package/skills/onboarding/scripts/ralph/ralph.sh +45 -0
  784. package/skills/onboarding/scripts/setup-cursor-skills.sh +40 -0
  785. package/skills/openai-image-gen/SKILL.md +31 -0
  786. package/skills/openai-image-gen/scripts/gen.py +173 -0
  787. package/skills/openai-whisper/SKILL.md +19 -0
  788. package/skills/openai-whisper-api/SKILL.md +43 -0
  789. package/skills/openai-whisper-api/scripts/transcribe.sh +85 -0
  790. package/skills/openhue/SKILL.md +30 -0
  791. package/skills/oracle/SKILL.md +105 -0
  792. package/skills/ordercli/SKILL.md +47 -0
  793. package/skills/peekaboo/SKILL.md +153 -0
  794. package/skills/qmd/SKILL.md +32 -0
  795. package/skills/qmd/docs/mcp.md +30 -0
  796. package/skills/qmd/docs/ollama.md +42 -0
  797. package/skills/qmd/docs/setup.md +44 -0
  798. package/skills/sag/SKILL.md +62 -0
  799. package/skills/skill-cli-template/SKILL.md +109 -0
  800. package/skills/skill-creator/scripts/__pycache__/quick_validate.cpython-311.pyc +0 -0
  801. package/skills/slack/SKILL.md +144 -0
  802. package/skills/songsee/SKILL.md +29 -0
  803. package/skills/sonoscli/SKILL.md +26 -0
  804. package/skills/spotify-player/SKILL.md +34 -0
  805. package/skills/summarize/SKILL.md +49 -0
  806. package/skills/telegram/SKILL.md +20 -0
  807. package/skills/telegram/docs/pairing.md +30 -0
  808. package/skills/telegram/docs/setup.md +41 -0
  809. package/skills/telegram/docs/webhook.md +17 -0
  810. package/skills/things-mac/SKILL.md +61 -0
  811. package/skills/tmux/SKILL.md +121 -0
  812. package/skills/tmux/scripts/find-sessions.sh +112 -0
  813. package/skills/tmux/scripts/wait-for-text.sh +83 -0
  814. package/skills/trello/SKILL.md +84 -0
  815. package/skills/upstream-sync/SKILL.md +151 -0
  816. package/skills/upstream-sync/scripts/auto-port.sh +227 -0
  817. package/skills/upstream-sync/scripts/check-all.sh +88 -0
  818. package/skills/upstream-sync/scripts/check-nexus.sh +146 -0
  819. package/skills/upstream-sync/scripts/check-pi-ai.sh +129 -0
  820. package/skills/video-frames/SKILL.md +29 -0
  821. package/skills/video-frames/scripts/frame.sh +81 -0
  822. package/skills/wacli/SKILL.md +48 -0
  823. package/skills/wacli/docs/auth.md +21 -0
  824. package/skills/wacli/docs/backup.md +9 -0
  825. package/skills/wacli/docs/troubleshooting.md +21 -0
  826. package/skills/weather/SKILL.md +53 -0
  827. package/skills/weather/docs/usage.md +40 -0
@@ -0,0 +1,2833 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Upstream Sync CLI
4
+ *
5
+ * Tracks upstream merge commits and dispatches Codex agents to port them to nexus.
6
+ *
7
+ * Usage:
8
+ * npx tsx src/cli/upstream-sync-cli.ts [command]
9
+ *
10
+ * Commands:
11
+ * (default) - Full sync: fetch, dispatch agents, show status
12
+ * status - Just show current status
13
+ * merge <pr> - Merge a pending_review PR into main
14
+ * ignore <pr> - Mark a PR as ignored
15
+ * retry <pr> - Re-dispatch agent for a PR
16
+ * show <pr> - Show diff for a PR
17
+ * pause - Disable new agent dispatch
18
+ * resume - Re-enable agent dispatch
19
+ * bundle-split - Split a bundle into smaller ones
20
+ * bundle-requeue - Reset a bundle for re-dispatch
21
+ * bundle-dispatch-oldest - Dispatch oldest pending bundles
22
+ */
23
+ import { execSync, spawn } from "node:child_process";
24
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, openSync, closeSync, statSync, } from "node:fs";
25
+ import { dirname, join, resolve } from "node:path";
26
+ import { fileURLToPath } from "node:url";
27
+ const __dirname = dirname(fileURLToPath(import.meta.url));
28
+ const PROJECT_ROOT = resolve(__dirname, "../..");
29
+ const STATE_FILE = join(PROJECT_ROOT, ".upstream-sync/state.json");
30
+ const TMUX_SOCKET = "/tmp/upstream-sync.sock";
31
+ const RUNS_DIR = join(PROJECT_ROOT, ".upstream-sync/runs");
32
+ // ============================================================================
33
+ // State Management
34
+ // ============================================================================
35
+ function loadState() {
36
+ if (!existsSync(STATE_FILE)) {
37
+ throw new Error(`State file not found: ${STATE_FILE}`);
38
+ }
39
+ return JSON.parse(readFileSync(STATE_FILE, "utf-8"));
40
+ }
41
+ function saveState(state) {
42
+ const dir = dirname(STATE_FILE);
43
+ if (!existsSync(dir)) {
44
+ mkdirSync(dir, { recursive: true });
45
+ }
46
+ writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
47
+ }
48
+ // ============================================================================
49
+ // Commit Bundling
50
+ // ============================================================================
51
+ // Normalize type to lowercase and map legacy module names to proper types
52
+ function normalizeType(type) {
53
+ const lower = type.toLowerCase();
54
+ // Map legacy module names to appropriate types
55
+ const typeMap = {
56
+ // These are module names, not commit types - treat as misc changes
57
+ agents: "chore",
58
+ auth: "chore",
59
+ browser: "chore",
60
+ changelog: "docs",
61
+ config: "chore",
62
+ cron: "chore",
63
+ deps: "chore",
64
+ discord: "chore",
65
+ models: "chore",
66
+ slack: "chore",
67
+ telegram: "chore",
68
+ thinking: "chore",
69
+ tui: "chore",
70
+ typing: "chore",
71
+ wizard: "chore",
72
+ // Plural forms
73
+ chores: "chore",
74
+ tests: "test",
75
+ };
76
+ return typeMap[lower] || lower;
77
+ }
78
+ // Parse conventional commit format: type(scope)!: description
79
+ function parseConventionalCommit(title) {
80
+ const match = title.match(/^(\w+)(?:\(([^)]+)\))?(!)?\s*:\s*(.+)$/);
81
+ if (match) {
82
+ return {
83
+ type: normalizeType(match[1]),
84
+ scope: match[2],
85
+ breaking: match[3] === "!",
86
+ description: match[4],
87
+ };
88
+ }
89
+ return { type: "other", breaking: false, description: title };
90
+ }
91
+ // Get priority for a commit type (higher = more important)
92
+ function getTypePriority(type, breaking) {
93
+ if (breaking)
94
+ return 100; // Breaking changes are highest priority
95
+ const normalized = normalizeType(type);
96
+ switch (normalized) {
97
+ case "feat":
98
+ return 80;
99
+ case "fix":
100
+ return 60;
101
+ case "perf":
102
+ return 50;
103
+ case "refactor":
104
+ return 40;
105
+ case "style":
106
+ return 20;
107
+ case "docs":
108
+ return 15;
109
+ case "test":
110
+ return 10;
111
+ case "chore":
112
+ return 5;
113
+ case "ci":
114
+ return 3;
115
+ default:
116
+ return 30;
117
+ }
118
+ }
119
+ // Group direct commits into bundles
120
+ function bundleDirectCommits(state) {
121
+ const directCommits = Object.entries(state.merges)
122
+ .filter(([_, e]) => e.isDirectCommit && e.status === "new" && !e.bundleId)
123
+ .map(([sha, e]) => ({ sha, entry: e, parsed: parseConventionalCommit(e.title) }));
124
+ if (directCommits.length === 0)
125
+ return [];
126
+ // Group by: date (day) + scope + type
127
+ const groups = new Map();
128
+ for (const commit of directCommits) {
129
+ const date = commit.entry.date.split("T")[0]; // YYYY-MM-DD
130
+ const scope = commit.parsed.scope || "general";
131
+ const type = commit.parsed.type;
132
+ const author = commit.entry.author || "unknown";
133
+ // Key: date|author|scope|type
134
+ const key = `${date}|${author}|${scope}|${type}`;
135
+ if (!groups.has(key)) {
136
+ groups.set(key, []);
137
+ }
138
+ groups.get(key).push(commit);
139
+ }
140
+ // Convert groups to bundles
141
+ const bundles = [];
142
+ for (const [key, commits] of groups) {
143
+ const [date, author, scope, type] = key.split("|");
144
+ const sorted = commits.sort((a, b) => new Date(a.entry.date).getTime() - new Date(b.entry.date).getTime());
145
+ // Calculate priority based on highest-priority commit in bundle
146
+ const maxPriority = Math.max(...commits.map((c) => getTypePriority(c.parsed.type, c.parsed.breaking)));
147
+ // Check if any commit is breaking
148
+ const hasBreaking = commits.some((c) => c.parsed.breaking);
149
+ // Generate bundle name
150
+ const typeName = hasBreaking ? `${type}!` : type;
151
+ const scopeName = scope === "general" ? "" : `(${scope})`;
152
+ const bundleName = commits.length === 1
153
+ ? commits[0].entry.title
154
+ : `${typeName}${scopeName}: ${commits.length} commits (${date})`;
155
+ // Generate description
156
+ const descriptions = commits.map((c) => `- ${c.entry.title}`).join("\n");
157
+ // Create unique bundle ID using first commit SHA
158
+ const firstSha = sorted[0].sha.slice(0, 8);
159
+ const bundle = {
160
+ id: `bundle-${type}-${scope}-${firstSha}`,
161
+ name: bundleName,
162
+ description: `${commits.length} ${type} commit(s) for ${scope === "general" ? "general" : scope}\n\nCommits:\n${descriptions}`,
163
+ commits: sorted.map((c) => c.sha),
164
+ author,
165
+ dateRange: {
166
+ start: sorted[0].entry.date,
167
+ end: sorted[sorted.length - 1].entry.date,
168
+ },
169
+ scope: scope === "general" ? undefined : scope,
170
+ type,
171
+ status: "pending",
172
+ priority: maxPriority,
173
+ };
174
+ bundles.push(bundle);
175
+ }
176
+ // Sort bundles by priority (highest first)
177
+ return bundles.sort((a, b) => b.priority - a.priority);
178
+ }
179
+ // Types that should ALWAYS be consolidated into daily bundles (low priority, safe to batch)
180
+ const LOW_PRIORITY_TYPES = new Set(["fix", "docs", "test", "chore", "style", "ci", "build"]);
181
+ const BUNDLE_STRATEGY = (process.env.NEXUS_UPSTREAM_SYNC_BUNDLE_STRATEGY ?? "aggressive");
182
+ const MIN_BUNDLE_LINES = (() => {
183
+ const raw = process.env.NEXUS_UPSTREAM_SYNC_BUNDLE_MIN_LINES ?? "1000";
184
+ const parsed = Number.parseInt(raw, 10);
185
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 1000;
186
+ })();
187
+ function parseNumstatLineTotal(line) {
188
+ const [addedRaw, deletedRaw] = line.trim().split("\t");
189
+ if (!addedRaw || !deletedRaw)
190
+ return 0;
191
+ if (addedRaw === "-" || deletedRaw === "-")
192
+ return 0;
193
+ const added = Number.parseInt(addedRaw, 10);
194
+ const deleted = Number.parseInt(deletedRaw, 10);
195
+ if (!Number.isFinite(added) || !Number.isFinite(deleted))
196
+ return 0;
197
+ return added + deleted;
198
+ }
199
+ function getCommitLineCount(upstreamPath, sha, cache) {
200
+ const existing = cache.get(sha);
201
+ if (existing != null)
202
+ return existing;
203
+ try {
204
+ const output = exec(`git show --numstat --format= ${sha}`, upstreamPath);
205
+ const total = output
206
+ .split("\n")
207
+ .map((line) => parseNumstatLineTotal(line))
208
+ .reduce((sum, value) => sum + value, 0);
209
+ cache.set(sha, total);
210
+ return total;
211
+ }
212
+ catch {
213
+ cache.set(sha, 0);
214
+ return 0;
215
+ }
216
+ }
217
+ function getBundleLineCount(bundle, upstreamPath, cache) {
218
+ return bundle.commits.reduce((sum, sha) => sum + getCommitLineCount(upstreamPath, sha, cache), 0);
219
+ }
220
+ // Merge bundles based on priority strategy:
221
+ // - Breaking changes: NEVER consolidate (individual review required)
222
+ // - Features: consolidate by scope when aggressive
223
+ // - Refactors: consolidate BY SCOPE (e.g., all refactor(ui) together)
224
+ // - Low priority (fix, docs, test, chore, etc.): consolidate by day (aggressive) or day+type (standard)
225
+ function consolidateBundles(state, bundles) {
226
+ const consolidated = [];
227
+ const aggressive = BUNDLE_STRATEGY === "aggressive";
228
+ const upstreamPath = getUpstreamPath(state);
229
+ const commitSizeCache = new Map();
230
+ const bundleLines = (bundle) => getBundleLineCount(bundle, upstreamPath, commitSizeCache);
231
+ // Categorize bundles by consolidation strategy
232
+ const keepSeparate = []; // Breaking
233
+ const scopeBundles = new Map(); // Consolidate by scope
234
+ const toConsolidate = []; // Consolidate by day
235
+ const buildAggregatedBundle = (params) => {
236
+ const allCommits = params.bundles.flatMap((b) => b.commits);
237
+ const maxPriority = Math.max(...params.bundles.map((b) => b.priority));
238
+ const authors = [...new Set(params.bundles.map((b) => b.author))];
239
+ const authorStr = authors.length === 1 ? authors[0] : `${authors.length} authors`;
240
+ const firstSha = allCommits[0].slice(0, 8);
241
+ const dateStart = params.bundles.reduce((min, b) => (b.dateRange.start < min ? b.dateRange.start : min), params.bundles[0].dateRange.start);
242
+ const dateEnd = params.bundles.reduce((max, b) => (b.dateRange.end > max ? b.dateRange.end : max), params.bundles[0].dateRange.end);
243
+ const dateLabel = dateStart.split("T")[0] === dateEnd.split("T")[0]
244
+ ? dateStart.split("T")[0]
245
+ : `${dateStart.split("T")[0]} to ${dateEnd.split("T")[0]}`;
246
+ const partLabel = params.part > 1 ? ` (part ${params.part})` : "";
247
+ const totalLines = params.bundles.reduce((sum, b) => sum + bundleLines(b), 0);
248
+ const description = `${params.descriptionPrefix ?? "Aggregated bundle"}\n` +
249
+ `\n**Total lines:** ~${totalLines} (target ${MIN_BUNDLE_LINES}+)\n` +
250
+ `**Authors:** ${authors.join(", ")}\n\n` +
251
+ params.bundles.map((b) => `### ${b.name}\n${b.description}`).join("\n\n");
252
+ return {
253
+ id: `${params.idPrefix}-${firstSha}${params.part > 1 ? `-p${params.part}` : ""}`,
254
+ name: `${params.namePrefix}: ${allCommits.length} commits (${dateLabel})${partLabel}`,
255
+ description,
256
+ commits: allCommits,
257
+ author: authorStr,
258
+ dateRange: { start: dateStart, end: dateEnd },
259
+ type: params.type,
260
+ scope: params.scope,
261
+ status: "pending",
262
+ priority: maxPriority,
263
+ };
264
+ };
265
+ const packByMinLines = (params) => {
266
+ const sorted = [...params.bundles].sort((a, b) => a.dateRange.start.localeCompare(b.dateRange.start));
267
+ const results = [];
268
+ let current = [];
269
+ let currentLines = 0;
270
+ let part = 1;
271
+ const flush = () => {
272
+ if (current.length === 0)
273
+ return;
274
+ results.push(buildAggregatedBundle({
275
+ ...params,
276
+ bundles: current,
277
+ part,
278
+ }));
279
+ current = [];
280
+ currentLines = 0;
281
+ part += 1;
282
+ };
283
+ for (const bundle of sorted) {
284
+ const lines = bundleLines(bundle);
285
+ if (lines >= MIN_BUNDLE_LINES) {
286
+ flush();
287
+ results.push(bundle);
288
+ continue;
289
+ }
290
+ if (currentLines + lines >= MIN_BUNDLE_LINES && current.length > 0) {
291
+ current.push(bundle);
292
+ flush();
293
+ continue;
294
+ }
295
+ current.push(bundle);
296
+ currentLines += lines;
297
+ }
298
+ flush();
299
+ return results;
300
+ };
301
+ for (const bundle of bundles) {
302
+ // Breaking changes (priority >= 100) - NEVER consolidate
303
+ if (bundle.priority >= 100) {
304
+ keepSeparate.push(bundle);
305
+ continue;
306
+ }
307
+ if (bundle.type === "feat" && !aggressive) {
308
+ keepSeparate.push(bundle);
309
+ continue;
310
+ }
311
+ if (bundle.type === "feat" || bundle.type === "refactor") {
312
+ const scope = bundle.scope || "general";
313
+ const key = `${bundle.type}|${scope}`;
314
+ if (!scopeBundles.has(key)) {
315
+ scopeBundles.set(key, []);
316
+ }
317
+ scopeBundles.get(key).push(bundle);
318
+ continue;
319
+ }
320
+ if (bundle.type && LOW_PRIORITY_TYPES.has(bundle.type)) {
321
+ toConsolidate.push(bundle);
322
+ continue;
323
+ }
324
+ toConsolidate.push(bundle);
325
+ }
326
+ consolidated.push(...keepSeparate);
327
+ for (const [key, group] of scopeBundles) {
328
+ if (group.length === 1) {
329
+ consolidated.push(group[0]);
330
+ continue;
331
+ }
332
+ const [type, scope] = key.split("|");
333
+ const scopeLabel = scope ? `(${scope})` : "";
334
+ const namePrefix = type === "feat" ? `✨ feat${scopeLabel}` : `ā™»ļø refactor${scopeLabel}`;
335
+ const descriptionPrefix = type === "feat"
336
+ ? `Aggregated feature bundle for ${scope || "general"}`
337
+ : `Aggregated refactor bundle for ${scope || "general"}`;
338
+ consolidated.push(...packByMinLines({
339
+ idPrefix: `bundle-${type}-${scope}`,
340
+ namePrefix,
341
+ descriptionPrefix,
342
+ bundles: group,
343
+ type,
344
+ scope: scope || undefined,
345
+ }));
346
+ }
347
+ if (aggressive) {
348
+ const byDate = new Map();
349
+ for (const bundle of toConsolidate) {
350
+ const date = bundle.dateRange.start.split("T")[0];
351
+ if (!byDate.has(date)) {
352
+ byDate.set(date, []);
353
+ }
354
+ byDate.get(date).push(bundle);
355
+ }
356
+ for (const [date, group] of byDate) {
357
+ if (group.length === 1) {
358
+ consolidated.push(group[0]);
359
+ continue;
360
+ }
361
+ const typeCounts = group.reduce((acc, b) => {
362
+ const t = b.type || "other";
363
+ acc[t] = (acc[t] || 0) + b.commits.length;
364
+ return acc;
365
+ }, {});
366
+ const typeSummary = Object.entries(typeCounts)
367
+ .map(([type, count]) => `${type}:${count}`)
368
+ .join(", ");
369
+ consolidated.push(...packByMinLines({
370
+ idPrefix: `bundle-daily-maintenance-${date.replace(/-/g, "")}`,
371
+ namePrefix: "🧰 Daily maintenance",
372
+ descriptionPrefix: `Daily maintenance bundle for ${date}\n\n**Types:** ${typeSummary}`,
373
+ bundles: group,
374
+ type: "maintenance",
375
+ }));
376
+ }
377
+ return consolidated.sort((a, b) => b.priority - a.priority);
378
+ }
379
+ // Standard strategy: group low-priority bundles by date + type
380
+ const byDateType = new Map();
381
+ for (const bundle of toConsolidate) {
382
+ const date = bundle.dateRange.start.split("T")[0];
383
+ const key = `${date}|${bundle.type}`;
384
+ if (!byDateType.has(key)) {
385
+ byDateType.set(key, []);
386
+ }
387
+ byDateType.get(key).push(bundle);
388
+ }
389
+ for (const [key, group] of byDateType) {
390
+ if (group.length === 1) {
391
+ consolidated.push(group[0]);
392
+ }
393
+ else {
394
+ const [date, type] = key.split("|");
395
+ const typeLabel = {
396
+ fix: "šŸ› Daily fixes",
397
+ docs: "šŸ“š Daily docs",
398
+ test: "🧪 Daily tests",
399
+ chore: "šŸ”§ Daily chores",
400
+ style: "šŸ’… Daily style",
401
+ ci: "āš™ļø Daily CI",
402
+ build: "šŸ—ļø Daily build",
403
+ }[type] || `šŸ“¦ Daily ${type}`;
404
+ consolidated.push(...packByMinLines({
405
+ idPrefix: `bundle-daily-${type}-${date.replace(/-/g, "")}`,
406
+ namePrefix: typeLabel,
407
+ descriptionPrefix: `Consolidated ${type} changes from ${date}`,
408
+ bundles: group,
409
+ type,
410
+ }));
411
+ }
412
+ }
413
+ return consolidated.sort((a, b) => b.priority - a.priority);
414
+ }
415
+ // Print bundle summary
416
+ function printBundles(bundles) {
417
+ if (bundles.length === 0) {
418
+ console.log("No bundles to display.");
419
+ return;
420
+ }
421
+ console.log("\n" + "═".repeat(60));
422
+ console.log(" DIRECT COMMIT BUNDLES");
423
+ console.log("═".repeat(60));
424
+ // Group by priority tier
425
+ const breaking = bundles.filter((b) => b.priority >= 100);
426
+ const features = bundles.filter((b) => b.priority >= 70 && b.priority < 100);
427
+ const fixes = bundles.filter((b) => b.priority >= 50 && b.priority < 70);
428
+ const refactors = bundles.filter((b) => b.priority >= 30 && b.priority < 50);
429
+ const other = bundles.filter((b) => b.priority < 30);
430
+ const printTier = (name, emoji, tier) => {
431
+ if (tier.length === 0)
432
+ return;
433
+ console.log(`\n ${emoji} ${name} (${tier.length} bundle${tier.length > 1 ? "s" : ""}):`);
434
+ for (const b of tier) {
435
+ const commitCount = b.commits.length;
436
+ console.log(` [${b.id.slice(0, 12)}] ${b.name}`);
437
+ console.log(` ${commitCount} commit(s) | ${b.author} | ${b.dateRange.start.split("T")[0]}`);
438
+ }
439
+ };
440
+ printTier("BREAKING CHANGES", "šŸ”„", breaking);
441
+ printTier("FEATURES", "✨", features);
442
+ printTier("FIXES", "šŸ›", fixes);
443
+ printTier("REFACTORS", "ā™»ļø", refactors);
444
+ printTier("OTHER", "šŸ“¦", other);
445
+ console.log("\n" + "═".repeat(60));
446
+ console.log(` Total: ${bundles.length} bundles, ${bundles.reduce((sum, b) => sum + b.commits.length, 0)} commits`);
447
+ console.log("═".repeat(60) + "\n");
448
+ }
449
+ function categorizeSplitBucket(title, type) {
450
+ const lower = title.toLowerCase();
451
+ if (lower.includes("appcast") || lower.includes("release") || lower.includes("changelog")) {
452
+ return "release";
453
+ }
454
+ if (type === "docs" ||
455
+ lower.startsWith("docs") ||
456
+ lower.includes("documentation") ||
457
+ lower.includes("faq") ||
458
+ lower.includes("readme")) {
459
+ return "docs";
460
+ }
461
+ if (type === "ci" ||
462
+ lower.includes("install") ||
463
+ lower.includes("smoke") ||
464
+ lower.includes("docker") ||
465
+ lower.includes("postinstall") ||
466
+ lower.includes("workflow")) {
467
+ return "install";
468
+ }
469
+ if (lower.includes("discord") ||
470
+ lower.includes("telegram") ||
471
+ lower.includes("slack") ||
472
+ lower.includes("whatsapp") ||
473
+ lower.includes("typing")) {
474
+ return "chat";
475
+ }
476
+ if (lower.includes("model") ||
477
+ lower.includes("gemini") ||
478
+ lower.includes("minimax") ||
479
+ lower.includes("moonshot") ||
480
+ lower.includes("kimi")) {
481
+ return "models";
482
+ }
483
+ if (lower.includes("cron") || lower.includes("gateway")) {
484
+ return "gateway";
485
+ }
486
+ if (lower.includes("voice") || lower.includes("plugin") || lower.includes("apply_patch")) {
487
+ return "voice";
488
+ }
489
+ if (type && type !== "other")
490
+ return type;
491
+ return "misc";
492
+ }
493
+ // ============================================================================
494
+ // Git Operations
495
+ // ============================================================================
496
+ function exec(cmd, cwd) {
497
+ try {
498
+ return execSync(cmd, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
499
+ }
500
+ catch (e) {
501
+ const error = e;
502
+ throw new Error(error.stderr || error.message || "Command failed");
503
+ }
504
+ }
505
+ function isDispatchDisabled(state) {
506
+ return process.env.NEXUS_UPSTREAM_SYNC_DISABLE_DISPATCH === "1" || Boolean(state?.dispatchDisabled);
507
+ }
508
+ function assertDispatchEnabled(state) {
509
+ if (isDispatchDisabled(state)) {
510
+ throw new Error("Dispatch is disabled. Run 'upstream-sync resume' or unset NEXUS_UPSTREAM_SYNC_DISABLE_DISPATCH=1.");
511
+ }
512
+ }
513
+ function branchExists(branchName) {
514
+ try {
515
+ execSync(`git show-ref --verify --quiet refs/heads/${branchName}`, {
516
+ cwd: PROJECT_ROOT,
517
+ stdio: "ignore",
518
+ });
519
+ return true;
520
+ }
521
+ catch {
522
+ return false;
523
+ }
524
+ }
525
+ function isGitAncestor(ancestor, target, cwd) {
526
+ try {
527
+ execSync(`git merge-base --is-ancestor ${ancestor} ${target}`, { cwd, stdio: "ignore" });
528
+ return true;
529
+ }
530
+ catch {
531
+ return false;
532
+ }
533
+ }
534
+ function assertBranchBasedOnMain(branchName, worktreePath) {
535
+ const mainCommit = exec("git rev-parse main", PROJECT_ROOT);
536
+ if (worktreePath && existsSync(worktreePath)) {
537
+ if (!isGitAncestor(mainCommit, "HEAD", worktreePath)) {
538
+ throw new Error(`Worktree ${worktreePath} is not based on current main (${mainCommit}). Remove it and re-dispatch.`);
539
+ }
540
+ return mainCommit;
541
+ }
542
+ if (branchExists(branchName) && !isGitAncestor(mainCommit, branchName, PROJECT_ROOT)) {
543
+ throw new Error(`Branch ${branchName} is not based on current main (${mainCommit}). Delete/recreate before dispatch.`);
544
+ }
545
+ return mainCommit;
546
+ }
547
+ function ensureRunsDir() {
548
+ if (!existsSync(RUNS_DIR)) {
549
+ mkdirSync(RUNS_DIR, { recursive: true });
550
+ }
551
+ }
552
+ function getRunPaths(runId) {
553
+ ensureRunsDir();
554
+ return {
555
+ metaPath: join(RUNS_DIR, `${runId}.json`),
556
+ logPath: join(RUNS_DIR, `${runId}.log`),
557
+ exitPath: join(RUNS_DIR, `${runId}.exit`),
558
+ };
559
+ }
560
+ function bashSingleQuote(s) {
561
+ // 'foo'"'"'bar' pattern
562
+ return `'${s.replace(/'/g, `'\"'\"'`)}'`;
563
+ }
564
+ function isPidRunning(pid) {
565
+ try {
566
+ execSync(`kill -0 ${pid}`, { stdio: "ignore" });
567
+ return true;
568
+ }
569
+ catch {
570
+ return false;
571
+ }
572
+ }
573
+ function readLastLines(filePath, maxLines) {
574
+ try {
575
+ const txt = readFileSync(filePath, "utf-8");
576
+ const lines = txt.split("\n");
577
+ return lines.slice(Math.max(0, lines.length - maxLines)).join("\n");
578
+ }
579
+ catch {
580
+ return "";
581
+ }
582
+ }
583
+ function isRunStalled(runLog, runStartedAt) {
584
+ if (!runLog || !runStartedAt)
585
+ return false;
586
+ try {
587
+ const st = statSync(runLog);
588
+ const lastLogMs = st.mtimeMs;
589
+ const startedMs = new Date(runStartedAt).getTime();
590
+ const now = Date.now();
591
+ // If it's been running a while but no log output recently, treat as stalled.
592
+ if (now - startedMs > 15 * 60 * 1000 && now - lastLogMs > 5 * 60 * 1000)
593
+ return true;
594
+ return false;
595
+ }
596
+ catch {
597
+ return false;
598
+ }
599
+ }
600
+ function startDetachedAgentRun(opts) {
601
+ const startedAt = new Date().toISOString();
602
+ const { metaPath, logPath, exitPath } = getRunPaths(opts.runId);
603
+ // One combined log for stdout/stderr
604
+ const fd = openSync(logPath, "a");
605
+ const promptArg = bashSingleQuote(opts.prompt);
606
+ const commitMsgArg = bashSingleQuote(opts.commitMessage);
607
+ const exitPathArg = bashSingleQuote(exitPath);
608
+ const cmd = `rc=0; ` +
609
+ `echo "== upstream-sync run ${opts.runId} =="; ` +
610
+ `echo "startedAt=${startedAt}"; ` +
611
+ `echo "cwd=${opts.cwd}"; ` +
612
+ `echo ""; ` +
613
+ `npm install || rc=$?; ` +
614
+ `if [ $rc -eq 0 ]; then codex exec --dangerously-bypass-approvals-and-sandbox ${promptArg} || rc=$?; fi; ` +
615
+ `if [ $rc -eq 0 ]; then ` +
616
+ ` git add -A || rc=$?; ` +
617
+ ` git reset PORT_TASK.md package-lock.json 2>/dev/null || true; ` +
618
+ ` if git diff --cached --quiet; then ` +
619
+ ` echo "NO_CHANGES_TO_COMMIT"; rc=10; ` +
620
+ ` else ` +
621
+ ` git commit -m ${commitMsgArg} || rc=$?; ` +
622
+ ` fi; ` +
623
+ `fi; ` +
624
+ `echo $rc > ${exitPathArg}; ` +
625
+ `echo "exitCode=$rc"; ` +
626
+ `exit $rc;`;
627
+ const child = spawn("/bin/bash", ["-lc", cmd], {
628
+ cwd: opts.cwd,
629
+ detached: true,
630
+ stdio: ["ignore", fd, fd],
631
+ env: process.env,
632
+ });
633
+ try {
634
+ writeFileSync(metaPath, JSON.stringify({ runId: opts.runId, pid: child.pid, startedAt, cwd: opts.cwd, logPath, exitPath }, null, 2) + "\n");
635
+ }
636
+ catch {
637
+ // ignore
638
+ }
639
+ child.unref();
640
+ closeSync(fd);
641
+ return { pid: child.pid, logPath, exitPath, metaPath, startedAt };
642
+ }
643
+ function getUpstreamPath(state) {
644
+ return resolve(PROJECT_ROOT, state.upstream.path);
645
+ }
646
+ function fetchUpstream(state) {
647
+ const upstreamPath = getUpstreamPath(state);
648
+ console.log("šŸ“” Fetching upstream...");
649
+ // Fetch only; avoid pull conflicts in local upstream clone.
650
+ exec("git fetch --prune origin", upstreamPath);
651
+ state.lastFetched = new Date().toISOString();
652
+ }
653
+ function findNewCommits(state) {
654
+ const upstreamPath = getUpstreamPath(state);
655
+ const since = state.trackingStartDate;
656
+ // Get ALL commits since tracking start date (both merges and direct commits)
657
+ // Format: SHA|subject|date|author|parent_count
658
+ const output = exec(`git log --format="%H|%s|%cI|%an|%P" --since="${since}" origin/main`, upstreamPath);
659
+ if (!output)
660
+ return [];
661
+ const commits = [];
662
+ for (const line of output.split("\n")) {
663
+ if (!line.trim())
664
+ continue;
665
+ const parts = line.split("|");
666
+ const sha = parts[0];
667
+ const subject = parts[1];
668
+ const date = parts[2];
669
+ const author = parts[3];
670
+ const parents = parts[4] || "";
671
+ // Skip if already tracked
672
+ if (state.merges[sha])
673
+ continue;
674
+ // Determine if this is a merge commit (has 2+ parents) or direct commit
675
+ const parentCount = parents
676
+ .trim()
677
+ .split(" ")
678
+ .filter((p) => p).length;
679
+ const isMerge = parentCount >= 2;
680
+ if (isMerge) {
681
+ // Extract PR number from "Merge pull request #XXX from user/branch"
682
+ const prMatch = subject.match(/Merge pull request #(\d+) from (.+)/);
683
+ const prNumber = prMatch ? parseInt(prMatch[1], 10) : undefined;
684
+ const title = prMatch ? prMatch[2] : subject;
685
+ commits.push({ sha, prNumber, title, date, isDirectCommit: false, author });
686
+ }
687
+ else {
688
+ // Check if this references a PR anywhere in the subject:
689
+ // 1. Squash merge: "feat: something (#123)"
690
+ // 2. Reference: "fix: something (thanks @user) (#123)"
691
+ // 3. Hash reference: "fix: related to #123"
692
+ const prRefMatch = subject.match(/\(#(\d+)\)/) || subject.match(/#(\d{3,})/);
693
+ if (prRefMatch) {
694
+ // References a PR - treat as a PR-related commit
695
+ const prNumber = parseInt(prRefMatch[1], 10);
696
+ // Clean title: remove (#XXX) patterns
697
+ const title = subject
698
+ .replace(/\s*\(#\d+\)\s*/g, " ")
699
+ .replace(/\s+/g, " ")
700
+ .trim();
701
+ commits.push({ sha, prNumber, title, date, isDirectCommit: false, author });
702
+ }
703
+ else {
704
+ // True direct commit - no PR number reference
705
+ commits.push({ sha, title: subject, date, isDirectCommit: true, author });
706
+ }
707
+ }
708
+ }
709
+ return commits;
710
+ }
711
+ // Legacy alias for compatibility
712
+ function findNewMerges(state) {
713
+ return findNewCommits(state);
714
+ }
715
+ // ============================================================================
716
+ // Agent Dispatch
717
+ // ============================================================================
718
+ function generatePortTask(state, sha, entry) {
719
+ const upstreamPath = getUpstreamPath(state);
720
+ const baseCommit = entry.baseCommit || exec("git rev-parse main", PROJECT_ROOT);
721
+ // Get the diff for this commit
722
+ const diff = exec(`git show ${sha} --stat`, upstreamPath);
723
+ const commitType = entry.isDirectCommit ? "Direct Commit" : `PR #${entry.prNumber || "unknown"}`;
724
+ const commitLabel = entry.isDirectCommit
725
+ ? `⭐ DIRECT COMMIT (HIGH PRIORITY)`
726
+ : `PR #${entry.prNumber}`;
727
+ return `# Port Task: ${commitLabel} - ${entry.title}
728
+
729
+ ## Source
730
+ ${entry.isDirectCommit ? "⭐ **DIRECT COMMIT** - Pushed directly to main by core maintainer" : `Upstream PR #${entry.prNumber}`}
731
+ ${entry.author ? `Author: ${entry.author}` : ""}
732
+ Commit: ${sha}
733
+ Base branch: main @ ${baseCommit}
734
+
735
+ ## Files Changed
736
+ ${diff}
737
+
738
+ ## Full Diff
739
+ Run: git show ${sha}
740
+
741
+ ## Instructions
742
+
743
+ 1. Review the changes in upstream at: ${upstreamPath}
744
+ 2. Apply equivalent changes to this nexus codebase
745
+ 3. Adapt imports/paths if they differ from upstream
746
+ 4. Confirm branch base: \`git merge-base --is-ancestor ${baseCommit} HEAD\`
747
+ 5. Run \`npm run build\` to verify compilation
748
+ 6. Run relevant tests if they exist
749
+
750
+ ## Commit Instructions
751
+ IMPORTANT: Use git directly, NOT scripts/committer:
752
+
753
+ \`\`\`bash
754
+ git add <changed-files>
755
+ git commit -m "feat: port ${commitType} - ${entry.title}
756
+
757
+ Ported from upstream ${commitType}"
758
+ \`\`\`
759
+
760
+ ## Notes
761
+ - Don't modify CHANGELOG.md
762
+ - If tests fail due to missing dependencies in worktree, commit anyway
763
+ - Focus on the core functionality, skip UI-only changes if any
764
+ - Do NOT use scripts/committer - use git add && git commit directly
765
+ ${entry.isDirectCommit ? "- ⭐ This is a DIRECT COMMIT from a core maintainer - handle with care!" : ""}
766
+ `;
767
+ }
768
+ function dispatchAgent(state, sha) {
769
+ assertDispatchEnabled(state);
770
+ const entry = state.merges[sha];
771
+ if (!entry)
772
+ throw new Error(`Unknown commit: ${sha}`);
773
+ const identifier = entry.prNumber || sha.slice(0, 8);
774
+ const branchName = `port/pr-${identifier}`;
775
+ const worktreePath = `/tmp/nexus-port-${identifier}`;
776
+ const mainCommit = assertBranchBasedOnMain(branchName, worktreePath);
777
+ const label = entry.isDirectCommit ? `⭐ DIRECT COMMIT` : `PR #${identifier}`;
778
+ console.log(`šŸš€ Dispatching agent for ${label}: ${entry.title}`);
779
+ // Check if worktree already exists
780
+ if (existsSync(worktreePath)) {
781
+ console.log(` Worktree exists at ${worktreePath}, reusing...`);
782
+ }
783
+ else {
784
+ // Create worktree
785
+ console.log(` Creating worktree at ${worktreePath}...`);
786
+ try {
787
+ exec(`git worktree add -b ${branchName} ${worktreePath} main`, PROJECT_ROOT);
788
+ }
789
+ catch (e) {
790
+ // Branch might already exist
791
+ try {
792
+ exec(`git worktree add ${worktreePath} ${branchName}`, PROJECT_ROOT);
793
+ }
794
+ catch {
795
+ throw new Error(`Failed to create worktree: ${e}`);
796
+ }
797
+ }
798
+ }
799
+ // Generate PORT_TASK.md
800
+ const taskContent = generatePortTask(state, sha, entry);
801
+ writeFileSync(join(worktreePath, "PORT_TASK.md"), taskContent);
802
+ console.log(` Generated PORT_TASK.md`);
803
+ // Start detached runner (no tmux control path)
804
+ const runId = `pr-${identifier}-${Date.now()}`;
805
+ const prompt = "Read PORT_TASK.md and complete the porting task. Commit your changes when done (a supervisor will also attempt to commit).";
806
+ const commitMessage = "feat: port upstream changes";
807
+ const run = startDetachedAgentRun({ runId, cwd: worktreePath, prompt, commitMessage });
808
+ console.log(` Started detached runner pid=${run.pid}`);
809
+ // Update state
810
+ entry.status = "porting";
811
+ entry.baseCommit = mainCommit;
812
+ entry.portBranch = branchName;
813
+ entry.worktree = worktreePath;
814
+ entry.runId = runId;
815
+ entry.runPid = run.pid;
816
+ entry.runLog = run.logPath;
817
+ entry.runStartedAt = run.startedAt;
818
+ saveState(state);
819
+ }
820
+ // ============================================================================
821
+ // Status Checking
822
+ // ============================================================================
823
+ function checkAgentStatus(state, sha) {
824
+ const entry = state.merges[sha];
825
+ if (!entry || entry.status !== "porting")
826
+ return "not_started";
827
+ // Prefer detached runner PID if present
828
+ if (typeof entry.runPid === "number") {
829
+ // If we already have a commit, it's done.
830
+ if (entry.portCommit)
831
+ return "done";
832
+ // If branch has a commit, it's done (even if process is still alive).
833
+ if (entry.portBranch) {
834
+ try {
835
+ const commits = exec(`git log main..${entry.portBranch} --oneline`, PROJECT_ROOT);
836
+ if (commits.trim()) {
837
+ const latestCommit = commits.split("\n")[0].split(" ")[0];
838
+ entry.portCommit = latestCommit;
839
+ return "done";
840
+ }
841
+ }
842
+ catch {
843
+ // ignore
844
+ }
845
+ }
846
+ if (!isPidRunning(entry.runPid)) {
847
+ return "failed";
848
+ }
849
+ // If the run looks stalled, treat as idle so supervisor can intervene.
850
+ if (isRunStalled(entry.runLog, entry.runStartedAt))
851
+ return "idle";
852
+ // If log indicates codex finished and is waiting on commit, treat as idle.
853
+ const tail = entry.runLog ? readLastLines(entry.runLog, 30) : "";
854
+ if (tail.includes("Your task is complete") || tail.includes("Run these commands NOW"))
855
+ return "idle";
856
+ return "working";
857
+ }
858
+ const sessionName = entry.tmuxSession;
859
+ if (!sessionName)
860
+ return "not_started";
861
+ // Check if tmux session exists
862
+ try {
863
+ exec(`tmux -S "${TMUX_SOCKET}" has-session -t ${sessionName} 2>/dev/null`);
864
+ }
865
+ catch {
866
+ // Session doesn't exist - check if there's a commit
867
+ if (entry.portBranch) {
868
+ try {
869
+ const commits = exec(`git log main..${entry.portBranch} --oneline`, PROJECT_ROOT);
870
+ if (commits.trim()) {
871
+ // There's a commit on the branch
872
+ const latestCommit = commits.split("\n")[0].split(" ")[0];
873
+ entry.portCommit = latestCommit;
874
+ return "done";
875
+ }
876
+ }
877
+ catch {
878
+ // Branch doesn't exist or no commits
879
+ }
880
+ }
881
+ return "failed";
882
+ }
883
+ // Session exists - check if agent is idle (waiting at prompt) or still working
884
+ try {
885
+ const output = exec(`tmux -S "${TMUX_SOCKET}" capture-pane -p -t ${sessionName} -S -10`);
886
+ // Check if codex is waiting for commit instructions
887
+ if (output.includes("Your task is complete") || output.includes("Run these commands NOW")) {
888
+ if (entry.portBranch) {
889
+ try {
890
+ const commits = exec(`git log main..${entry.portBranch} --oneline`, PROJECT_ROOT);
891
+ if (commits.trim()) {
892
+ const latestCommit = commits.split("\n")[0].split(" ")[0];
893
+ entry.portCommit = latestCommit;
894
+ return "done";
895
+ }
896
+ }
897
+ catch {
898
+ // No commits yet
899
+ }
900
+ }
901
+ return "idle";
902
+ }
903
+ // Check if codex is at its interactive prompt (idle, waiting for input)
904
+ // The prompt looks like: "› " with "context left" nearby
905
+ if (output.includes("context left") && output.includes("›")) {
906
+ // Agent is idle at prompt - check if there's already a commit
907
+ if (entry.portBranch) {
908
+ try {
909
+ const commits = exec(`git log main..${entry.portBranch} --oneline`, PROJECT_ROOT);
910
+ if (commits.trim()) {
911
+ const latestCommit = commits.split("\n")[0].split(" ")[0];
912
+ entry.portCommit = latestCommit;
913
+ return "done"; // Has commit, just need to close session
914
+ }
915
+ }
916
+ catch {
917
+ // No commits yet
918
+ }
919
+ }
920
+ return "idle"; // At prompt but no commit yet
921
+ }
922
+ // Check for shell prompt (agent exited but session still open)
923
+ if (output.includes("$ ") || output.match(/\n%\s*$/)) {
924
+ if (entry.portBranch) {
925
+ try {
926
+ const commits = exec(`git log main..${entry.portBranch} --oneline`, PROJECT_ROOT);
927
+ if (commits.trim()) {
928
+ const latestCommit = commits.split("\n")[0].split(" ")[0];
929
+ entry.portCommit = latestCommit;
930
+ return "done";
931
+ }
932
+ }
933
+ catch {
934
+ // No commits
935
+ }
936
+ }
937
+ return "failed";
938
+ }
939
+ return "working";
940
+ }
941
+ catch {
942
+ return "working";
943
+ }
944
+ }
945
+ function nudgeIdleAgent(worktreePath, sessionName) {
946
+ // Commit directly from the supervisor to avoid tmux prompt issues
947
+ if (worktreePath) {
948
+ try {
949
+ execSync("git add -A", { cwd: worktreePath, stdio: "ignore" });
950
+ try {
951
+ execSync("git reset PORT_TASK.md package-lock.json", {
952
+ cwd: worktreePath,
953
+ stdio: "ignore",
954
+ });
955
+ }
956
+ catch {
957
+ // Ignore missing files
958
+ }
959
+ let hasChanges = false;
960
+ try {
961
+ execSync("git diff --cached --quiet", { cwd: worktreePath, stdio: "ignore" });
962
+ }
963
+ catch {
964
+ hasChanges = true;
965
+ }
966
+ if (hasChanges) {
967
+ execSync("git commit -m 'feat: port upstream changes'", {
968
+ cwd: worktreePath,
969
+ stdio: "ignore",
970
+ });
971
+ }
972
+ if (sessionName) {
973
+ killAgentSession(sessionName);
974
+ }
975
+ return;
976
+ }
977
+ catch {
978
+ // Fall back to tmux send-keys if direct commit fails
979
+ }
980
+ }
981
+ if (sessionName) {
982
+ try {
983
+ const cmd = `git add -A && ` +
984
+ `git reset PORT_TASK.md package-lock.json 2>/dev/null || true && ` +
985
+ `git diff --cached --quiet || git commit -m 'feat: port upstream changes' && exit`;
986
+ exec(`tmux -S "${TMUX_SOCKET}" send-keys -t ${sessionName} "${cmd}" Enter`);
987
+ }
988
+ catch {
989
+ // Session might not exist
990
+ }
991
+ }
992
+ }
993
+ function killAgentPid(pid) {
994
+ if (typeof pid !== "number")
995
+ return;
996
+ try {
997
+ execSync(`kill ${pid}`, { stdio: "ignore" });
998
+ }
999
+ catch {
1000
+ // ignore
1001
+ }
1002
+ }
1003
+ function killAgentSession(sessionName) {
1004
+ try {
1005
+ exec(`tmux -S "${TMUX_SOCKET}" kill-session -t ${sessionName} 2>/dev/null`);
1006
+ }
1007
+ catch {
1008
+ // Session might not exist
1009
+ }
1010
+ }
1011
+ function updateAllStatuses(state) {
1012
+ // Update individual merge entries
1013
+ for (const [sha, entry] of Object.entries(state.merges)) {
1014
+ if (entry.bundleId)
1015
+ continue;
1016
+ if (entry.status === "porting") {
1017
+ const label = entry.prNumber ? `PR #${entry.prNumber}` : `Commit ${sha.slice(0, 8)}`;
1018
+ const status = checkAgentStatus(state, sha);
1019
+ if (status === "done") {
1020
+ // Agent finished and committed - close session and mark complete
1021
+ if (entry.tmuxSession) {
1022
+ killAgentSession(entry.tmuxSession);
1023
+ }
1024
+ entry.status = "pending_review";
1025
+ console.log(`āœ… ${label} completed - ready for review`);
1026
+ }
1027
+ else if (status === "idle") {
1028
+ // Agent is at prompt but hasn't committed - nudge it
1029
+ if (entry.worktree || entry.tmuxSession) {
1030
+ console.log(`šŸ”” ${label} idle - sending commit nudge...`);
1031
+ nudgeIdleAgent(entry.worktree, entry.tmuxSession);
1032
+ // If this was a detached runner stuck at "task complete", kill it after committing
1033
+ if (typeof entry.runPid === "number") {
1034
+ killAgentPid(entry.runPid);
1035
+ }
1036
+ }
1037
+ }
1038
+ else if (status === "failed") {
1039
+ entry.status = "porting"; // Keep as porting with error, can retry
1040
+ entry.error = "Agent session ended without commit";
1041
+ console.log(`āŒ ${label} failed - no commit found`);
1042
+ }
1043
+ }
1044
+ }
1045
+ // Update bundle statuses
1046
+ if (state.bundles) {
1047
+ for (const [bundleId, bundle] of Object.entries(state.bundles)) {
1048
+ if (bundle.status === "porting") {
1049
+ const status = checkBundleAgentStatus(bundleId);
1050
+ if (status === "done") {
1051
+ bundle.status = "pending_review";
1052
+ for (const sha of bundle.commits) {
1053
+ const entry = state.merges[sha];
1054
+ if (!entry)
1055
+ continue;
1056
+ entry.status = "pending_review";
1057
+ delete entry.error;
1058
+ }
1059
+ console.log(`āœ… Bundle ${bundleId.slice(0, 20)} completed - ready for review`);
1060
+ }
1061
+ else if (status === "idle") {
1062
+ // Nudge the bundle agent
1063
+ const shortId = bundleId.split("-").pop() || bundleId.slice(-8);
1064
+ const worktreePath = `/tmp/nexus-bundle-${shortId}`;
1065
+ console.log(`šŸ”” Bundle ${bundleId.slice(0, 20)} idle - sending commit nudge...`);
1066
+ nudgeIdleAgent(worktreePath, bundle.tmuxSession);
1067
+ if (typeof bundle.runPid === "number") {
1068
+ killAgentPid(bundle.runPid);
1069
+ }
1070
+ }
1071
+ }
1072
+ }
1073
+ }
1074
+ saveState(state);
1075
+ }
1076
+ // Check bundle agent status by looking at the worktree
1077
+ function checkBundleAgentStatus(bundleId) {
1078
+ const shortId = bundleId.split("-").pop() || bundleId.slice(-8);
1079
+ const worktreePath = `/tmp/nexus-bundle-${shortId}`;
1080
+ const sessionName = `bundle-${shortId}`;
1081
+ // Check if worktree exists
1082
+ try {
1083
+ execSync(`test -d "${worktreePath}"`, { stdio: "ignore" });
1084
+ }
1085
+ catch {
1086
+ return "failed"; // Worktree doesn't exist
1087
+ }
1088
+ // If bundle has a detached runner, trust PID/log for liveness and stall detection.
1089
+ // (We still fall back to tmux checks for older runs.)
1090
+ try {
1091
+ const state = loadState();
1092
+ const bundle = state.bundles?.[bundleId];
1093
+ if (bundle && typeof bundle.runPid === "number") {
1094
+ // If there's a valid port commit with real changes, we're done (or idle if process still alive).
1095
+ try {
1096
+ const subject = execSync(`git log --oneline -1 --format="%s"`, {
1097
+ cwd: worktreePath,
1098
+ encoding: "utf-8",
1099
+ stdio: ["pipe", "pipe", "ignore"],
1100
+ }).trim();
1101
+ if (subject) {
1102
+ const files = execSync(`git show --name-only --pretty=format: -1`, {
1103
+ cwd: worktreePath,
1104
+ encoding: "utf-8",
1105
+ stdio: ["pipe", "pipe", "ignore"],
1106
+ })
1107
+ .split("\n")
1108
+ .map((line) => line.trim())
1109
+ .filter(Boolean)
1110
+ .filter((file) => file !== "PORT_TASK.md" && file !== "package-lock.json");
1111
+ if (files.length > 0) {
1112
+ if (isPidRunning(bundle.runPid))
1113
+ return "idle";
1114
+ return "done";
1115
+ }
1116
+ }
1117
+ }
1118
+ catch {
1119
+ // ignore
1120
+ }
1121
+ if (!isPidRunning(bundle.runPid))
1122
+ return "failed";
1123
+ if (isRunStalled(bundle.runLog, bundle.runStartedAt))
1124
+ return "idle";
1125
+ const tail = bundle.runLog ? readLastLines(bundle.runLog, 40) : "";
1126
+ if (tail.includes("Your task is complete") || tail.includes("Run these commands NOW"))
1127
+ return "idle";
1128
+ return "working";
1129
+ }
1130
+ }
1131
+ catch {
1132
+ // ignore
1133
+ }
1134
+ // Check if there's a port commit with real changes
1135
+ try {
1136
+ const log = execSync(`git log --oneline -1 --format="%s"`, {
1137
+ cwd: worktreePath,
1138
+ encoding: "utf-8",
1139
+ stdio: ["pipe", "pipe", "ignore"],
1140
+ }).trim();
1141
+ if (log.includes("port") ||
1142
+ log.includes("feat:") ||
1143
+ log.includes("fix:") ||
1144
+ log.includes("refactor")) {
1145
+ // Ensure the commit includes actual changes beyond PORT_TASK/package-lock
1146
+ try {
1147
+ const files = execSync(`git show --name-only --pretty=format: -1`, {
1148
+ cwd: worktreePath,
1149
+ encoding: "utf-8",
1150
+ stdio: ["pipe", "pipe", "ignore"],
1151
+ })
1152
+ .split("\n")
1153
+ .map((line) => line.trim())
1154
+ .filter(Boolean)
1155
+ .filter((file) => file !== "PORT_TASK.md" && file !== "package-lock.json");
1156
+ if (files.length === 0) {
1157
+ return "failed";
1158
+ }
1159
+ }
1160
+ catch {
1161
+ // If we can't read files, keep going
1162
+ }
1163
+ // Has a port commit - check if session is still running
1164
+ try {
1165
+ execSync(`tmux -S ${TMUX_SOCKET} has-session -t "${sessionName}" 2>/dev/null`, {
1166
+ stdio: "ignore",
1167
+ });
1168
+ // Session still running - check if idle or working
1169
+ const pane = execSync(`tmux -S ${TMUX_SOCKET} capture-pane -t "${sessionName}" -p 2>/dev/null`, {
1170
+ encoding: "utf-8",
1171
+ });
1172
+ if (pane.includes("Your task is complete") || pane.includes("Run these commands NOW")) {
1173
+ return "idle"; // Done but hasn't exited
1174
+ }
1175
+ return "working";
1176
+ }
1177
+ catch {
1178
+ // Session ended - if we have a commit, it's done
1179
+ return "done";
1180
+ }
1181
+ }
1182
+ }
1183
+ catch {
1184
+ // No commit yet
1185
+ }
1186
+ // Check if session is still running
1187
+ try {
1188
+ execSync(`tmux -S ${TMUX_SOCKET} has-session -t "${sessionName}" 2>/dev/null`, {
1189
+ stdio: "ignore",
1190
+ });
1191
+ return "working";
1192
+ }
1193
+ catch {
1194
+ return "failed";
1195
+ }
1196
+ }
1197
+ // ============================================================================
1198
+ // Display
1199
+ // ============================================================================
1200
+ function getBundleCategory(bundle) {
1201
+ if (bundle.priority >= 100)
1202
+ return "breaking";
1203
+ if (bundle.priority >= 70)
1204
+ return "features";
1205
+ if (bundle.priority >= 50)
1206
+ return "fixes";
1207
+ if (bundle.priority >= 30)
1208
+ return "refactors";
1209
+ return "other";
1210
+ }
1211
+ function getBundleSessionName(bundleId) {
1212
+ const shortId = bundleId.split("-").pop() || bundleId.slice(-8);
1213
+ return `bundle-${shortId}`;
1214
+ }
1215
+ function getTmuxSessions() {
1216
+ const sessions = new Map();
1217
+ try {
1218
+ const output = execSync(`tmux -S "${TMUX_SOCKET}" list-sessions -F "#{session_name} #{session_created}"`, {
1219
+ encoding: "utf-8",
1220
+ stdio: ["pipe", "pipe", "ignore"],
1221
+ }).trim();
1222
+ if (!output)
1223
+ return sessions;
1224
+ for (const line of output.split("\n")) {
1225
+ const [name, createdStr] = line.trim().split(/\s+/);
1226
+ const created = createdStr ? parseInt(createdStr, 10) : 0;
1227
+ if (name)
1228
+ sessions.set(name, { created });
1229
+ }
1230
+ }
1231
+ catch {
1232
+ // tmux may not be running
1233
+ }
1234
+ return sessions;
1235
+ }
1236
+ function formatDuration(seconds) {
1237
+ if (!Number.isFinite(seconds) || seconds <= 0)
1238
+ return "unknown";
1239
+ const mins = Math.floor(seconds / 60);
1240
+ const hours = Math.floor(mins / 60);
1241
+ const days = Math.floor(hours / 24);
1242
+ if (days > 0)
1243
+ return `${days}d ${hours % 24}h`;
1244
+ if (hours > 0)
1245
+ return `${hours}h ${mins % 60}m`;
1246
+ return `${mins}m`;
1247
+ }
1248
+ function printStatus(state) {
1249
+ const counts = { new: 0, porting: 0, pending_review: 0, shelved: 0, merged: 0, ignored: 0 };
1250
+ const byStatus = {
1251
+ pending_review: [],
1252
+ shelved: [],
1253
+ porting: [],
1254
+ new: [],
1255
+ merged: [],
1256
+ ignored: [],
1257
+ };
1258
+ for (const [sha, entry] of Object.entries(state.merges)) {
1259
+ const normalizedStatus = entry.status === "pending" ? "new" : entry.status;
1260
+ const statusKey = (normalizedStatus in counts ? normalizedStatus : "new");
1261
+ counts[statusKey]++;
1262
+ byStatus[statusKey].push({ sha, entry });
1263
+ }
1264
+ console.log("\n" + "═".repeat(60));
1265
+ console.log(" UPSTREAM SYNC STATUS");
1266
+ console.log("═".repeat(60));
1267
+ console.log(` Last fetched: ${state.lastFetched}`);
1268
+ console.log(` Tracking since: ${state.trackingStartDate}`);
1269
+ console.log("");
1270
+ console.log(` šŸ“Š Summary:`);
1271
+ console.log(` āœ… Merged: ${counts.merged}`);
1272
+ console.log(` šŸ‘€ Pending Review: ${counts.pending_review}`);
1273
+ console.log(` šŸ“¦ Shelved: ${counts.shelved}`);
1274
+ console.log(` āš™ļø Porting: ${counts.porting}`);
1275
+ console.log(` šŸ†• New: ${counts.new}`);
1276
+ console.log(` ā­ļø Ignored: ${counts.ignored}`);
1277
+ if (isDispatchDisabled(state)) {
1278
+ console.log(` ā›” Dispatch: paused`);
1279
+ }
1280
+ console.log("");
1281
+ // Bundle summary (direct commits)
1282
+ const bundles = Object.values(state.bundles || {});
1283
+ const bundleCounts = {
1284
+ pending: bundles.filter((b) => b.status === "pending").length,
1285
+ porting: bundles.filter((b) => b.status === "porting").length,
1286
+ pending_review: bundles.filter((b) => b.status === "pending_review").length,
1287
+ merged: bundles.filter((b) => b.status === "merged").length,
1288
+ ignored: bundles.filter((b) => b.status === "ignored").length,
1289
+ };
1290
+ const bundleOutstanding = bundleCounts.pending + bundleCounts.porting + bundleCounts.pending_review;
1291
+ console.log(" šŸ“¦ BUNDLE STATUS (Direct commits):");
1292
+ console.log(` Outstanding: ${bundleOutstanding}`);
1293
+ console.log(` Ready Review: ${bundleCounts.pending_review}`);
1294
+ console.log(` In Progress: ${bundleCounts.porting}`);
1295
+ console.log(` Waiting Agents: ${bundleCounts.pending}`);
1296
+ console.log("");
1297
+ // Agent status for bundles in progress
1298
+ if (bundleCounts.porting > 0) {
1299
+ const tmuxSessions = getTmuxSessions();
1300
+ console.log(" 🧵 BUNDLE AGENTS (status + runtime):");
1301
+ for (const bundle of bundles.filter((b) => b.status === "porting")) {
1302
+ const sessionName = getBundleSessionName(bundle.id);
1303
+ const agentStatus = checkBundleAgentStatus(bundle.id);
1304
+ const session = tmuxSessions.get(sessionName);
1305
+ const runtimeSec = session ? Math.floor(Date.now() / 1000) - session.created : 0;
1306
+ const runtime = formatDuration(runtimeSec);
1307
+ const statusLabel = agentStatus === "idle"
1308
+ ? "stuck (idle)"
1309
+ : agentStatus === "failed"
1310
+ ? "failed"
1311
+ : agentStatus === "done"
1312
+ ? "done"
1313
+ : "running";
1314
+ console.log(` ${bundle.name.slice(0, 60)} [${statusLabel}] (${runtime})`);
1315
+ }
1316
+ console.log("");
1317
+ }
1318
+ // Bundle grouping by priority tiers, list features
1319
+ if (bundles.length > 0) {
1320
+ const outstandingBundles = bundles.filter((b) => b.status !== "merged" && b.status !== "ignored");
1321
+ const grouped = {
1322
+ breaking: outstandingBundles.filter((b) => getBundleCategory(b) === "breaking"),
1323
+ features: outstandingBundles.filter((b) => getBundleCategory(b) === "features"),
1324
+ fixes: outstandingBundles.filter((b) => getBundleCategory(b) === "fixes"),
1325
+ refactors: outstandingBundles.filter((b) => getBundleCategory(b) === "refactors"),
1326
+ other: outstandingBundles.filter((b) => getBundleCategory(b) === "other"),
1327
+ };
1328
+ console.log(" šŸ“Š BUNDLES BY CATEGORY (outstanding):");
1329
+ console.log(` šŸ”„ Breaking: ${grouped.breaking.length}`);
1330
+ console.log(` ✨ Features: ${grouped.features.length}`);
1331
+ console.log(` šŸ› Fixes: ${grouped.fixes.length}`);
1332
+ console.log(` ā™»ļø Refactors:${grouped.refactors.length}`);
1333
+ console.log(` šŸ“¦ Other: ${grouped.other.length}`);
1334
+ console.log("");
1335
+ if (grouped.features.length > 0) {
1336
+ console.log(" ✨ FEATURE BUNDLES (outstanding):");
1337
+ for (const bundle of grouped.features) {
1338
+ console.log(` - ${bundle.name} [${bundle.status}]`);
1339
+ }
1340
+ console.log("");
1341
+ }
1342
+ }
1343
+ if (byStatus.pending_review.length > 0) {
1344
+ console.log(" šŸ‘€ READY FOR REVIEW (new):");
1345
+ for (const { entry } of byStatus.pending_review) {
1346
+ console.log(` PR #${entry.prNumber}: ${entry.title}`);
1347
+ console.log(` Branch: ${entry.portBranch} | Commit: ${entry.portCommit}`);
1348
+ }
1349
+ console.log("");
1350
+ }
1351
+ if (byStatus.shelved.length > 0) {
1352
+ console.log(" šŸ“¦ SHELVED (reviewed, keeping for later):");
1353
+ for (const { entry } of byStatus.shelved) {
1354
+ console.log(` PR #${entry.prNumber}: ${entry.title}`);
1355
+ console.log(` Branch: ${entry.portBranch}`);
1356
+ }
1357
+ console.log("");
1358
+ }
1359
+ if (byStatus.porting.length > 0) {
1360
+ console.log(" āš™ļø IN PROGRESS:");
1361
+ for (const { entry } of byStatus.porting) {
1362
+ const status = entry.tmuxSession ? `tmux: ${entry.tmuxSession}` : "unknown";
1363
+ console.log(` PR #${entry.prNumber}: ${entry.title} [${status}]`);
1364
+ }
1365
+ console.log("");
1366
+ }
1367
+ if (byStatus.new.length > 0) {
1368
+ // Sort: direct commits first
1369
+ const sortedNew = [...byStatus.new].sort((a, b) => {
1370
+ if (a.entry.isDirectCommit && !b.entry.isDirectCommit)
1371
+ return -1;
1372
+ if (!a.entry.isDirectCommit && b.entry.isDirectCommit)
1373
+ return 1;
1374
+ return new Date(b.entry.date).getTime() - new Date(a.entry.date).getTime();
1375
+ });
1376
+ const directCount = sortedNew.filter((x) => x.entry.isDirectCommit).length;
1377
+ console.log(` šŸ†• NEW (will dispatch agents): ${directCount} direct commits, ${byStatus.new.length - directCount} PRs`);
1378
+ // Show direct commits first with star
1379
+ for (const { entry } of sortedNew.filter((x) => x.entry.isDirectCommit)) {
1380
+ console.log(` ⭐ DIRECT: ${entry.title} (${entry.author || "unknown"})`);
1381
+ }
1382
+ // Then PRs
1383
+ for (const { entry } of sortedNew.filter((x) => !x.entry.isDirectCommit).slice(0, 10)) {
1384
+ console.log(` PR #${entry.prNumber}: ${entry.title}`);
1385
+ }
1386
+ const remainingPRs = sortedNew.filter((x) => !x.entry.isDirectCommit).length - 10;
1387
+ if (remainingPRs > 0) {
1388
+ console.log(` ... and ${remainingPRs} more PRs`);
1389
+ }
1390
+ console.log("");
1391
+ }
1392
+ console.log("═".repeat(60));
1393
+ console.log(" Commands:");
1394
+ console.log(" daemon [mins] - Run continuously until all done (default: 2 min interval)");
1395
+ console.log(" pause - Disable new agent dispatch");
1396
+ console.log(" resume - Re-enable agent dispatch");
1397
+ console.log(" review - Generate markdown review queue");
1398
+ console.log(" merge <pr#> [pr#]... - Merge PRs (with conflict check)");
1399
+ console.log(" shelve <pr#> - Mark as reviewed, keep for later");
1400
+ console.log(" unshelve <pr#> - Move back to pending review");
1401
+ console.log(" ignore <pr#> [reason] - Mark as ignored permanently");
1402
+ console.log(" retry <pr#> - Re-dispatch agent for a PR");
1403
+ console.log(" show <pr#> - Show diff for a PR");
1404
+ console.log("");
1405
+ console.log(" Bundle Commands (for direct commits):");
1406
+ console.log(" bundles - Analyze and show direct commit bundles");
1407
+ console.log(" bundle-dispatch <id> - Dispatch agent for a specific bundle");
1408
+ console.log(" bundle-split <id> - Split a bundle into smaller bundles");
1409
+ console.log(" bundle-requeue <id> - Reset bundle for re-dispatch");
1410
+ console.log(" bundle-dispatch-oldest - Dispatch oldest pending bundles");
1411
+ console.log(" bundle-ignore <id> - Ignore a bundle");
1412
+ console.log(" bundle-auto - Auto-dispatch high-priority bundles");
1413
+ console.log("═".repeat(60) + "\n");
1414
+ }
1415
+ // ============================================================================
1416
+ // Commands
1417
+ // ============================================================================
1418
+ function findMergeBySha(state, prNumOrSha) {
1419
+ // Try PR number first
1420
+ const prNum = parseInt(prNumOrSha, 10);
1421
+ if (!isNaN(prNum)) {
1422
+ for (const [sha, entry] of Object.entries(state.merges)) {
1423
+ if (entry.prNumber === prNum) {
1424
+ return { sha, entry };
1425
+ }
1426
+ }
1427
+ }
1428
+ // Try SHA
1429
+ if (state.merges[prNumOrSha]) {
1430
+ return { sha: prNumOrSha, entry: state.merges[prNumOrSha] };
1431
+ }
1432
+ // Try partial SHA
1433
+ for (const [sha, entry] of Object.entries(state.merges)) {
1434
+ if (sha.startsWith(prNumOrSha)) {
1435
+ return { sha, entry };
1436
+ }
1437
+ }
1438
+ return null;
1439
+ }
1440
+ function cmdMerge(state, prNumOrSha) {
1441
+ const found = findMergeBySha(state, prNumOrSha);
1442
+ if (!found) {
1443
+ console.error(`āŒ PR not found: ${prNumOrSha}`);
1444
+ return;
1445
+ }
1446
+ const { sha, entry } = found;
1447
+ if (entry.status !== "pending_review") {
1448
+ console.error(`āŒ PR #${entry.prNumber} is not ready for review (status: ${entry.status})`);
1449
+ return;
1450
+ }
1451
+ if (!entry.portBranch) {
1452
+ console.error(`āŒ No port branch found for PR #${entry.prNumber}`);
1453
+ return;
1454
+ }
1455
+ console.log(`šŸ”€ Merging ${entry.portBranch} into main...`);
1456
+ try {
1457
+ exec(`git checkout main`, PROJECT_ROOT);
1458
+ exec(`git merge ${entry.portBranch} --no-edit`, PROJECT_ROOT);
1459
+ entry.status = "merged";
1460
+ saveState(state);
1461
+ console.log(`āœ… PR #${entry.prNumber} merged successfully!`);
1462
+ // Clean up worktree
1463
+ if (entry.worktree && existsSync(entry.worktree)) {
1464
+ console.log(`🧹 Cleaning up worktree...`);
1465
+ exec(`git worktree remove ${entry.worktree}`, PROJECT_ROOT);
1466
+ }
1467
+ }
1468
+ catch (e) {
1469
+ console.error(`āŒ Merge failed: ${e}`);
1470
+ }
1471
+ }
1472
+ function cmdIgnore(state, prNumOrSha, reason) {
1473
+ const found = findMergeBySha(state, prNumOrSha);
1474
+ if (!found) {
1475
+ console.error(`āŒ PR not found: ${prNumOrSha}`);
1476
+ return;
1477
+ }
1478
+ const { entry } = found;
1479
+ entry.status = "ignored";
1480
+ entry.ignoreReason = reason || "Manually ignored";
1481
+ saveState(state);
1482
+ console.log(`ā­ļø PR #${entry.prNumber} marked as ignored`);
1483
+ }
1484
+ function cmdRetry(state, prNumOrSha) {
1485
+ const found = findMergeBySha(state, prNumOrSha);
1486
+ if (!found) {
1487
+ console.error(`āŒ PR not found: ${prNumOrSha}`);
1488
+ return;
1489
+ }
1490
+ const { sha, entry } = found;
1491
+ // Kill existing tmux session if any
1492
+ if (entry.tmuxSession) {
1493
+ try {
1494
+ exec(`tmux -S "${TMUX_SOCKET}" kill-session -t ${entry.tmuxSession} 2>/dev/null`);
1495
+ }
1496
+ catch {
1497
+ // Session might not exist
1498
+ }
1499
+ }
1500
+ // Reset status
1501
+ entry.status = "new";
1502
+ delete entry.tmuxSession;
1503
+ delete entry.error;
1504
+ saveState(state);
1505
+ // Dispatch again
1506
+ dispatchAgent(state, sha);
1507
+ }
1508
+ function cmdShow(state, prNumOrSha) {
1509
+ const found = findMergeBySha(state, prNumOrSha);
1510
+ if (!found) {
1511
+ console.error(`āŒ PR not found: ${prNumOrSha}`);
1512
+ return;
1513
+ }
1514
+ const { sha, entry } = found;
1515
+ const upstreamPath = getUpstreamPath(state);
1516
+ console.log(`\nšŸ“‹ PR #${entry.prNumber}: ${entry.title}`);
1517
+ console.log(` Status: ${entry.status}`);
1518
+ console.log(` Date: ${entry.date}`);
1519
+ console.log(` SHA: ${sha}`);
1520
+ if (entry.portBranch) {
1521
+ console.log(` Port Branch: ${entry.portBranch}`);
1522
+ }
1523
+ console.log("\nšŸ“ Changes:");
1524
+ try {
1525
+ const diff = exec(`git show ${sha} --stat`, upstreamPath);
1526
+ console.log(diff);
1527
+ }
1528
+ catch (e) {
1529
+ console.error(` Could not get diff: ${e}`);
1530
+ }
1531
+ }
1532
+ function cmdShelve(state, prNumOrSha) {
1533
+ const found = findMergeBySha(state, prNumOrSha);
1534
+ if (!found) {
1535
+ console.error(`āŒ PR not found: ${prNumOrSha}`);
1536
+ return;
1537
+ }
1538
+ const { entry } = found;
1539
+ if (entry.status !== "pending_review" && entry.status !== "shelved") {
1540
+ console.error(`āŒ PR #${entry.prNumber} cannot be shelved (status: ${entry.status})`);
1541
+ return;
1542
+ }
1543
+ entry.status = "shelved";
1544
+ saveState(state);
1545
+ console.log(`šŸ“¦ PR #${entry.prNumber} shelved for later`);
1546
+ // Clean up worktree if exists (but keep branch)
1547
+ if (entry.worktree && existsSync(entry.worktree)) {
1548
+ console.log(` 🧹 Cleaning up worktree (branch preserved)...`);
1549
+ try {
1550
+ exec(`git worktree remove ${entry.worktree}`, PROJECT_ROOT);
1551
+ delete entry.worktree;
1552
+ saveState(state);
1553
+ }
1554
+ catch {
1555
+ console.log(` āš ļø Could not remove worktree`);
1556
+ }
1557
+ }
1558
+ }
1559
+ function cmdUnshelve(state, prNumOrSha) {
1560
+ const found = findMergeBySha(state, prNumOrSha);
1561
+ if (!found) {
1562
+ console.error(`āŒ PR not found: ${prNumOrSha}`);
1563
+ return;
1564
+ }
1565
+ const { entry } = found;
1566
+ if (entry.status !== "shelved") {
1567
+ console.error(`āŒ PR #${entry.prNumber} is not shelved (status: ${entry.status})`);
1568
+ return;
1569
+ }
1570
+ entry.status = "pending_review";
1571
+ saveState(state);
1572
+ console.log(`šŸ‘€ PR #${entry.prNumber} moved back to pending review`);
1573
+ }
1574
+ function generatePrDescription(state, sha, entry) {
1575
+ const upstreamPath = getUpstreamPath(state);
1576
+ try {
1577
+ // Get commit message body for description
1578
+ const commitMsg = exec(`git log -1 --format="%B" ${sha}`, upstreamPath);
1579
+ const stat = exec(`git show ${sha} --stat --format="" | tail -5`, upstreamPath);
1580
+ // Extract meaningful description from commit message
1581
+ const lines = commitMsg
1582
+ .split("\n")
1583
+ .filter((l) => l.trim() && !l.startsWith("Merge pull request"));
1584
+ const description = lines.slice(0, 3).join(" ").slice(0, 200) || "No description available";
1585
+ return `${description}\n\nFiles: ${stat.split("\n").pop()?.trim() || "unknown"}`;
1586
+ }
1587
+ catch {
1588
+ return "Could not fetch description";
1589
+ }
1590
+ }
1591
+ // Visual timeline showing sync progress
1592
+ function cmdTimeline(state) {
1593
+ const entries = Object.entries(state.merges);
1594
+ // Group by date
1595
+ const byDate = new Map();
1596
+ for (const [sha, entry] of entries) {
1597
+ const date = entry.date.split("T")[0];
1598
+ if (!byDate.has(date)) {
1599
+ byDate.set(date, {
1600
+ merged: 0,
1601
+ porting: 0,
1602
+ pending: 0,
1603
+ new: 0,
1604
+ ignored: 0,
1605
+ prs: 0,
1606
+ direct: 0,
1607
+ });
1608
+ }
1609
+ const day = byDate.get(date);
1610
+ if (entry.status === "merged")
1611
+ day.merged++;
1612
+ else if (entry.status === "porting")
1613
+ day.porting++;
1614
+ else if (entry.status === "pending_review")
1615
+ day.pending++;
1616
+ else if (entry.status === "new")
1617
+ day.new++;
1618
+ else if (entry.status === "ignored")
1619
+ day.ignored++;
1620
+ if (entry.prNumber)
1621
+ day.prs++;
1622
+ if (entry.isDirectCommit)
1623
+ day.direct++;
1624
+ }
1625
+ // Sort dates
1626
+ const dates = [...byDate.keys()].sort();
1627
+ // Calculate totals
1628
+ const totals = { merged: 0, porting: 0, pending: 0, new: 0, ignored: 0, prs: 0, direct: 0 };
1629
+ for (const day of byDate.values()) {
1630
+ totals.merged += day.merged;
1631
+ totals.porting += day.porting;
1632
+ totals.pending += day.pending;
1633
+ totals.new += day.new;
1634
+ totals.ignored += day.ignored;
1635
+ totals.prs += day.prs;
1636
+ totals.direct += day.direct;
1637
+ }
1638
+ const totalCommits = totals.merged + totals.porting + totals.pending + totals.new + totals.ignored;
1639
+ const ported = totals.merged + totals.porting + totals.pending;
1640
+ const portedPct = Math.round((ported / totalCommits) * 100);
1641
+ // Header
1642
+ console.log("\n" + "═".repeat(80));
1643
+ console.log(" šŸ“Š NEXUS ← UPSTREAM SYNC TIMELINE");
1644
+ console.log("═".repeat(80));
1645
+ console.log(` Tracking since: ${state.trackingStartDate}`);
1646
+ console.log(` Last fetched: ${state.lastFetched}`);
1647
+ console.log("");
1648
+ // Overall progress bar
1649
+ const barWidth = 50;
1650
+ const mergedBar = Math.round((totals.merged / totalCommits) * barWidth);
1651
+ const portingBar = Math.round((totals.porting / totalCommits) * barWidth);
1652
+ const pendingBar = Math.round((totals.pending / totalCommits) * barWidth);
1653
+ const newBar = barWidth - mergedBar - portingBar - pendingBar;
1654
+ console.log(" OVERALL PROGRESS:");
1655
+ console.log(` ${"ā–ˆ".repeat(mergedBar)}${"ā–“".repeat(portingBar)}${"ā–‘".repeat(pendingBar)}${"Ā·".repeat(Math.max(0, newBar))} ${portedPct}%`);
1656
+ console.log(` ${"ā–ˆ"} merged (${totals.merged}) ${"ā–“"} porting (${totals.porting}) ${"ā–‘"} pending (${totals.pending}) ${"Ā·"} new (${totals.new})`);
1657
+ console.log("");
1658
+ // Summary stats
1659
+ console.log(" COMMIT BREAKDOWN:");
1660
+ console.log(` ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”`);
1661
+ console.log(` │ │ PRs │ Direct │`);
1662
+ console.log(` ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤`);
1663
+ console.log(` │ Total tracked │ ${String(totals.prs).padStart(10)} │ ${String(totals.direct).padStart(10)} │`);
1664
+ console.log(` ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜`);
1665
+ console.log("");
1666
+ // Daily timeline
1667
+ console.log(" DAILY TIMELINE:");
1668
+ console.log(" " + "─".repeat(76));
1669
+ console.log(" Date │ Total │ Merged │ Porting │ Pending │ New │ Progress");
1670
+ console.log(" " + "─".repeat(76));
1671
+ for (const date of dates) {
1672
+ const day = byDate.get(date);
1673
+ const dayTotal = day.merged + day.porting + day.pending + day.new + day.ignored;
1674
+ const dayPorted = day.merged + day.porting + day.pending;
1675
+ const dayPct = dayTotal > 0 ? Math.round((dayPorted / dayTotal) * 100) : 0;
1676
+ // Mini progress bar
1677
+ const miniWidth = 20;
1678
+ const miniMerged = Math.round((day.merged / dayTotal) * miniWidth);
1679
+ const miniPorting = Math.round((day.porting / dayTotal) * miniWidth);
1680
+ const miniPending = Math.round((day.pending / dayTotal) * miniWidth);
1681
+ const miniNew = miniWidth - miniMerged - miniPorting - miniPending;
1682
+ const miniBar = "ā–ˆ".repeat(miniMerged) +
1683
+ "ā–“".repeat(miniPorting) +
1684
+ "ā–‘".repeat(miniPending) +
1685
+ "Ā·".repeat(Math.max(0, miniNew));
1686
+ console.log(` ${date} │ ${String(dayTotal).padStart(5)} │ ${String(day.merged).padStart(6)} │ ${String(day.porting).padStart(7)} │ ${String(day.pending).padStart(7)} │ ${String(day.new).padStart(5)} │ ${miniBar} ${dayPct}%`);
1687
+ }
1688
+ console.log(" " + "─".repeat(76));
1689
+ console.log(` TOTAL │ ${String(totalCommits).padStart(5)} │ ${String(totals.merged).padStart(6)} │ ${String(totals.porting).padStart(7)} │ ${String(totals.pending).padStart(7)} │ ${String(totals.new).padStart(5)} │`);
1690
+ console.log("");
1691
+ // Bundle summary
1692
+ const bundles = Object.values(state.bundles || {});
1693
+ const bundlePending = bundles.filter((b) => b.status === "pending").length;
1694
+ const bundlePorting = bundles.filter((b) => b.status === "porting").length;
1695
+ const bundleMerged = bundles.filter((b) => b.status === "merged").length;
1696
+ console.log(" BUNDLE STATUS (Direct Commits):");
1697
+ console.log(` ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”`);
1698
+ console.log(` │ Pending │ ${String(bundlePending).padStart(5)} │ ← Ready to dispatch`);
1699
+ console.log(` │ Porting │ ${String(bundlePorting).padStart(5)} │ ← Agents working`);
1700
+ console.log(` │ Merged │ ${String(bundleMerged).padStart(5)} │ ← Done`);
1701
+ console.log(` ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜`);
1702
+ console.log("");
1703
+ // What's being worked on
1704
+ const portingEntries = entries.filter(([_, e]) => e.status === "porting");
1705
+ if (portingEntries.length > 0) {
1706
+ console.log(" šŸ”§ CURRENTLY PORTING:");
1707
+ for (const [sha, entry] of portingEntries.slice(0, 8)) {
1708
+ const type = entry.isDirectCommit ? "DIRECT" : `PR #${entry.prNumber}`;
1709
+ const session = entry.tmuxSession ? ` [${entry.tmuxSession}]` : "";
1710
+ console.log(` ${type}: ${entry.title.slice(0, 50)}...${session}`);
1711
+ }
1712
+ if (portingEntries.length > 8) {
1713
+ console.log(` ... and ${portingEntries.length - 8} more`);
1714
+ }
1715
+ console.log("");
1716
+ }
1717
+ // What's next
1718
+ console.log(" šŸ“‹ WHAT'S NEXT:");
1719
+ const newPRs = entries.filter(([_, e]) => e.status === "new" && e.prNumber);
1720
+ const newDirect = entries.filter(([_, e]) => e.status === "new" && e.isDirectCommit);
1721
+ console.log(` • ${newPRs.length} PRs still need porting`);
1722
+ console.log(` • ${newDirect.length} direct commits (in ${bundlePending} bundles) need porting`);
1723
+ console.log(` • ${totals.pending} commits ready for your review`);
1724
+ console.log("");
1725
+ // Key insight
1726
+ console.log(" šŸ’” KEY INSIGHT:");
1727
+ console.log(` You've merged ${totals.merged} PR-based commits (mostly from overnight run).`);
1728
+ console.log(` But Peter pushes ${totals.direct} commits directly to main (no PRs).`);
1729
+ console.log(` We just discovered these today - that's why there's so much "new" work.`);
1730
+ console.log("");
1731
+ console.log("═".repeat(80) + "\n");
1732
+ }
1733
+ function cmdReview(state) {
1734
+ const reviewable = Object.entries(state.merges)
1735
+ .filter(([_, e]) => e.status === "pending_review" || e.status === "shelved")
1736
+ .sort((a, b) => {
1737
+ // pending_review first, then shelved, then by date
1738
+ if (a[1].status !== b[1].status) {
1739
+ return a[1].status === "pending_review" ? -1 : 1;
1740
+ }
1741
+ return new Date(b[1].date).getTime() - new Date(a[1].date).getTime();
1742
+ });
1743
+ if (reviewable.length === 0) {
1744
+ console.log("No PRs ready for review.");
1745
+ return;
1746
+ }
1747
+ const pendingCount = reviewable.filter(([_, e]) => e.status === "pending_review").length;
1748
+ const shelvedCount = reviewable.filter(([_, e]) => e.status === "shelved").length;
1749
+ console.log(`# Upstream Port Review Queue\n`);
1750
+ console.log(`**${pendingCount} new** | **${shelvedCount} shelved**\n`);
1751
+ console.log(`---\n`);
1752
+ // Group by status
1753
+ const pending = reviewable.filter(([_, e]) => e.status === "pending_review");
1754
+ const shelved = reviewable.filter(([_, e]) => e.status === "shelved");
1755
+ if (pending.length > 0) {
1756
+ // Sort: direct commits first
1757
+ const sortedPending = [...pending].sort((a, b) => {
1758
+ if (a[1].isDirectCommit && !b[1].isDirectCommit)
1759
+ return -1;
1760
+ if (!a[1].isDirectCommit && b[1].isDirectCommit)
1761
+ return 1;
1762
+ return new Date(b[1].date).getTime() - new Date(a[1].date).getTime();
1763
+ });
1764
+ console.log(`## šŸ‘€ New (Pending Review)\n`);
1765
+ for (const [sha, entry] of sortedPending) {
1766
+ const desc = generatePrDescription(state, sha, entry);
1767
+ const label = entry.isDirectCommit
1768
+ ? `⭐ DIRECT: ${entry.title}`
1769
+ : `PR #${entry.prNumber}: ${entry.title}`;
1770
+ console.log(`### ${label}\n`);
1771
+ if (entry.isDirectCommit && entry.author) {
1772
+ console.log(`**Author:** ${entry.author}\n`);
1773
+ }
1774
+ console.log(`${desc}\n`);
1775
+ console.log(`- **Branch:** \`${entry.portBranch}\``);
1776
+ console.log(`- **Commit:** \`${entry.portCommit}\``);
1777
+ console.log(`- **Date:** ${entry.date}\n`);
1778
+ console.log(`---\n`);
1779
+ }
1780
+ }
1781
+ if (shelved.length > 0) {
1782
+ console.log(`## šŸ“¦ Shelved (Reviewed, Keeping for Later)\n`);
1783
+ for (const [sha, entry] of shelved) {
1784
+ const desc = generatePrDescription(state, sha, entry);
1785
+ console.log(`### PR #${entry.prNumber}: ${entry.title}\n`);
1786
+ console.log(`${desc}\n`);
1787
+ console.log(`- **Branch:** \`${entry.portBranch}\``);
1788
+ console.log(`- **Date:** ${entry.date}\n`);
1789
+ console.log(`---\n`);
1790
+ }
1791
+ }
1792
+ console.log(`## Commands\n`);
1793
+ console.log(`\`\`\`bash`);
1794
+ console.log(`# Merge specific PRs (checks for conflicts first)`);
1795
+ console.log(`npx tsx src/cli/upstream-sync-cli.ts merge 832 722 583`);
1796
+ console.log(``);
1797
+ console.log(`# Shelve a PR for later`);
1798
+ console.log(`npx tsx src/cli/upstream-sync-cli.ts shelve 740`);
1799
+ console.log(``);
1800
+ console.log(`# Unshelve a PR back to pending`);
1801
+ console.log(`npx tsx src/cli/upstream-sync-cli.ts unshelve 740`);
1802
+ console.log(``);
1803
+ console.log(`# Ignore a PR permanently`);
1804
+ console.log(`npx tsx src/cli/upstream-sync-cli.ts ignore 748 "Not applicable to fork"`);
1805
+ console.log(`\`\`\``);
1806
+ }
1807
+ // ============================================================================
1808
+ // Bundle Commands
1809
+ // ============================================================================
1810
+ function cmdBundles(state) {
1811
+ console.log("šŸ” Analyzing direct commits...\n");
1812
+ // Generate bundles
1813
+ const rawBundles = bundleDirectCommits(state);
1814
+ const bundles = consolidateBundles(state, rawBundles);
1815
+ // Save bundles to state
1816
+ state.bundles = state.bundles || {};
1817
+ for (const bundle of bundles) {
1818
+ if (!state.bundles[bundle.id]) {
1819
+ state.bundles[bundle.id] = bundle;
1820
+ // Mark commits as bundled
1821
+ for (const sha of bundle.commits) {
1822
+ if (state.merges[sha]) {
1823
+ state.merges[sha].bundleId = bundle.id;
1824
+ }
1825
+ }
1826
+ }
1827
+ }
1828
+ saveState(state);
1829
+ // Print summary
1830
+ printBundles(bundles);
1831
+ // Show quick stats
1832
+ const directCount = Object.values(state.merges).filter((e) => e.isDirectCommit && e.status === "new").length;
1833
+ const prCount = Object.values(state.merges).filter((e) => !e.isDirectCommit && e.status === "new").length;
1834
+ console.log("Summary:");
1835
+ console.log(` šŸ“¦ ${bundles.length} bundles created from ${directCount} direct commits`);
1836
+ console.log(` šŸ“‹ ${prCount} PR-based commits (handled separately)`);
1837
+ console.log("");
1838
+ console.log("Next steps:");
1839
+ console.log(" - Review bundles above");
1840
+ console.log(" - Run 'bundle-dispatch <id>' for high-priority bundles");
1841
+ console.log(" - Run 'bundle-auto' to dispatch all BREAKING/FEATURE bundles");
1842
+ console.log(" - Run 'bundle-ignore <id>' to skip low-priority bundles");
1843
+ }
1844
+ // Reset and rebuild all bundles with current consolidation logic
1845
+ function cmdRebundle(state) {
1846
+ console.log("šŸ”„ Rebuilding bundles with new consolidation logic...\n");
1847
+ // Count existing bundles before cleanup
1848
+ const existingBundles = Object.keys(state.bundles || {}).length;
1849
+ const portingBundles = Object.values(state.bundles || {}).filter((b) => b.status === "porting");
1850
+ if (portingBundles.length > 0) {
1851
+ console.log(`āš ļø Warning: ${portingBundles.length} bundle(s) are currently being ported.`);
1852
+ console.log(" These will be preserved. Stop their agents first if you want to rebundle them.\n");
1853
+ }
1854
+ // 1. Clear bundleIds from all "new" direct commits (not porting ones)
1855
+ let clearedCommits = 0;
1856
+ for (const [sha, entry] of Object.entries(state.merges)) {
1857
+ if (entry.isDirectCommit && entry.bundleId && entry.status === "new") {
1858
+ delete entry.bundleId;
1859
+ clearedCommits++;
1860
+ }
1861
+ }
1862
+ console.log(` Cleared bundleIds from ${clearedCommits} commits`);
1863
+ // 2. Remove all pending bundles (keep porting/merged/ignored)
1864
+ let removedBundles = 0;
1865
+ if (state.bundles) {
1866
+ for (const [id, bundle] of Object.entries(state.bundles)) {
1867
+ if (bundle.status === "pending") {
1868
+ delete state.bundles[id];
1869
+ removedBundles++;
1870
+ }
1871
+ }
1872
+ }
1873
+ console.log(` Removed ${removedBundles} pending bundles`);
1874
+ // 3. Regenerate bundles
1875
+ const rawBundles = bundleDirectCommits(state);
1876
+ const bundles = consolidateBundles(state, rawBundles);
1877
+ // 4. Save new bundles
1878
+ state.bundles = state.bundles || {};
1879
+ for (const bundle of bundles) {
1880
+ state.bundles[bundle.id] = bundle;
1881
+ // Mark commits as bundled
1882
+ for (const sha of bundle.commits) {
1883
+ if (state.merges[sha]) {
1884
+ state.merges[sha].bundleId = bundle.id;
1885
+ }
1886
+ }
1887
+ }
1888
+ saveState(state);
1889
+ console.log(` Created ${bundles.length} new bundles\n`);
1890
+ // Print summary
1891
+ printBundles(bundles);
1892
+ // Stats
1893
+ const totalBundles = Object.keys(state.bundles).length;
1894
+ const totalCommits = bundles.reduce((sum, b) => sum + b.commits.length, 0);
1895
+ console.log("Rebundle complete:");
1896
+ console.log(` Before: ${existingBundles} bundles`);
1897
+ console.log(` After: ${totalBundles} bundles (${bundles.length} pending, ${portingBundles.length} porting)`);
1898
+ console.log(` Commits covered: ${totalCommits}`);
1899
+ }
1900
+ function generateBundlePortTask(state, bundle) {
1901
+ const upstreamPath = getUpstreamPath(state);
1902
+ const baseCommit = bundle.baseCommit || exec("git rev-parse main", PROJECT_ROOT);
1903
+ // Get combined diff stats for all commits
1904
+ const commitDiffs = [];
1905
+ for (const sha of bundle.commits) {
1906
+ try {
1907
+ const stat = exec(`git show ${sha} --stat --format="Commit: %h - %s"`, upstreamPath);
1908
+ commitDiffs.push(stat);
1909
+ }
1910
+ catch {
1911
+ commitDiffs.push(`Commit ${sha.slice(0, 8)}: [could not get diff]`);
1912
+ }
1913
+ }
1914
+ const priorityLabel = bundle.priority >= 100
1915
+ ? "šŸ”„ BREAKING CHANGE"
1916
+ : bundle.priority >= 70
1917
+ ? "✨ FEATURE"
1918
+ : bundle.priority >= 50
1919
+ ? "šŸ› FIX"
1920
+ : bundle.priority >= 30
1921
+ ? "ā™»ļø REFACTOR"
1922
+ : "šŸ“¦ OTHER";
1923
+ return `# Port Bundle: ${bundle.name}
1924
+
1925
+ ## Priority: ${priorityLabel}
1926
+
1927
+ ## Bundle Info
1928
+ - **ID:** ${bundle.id}
1929
+ - **Author:** ${bundle.author}
1930
+ - **Date Range:** ${bundle.dateRange.start.split("T")[0]} to ${bundle.dateRange.end.split("T")[0]}
1931
+ - **Commits:** ${bundle.commits.length}
1932
+ - **Base branch:** main @ ${baseCommit}
1933
+
1934
+ ## Description
1935
+ ${bundle.description}
1936
+
1937
+ ## All Commits (in order)
1938
+ ${commitDiffs.join("\n\n")}
1939
+
1940
+ ## Instructions
1941
+
1942
+ 1. Review ALL ${bundle.commits.length} commits in upstream at: ${upstreamPath}
1943
+ 2. Apply the changes as ONE cohesive update to nexus
1944
+ 3. Don't port commit-by-commit - understand the full change first
1945
+ 4. Confirm branch base: \`git merge-base --is-ancestor ${baseCommit} HEAD\`
1946
+ 5. Adapt imports/paths if they differ from upstream
1947
+ 6. Run \`npm run build\` to verify compilation
1948
+ 7. Run relevant tests if they exist
1949
+
1950
+ ## Commit Instructions
1951
+ Create a SINGLE commit that encompasses all changes:
1952
+
1953
+ \`\`\`bash
1954
+ git add <changed-files>
1955
+ git commit -m "feat: port ${bundle.name}
1956
+
1957
+ Ported from upstream bundle: ${bundle.id}
1958
+ Includes ${bundle.commits.length} commits from ${bundle.author}"
1959
+ \`\`\`
1960
+
1961
+ ## Notes
1962
+ - This is a BUNDLED port - multiple upstream commits as one
1963
+ - Focus on the end result, not intermediate states
1964
+ - If the bundle includes breaking changes, note them in the commit message
1965
+ - Do NOT use scripts/committer - use git add && git commit directly
1966
+ `;
1967
+ }
1968
+ function dispatchBundleAgent(state, bundleId) {
1969
+ assertDispatchEnabled(state);
1970
+ if (!state.bundles || !state.bundles[bundleId]) {
1971
+ throw new Error(`Bundle not found: ${bundleId}`);
1972
+ }
1973
+ const bundle = state.bundles[bundleId];
1974
+ if (bundle.status !== "pending" && bundle.status !== "ready") {
1975
+ throw new Error(`Bundle ${bundleId} is not ready for dispatch (status: ${bundle.status})`);
1976
+ }
1977
+ // Use last part of bundle ID (the commit SHA) for uniqueness
1978
+ const parts = bundleId.split("-");
1979
+ const shortId = parts[parts.length - 1]; // Get the SHA part
1980
+ const branchName = `port/bundle-${shortId}`;
1981
+ const worktreePath = `/tmp/nexus-bundle-${shortId}`;
1982
+ const mainCommit = assertBranchBasedOnMain(branchName, worktreePath);
1983
+ console.log(`šŸš€ Dispatching agent for bundle: ${bundle.name}`);
1984
+ console.log(` ${bundle.commits.length} commits | ${bundle.author} | Priority: ${bundle.priority}`);
1985
+ // Check if worktree already exists
1986
+ if (existsSync(worktreePath)) {
1987
+ console.log(` Worktree exists at ${worktreePath}, reusing...`);
1988
+ }
1989
+ else {
1990
+ // Create worktree
1991
+ console.log(` Creating worktree at ${worktreePath}...`);
1992
+ try {
1993
+ exec(`git worktree add -b ${branchName} ${worktreePath} main`, PROJECT_ROOT);
1994
+ }
1995
+ catch (e) {
1996
+ try {
1997
+ exec(`git worktree add ${worktreePath} ${branchName}`, PROJECT_ROOT);
1998
+ }
1999
+ catch {
2000
+ throw new Error(`Failed to create worktree: ${e}`);
2001
+ }
2002
+ }
2003
+ }
2004
+ // Generate PORT_TASK.md for the bundle
2005
+ const taskContent = generateBundlePortTask(state, bundle);
2006
+ writeFileSync(join(worktreePath, "PORT_TASK.md"), taskContent);
2007
+ console.log(` Generated PORT_TASK.md`);
2008
+ // Start detached runner (no tmux control path)
2009
+ const runId = `bundle-${shortId}-${Date.now()}`;
2010
+ const prompt = "Read PORT_TASK.md and complete the porting task. This is a BUNDLED port - apply all changes as one cohesive update. Commit your changes when done (a supervisor will also attempt to commit).";
2011
+ const commitMessage = `feat: port ${bundle.name}`;
2012
+ const run = startDetachedAgentRun({ runId, cwd: worktreePath, prompt, commitMessage });
2013
+ console.log(` Started detached runner pid=${run.pid}`);
2014
+ // Update bundle status
2015
+ bundle.status = "porting";
2016
+ bundle.baseCommit = mainCommit;
2017
+ bundle.runId = runId;
2018
+ bundle.runPid = run.pid;
2019
+ bundle.runLog = run.logPath;
2020
+ bundle.runStartedAt = run.startedAt;
2021
+ // Mark all commits in bundle as part of this dispatch
2022
+ for (const sha of bundle.commits) {
2023
+ if (state.merges[sha]) {
2024
+ state.merges[sha].status = "porting";
2025
+ state.merges[sha].portBranch = branchName;
2026
+ state.merges[sha].worktree = worktreePath;
2027
+ state.merges[sha].runId = runId;
2028
+ state.merges[sha].runPid = run.pid;
2029
+ state.merges[sha].runLog = run.logPath;
2030
+ state.merges[sha].runStartedAt = run.startedAt;
2031
+ }
2032
+ }
2033
+ saveState(state);
2034
+ console.log(` āœ… Agent dispatched!`);
2035
+ }
2036
+ function cmdBundleDispatch(state, bundleId) {
2037
+ // Find bundle by ID or partial ID
2038
+ if (!state.bundles) {
2039
+ console.error("āŒ No bundles found. Run 'bundles' first to analyze commits.");
2040
+ return;
2041
+ }
2042
+ let found;
2043
+ for (const id of Object.keys(state.bundles)) {
2044
+ if (id === bundleId || id.includes(bundleId)) {
2045
+ found = id;
2046
+ break;
2047
+ }
2048
+ }
2049
+ if (!found) {
2050
+ console.error(`āŒ Bundle not found: ${bundleId}`);
2051
+ return;
2052
+ }
2053
+ try {
2054
+ dispatchBundleAgent(state, found);
2055
+ }
2056
+ catch (e) {
2057
+ console.error(`āŒ Failed to dispatch: ${e}`);
2058
+ }
2059
+ }
2060
+ function cmdBundleIgnore(state, bundleId, reason) {
2061
+ if (!state.bundles) {
2062
+ console.error("āŒ No bundles found. Run 'bundles' first to analyze commits.");
2063
+ return;
2064
+ }
2065
+ let found;
2066
+ for (const id of Object.keys(state.bundles)) {
2067
+ if (id === bundleId || id.includes(bundleId)) {
2068
+ found = id;
2069
+ break;
2070
+ }
2071
+ }
2072
+ if (!found) {
2073
+ console.error(`āŒ Bundle not found: ${bundleId}`);
2074
+ return;
2075
+ }
2076
+ const bundle = state.bundles[found];
2077
+ bundle.status = "ignored";
2078
+ // Mark all commits in bundle as ignored
2079
+ for (const sha of bundle.commits) {
2080
+ if (state.merges[sha]) {
2081
+ state.merges[sha].status = "ignored";
2082
+ state.merges[sha].ignoreReason = reason || "Bundle ignored";
2083
+ }
2084
+ }
2085
+ saveState(state);
2086
+ console.log(`ā­ļø Bundle ignored: ${bundle.name} (${bundle.commits.length} commits)`);
2087
+ }
2088
+ function cmdBundleSplit(state, bundleId) {
2089
+ if (!state.bundles) {
2090
+ console.error("āŒ No bundles found. Run 'bundles' first to analyze commits.");
2091
+ return;
2092
+ }
2093
+ let found;
2094
+ for (const id of Object.keys(state.bundles)) {
2095
+ if (id === bundleId || id.includes(bundleId)) {
2096
+ found = id;
2097
+ break;
2098
+ }
2099
+ }
2100
+ if (!found) {
2101
+ console.error(`āŒ Bundle not found: ${bundleId}`);
2102
+ return;
2103
+ }
2104
+ const bundle = state.bundles[found];
2105
+ if (bundle.status === "porting") {
2106
+ console.error(`āŒ Bundle ${bundle.id} is currently porting. Stop it before splitting.`);
2107
+ return;
2108
+ }
2109
+ if (bundle.status === "merged" || bundle.status === "ignored") {
2110
+ console.error(`āŒ Bundle ${bundle.id} is ${bundle.status} and cannot be split.`);
2111
+ return;
2112
+ }
2113
+ const buckets = new Map();
2114
+ for (const sha of bundle.commits) {
2115
+ const entry = state.merges[sha];
2116
+ if (!entry)
2117
+ continue;
2118
+ const parsed = parseConventionalCommit(entry.title);
2119
+ const bucket = categorizeSplitBucket(entry.title, parsed.type);
2120
+ const items = buckets.get(bucket) ?? [];
2121
+ items.push({ sha, entry, parsed });
2122
+ buckets.set(bucket, items);
2123
+ }
2124
+ if (buckets.size <= 1) {
2125
+ console.error(`āŒ Bundle ${bundle.id} did not split into multiple buckets.`);
2126
+ return;
2127
+ }
2128
+ delete state.bundles[found];
2129
+ const newBundles = [];
2130
+ for (const [bucket, items] of buckets.entries()) {
2131
+ const sorted = items;
2132
+ const firstEntry = sorted[0]?.entry;
2133
+ const lastEntry = sorted[sorted.length - 1]?.entry;
2134
+ const commitTitles = sorted.map((c) => `- ${c.entry.title}`).join("\n");
2135
+ const maxPriority = Math.max(...sorted.map((c) => getTypePriority(c.parsed.type, c.parsed.breaking)));
2136
+ const firstSha = sorted[0]?.sha?.slice(0, 8) || `${Date.now()}`;
2137
+ let id = `bundle-${bucket}-${firstSha}`;
2138
+ let suffix = 1;
2139
+ while (state.bundles[id]) {
2140
+ id = `bundle-${bucket}-${firstSha}-${suffix}`;
2141
+ suffix += 1;
2142
+ }
2143
+ const types = new Set(sorted.map((c) => c.parsed.type));
2144
+ const scopes = new Set(sorted.map((c) => c.parsed.scope).filter(Boolean));
2145
+ const authors = new Set(sorted.map((c) => c.entry.author).filter(Boolean));
2146
+ const author = authors.size === 1 ? Array.from(authors)[0] : bundle.author || "multiple";
2147
+ const dateLabel = firstEntry?.date?.split("T")[0] ?? "unknown";
2148
+ const name = sorted.length === 1
2149
+ ? sorted[0]?.entry.title
2150
+ : `${bucket}: ${sorted.length} commits (${dateLabel})`;
2151
+ newBundles.push({
2152
+ id,
2153
+ name,
2154
+ description: `${sorted.length} commit(s) split from ${bundle.id}\n\nCommits:\n${commitTitles}`,
2155
+ commits: sorted.map((c) => c.sha),
2156
+ author,
2157
+ dateRange: {
2158
+ start: firstEntry?.date ?? bundle.dateRange.start,
2159
+ end: lastEntry?.date ?? bundle.dateRange.end,
2160
+ },
2161
+ scope: scopes.size === 1 ? Array.from(scopes)[0] : undefined,
2162
+ type: types.size === 1 ? Array.from(types)[0] : undefined,
2163
+ status: bundle.status,
2164
+ priority: maxPriority,
2165
+ baseCommit: bundle.baseCommit,
2166
+ });
2167
+ }
2168
+ for (const next of newBundles) {
2169
+ state.bundles[next.id] = next;
2170
+ for (const sha of next.commits) {
2171
+ if (state.merges[sha]) {
2172
+ state.merges[sha].bundleId = next.id;
2173
+ }
2174
+ }
2175
+ }
2176
+ saveState(state);
2177
+ console.log(`āœ… Split bundle ${bundle.id} into ${newBundles.length} bundle(s):`);
2178
+ for (const next of newBundles) {
2179
+ console.log(` - ${next.id}: ${next.name}`);
2180
+ }
2181
+ }
2182
+ function cmdBundleRequeue(state, bundleId) {
2183
+ if (!state.bundles) {
2184
+ console.error("āŒ No bundles found. Run 'bundles' first to analyze commits.");
2185
+ return;
2186
+ }
2187
+ let found;
2188
+ for (const id of Object.keys(state.bundles)) {
2189
+ if (id === bundleId || id.includes(bundleId)) {
2190
+ found = id;
2191
+ break;
2192
+ }
2193
+ }
2194
+ if (!found) {
2195
+ console.error(`āŒ Bundle not found: ${bundleId}`);
2196
+ return;
2197
+ }
2198
+ const bundle = state.bundles[found];
2199
+ if (bundle.status === "porting") {
2200
+ console.error(`āŒ Bundle ${bundle.id} is currently porting. Stop it before requeueing.`);
2201
+ return;
2202
+ }
2203
+ bundle.status = "pending";
2204
+ delete bundle.portBranch;
2205
+ delete bundle.portCommit;
2206
+ delete bundle.tmuxSession;
2207
+ delete bundle.worktree;
2208
+ delete bundle.runId;
2209
+ delete bundle.runPid;
2210
+ delete bundle.runLog;
2211
+ delete bundle.runStartedAt;
2212
+ delete bundle.error;
2213
+ for (const sha of bundle.commits) {
2214
+ const entry = state.merges[sha];
2215
+ if (!entry)
2216
+ continue;
2217
+ entry.status = "new";
2218
+ delete entry.portBranch;
2219
+ delete entry.portCommit;
2220
+ delete entry.tmuxSession;
2221
+ delete entry.worktree;
2222
+ delete entry.runId;
2223
+ delete entry.runPid;
2224
+ delete entry.runLog;
2225
+ delete entry.runStartedAt;
2226
+ delete entry.error;
2227
+ }
2228
+ saveState(state);
2229
+ console.log(`šŸ” Bundle requeued: ${bundle.id} (${bundle.commits.length} commits)`);
2230
+ }
2231
+ function cmdBundleAuto(state, minPriority = 70) {
2232
+ if (!state.bundles) {
2233
+ console.log("šŸ” Analyzing direct commits first...\n");
2234
+ cmdBundles(state);
2235
+ }
2236
+ const pending = Object.values(state.bundles)
2237
+ .filter((b) => b.status === "pending" && b.priority >= minPriority)
2238
+ .sort((a, b) => b.priority - a.priority);
2239
+ if (pending.length === 0) {
2240
+ console.log("✨ No high-priority bundles to dispatch.");
2241
+ return;
2242
+ }
2243
+ // Check how many agents are already running
2244
+ const currentlyRunning = Object.values(state.bundles).filter((b) => b.status === "porting").length;
2245
+ const maxConcurrent = 6;
2246
+ const canDispatch = Math.max(0, maxConcurrent - currentlyRunning);
2247
+ if (canDispatch === 0) {
2248
+ console.log(`ā³ Max agents (${maxConcurrent}) already running. Wait for some to complete.`);
2249
+ return;
2250
+ }
2251
+ const toDispatch = pending.slice(0, canDispatch);
2252
+ console.log(`šŸš€ Auto-dispatching ${toDispatch.length} high-priority bundle(s)...\n`);
2253
+ for (const bundle of toDispatch) {
2254
+ const priorityLabel = bundle.priority >= 100 ? "šŸ”„ BREAKING" : bundle.priority >= 70 ? "✨ FEATURE" : "šŸ› FIX";
2255
+ console.log(`\n${priorityLabel} ${bundle.name}`);
2256
+ try {
2257
+ dispatchBundleAgent(state, bundle.id);
2258
+ }
2259
+ catch (e) {
2260
+ console.error(` āŒ Failed: ${e}`);
2261
+ }
2262
+ }
2263
+ console.log(`\nāœ… Dispatched ${toDispatch.length} bundle(s).`);
2264
+ if (pending.length > toDispatch.length) {
2265
+ console.log(` ${pending.length - toDispatch.length} more pending (run again after agents complete).`);
2266
+ }
2267
+ }
2268
+ function cmdBundleDispatchOldest(state, requested) {
2269
+ if (!state.bundles) {
2270
+ console.log("šŸ” Analyzing direct commits first...\n");
2271
+ cmdBundles(state);
2272
+ }
2273
+ if (isDispatchDisabled(state)) {
2274
+ console.log("ā›” Dispatch paused; skipping bundle dispatch.");
2275
+ return;
2276
+ }
2277
+ const portingBundles = state.bundles
2278
+ ? Object.values(state.bundles).filter((b) => b.status === "porting").length
2279
+ : 0;
2280
+ const portingIndividual = Object.values(state.merges).filter((e) => e.status === "porting" && !e.bundleId).length;
2281
+ const currentlyPorting = portingBundles + portingIndividual;
2282
+ const maxConcurrent = 6;
2283
+ const available = Math.max(0, maxConcurrent - currentlyPorting);
2284
+ if (available === 0) {
2285
+ console.log(`ā³ All ${maxConcurrent} slots occupied. Wait for current bundles to finish.`);
2286
+ return;
2287
+ }
2288
+ const pending = Object.values(state.bundles)
2289
+ .filter((b) => b.status === "pending")
2290
+ .sort((a, b) => {
2291
+ const dateDiff = a.dateRange.start.localeCompare(b.dateRange.start);
2292
+ if (dateDiff !== 0)
2293
+ return dateDiff;
2294
+ return b.priority - a.priority;
2295
+ });
2296
+ if (pending.length === 0) {
2297
+ console.log("✨ No pending bundles to dispatch.");
2298
+ return;
2299
+ }
2300
+ const limit = typeof requested === "number" && Number.isFinite(requested) && requested > 0
2301
+ ? Math.min(requested, available)
2302
+ : available;
2303
+ const toDispatch = pending.slice(0, limit);
2304
+ console.log(`šŸš€ Dispatching ${toDispatch.length} oldest pending bundle(s)...`);
2305
+ for (const bundle of toDispatch) {
2306
+ try {
2307
+ dispatchBundleAgent(state, bundle.id);
2308
+ console.log(` āœ“ ${bundle.name} (${bundle.commits.length} commits)`);
2309
+ }
2310
+ catch (e) {
2311
+ console.error(` āœ— ${bundle.id}: ${e}`);
2312
+ }
2313
+ }
2314
+ }
2315
+ function checkMergeConflicts(state, prNums) {
2316
+ const conflicts = [];
2317
+ // Create a temporary branch to test merges
2318
+ const testBranch = `test-merge-${Date.now()}`;
2319
+ try {
2320
+ exec(`git checkout -b ${testBranch} main`, PROJECT_ROOT);
2321
+ for (const prNum of prNums) {
2322
+ const found = findMergeBySha(state, String(prNum));
2323
+ if (!found || !found.entry.portBranch)
2324
+ continue;
2325
+ try {
2326
+ exec(`git merge --no-commit --no-ff ${found.entry.portBranch}`, PROJECT_ROOT);
2327
+ exec(`git reset --hard HEAD`, PROJECT_ROOT);
2328
+ }
2329
+ catch (e) {
2330
+ // Get conflicting files
2331
+ try {
2332
+ const conflictFiles = exec(`git diff --name-only --diff-filter=U`, PROJECT_ROOT);
2333
+ conflicts.push({
2334
+ pr: prNum,
2335
+ files: conflictFiles.split("\n").filter((f) => f.trim()),
2336
+ });
2337
+ }
2338
+ catch {
2339
+ conflicts.push({ pr: prNum, files: ["unknown"] });
2340
+ }
2341
+ exec(`git merge --abort`, PROJECT_ROOT);
2342
+ }
2343
+ }
2344
+ }
2345
+ finally {
2346
+ // Clean up
2347
+ exec(`git checkout main`, PROJECT_ROOT);
2348
+ try {
2349
+ exec(`git branch -D ${testBranch}`, PROJECT_ROOT);
2350
+ }
2351
+ catch {
2352
+ // Branch might not exist
2353
+ }
2354
+ }
2355
+ return { canMerge: conflicts.length === 0, conflicts };
2356
+ }
2357
+ function cmdMergeMultiple(state, prNums) {
2358
+ const nums = prNums.map((p) => parseInt(p, 10)).filter((n) => !isNaN(n));
2359
+ if (nums.length === 0) {
2360
+ console.error("Usage: upstream-sync merge <pr#> [pr#] [pr#]...");
2361
+ return;
2362
+ }
2363
+ // Validate all PRs exist and are mergeable
2364
+ const toMerge = [];
2365
+ for (const prNum of nums) {
2366
+ const found = findMergeBySha(state, String(prNum));
2367
+ if (!found) {
2368
+ console.error(`āŒ PR #${prNum} not found`);
2369
+ return;
2370
+ }
2371
+ if (found.entry.status !== "pending_review" && found.entry.status !== "shelved") {
2372
+ console.error(`āŒ PR #${prNum} is not ready for merge (status: ${found.entry.status})`);
2373
+ return;
2374
+ }
2375
+ if (!found.entry.portBranch) {
2376
+ console.error(`āŒ PR #${prNum} has no port branch`);
2377
+ return;
2378
+ }
2379
+ toMerge.push({ ...found, prNum });
2380
+ }
2381
+ console.log(`\nšŸ” Checking for conflicts among ${toMerge.length} PRs...`);
2382
+ // Check for conflicts
2383
+ const { canMerge, conflicts } = checkMergeConflicts(state, nums);
2384
+ if (!canMerge) {
2385
+ console.log(`\nāš ļø CONFLICTS DETECTED:\n`);
2386
+ for (const c of conflicts) {
2387
+ console.log(` PR #${c.pr} conflicts in:`);
2388
+ for (const f of c.files) {
2389
+ console.log(` - ${f}`);
2390
+ }
2391
+ }
2392
+ console.log(`\nPlease resolve these conflicts before merging, or merge PRs individually.`);
2393
+ return;
2394
+ }
2395
+ console.log(`āœ… No conflicts detected!\n`);
2396
+ // Merge one at a time
2397
+ for (const { entry, prNum } of toMerge) {
2398
+ console.log(`šŸ”€ Merging PR #${prNum}: ${entry.title}...`);
2399
+ try {
2400
+ exec(`git checkout main`, PROJECT_ROOT);
2401
+ exec(`git merge ${entry.portBranch} --no-edit`, PROJECT_ROOT);
2402
+ entry.status = "merged";
2403
+ saveState(state);
2404
+ console.log(` āœ… Merged successfully!`);
2405
+ // Clean up worktree if exists
2406
+ if (entry.worktree && existsSync(entry.worktree)) {
2407
+ try {
2408
+ exec(`git worktree remove ${entry.worktree}`, PROJECT_ROOT);
2409
+ delete entry.worktree;
2410
+ saveState(state);
2411
+ }
2412
+ catch {
2413
+ // Ignore cleanup errors
2414
+ }
2415
+ }
2416
+ }
2417
+ catch (e) {
2418
+ console.error(` āŒ Merge failed: ${e}`);
2419
+ console.log(` Stopping merge sequence. Please resolve manually.`);
2420
+ return;
2421
+ }
2422
+ }
2423
+ console.log(`\nšŸŽ‰ All ${toMerge.length} PRs merged successfully!`);
2424
+ }
2425
+ // ============================================================================
2426
+ // Daemon Mode
2427
+ // ============================================================================
2428
+ async function sleep(ms) {
2429
+ return new Promise((resolve) => setTimeout(resolve, ms));
2430
+ }
2431
+ async function runDaemon(state, intervalMinutes = 2) {
2432
+ console.log("šŸ¤– UPSTREAM SYNC DAEMON STARTED");
2433
+ console.log(` Interval: ${intervalMinutes} minute(s)`);
2434
+ console.log(` Max concurrent agents: 6`);
2435
+ console.log(` Press Ctrl+C to stop\n`);
2436
+ console.log("═".repeat(60) + "\n");
2437
+ let iteration = 0;
2438
+ while (true) {
2439
+ iteration++;
2440
+ const timestamp = new Date().toLocaleTimeString();
2441
+ console.log(`\nā° [${timestamp}] Iteration ${iteration}`);
2442
+ console.log("─".repeat(40));
2443
+ // Reload state (in case of external changes)
2444
+ try {
2445
+ state = loadState();
2446
+ }
2447
+ catch (e) {
2448
+ console.error(` āŒ Failed to load state: ${e}`);
2449
+ await sleep(intervalMinutes * 60 * 1000);
2450
+ continue;
2451
+ }
2452
+ // 1. Fetch latest from upstream
2453
+ try {
2454
+ fetchUpstream(state);
2455
+ }
2456
+ catch (e) {
2457
+ console.error(` āš ļø Fetch failed: ${e}`);
2458
+ }
2459
+ // 2. Find new commits (both merges and direct commits)
2460
+ const newCommits = findNewCommits(state);
2461
+ if (newCommits.length > 0) {
2462
+ const directCount = newCommits.filter((c) => c.isDirectCommit).length;
2463
+ const mergeCount = newCommits.length - directCount;
2464
+ console.log(` šŸ“¦ Found ${newCommits.length} new commit(s): ${directCount} direct, ${mergeCount} PRs`);
2465
+ for (const commit of newCommits) {
2466
+ state.merges[commit.sha] = {
2467
+ status: "new",
2468
+ prNumber: commit.prNumber,
2469
+ title: commit.title,
2470
+ date: commit.date,
2471
+ isDirectCommit: commit.isDirectCommit,
2472
+ author: commit.author,
2473
+ };
2474
+ if (commit.isDirectCommit) {
2475
+ console.log(` ⭐ DIRECT: ${commit.title} (${commit.author})`);
2476
+ }
2477
+ }
2478
+ saveState(state);
2479
+ }
2480
+ // 3. Update statuses of in-progress agents
2481
+ console.log(` šŸ” Checking agent statuses...`);
2482
+ const beforePending = Object.values(state.merges).filter((e) => e.status === "pending_review").length;
2483
+ updateAllStatuses(state);
2484
+ const afterPending = Object.values(state.merges).filter((e) => e.status === "pending_review").length;
2485
+ if (afterPending > beforePending) {
2486
+ console.log(` šŸŽ‰ ${afterPending - beforePending} agent(s) completed!`);
2487
+ }
2488
+ // 4. Count current state (including bundles)
2489
+ const mergeCounts = {
2490
+ new: 0,
2491
+ pending: 0,
2492
+ porting: 0,
2493
+ pending_review: 0,
2494
+ shelved: 0,
2495
+ merged: 0,
2496
+ ignored: 0,
2497
+ };
2498
+ for (const entry of Object.values(state.merges)) {
2499
+ mergeCounts[entry.status]++;
2500
+ }
2501
+ const bundleCounts = { pending: 0, porting: 0, pending_review: 0, merged: 0, ignored: 0 };
2502
+ if (state.bundles) {
2503
+ for (const bundle of Object.values(state.bundles)) {
2504
+ if (bundle.status in bundleCounts) {
2505
+ bundleCounts[bundle.status]++;
2506
+ }
2507
+ }
2508
+ }
2509
+ const totalPorting = bundleCounts.porting +
2510
+ Object.values(state.merges).filter((e) => e.status === "porting" && !e.bundleId).length;
2511
+ const totalReady = bundleCounts.pending_review + mergeCounts.pending_review;
2512
+ const totalQueued = bundleCounts.pending +
2513
+ Object.values(state.merges).filter((e) => e.status === "new" && !e.bundleId && !e.isDirectCommit).length;
2514
+ console.log(` šŸ“Š Status: ${totalPorting} porting | ${totalReady} ready | ${totalQueued} queued (${bundleCounts.pending} bundles + ${totalQueued - bundleCounts.pending} PRs)`);
2515
+ // 5. Check if we're done
2516
+ if (totalQueued === 0 && totalPorting === 0) {
2517
+ console.log("\n" + "═".repeat(60));
2518
+ console.log("šŸŽ‰ ALL DONE! No more work to process.");
2519
+ console.log(` āœ… Merged: ${mergeCounts.merged}`);
2520
+ console.log(` šŸ‘€ Ready for review: ${totalReady}`);
2521
+ console.log(` šŸ“¦ Shelved: ${mergeCounts.shelved}`);
2522
+ console.log(` ā­ļø Ignored: ${mergeCounts.ignored}`);
2523
+ console.log("═".repeat(60) + "\n");
2524
+ console.log("Daemon exiting. Run 'upstream-sync review' to see pending PRs.");
2525
+ break;
2526
+ }
2527
+ // 6. Dispatch agents for new work (up to max concurrent)
2528
+ // Handle both bundles (for direct commits) and individual PRs
2529
+ const maxConcurrent = 6;
2530
+ // Count currently porting (both bundle and individual)
2531
+ const portingBundles = state.bundles
2532
+ ? Object.values(state.bundles).filter((b) => b.status === "porting").length
2533
+ : 0;
2534
+ const portingIndividual = Object.values(state.merges).filter((e) => e.status === "porting" && !e.bundleId).length;
2535
+ const currentlyPorting = portingBundles + portingIndividual;
2536
+ const canDispatch = Math.max(0, maxConcurrent - currentlyPorting);
2537
+ if (canDispatch > 0 && !isDispatchDisabled(state)) {
2538
+ let dispatched = 0;
2539
+ // First: dispatch pending bundles (direct commits) - OLDEST FIRST
2540
+ if (state.bundles) {
2541
+ const allBundles = Object.entries(state.bundles);
2542
+ const breakingActive = allBundles.some(([_, b]) => getBundleCategory(b) === "breaking" &&
2543
+ (b.status === "porting" || b.status === "pending_review"));
2544
+ let pendingBundles = allBundles
2545
+ .filter(([_, b]) => b.status === "pending")
2546
+ .sort((a, b) => {
2547
+ // Oldest first, then higher priority
2548
+ const dateDiff = new Date(a[1].dateRange.start).getTime() - new Date(b[1].dateRange.start).getTime();
2549
+ if (dateDiff !== 0)
2550
+ return dateDiff;
2551
+ return b[1].priority - a[1].priority;
2552
+ });
2553
+ if (breakingActive) {
2554
+ // Hold other bundles until breaking change is merged
2555
+ const breakingPending = pendingBundles.filter(([_, b]) => getBundleCategory(b) === "breaking");
2556
+ if (breakingPending.length === 0) {
2557
+ console.log(" āøļø Holding new bundles until breaking change is merged");
2558
+ pendingBundles = [];
2559
+ }
2560
+ else {
2561
+ pendingBundles = breakingPending;
2562
+ }
2563
+ }
2564
+ for (const [bundleId] of pendingBundles) {
2565
+ if (dispatched >= canDispatch)
2566
+ break;
2567
+ try {
2568
+ dispatchBundleAgent(state, bundleId);
2569
+ const bundle = state.bundles[bundleId];
2570
+ console.log(` āœ“ šŸ“¦ Bundle: ${bundle.name.slice(0, 50)}`);
2571
+ dispatched++;
2572
+ }
2573
+ catch (e) {
2574
+ console.error(` āœ— Bundle ${bundleId}: ${e}`);
2575
+ }
2576
+ }
2577
+ }
2578
+ // Second: dispatch new PR merges (non-bundled commits) - OLDEST FIRST
2579
+ if (dispatched < canDispatch) {
2580
+ const newPRs = Object.entries(state.merges)
2581
+ .filter(([_, e]) => e.status === "new" && !e.isDirectCommit && !e.bundleId)
2582
+ .sort((a, b) => {
2583
+ // Oldest first
2584
+ return new Date(a[1].date).getTime() - new Date(b[1].date).getTime();
2585
+ });
2586
+ for (const [sha, entry] of newPRs) {
2587
+ if (dispatched >= canDispatch)
2588
+ break;
2589
+ try {
2590
+ dispatchAgent(state, sha);
2591
+ console.log(` āœ“ PR #${entry.prNumber}: ${entry.title.slice(0, 40)}`);
2592
+ dispatched++;
2593
+ }
2594
+ catch (e) {
2595
+ console.error(` āœ— PR #${entry.prNumber}: ${e}`);
2596
+ }
2597
+ }
2598
+ }
2599
+ if (dispatched > 0) {
2600
+ console.log(` šŸš€ Dispatched ${dispatched} agent(s)`);
2601
+ }
2602
+ else {
2603
+ console.log(` āøļø No new work to dispatch`);
2604
+ }
2605
+ }
2606
+ else if (isDispatchDisabled(state)) {
2607
+ console.log(` ā›” Dispatch paused; skipping new agents`);
2608
+ }
2609
+ else {
2610
+ console.log(` ā³ All ${maxConcurrent} slots occupied`);
2611
+ }
2612
+ // 7. Sleep before next iteration
2613
+ console.log(` šŸ’¤ Sleeping ${intervalMinutes} minute(s)...`);
2614
+ await sleep(intervalMinutes * 60 * 1000);
2615
+ }
2616
+ }
2617
+ // ============================================================================
2618
+ // Main
2619
+ // ============================================================================
2620
+ async function main() {
2621
+ const args = process.argv.slice(2);
2622
+ const command = args[0] || "sync";
2623
+ let state;
2624
+ try {
2625
+ state = loadState();
2626
+ }
2627
+ catch (e) {
2628
+ console.error(`āŒ ${e}`);
2629
+ process.exit(1);
2630
+ }
2631
+ switch (command) {
2632
+ case "daemon": {
2633
+ // Parse interval from args (default 2 minutes)
2634
+ const interval = args[1] ? parseInt(args[1], 10) : 2;
2635
+ await runDaemon(state, interval);
2636
+ break;
2637
+ }
2638
+ case "status":
2639
+ updateAllStatuses(state);
2640
+ printStatus(state);
2641
+ break;
2642
+ case "timeline":
2643
+ cmdTimeline(state);
2644
+ break;
2645
+ case "merge":
2646
+ if (!args[1]) {
2647
+ console.error("Usage: upstream-sync merge <pr#> [pr#] [pr#]...");
2648
+ process.exit(1);
2649
+ }
2650
+ cmdMergeMultiple(state, args.slice(1));
2651
+ break;
2652
+ case "review":
2653
+ cmdReview(state);
2654
+ break;
2655
+ case "shelve":
2656
+ if (!args[1]) {
2657
+ console.error("Usage: upstream-sync shelve <pr#>");
2658
+ process.exit(1);
2659
+ }
2660
+ cmdShelve(state, args[1]);
2661
+ break;
2662
+ case "unshelve":
2663
+ if (!args[1]) {
2664
+ console.error("Usage: upstream-sync unshelve <pr#>");
2665
+ process.exit(1);
2666
+ }
2667
+ cmdUnshelve(state, args[1]);
2668
+ break;
2669
+ case "ignore":
2670
+ if (!args[1]) {
2671
+ console.error("Usage: upstream-sync ignore <pr#> [reason]");
2672
+ process.exit(1);
2673
+ }
2674
+ cmdIgnore(state, args[1], args.slice(2).join(" "));
2675
+ break;
2676
+ case "retry":
2677
+ if (!args[1]) {
2678
+ console.error("Usage: upstream-sync retry <pr#>");
2679
+ process.exit(1);
2680
+ }
2681
+ cmdRetry(state, args[1]);
2682
+ break;
2683
+ case "show":
2684
+ if (!args[1]) {
2685
+ console.error("Usage: upstream-sync show <pr#>");
2686
+ process.exit(1);
2687
+ }
2688
+ cmdShow(state, args[1]);
2689
+ break;
2690
+ case "bundles":
2691
+ cmdBundles(state);
2692
+ break;
2693
+ case "rebundle":
2694
+ cmdRebundle(state);
2695
+ break;
2696
+ case "bundle-dispatch":
2697
+ if (!args[1]) {
2698
+ console.error("Usage: upstream-sync bundle-dispatch <bundle-id>");
2699
+ process.exit(1);
2700
+ }
2701
+ cmdBundleDispatch(state, args[1]);
2702
+ break;
2703
+ case "bundle-ignore":
2704
+ if (!args[1]) {
2705
+ console.error("Usage: upstream-sync bundle-ignore <bundle-id> [reason]");
2706
+ process.exit(1);
2707
+ }
2708
+ cmdBundleIgnore(state, args[1], args.slice(2).join(" "));
2709
+ break;
2710
+ case "bundle-split":
2711
+ if (!args[1]) {
2712
+ console.error("Usage: upstream-sync bundle-split <bundle-id>");
2713
+ process.exit(1);
2714
+ }
2715
+ cmdBundleSplit(state, args[1]);
2716
+ break;
2717
+ case "bundle-requeue":
2718
+ if (!args[1]) {
2719
+ console.error("Usage: upstream-sync bundle-requeue <bundle-id>");
2720
+ process.exit(1);
2721
+ }
2722
+ cmdBundleRequeue(state, args[1]);
2723
+ break;
2724
+ case "bundle-dispatch-oldest": {
2725
+ const rawCount = args[1] ? parseInt(args[1], 10) : undefined;
2726
+ cmdBundleDispatchOldest(state, Number.isFinite(rawCount) ? rawCount : undefined);
2727
+ break;
2728
+ }
2729
+ case "bundle-auto":
2730
+ // Optional: specify minimum priority (default 70 = features+)
2731
+ const minPriority = args[1] ? parseInt(args[1], 10) : 70;
2732
+ cmdBundleAuto(state, minPriority);
2733
+ break;
2734
+ case "pause":
2735
+ state.dispatchDisabled = true;
2736
+ saveState(state);
2737
+ console.log("ā›” Dispatch paused. New agents will not be started.");
2738
+ break;
2739
+ case "resume":
2740
+ state.dispatchDisabled = false;
2741
+ saveState(state);
2742
+ console.log("āœ… Dispatch resumed. New agents can be started.");
2743
+ break;
2744
+ case "sync":
2745
+ default:
2746
+ // Full sync flow
2747
+ console.log("šŸ”„ UPSTREAM SYNC\n");
2748
+ // 1. Fetch
2749
+ fetchUpstream(state);
2750
+ // 2. Find new commits (both merges and direct commits)
2751
+ const newCommits = findNewCommits(state);
2752
+ if (newCommits.length > 0) {
2753
+ const directCommits = newCommits.filter((c) => c.isDirectCommit);
2754
+ const prCommits = newCommits.filter((c) => !c.isDirectCommit);
2755
+ console.log(`\nšŸ“¦ Found ${newCommits.length} new commit(s):`);
2756
+ // Show direct commits first (more important)
2757
+ if (directCommits.length > 0) {
2758
+ console.log(`\n ⭐ DIRECT COMMITS (${directCommits.length}) - PRIORITY:`);
2759
+ for (const commit of directCommits) {
2760
+ console.log(` ${commit.title}`);
2761
+ console.log(` Author: ${commit.author} | Date: ${commit.date}`);
2762
+ state.merges[commit.sha] = {
2763
+ status: "new",
2764
+ title: commit.title,
2765
+ date: commit.date,
2766
+ isDirectCommit: true,
2767
+ author: commit.author,
2768
+ };
2769
+ }
2770
+ }
2771
+ // Then PR merges
2772
+ if (prCommits.length > 0) {
2773
+ console.log(`\n šŸ“‹ PR MERGES (${prCommits.length}):`);
2774
+ for (const commit of prCommits) {
2775
+ console.log(` PR #${commit.prNumber}: ${commit.title}`);
2776
+ state.merges[commit.sha] = {
2777
+ status: "new",
2778
+ prNumber: commit.prNumber,
2779
+ title: commit.title,
2780
+ date: commit.date,
2781
+ isDirectCommit: false,
2782
+ author: commit.author,
2783
+ };
2784
+ }
2785
+ }
2786
+ saveState(state);
2787
+ }
2788
+ else {
2789
+ console.log("\n✨ No new commits found");
2790
+ }
2791
+ // 3. Update statuses of in-progress agents
2792
+ console.log("\nšŸ” Checking agent statuses...");
2793
+ updateAllStatuses(state);
2794
+ // 4. Dispatch agents - PRIORITIZE direct commits
2795
+ const currentlyPorting = Object.values(state.merges).filter((e) => e.status === "porting").length;
2796
+ const maxConcurrentSync = 6;
2797
+ const canDispatchSync = Math.max(0, maxConcurrentSync - currentlyPorting);
2798
+ // Sort: direct commits first, then by date
2799
+ const newEntries = Object.entries(state.merges)
2800
+ .filter(([_, e]) => e.status === "new")
2801
+ .sort((a, b) => {
2802
+ if (a[1].isDirectCommit && !b[1].isDirectCommit)
2803
+ return -1;
2804
+ if (!a[1].isDirectCommit && b[1].isDirectCommit)
2805
+ return 1;
2806
+ return new Date(b[1].date).getTime() - new Date(a[1].date).getTime();
2807
+ });
2808
+ const toDispatchSync = newEntries.slice(0, canDispatchSync);
2809
+ if (toDispatchSync.length > 0 && !isDispatchDisabled(state)) {
2810
+ console.log(`\nšŸš€ Dispatching ${toDispatchSync.length} agent(s)...`);
2811
+ for (const [sha, entry] of toDispatchSync) {
2812
+ try {
2813
+ dispatchAgent(state, sha);
2814
+ const label = entry.isDirectCommit ? "⭐ DIRECT" : `PR #${entry.prNumber}`;
2815
+ console.log(` āœ“ ${label}: ${entry.title}`);
2816
+ }
2817
+ catch (e) {
2818
+ console.error(` Failed to dispatch: ${e}`);
2819
+ }
2820
+ }
2821
+ }
2822
+ else if (isDispatchDisabled(state) && toDispatchSync.length > 0) {
2823
+ console.log(`\nā›” Dispatch paused; ${toDispatchSync.length} agent(s) queued`);
2824
+ }
2825
+ // 5. Show status
2826
+ printStatus(state);
2827
+ break;
2828
+ }
2829
+ }
2830
+ main().catch((e) => {
2831
+ console.error("Fatal error:", e);
2832
+ process.exit(1);
2833
+ });