@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.
- package/CHANGELOG.md +222 -0
- package/LICENSE +21 -0
- package/README-header.png +0 -0
- package/README.md +462 -0
- package/dist/agents/agent-paths.js +16 -0
- package/dist/agents/agent-scope.js +44 -0
- package/dist/agents/auth-profiles.js +626 -0
- package/dist/agents/bash-process-registry.js +126 -0
- package/dist/agents/bash-tools.js +838 -0
- package/dist/agents/chutes-oauth.js +47 -0
- package/dist/agents/clawdbot-tools.js +62 -0
- package/dist/agents/context.js +34 -0
- package/dist/agents/defaults.js +6 -0
- package/dist/agents/memory-search.js +80 -0
- package/dist/agents/model-auth.js +115 -0
- package/dist/agents/model-catalog.js +55 -0
- package/dist/agents/model-fallback.js +210 -0
- package/dist/agents/model-scan.js +263 -0
- package/dist/agents/model-selection.js +152 -0
- package/dist/agents/models-config.js +171 -0
- package/dist/agents/nexus-tools.js +46 -0
- package/dist/agents/pi-embedded-block-chunker.js +188 -0
- package/dist/agents/pi-embedded-helpers.js +139 -0
- package/dist/agents/pi-embedded-runner.js +932 -0
- package/dist/agents/pi-embedded-subscribe.js +541 -0
- package/dist/agents/pi-embedded-utils.js +20 -0
- package/dist/agents/pi-embedded.js +1 -0
- package/dist/agents/pi-extensions/compaction-safeguard.js +140 -0
- package/dist/agents/pi-tool-definition-adapter.js +17 -0
- package/dist/agents/pi-tools.js +510 -0
- package/dist/agents/pi-tools.schema.js +358 -0
- package/dist/agents/sandbox-paths.js +68 -0
- package/dist/agents/sandbox.js +667 -0
- package/dist/agents/shell-utils.js +53 -0
- package/dist/agents/skill-runner.js +224 -0
- package/dist/agents/skill-state.js +164 -0
- package/dist/agents/skill-tools.js +191 -0
- package/dist/agents/skill-usage.js +43 -0
- package/dist/agents/skills-install.js +244 -0
- package/dist/agents/skills-status.js +157 -0
- package/dist/agents/skills.js +472 -0
- package/dist/agents/subagent-registry.js +321 -0
- package/dist/agents/subagent-registry.store.js +47 -0
- package/dist/agents/system-prompt.js +179 -0
- package/dist/agents/timeout.js +26 -0
- package/dist/agents/tool-display.js +155 -0
- package/dist/agents/tool-display.json +236 -0
- package/dist/agents/tool-images.js +138 -0
- package/dist/agents/tool-policy.js +87 -0
- package/dist/agents/tools/agent-step.js +41 -0
- package/dist/agents/tools/browser-tool.js +295 -0
- package/dist/agents/tools/canvas-tool.js +193 -0
- package/dist/agents/tools/common.js +88 -0
- package/dist/agents/tools/cron-tool.js +141 -0
- package/dist/agents/tools/discord-actions-guild.js +186 -0
- package/dist/agents/tools/discord-actions-messaging.js +313 -0
- package/dist/agents/tools/discord-actions-moderation.js +70 -0
- package/dist/agents/tools/discord-actions.js +56 -0
- package/dist/agents/tools/discord-schema.js +199 -0
- package/dist/agents/tools/discord-tool.js +16 -0
- package/dist/agents/tools/gateway-tool.js +46 -0
- package/dist/agents/tools/gateway.js +28 -0
- package/dist/agents/tools/image-tool.js +140 -0
- package/dist/agents/tools/memory-tool.js +92 -0
- package/dist/agents/tools/nodes-tool.js +413 -0
- package/dist/agents/tools/nodes-utils.js +92 -0
- package/dist/agents/tools/sessions-announce-target.js +35 -0
- package/dist/agents/tools/sessions-helpers.js +88 -0
- package/dist/agents/tools/sessions-history-tool.js +137 -0
- package/dist/agents/tools/sessions-list-tool.js +196 -0
- package/dist/agents/tools/sessions-send-helpers.js +103 -0
- package/dist/agents/tools/sessions-send-tool.js +371 -0
- package/dist/agents/tools/sessions-spawn-tool.js +319 -0
- package/dist/agents/tools/slack-actions.js +129 -0
- package/dist/agents/tools/slack-schema.js +59 -0
- package/dist/agents/tools/slack-tool.js +16 -0
- package/dist/agents/tools/telegram-actions.js +159 -0
- package/dist/agents/tools/telegram-schema.js +28 -0
- package/dist/agents/tools/telegram-tool.js +16 -0
- package/dist/agents/tools/whatsapp-login-tool.js +63 -0
- package/dist/agents/usage.js +58 -0
- package/dist/agents/workspace.js +264 -0
- package/dist/auto-reply/chunk.js +177 -0
- package/dist/auto-reply/command-auth.js +44 -0
- package/dist/auto-reply/command-detection.js +22 -0
- package/dist/auto-reply/envelope.js +30 -0
- package/dist/auto-reply/group-activation.js +20 -0
- package/dist/auto-reply/heartbeat.js +58 -0
- package/dist/auto-reply/model.js +22 -0
- package/dist/auto-reply/reply/abort.js +14 -0
- package/dist/auto-reply/reply/agent-runner.js +426 -0
- package/dist/auto-reply/reply/bash-command.js +314 -0
- package/dist/auto-reply/reply/block-streaming.js +34 -0
- package/dist/auto-reply/reply/body.js +29 -0
- package/dist/auto-reply/reply/commands.js +332 -0
- package/dist/auto-reply/reply/directive-handling.js +626 -0
- package/dist/auto-reply/reply/directives.js +59 -0
- package/dist/auto-reply/reply/dispatch-from-config.js +23 -0
- package/dist/auto-reply/reply/followup-runner.js +181 -0
- package/dist/auto-reply/reply/groups.js +152 -0
- package/dist/auto-reply/reply/mentions.js +64 -0
- package/dist/auto-reply/reply/model-selection.js +209 -0
- package/dist/auto-reply/reply/queue.js +399 -0
- package/dist/auto-reply/reply/reply-dispatcher.js +68 -0
- package/dist/auto-reply/reply/reply-tags.js +26 -0
- package/dist/auto-reply/reply/session-updates.js +103 -0
- package/dist/auto-reply/reply/session.js +169 -0
- package/dist/auto-reply/reply/typing.js +125 -0
- package/dist/auto-reply/reply.js +655 -0
- package/dist/auto-reply/send-policy.js +28 -0
- package/dist/auto-reply/status.js +197 -0
- package/dist/auto-reply/templating.js +9 -0
- package/dist/auto-reply/thinking.js +49 -0
- package/dist/auto-reply/tokens.js +2 -0
- package/dist/auto-reply/tool-meta.js +74 -0
- package/dist/auto-reply/transcription.js +57 -0
- package/dist/auto-reply/types.js +1 -0
- package/dist/browser/bridge-server.js +37 -0
- package/dist/browser/cdp.js +382 -0
- package/dist/browser/chrome.js +432 -0
- package/dist/browser/client-actions-core.js +67 -0
- package/dist/browser/client-actions-observe.js +24 -0
- package/dist/browser/client-actions-types.js +1 -0
- package/dist/browser/client-actions.js +3 -0
- package/dist/browser/client-fetch.js +43 -0
- package/dist/browser/client.js +105 -0
- package/dist/browser/config.js +155 -0
- package/dist/browser/constants.js +5 -0
- package/dist/browser/profiles-service.js +124 -0
- package/dist/browser/profiles.js +96 -0
- package/dist/browser/pw-ai.js +2 -0
- package/dist/browser/pw-session.js +144 -0
- package/dist/browser/pw-tools-core.js +363 -0
- package/dist/browser/routes/agent.js +535 -0
- package/dist/browser/routes/basic.js +155 -0
- package/dist/browser/routes/index.js +8 -0
- package/dist/browser/routes/tabs.js +105 -0
- package/dist/browser/routes/utils.js +62 -0
- package/dist/browser/screenshot.js +40 -0
- package/dist/browser/server-context.js +377 -0
- package/dist/browser/server.js +81 -0
- package/dist/browser/target-id.js +18 -0
- package/dist/browser/trash.js +21 -0
- package/dist/canvas-host/a2ui/a2ui.bundle.js +17768 -0
- package/dist/canvas-host/a2ui/index.html +246 -0
- package/dist/canvas-host/a2ui.js +187 -0
- package/dist/canvas-host/server.js +382 -0
- package/dist/channel-web.js +8 -0
- package/dist/cli/browser-cli-actions-input.js +459 -0
- package/dist/cli/browser-cli-actions-observe.js +56 -0
- package/dist/cli/browser-cli-examples.js +31 -0
- package/dist/cli/browser-cli-inspect.js +97 -0
- package/dist/cli/browser-cli-manage.js +286 -0
- package/dist/cli/browser-cli-shared.js +1 -0
- package/dist/cli/browser-cli.js +26 -0
- package/dist/cli/canvas-cli.js +416 -0
- package/dist/cli/cloud-cli.js +336 -0
- package/dist/cli/credential-cli.js +227 -0
- package/dist/cli/cron-cli.js +454 -0
- package/dist/cli/deps.js +17 -0
- package/dist/cli/dns-cli.js +180 -0
- package/dist/cli/gateway-cli.js +665 -0
- package/dist/cli/gateway-rpc.js +20 -0
- package/dist/cli/hooks-cli.js +135 -0
- package/dist/cli/memory-cli.js +101 -0
- package/dist/cli/models-cli.js +248 -0
- package/dist/cli/nodes-camera.js +57 -0
- package/dist/cli/nodes-canvas.js +26 -0
- package/dist/cli/nodes-cli.js +946 -0
- package/dist/cli/nodes-screen.js +37 -0
- package/dist/cli/pairing-cli.js +100 -0
- package/dist/cli/parse-duration.js +20 -0
- package/dist/cli/plugins-cli.js +158 -0
- package/dist/cli/ports.js +97 -0
- package/dist/cli/profile.js +81 -0
- package/dist/cli/program.js +162 -0
- package/dist/cli/prompt.js +19 -0
- package/dist/cli/run-main.js +48 -0
- package/dist/cli/skills-cli.js +132 -0
- package/dist/cli/skills-hub-cli.js +1093 -0
- package/dist/cli/telegram-cli.js +56 -0
- package/dist/cli/tool-connector-cli.js +118 -0
- package/dist/cli/tui-cli.js +35 -0
- package/dist/cli/upstream-sync-cli.js +2833 -0
- package/dist/cli/usage-cli.js +24 -0
- package/dist/cli/wait.js +8 -0
- package/dist/commands/agent-via-gateway.js +115 -0
- package/dist/commands/agent.js +665 -0
- package/dist/commands/antigravity-oauth.js +327 -0
- package/dist/commands/auth-choice-options.js +80 -0
- package/dist/commands/auth-choice.js +134 -0
- package/dist/commands/auth-token.js +31 -0
- package/dist/commands/bootstrap-preset.js +214 -0
- package/dist/commands/capabilities.js +36 -0
- package/dist/commands/chutes-oauth.js +144 -0
- package/dist/commands/claude-md.js +137 -0
- package/dist/commands/config-view.js +11 -0
- package/dist/commands/configure.js +648 -0
- package/dist/commands/credential.js +236 -0
- package/dist/commands/cursor-rules.js +230 -0
- package/dist/commands/doctor-state-migrations.js +358 -0
- package/dist/commands/doctor-ui.js +113 -0
- package/dist/commands/doctor.js +673 -0
- package/dist/commands/health.js +112 -0
- package/dist/commands/identity.js +54 -0
- package/dist/commands/init.js +167 -0
- package/dist/commands/models/aliases.js +85 -0
- package/dist/commands/models/fallbacks.js +123 -0
- package/dist/commands/models/image-fallbacks.js +123 -0
- package/dist/commands/models/list.js +347 -0
- package/dist/commands/models/scan.js +271 -0
- package/dist/commands/models/set-image.js +27 -0
- package/dist/commands/models/set.js +27 -0
- package/dist/commands/models/shared.js +73 -0
- package/dist/commands/models.js +7 -0
- package/dist/commands/onboard-auth.js +101 -0
- package/dist/commands/onboard-channels.js +814 -0
- package/dist/commands/onboard-eve-identity.js +98 -0
- package/dist/commands/onboard-github.js +153 -0
- package/dist/commands/onboard-helpers.js +303 -0
- package/dist/commands/onboard-interactive.js +17 -0
- package/dist/commands/onboard-non-interactive.js +228 -0
- package/dist/commands/onboard-providers.js +829 -0
- package/dist/commands/onboard-quickstart.js +185 -0
- package/dist/commands/onboard-remote.js +120 -0
- package/dist/commands/onboard-skills.js +148 -0
- package/dist/commands/onboard-types.js +1 -0
- package/dist/commands/onboard.js +19 -0
- package/dist/commands/openai-codex-model-default.js +38 -0
- package/dist/commands/poll.js +64 -0
- package/dist/commands/quest.js +27 -0
- package/dist/commands/reset.js +61 -0
- package/dist/commands/send.js +124 -0
- package/dist/commands/sessions-ingest.js +359 -0
- package/dist/commands/sessions.js +212 -0
- package/dist/commands/setup.js +59 -0
- package/dist/commands/signal-install.js +135 -0
- package/dist/commands/skills-manifest.js +235 -0
- package/dist/commands/status.js +139 -0
- package/dist/commands/suggestions.js +54 -0
- package/dist/commands/systemd-linger.js +71 -0
- package/dist/commands/update.js +16 -0
- package/dist/commands/usage-upload.js +27 -0
- package/dist/config/config.js +6 -0
- package/dist/config/defaults.js +140 -0
- package/dist/config/group-policy.js +49 -0
- package/dist/config/includes.js +183 -0
- package/dist/config/io.js +188 -0
- package/dist/config/legacy-migrate.js +13 -0
- package/dist/config/legacy.js +425 -0
- package/dist/config/paths.js +82 -0
- package/dist/config/port-defaults.js +32 -0
- package/dist/config/schema.js +173 -0
- package/dist/config/sessions.js +611 -0
- package/dist/config/talk.js +31 -0
- package/dist/config/types.js +1 -0
- package/dist/config/validation.js +29 -0
- package/dist/config/zod-schema.js +1110 -0
- package/dist/control-ui/assets/index-D8Q5AI4D.js +2393 -0
- package/dist/control-ui/assets/index-D8Q5AI4D.js.map +1 -0
- package/dist/control-ui/assets/index-g06q5Xc3.css +1 -0
- package/dist/control-ui/favicon.ico +0 -0
- package/dist/control-ui/index.html +16 -0
- package/dist/cron/isolated-agent.js +529 -0
- package/dist/cron/normalize.js +73 -0
- package/dist/cron/parse.js +24 -0
- package/dist/cron/run-log.js +72 -0
- package/dist/cron/schedule.js +24 -0
- package/dist/cron/service.js +471 -0
- package/dist/cron/store.js +43 -0
- package/dist/cron/types.js +1 -0
- package/dist/daemon/constants.js +10 -0
- package/dist/daemon/launchd.js +295 -0
- package/dist/daemon/legacy.js +63 -0
- package/dist/daemon/program-args.js +141 -0
- package/dist/daemon/schtasks.js +269 -0
- package/dist/daemon/service.js +69 -0
- package/dist/daemon/systemd.js +332 -0
- package/dist/discord/index.js +2 -0
- package/dist/discord/monitor.js +1089 -0
- package/dist/discord/probe.js +54 -0
- package/dist/discord/send.js +652 -0
- package/dist/discord/token.js +8 -0
- package/dist/entry.js +16 -0
- package/dist/gateway/auth.js +121 -0
- package/dist/gateway/call.js +103 -0
- package/dist/gateway/chat-attachments.js +41 -0
- package/dist/gateway/client.js +180 -0
- package/dist/gateway/config-reload.js +274 -0
- package/dist/gateway/control-ui.js +184 -0
- package/dist/gateway/hooks-mapping.js +282 -0
- package/dist/gateway/hooks.js +168 -0
- package/dist/gateway/net.js +29 -0
- package/dist/gateway/protocol/index.js +62 -0
- package/dist/gateway/protocol/schema.js +577 -0
- package/dist/gateway/server-bridge-subscriptions.js +93 -0
- package/dist/gateway/server-bridge.js +1066 -0
- package/dist/gateway/server-browser.js +11 -0
- package/dist/gateway/server-channels.js +680 -0
- package/dist/gateway/server-chat.js +159 -0
- package/dist/gateway/server-constants.js +8 -0
- package/dist/gateway/server-discovery.js +62 -0
- package/dist/gateway/server-http.js +165 -0
- package/dist/gateway/server-methods/agent-job.js +114 -0
- package/dist/gateway/server-methods/agent.js +254 -0
- package/dist/gateway/server-methods/channels.js +239 -0
- package/dist/gateway/server-methods/chat.js +207 -0
- package/dist/gateway/server-methods/config.js +50 -0
- package/dist/gateway/server-methods/connect.js +6 -0
- package/dist/gateway/server-methods/cron.js +89 -0
- package/dist/gateway/server-methods/health.js +28 -0
- package/dist/gateway/server-methods/models.js +16 -0
- package/dist/gateway/server-methods/nodes.js +294 -0
- package/dist/gateway/server-methods/providers.js +257 -0
- package/dist/gateway/server-methods/send.js +254 -0
- package/dist/gateway/server-methods/sessions.js +382 -0
- package/dist/gateway/server-methods/skills.js +83 -0
- package/dist/gateway/server-methods/system.js +118 -0
- package/dist/gateway/server-methods/talk.js +22 -0
- package/dist/gateway/server-methods/types.js +1 -0
- package/dist/gateway/server-methods/voicewake.js +30 -0
- package/dist/gateway/server-methods/web.js +81 -0
- package/dist/gateway/server-methods/wizard.js +100 -0
- package/dist/gateway/server-methods.js +53 -0
- package/dist/gateway/server-providers.js +687 -0
- package/dist/gateway/server-shared.js +1 -0
- package/dist/gateway/server-utils.js +35 -0
- package/dist/gateway/server.js +1478 -0
- package/dist/gateway/session-utils.js +355 -0
- package/dist/gateway/ws-log.js +343 -0
- package/dist/gateway/ws-logging.js +8 -0
- package/dist/globals.js +41 -0
- package/dist/hooks/gmail-ops.js +236 -0
- package/dist/hooks/gmail-setup-utils.js +278 -0
- package/dist/hooks/gmail-watcher.js +190 -0
- package/dist/hooks/gmail.js +177 -0
- package/dist/imessage/client.js +165 -0
- package/dist/imessage/index.js +3 -0
- package/dist/imessage/monitor.js +365 -0
- package/dist/imessage/probe.js +26 -0
- package/dist/imessage/send.js +83 -0
- package/dist/imessage/targets.js +176 -0
- package/dist/index.js +55 -0
- package/dist/infra/agent-events.js +46 -0
- package/dist/infra/binaries.js +9 -0
- package/dist/infra/bonjour-discovery.js +163 -0
- package/dist/infra/bonjour.js +200 -0
- package/dist/infra/bridge/server.js +564 -0
- package/dist/infra/canvas-host-url.js +54 -0
- package/dist/infra/channel-summary.js +78 -0
- package/dist/infra/control-ui-assets.js +112 -0
- package/dist/infra/dotenv.js +15 -0
- package/dist/infra/env.js +8 -0
- package/dist/infra/errors.js +28 -0
- package/dist/infra/event-log.js +251 -0
- package/dist/infra/gateway-lock.js +8 -0
- package/dist/infra/git-commit.js +91 -0
- package/dist/infra/heartbeat-events.js +21 -0
- package/dist/infra/heartbeat-runner.js +458 -0
- package/dist/infra/heartbeat-wake.js +61 -0
- package/dist/infra/is-main.js +37 -0
- package/dist/infra/json-file.js +21 -0
- package/dist/infra/machine-name.js +40 -0
- package/dist/infra/nexus-root.js +56 -0
- package/dist/infra/node-pairing.js +212 -0
- package/dist/infra/path-env.js +93 -0
- package/dist/infra/ports.js +87 -0
- package/dist/infra/provider-summary.js +80 -0
- package/dist/infra/provider-usage.auth.js +189 -0
- package/dist/infra/provider-usage.fetch.claude.js +129 -0
- package/dist/infra/provider-usage.fetch.codex.js +62 -0
- package/dist/infra/provider-usage.fetch.copilot.js +42 -0
- package/dist/infra/provider-usage.fetch.gemini.js +57 -0
- package/dist/infra/provider-usage.fetch.js +6 -0
- package/dist/infra/provider-usage.fetch.minimax.js +214 -0
- package/dist/infra/provider-usage.fetch.shared.js +11 -0
- package/dist/infra/provider-usage.fetch.zai.js +62 -0
- package/dist/infra/provider-usage.format.js +77 -0
- package/dist/infra/provider-usage.js +145 -0
- package/dist/infra/provider-usage.load.js +54 -0
- package/dist/infra/provider-usage.shared.js +19 -0
- package/dist/infra/provider-usage.types.js +1 -0
- package/dist/infra/restart.js +29 -0
- package/dist/infra/retry.js +16 -0
- package/dist/infra/runtime-guard.js +59 -0
- package/dist/infra/shell-env.js +88 -0
- package/dist/infra/system-events.js +71 -0
- package/dist/infra/system-presence.js +217 -0
- package/dist/infra/tailnet.js +46 -0
- package/dist/infra/tailscale.js +149 -0
- package/dist/infra/unhandled-rejections.js +19 -0
- package/dist/infra/usage-suggestions.js +241 -0
- package/dist/infra/usage-upload.js +290 -0
- package/dist/infra/voicewake.js +78 -0
- package/dist/infra/widearea-dns.js +123 -0
- package/dist/infra/ws.js +13 -0
- package/dist/logger.js +52 -0
- package/dist/logging.js +506 -0
- package/dist/macos/gateway-daemon.js +145 -0
- package/dist/macos/relay.js +49 -0
- package/dist/media/constants.js +33 -0
- package/dist/media/host.js +42 -0
- package/dist/media/image-ops.js +119 -0
- package/dist/media/mime.js +123 -0
- package/dist/media/parse.js +83 -0
- package/dist/media/server.js +64 -0
- package/dist/media/store.js +139 -0
- package/dist/polls.js +43 -0
- package/dist/process/command-queue.js +97 -0
- package/dist/process/exec.js +75 -0
- package/dist/provider-web.js +8 -0
- package/dist/providers/github-copilot-auth.js +123 -0
- package/dist/providers/github-copilot-models.js +35 -0
- package/dist/providers/github-copilot-token.js +11 -0
- package/dist/providers/location.js +48 -0
- package/dist/providers/web/index.js +2 -0
- package/dist/runtime.js +8 -0
- package/dist/sessions/level-overrides.js +9 -0
- package/dist/sessions/send-policy.js +68 -0
- package/dist/signal/client.js +134 -0
- package/dist/signal/daemon.js +69 -0
- package/dist/signal/index.js +3 -0
- package/dist/signal/monitor.js +411 -0
- package/dist/signal/probe.js +46 -0
- package/dist/signal/send.js +91 -0
- package/dist/slack/actions.js +97 -0
- package/dist/slack/index.js +5 -0
- package/dist/slack/monitor.js +1270 -0
- package/dist/slack/probe.js +47 -0
- package/dist/slack/send.js +131 -0
- package/dist/slack/token.js +10 -0
- package/dist/telegram/allowed-updates.js +8 -0
- package/dist/telegram/bot.js +724 -0
- package/dist/telegram/download.js +34 -0
- package/dist/telegram/index.js +4 -0
- package/dist/telegram/monitor.js +47 -0
- package/dist/telegram/pairing-store.js +77 -0
- package/dist/telegram/probe.js +63 -0
- package/dist/telegram/proxy.js +9 -0
- package/dist/telegram/reaction-level.js +45 -0
- package/dist/telegram/send.js +151 -0
- package/dist/telegram/sent-message-cache.js +65 -0
- package/dist/telegram/token.js +30 -0
- package/dist/telegram/update-offset-store.js +61 -0
- package/dist/telegram/webhook-set.js +12 -0
- package/dist/telegram/webhook.js +56 -0
- package/dist/tui/commands.js +87 -0
- package/dist/tui/components/assistant-message.js +16 -0
- package/dist/tui/components/chat-log.js +92 -0
- package/dist/tui/components/custom-editor.js +55 -0
- package/dist/tui/components/selectors.js +8 -0
- package/dist/tui/components/tool-execution.js +111 -0
- package/dist/tui/components/user-message.js +17 -0
- package/dist/tui/gateway-chat.js +140 -0
- package/dist/tui/theme/theme.js +80 -0
- package/dist/tui/tui.js +708 -0
- package/dist/utils.js +153 -0
- package/dist/version.js +18 -0
- package/dist/web/accounts.js +86 -0
- package/dist/web/active-listener.js +25 -0
- package/dist/web/auto-reply.js +1256 -0
- package/dist/web/inbound.js +649 -0
- package/dist/web/login-qr.js +230 -0
- package/dist/web/login.js +71 -0
- package/dist/web/media.js +175 -0
- package/dist/web/outbound.js +102 -0
- package/dist/web/qr-image.js +97 -0
- package/dist/web/reconnect.js +60 -0
- package/dist/web/session.js +370 -0
- package/dist/wizard/clack-prompter.js +56 -0
- package/dist/wizard/onboarding.js +620 -0
- package/dist/wizard/prompts.js +6 -0
- package/dist/wizard/session.js +203 -0
- package/docs/AGENTS.default.md +116 -0
- package/docs/CAPABILITIES.md +444 -0
- package/docs/CNAME +1 -0
- package/docs/NEXUS_CORE_REWRITE_SPEC.md +226 -0
- package/docs/RELEASING.md +69 -0
- package/docs/_config.yml +53 -0
- package/docs/_layouts/default.html +145 -0
- package/docs/agent-assisted-install.md +95 -0
- package/docs/agent-loop.md +61 -0
- package/docs/agent-send.md +21 -0
- package/docs/agent.md +108 -0
- package/docs/android.md +133 -0
- package/docs/architecture.md +114 -0
- package/docs/assets/markdown.css +133 -0
- package/docs/assets/pixel-lobster.svg +60 -0
- package/docs/assets/terminal.css +470 -0
- package/docs/assets/theme.js +55 -0
- package/docs/audio.md +48 -0
- package/docs/automation/nexus-sync.md +371 -0
- package/docs/background-process.md +74 -0
- package/docs/bash.md +32 -0
- package/docs/bedrock.md +71 -0
- package/docs/bonjour.md +159 -0
- package/docs/browser-linux-troubleshooting.md +114 -0
- package/docs/browser.md +293 -0
- package/docs/bun.md +56 -0
- package/docs/camera.md +152 -0
- package/docs/clawd.md +212 -0
- package/docs/concepts/usage-tracking.md +29 -0
- package/docs/configuration.md +1666 -0
- package/docs/control-ui.md +83 -0
- package/docs/cron.md +385 -0
- package/docs/dashboard.md +17 -0
- package/docs/device-models.md +46 -0
- package/docs/discord.md +308 -0
- package/docs/discovery.md +112 -0
- package/docs/docker.md +258 -0
- package/docs/docs.json +105 -0
- package/docs/doctor.md +68 -0
- package/docs/elevated.md +31 -0
- package/docs/faq.md +736 -0
- package/docs/feature-inventory/overview.md +141 -0
- package/docs/feature-inventory/rollout-checklist.md +53 -0
- package/docs/feature-inventory/test-matrix.md +87 -0
- package/docs/feature-inventory.md +9 -0
- package/docs/gateway/configuration-examples.md +221 -0
- package/docs/gateway/configuration.md +172 -0
- package/docs/gateway/cron.md +61 -0
- package/docs/gateway/heartbeat.md +207 -0
- package/docs/gateway/pairing.md +109 -0
- package/docs/gateway-lock.md +28 -0
- package/docs/gateway.md +227 -0
- package/docs/gmail-pubsub.md +191 -0
- package/docs/grammy.md +27 -0
- package/docs/group-messages.md +73 -0
- package/docs/groups.md +130 -0
- package/docs/health.md +28 -0
- package/docs/heartbeat.md +73 -0
- package/docs/home-userspace.md +277 -0
- package/docs/hubs.md +148 -0
- package/docs/images.md +51 -0
- package/docs/imessage.md +94 -0
- package/docs/index.md +196 -0
- package/docs/ios.md +372 -0
- package/docs/linux.md +11 -0
- package/docs/location-command.md +95 -0
- package/docs/location.md +46 -0
- package/docs/logging.md +110 -0
- package/docs/lore.md +131 -0
- package/docs/mac/bun.md +133 -0
- package/docs/mac/canvas.md +161 -0
- package/docs/mac/child-process.md +72 -0
- package/docs/mac/dev-setup.md +81 -0
- package/docs/mac/health.md +28 -0
- package/docs/mac/icon.md +26 -0
- package/docs/mac/logging.md +51 -0
- package/docs/mac/menu-bar.md +69 -0
- package/docs/mac/peekaboo.md +170 -0
- package/docs/mac/permissions.md +40 -0
- package/docs/mac/release.md +76 -0
- package/docs/mac/remote.md +57 -0
- package/docs/mac/signing.md +41 -0
- package/docs/mac/skills.md +27 -0
- package/docs/mac/voice-overlay.md +52 -0
- package/docs/mac/voicewake.md +56 -0
- package/docs/mac/webchat.md +27 -0
- package/docs/mac/xpc.md +40 -0
- package/docs/macos.md +104 -0
- package/docs/model-failover.md +75 -0
- package/docs/models.md +91 -0
- package/docs/multi-agent.md +74 -0
- package/docs/nix.md +95 -0
- package/docs/nodes.md +157 -0
- package/docs/onboarding-config-protocol.md +34 -0
- package/docs/onboarding.md +189 -0
- package/docs/pairing.md +85 -0
- package/docs/plans/cron-add-hardening.md +72 -0
- package/docs/plans/group-policy-hardening.md +121 -0
- package/docs/poll.md +52 -0
- package/docs/prereqs.md +67 -0
- package/docs/presence.md +133 -0
- package/docs/proposals/model-config.md +147 -0
- package/docs/provider-routing.md +25 -0
- package/docs/queue.md +78 -0
- package/docs/reference/templates/AGENTS.md +164 -0
- package/docs/remote-gateway-readme.md +153 -0
- package/docs/remote.md +61 -0
- package/docs/research/memory.md +227 -0
- package/docs/rpc.md +35 -0
- package/docs/security.md +200 -0
- package/docs/session-ingestion.md +119 -0
- package/docs/session-tool.md +154 -0
- package/docs/session.md +85 -0
- package/docs/sessions.md +8 -0
- package/docs/setup.md +131 -0
- package/docs/showcase.md +37 -0
- package/docs/signal.md +122 -0
- package/docs/skills-config.md +58 -0
- package/docs/skills.md +153 -0
- package/docs/slack.md +221 -0
- package/docs/subagents.md +72 -0
- package/docs/tailscale.md +71 -0
- package/docs/talk.md +79 -0
- package/docs/telegram.md +96 -0
- package/docs/templates/AGENTS.md +286 -0
- package/docs/templates/BOOTSTRAP.md +35 -0
- package/docs/templates/IDENTITY.md +17 -0
- package/docs/templates/PROFILE.md +14 -0
- package/docs/templates/SOUL.md +41 -0
- package/docs/templates/TOOLS.md +41 -0
- package/docs/templates/USER.md +8 -0
- package/docs/test.md +43 -0
- package/docs/testing-onboarding-quickstart.md +76 -0
- package/docs/testing-philosophy.md +211 -0
- package/docs/thinking.md +46 -0
- package/docs/timezone.md +40 -0
- package/docs/tools.md +346 -0
- package/docs/troubleshooting.md +257 -0
- package/docs/tui.md +71 -0
- package/docs/typebox.md +42 -0
- package/docs/updating.md +138 -0
- package/docs/usage-cloud-aggregation-spec.md +133 -0
- package/docs/usage-suggestions-pipeline.md +126 -0
- package/docs/voicewake.md +61 -0
- package/docs/web.md +115 -0
- package/docs/webchat.md +34 -0
- package/docs/webhook.md +132 -0
- package/docs/whatsapp-clawd.jpg +0 -0
- package/docs/whatsapp.md +170 -0
- package/docs/windows.md +11 -0
- package/docs/wizard.md +167 -0
- package/package.json +209 -0
- package/skills/1password/SKILL.md +54 -0
- package/skills/1password/docs/setup.md +85 -0
- package/skills/1password/docs/troubleshooting.md +63 -0
- package/skills/1password/references/cli-examples.md +29 -0
- package/skills/1password/references/get-started.md +17 -0
- package/skills/agent-browser/SKILL.md +450 -0
- package/skills/agent-browser/docs/browser-use-eval.md +95 -0
- package/skills/agent-browser/docs/first-tests.md +261 -0
- package/skills/agent-browser/docs/wordle-nyt-eval.js +32 -0
- package/skills/aix/SKILL.md +93 -0
- package/skills/aix/docs/embeddings.md +40 -0
- package/skills/aix/docs/setup.md +58 -0
- package/skills/aix/docs/troubleshooting.md +41 -0
- package/skills/aix/references/sql.md +48 -0
- package/skills/apple-notes/SKILL.md +50 -0
- package/skills/apple-reminders/SKILL.md +67 -0
- package/skills/bear-notes/SKILL.md +79 -0
- package/skills/bird/SKILL.md +32 -0
- package/skills/bird/docs/auth.md +31 -0
- package/skills/bird/docs/troubleshooting.md +31 -0
- package/skills/blogwatcher/SKILL.md +46 -0
- package/skills/blucli/SKILL.md +27 -0
- package/skills/brave-search/SKILL.md +36 -0
- package/skills/brave-search/docs/setup.md +40 -0
- package/skills/brave-search/docs/troubleshooting.md +37 -0
- package/skills/brave-search/docs/usage.md +28 -0
- package/skills/brave-search/scripts/content.mjs +53 -0
- package/skills/brave-search/scripts/search.mjs +79 -0
- package/skills/browser-use-agent-sdk/SKILL.md +90 -0
- package/skills/camsnap/SKILL.md +25 -0
- package/skills/clawdhub/SKILL.md +53 -0
- package/skills/coding-agent/SKILL.md +274 -0
- package/skills/comms/SKILL.md +249 -0
- package/skills/comms/docs/adapters.md +54 -0
- package/skills/comms/docs/setup.md +56 -0
- package/skills/comms/docs/troubleshooting.md +44 -0
- package/skills/comms/references/schema.md +49 -0
- package/skills/computer-use/SKILL.md +204 -0
- package/skills/computer-use/docs/open-interpreter.md +26 -0
- package/skills/computer-use/docs/peekaboo.md +26 -0
- package/skills/computer-use/docs/setup.md +47 -0
- package/skills/computer-use/docs/troubleshooting.md +33 -0
- package/skills/discord/SKILL.md +370 -0
- package/skills/eightctl/SKILL.md +29 -0
- package/skills/eve/SKILL.md +215 -0
- package/skills/eve/docs/dual-account.md +84 -0
- package/skills/eve/docs/intelligence.md +58 -0
- package/skills/eve/docs/setup.md +60 -0
- package/skills/eve/docs/troubleshooting.md +54 -0
- package/skills/eve/scripts/setup-dual-account.sh +125 -0
- package/skills/filesystem/SKILL.md +217 -0
- package/skills/food-order/SKILL.md +41 -0
- package/skills/gemini/SKILL.md +23 -0
- package/skills/gh/SKILL.md +22 -0
- package/skills/gh/docs/usage.md +41 -0
- package/skills/gifgrep/SKILL.md +47 -0
- package/skills/github/SKILL.md +26 -0
- package/skills/github/docs/setup.md +21 -0
- package/skills/github/docs/troubleshooting.md +24 -0
- package/skills/gog/SKILL.md +104 -0
- package/skills/gog/docs/portability.md +94 -0
- package/skills/gog/docs/setup.md +76 -0
- package/skills/gog/docs/troubleshooting.md +94 -0
- package/skills/gog/scripts/cdp/README.md +90 -0
- package/skills/gog/scripts/cdp/add_test_users.py +69 -0
- package/skills/gog/scripts/cdp/auth_add_accounts.py +209 -0
- package/skills/gog/scripts/cdp/auth_add_accounts_manual.py +206 -0
- package/skills/gog/scripts/cdp/create_oauth_client.py +165 -0
- package/skills/gog/scripts/cdp/launch_cdp_chrome.sh +58 -0
- package/skills/google-oauth/SKILL.md +94 -0
- package/skills/goplaces/SKILL.md +30 -0
- package/skills/imsg/SKILL.md +25 -0
- package/skills/json-render/SKILL.md +154 -0
- package/skills/json-render/assets/components/README.md +21 -0
- package/skills/json-render/assets/components/catalog.ts +78 -0
- package/skills/json-render/assets/components/registry.tsx +172 -0
- package/skills/json-render/assets/demo/App.css +397 -0
- package/skills/json-render/assets/demo/App.tsx +897 -0
- package/skills/json-render/assets/demo/README.md +22 -0
- package/skills/json-render/assets/demo/catalog.ts +78 -0
- package/skills/json-render/assets/demo/data/nexus-core.json +31 -0
- package/skills/json-render/assets/demo/index.css +27 -0
- package/skills/json-render/assets/demo/registry.tsx +150 -0
- package/skills/json-render/docs/nexus-state-demo.md +84 -0
- package/skills/json-render/docs/shadcn-preset.md +33 -0
- package/skills/json-render/scripts/create-vite-demo.sh +45 -0
- package/skills/json-render/scripts/llm-server/README.md +33 -0
- package/skills/json-render/scripts/llm-server/catalog.ts +78 -0
- package/skills/json-render/scripts/llm-server/package-lock.json +702 -0
- package/skills/json-render/scripts/llm-server/package.json +18 -0
- package/skills/json-render/scripts/llm-server/server.ts +285 -0
- package/skills/local-places/SERVER_README.md +101 -0
- package/skills/local-places/SKILL.md +91 -0
- package/skills/local-places/pyproject.toml +27 -0
- package/skills/local-places/src/local_places/__init__.py +2 -0
- package/skills/local-places/src/local_places/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/local-places/src/local_places/__pycache__/google_places.cpython-314.pyc +0 -0
- package/skills/local-places/src/local_places/__pycache__/main.cpython-314.pyc +0 -0
- package/skills/local-places/src/local_places/__pycache__/schemas.cpython-314.pyc +0 -0
- package/skills/local-places/src/local_places/google_places.py +314 -0
- package/skills/local-places/src/local_places/main.py +65 -0
- package/skills/local-places/src/local_places/schemas.py +107 -0
- package/skills/mcporter/SKILL.md +38 -0
- package/skills/model-usage/SKILL.md +45 -0
- package/skills/model-usage/references/codexbar-cli.md +28 -0
- package/skills/model-usage/scripts/model_usage.py +310 -0
- package/skills/nano-banana-pro/SKILL.md +30 -0
- package/skills/nano-banana-pro/scripts/generate_image.py +169 -0
- package/skills/nano-pdf/SKILL.md +20 -0
- package/skills/nexus-cloud/SKILL.md +53 -0
- package/skills/nexus-cloud/docs/security.md +24 -0
- package/skills/nexus-cloud/docs/setup.md +51 -0
- package/skills/nexus-cloud/docs/troubleshooting.md +28 -0
- package/skills/notion/SKILL.md +156 -0
- package/skills/obsidian/SKILL.md +55 -0
- package/skills/onboarding/SKILL.md +515 -0
- package/skills/onboarding/docs/CAPABILITIES.md +444 -0
- package/skills/onboarding/docs/CAPABILITY_TAXONOMY.md +608 -0
- package/skills/onboarding/docs/CLI_GRAMMAR.md +797 -0
- package/skills/onboarding/docs/CLI_GRAMMAR_CREDENTIALS.md +632 -0
- package/skills/onboarding/docs/CLI_GRAMMAR_ONBOARDING.md +815 -0
- package/skills/onboarding/docs/CLI_GRAMMAR_SKILLS.md +449 -0
- package/skills/onboarding/docs/DOCUMENTATION_OVERVIEW.md +290 -0
- package/skills/onboarding/docs/ENTITY_MODEL.md +582 -0
- package/skills/onboarding/docs/GOAL_STATE_ARCHITECTURE.md +395 -0
- package/skills/onboarding/docs/NEXUS_SYSTEM_OVERVIEW.md +476 -0
- package/skills/onboarding/docs/SKILLS_HUB_SPEC.md +477 -0
- package/skills/onboarding/docs/SKILLS_SPECIFICATION.md +947 -0
- package/skills/onboarding/docs/SKILL_GATEWAY_DESIGN.md +702 -0
- package/skills/onboarding/docs/SKILL_GATEWAY_PRD.md +278 -0
- package/skills/onboarding/docs/SKILL_INVENTORY.md +266 -0
- package/skills/onboarding/docs/STATE_ARCHITECTURE.md +547 -0
- package/skills/onboarding/docs/TROUBLESHOOTING.md +363 -0
- package/skills/onboarding/docs/USER_JOURNEY.md +797 -0
- package/skills/onboarding/docs/WOW_MOMENTS.md +232 -0
- package/skills/onboarding/docs/agent-apple-id.md +289 -0
- package/skills/onboarding/docs/skill-deep-dives/1password.md +367 -0
- package/skills/onboarding/docs/skill-deep-dives/TEMPLATE.md +197 -0
- package/skills/onboarding/docs/skill-deep-dives/aix.md +498 -0
- package/skills/onboarding/docs/skill-deep-dives/bird.md +357 -0
- package/skills/onboarding/docs/skill-deep-dives/brave-search.md +601 -0
- package/skills/onboarding/docs/skill-deep-dives/comms.md +607 -0
- package/skills/onboarding/docs/skill-deep-dives/computer-use.md +599 -0
- package/skills/onboarding/docs/skill-deep-dives/cron-and-heartbeat.md +576 -0
- package/skills/onboarding/docs/skill-deep-dives/eve.md +711 -0
- package/skills/onboarding/docs/skill-deep-dives/github.md +333 -0
- package/skills/onboarding/docs/skill-deep-dives/gog.md +640 -0
- package/skills/onboarding/docs/skill-deep-dives/homebrew-prereqs.md +785 -0
- package/skills/onboarding/docs/skill-deep-dives/nexus-cloud.md +689 -0
- package/skills/onboarding/docs/skill-deep-dives/qmd.md +742 -0
- package/skills/onboarding/docs/skill-deep-dives/telegram.md +379 -0
- package/skills/onboarding/docs/skill-deep-dives/wacli.md +399 -0
- package/skills/onboarding/docs/skill-deep-dives/weather.md +513 -0
- package/skills/onboarding/scripts/ralph/prd.json +215 -0
- package/skills/onboarding/scripts/ralph/progress.txt +99 -0
- package/skills/onboarding/scripts/ralph/prompt.md +87 -0
- package/skills/onboarding/scripts/ralph/ralph.log +84 -0
- package/skills/onboarding/scripts/ralph/ralph.sh +45 -0
- package/skills/onboarding/scripts/setup-cursor-skills.sh +40 -0
- package/skills/openai-image-gen/SKILL.md +31 -0
- package/skills/openai-image-gen/scripts/gen.py +173 -0
- package/skills/openai-whisper/SKILL.md +19 -0
- package/skills/openai-whisper-api/SKILL.md +43 -0
- package/skills/openai-whisper-api/scripts/transcribe.sh +85 -0
- package/skills/openhue/SKILL.md +30 -0
- package/skills/oracle/SKILL.md +105 -0
- package/skills/ordercli/SKILL.md +47 -0
- package/skills/peekaboo/SKILL.md +153 -0
- package/skills/qmd/SKILL.md +32 -0
- package/skills/qmd/docs/mcp.md +30 -0
- package/skills/qmd/docs/ollama.md +42 -0
- package/skills/qmd/docs/setup.md +44 -0
- package/skills/sag/SKILL.md +62 -0
- package/skills/skill-cli-template/SKILL.md +109 -0
- package/skills/skill-creator/scripts/__pycache__/quick_validate.cpython-311.pyc +0 -0
- package/skills/slack/SKILL.md +144 -0
- package/skills/songsee/SKILL.md +29 -0
- package/skills/sonoscli/SKILL.md +26 -0
- package/skills/spotify-player/SKILL.md +34 -0
- package/skills/summarize/SKILL.md +49 -0
- package/skills/telegram/SKILL.md +20 -0
- package/skills/telegram/docs/pairing.md +30 -0
- package/skills/telegram/docs/setup.md +41 -0
- package/skills/telegram/docs/webhook.md +17 -0
- package/skills/things-mac/SKILL.md +61 -0
- package/skills/tmux/SKILL.md +121 -0
- package/skills/tmux/scripts/find-sessions.sh +112 -0
- package/skills/tmux/scripts/wait-for-text.sh +83 -0
- package/skills/trello/SKILL.md +84 -0
- package/skills/upstream-sync/SKILL.md +151 -0
- package/skills/upstream-sync/scripts/auto-port.sh +227 -0
- package/skills/upstream-sync/scripts/check-all.sh +88 -0
- package/skills/upstream-sync/scripts/check-nexus.sh +146 -0
- package/skills/upstream-sync/scripts/check-pi-ai.sh +129 -0
- package/skills/video-frames/SKILL.md +29 -0
- package/skills/video-frames/scripts/frame.sh +81 -0
- package/skills/wacli/SKILL.md +48 -0
- package/skills/wacli/docs/auth.md +21 -0
- package/skills/wacli/docs/backup.md +9 -0
- package/skills/wacli/docs/troubleshooting.md +21 -0
- package/skills/weather/SKILL.md +53 -0
- 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
|
+
});
|