hungry-ghost-hive 0.3.0
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/LICENSE +111 -0
- package/README.md +352 -0
- package/dist/agents/base-agent.d.ts +63 -0
- package/dist/agents/base-agent.d.ts.map +1 -0
- package/dist/agents/base-agent.js +189 -0
- package/dist/agents/base-agent.js.map +1 -0
- package/dist/agents/index.d.ts +7 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +7 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/intermediate.d.ts +15 -0
- package/dist/agents/intermediate.d.ts.map +1 -0
- package/dist/agents/intermediate.js +142 -0
- package/dist/agents/intermediate.js.map +1 -0
- package/dist/agents/junior.d.ts +15 -0
- package/dist/agents/junior.d.ts.map +1 -0
- package/dist/agents/junior.js +147 -0
- package/dist/agents/junior.js.map +1 -0
- package/dist/agents/qa.d.ts +23 -0
- package/dist/agents/qa.d.ts.map +1 -0
- package/dist/agents/qa.js +238 -0
- package/dist/agents/qa.js.map +1 -0
- package/dist/agents/senior.d.ts +18 -0
- package/dist/agents/senior.d.ts.map +1 -0
- package/dist/agents/senior.js +267 -0
- package/dist/agents/senior.js.map +1 -0
- package/dist/agents/tech-lead.d.ts +17 -0
- package/dist/agents/tech-lead.d.ts.map +1 -0
- package/dist/agents/tech-lead.js +274 -0
- package/dist/agents/tech-lead.js.map +1 -0
- package/dist/cli/commands/add-repo.d.ts +3 -0
- package/dist/cli/commands/add-repo.d.ts.map +1 -0
- package/dist/cli/commands/add-repo.js +84 -0
- package/dist/cli/commands/add-repo.js.map +1 -0
- package/dist/cli/commands/agents.d.ts +3 -0
- package/dist/cli/commands/agents.d.ts.map +1 -0
- package/dist/cli/commands/agents.js +214 -0
- package/dist/cli/commands/agents.js.map +1 -0
- package/dist/cli/commands/assign.d.ts +3 -0
- package/dist/cli/commands/assign.d.ts.map +1 -0
- package/dist/cli/commands/assign.js +81 -0
- package/dist/cli/commands/assign.js.map +1 -0
- package/dist/cli/commands/config.d.ts +3 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +118 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/escalations.d.ts +3 -0
- package/dist/cli/commands/escalations.d.ts.map +1 -0
- package/dist/cli/commands/escalations.js +157 -0
- package/dist/cli/commands/escalations.js.map +1 -0
- package/dist/cli/commands/index.d.ts +17 -0
- package/dist/cli/commands/index.d.ts.map +1 -0
- package/dist/cli/commands/index.js +17 -0
- package/dist/cli/commands/index.js.map +1 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +59 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/manager.d.ts +3 -0
- package/dist/cli/commands/manager.d.ts.map +1 -0
- package/dist/cli/commands/manager.js +775 -0
- package/dist/cli/commands/manager.js.map +1 -0
- package/dist/cli/commands/manager.test.d.ts +2 -0
- package/dist/cli/commands/manager.test.d.ts.map +1 -0
- package/dist/cli/commands/manager.test.js +45 -0
- package/dist/cli/commands/manager.test.js.map +1 -0
- package/dist/cli/commands/msg.d.ts +3 -0
- package/dist/cli/commands/msg.d.ts.map +1 -0
- package/dist/cli/commands/msg.js +190 -0
- package/dist/cli/commands/msg.js.map +1 -0
- package/dist/cli/commands/my-stories.d.ts +3 -0
- package/dist/cli/commands/my-stories.d.ts.map +1 -0
- package/dist/cli/commands/my-stories.js +174 -0
- package/dist/cli/commands/my-stories.js.map +1 -0
- package/dist/cli/commands/nuke.d.ts +3 -0
- package/dist/cli/commands/nuke.d.ts.map +1 -0
- package/dist/cli/commands/nuke.js +189 -0
- package/dist/cli/commands/nuke.js.map +1 -0
- package/dist/cli/commands/pr.d.ts +3 -0
- package/dist/cli/commands/pr.d.ts.map +1 -0
- package/dist/cli/commands/pr.js +488 -0
- package/dist/cli/commands/pr.js.map +1 -0
- package/dist/cli/commands/req.d.ts +3 -0
- package/dist/cli/commands/req.d.ts.map +1 -0
- package/dist/cli/commands/req.js +212 -0
- package/dist/cli/commands/req.js.map +1 -0
- package/dist/cli/commands/resume.d.ts +3 -0
- package/dist/cli/commands/resume.d.ts.map +1 -0
- package/dist/cli/commands/resume.js +114 -0
- package/dist/cli/commands/resume.js.map +1 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +259 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/stories.d.ts +3 -0
- package/dist/cli/commands/stories.d.ts.map +1 -0
- package/dist/cli/commands/stories.js +111 -0
- package/dist/cli/commands/stories.js.map +1 -0
- package/dist/cli/commands/teams.d.ts +3 -0
- package/dist/cli/commands/teams.d.ts.map +1 -0
- package/dist/cli/commands/teams.js +137 -0
- package/dist/cli/commands/teams.js.map +1 -0
- package/dist/cli/dashboard/index.d.ts +5 -0
- package/dist/cli/dashboard/index.d.ts.map +1 -0
- package/dist/cli/dashboard/index.js +128 -0
- package/dist/cli/dashboard/index.js.map +1 -0
- package/dist/cli/dashboard/panels/activity.d.ts +5 -0
- package/dist/cli/dashboard/panels/activity.d.ts.map +1 -0
- package/dist/cli/dashboard/panels/activity.js +64 -0
- package/dist/cli/dashboard/panels/activity.js.map +1 -0
- package/dist/cli/dashboard/panels/agents.d.ts +5 -0
- package/dist/cli/dashboard/panels/agents.d.ts.map +1 -0
- package/dist/cli/dashboard/panels/agents.js +196 -0
- package/dist/cli/dashboard/panels/agents.js.map +1 -0
- package/dist/cli/dashboard/panels/escalations.d.ts +5 -0
- package/dist/cli/dashboard/panels/escalations.d.ts.map +1 -0
- package/dist/cli/dashboard/panels/escalations.js +93 -0
- package/dist/cli/dashboard/panels/escalations.js.map +1 -0
- package/dist/cli/dashboard/panels/merge-queue.d.ts +5 -0
- package/dist/cli/dashboard/panels/merge-queue.d.ts.map +1 -0
- package/dist/cli/dashboard/panels/merge-queue.js +57 -0
- package/dist/cli/dashboard/panels/merge-queue.js.map +1 -0
- package/dist/cli/dashboard/panels/pipeline.d.ts +5 -0
- package/dist/cli/dashboard/panels/pipeline.d.ts.map +1 -0
- package/dist/cli/dashboard/panels/pipeline.js +54 -0
- package/dist/cli/dashboard/panels/pipeline.js.map +1 -0
- package/dist/cli/dashboard/panels/stories.d.ts +5 -0
- package/dist/cli/dashboard/panels/stories.d.ts.map +1 -0
- package/dist/cli/dashboard/panels/stories.js +79 -0
- package/dist/cli/dashboard/panels/stories.js.map +1 -0
- package/dist/cli-runtimes/claude.d.ts +8 -0
- package/dist/cli-runtimes/claude.d.ts.map +1 -0
- package/dist/cli-runtimes/claude.js +27 -0
- package/dist/cli-runtimes/claude.js.map +1 -0
- package/dist/cli-runtimes/codex.d.ts +8 -0
- package/dist/cli-runtimes/codex.d.ts.map +1 -0
- package/dist/cli-runtimes/codex.js +27 -0
- package/dist/cli-runtimes/codex.js.map +1 -0
- package/dist/cli-runtimes/gemini.d.ts +8 -0
- package/dist/cli-runtimes/gemini.d.ts.map +1 -0
- package/dist/cli-runtimes/gemini.js +29 -0
- package/dist/cli-runtimes/gemini.js.map +1 -0
- package/dist/cli-runtimes/index.d.ts +25 -0
- package/dist/cli-runtimes/index.d.ts.map +1 -0
- package/dist/cli-runtimes/index.js +48 -0
- package/dist/cli-runtimes/index.js.map +1 -0
- package/dist/cli-runtimes/index.test.d.ts +2 -0
- package/dist/cli-runtimes/index.test.d.ts.map +1 -0
- package/dist/cli-runtimes/index.test.js +216 -0
- package/dist/cli-runtimes/index.test.js.map +1 -0
- package/dist/cli-runtimes/types.d.ts +27 -0
- package/dist/cli-runtimes/types.d.ts.map +1 -0
- package/dist/cli-runtimes/types.js +2 -0
- package/dist/cli-runtimes/types.js.map +1 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +3 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +11 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +72 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +660 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +217 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/schema.test.d.ts +2 -0
- package/dist/config/schema.test.d.ts.map +1 -0
- package/dist/config/schema.test.js +123 -0
- package/dist/config/schema.test.js.map +1 -0
- package/dist/context-files/generator.d.ts +32 -0
- package/dist/context-files/generator.d.ts.map +1 -0
- package/dist/context-files/generator.js +120 -0
- package/dist/context-files/generator.js.map +1 -0
- package/dist/context-files/index.d.ts +38 -0
- package/dist/context-files/index.d.ts.map +1 -0
- package/dist/context-files/index.js +76 -0
- package/dist/context-files/index.js.map +1 -0
- package/dist/context-files/index.test.d.ts +2 -0
- package/dist/context-files/index.test.d.ts.map +1 -0
- package/dist/context-files/index.test.js +265 -0
- package/dist/context-files/index.test.js.map +1 -0
- package/dist/context-files/templates.d.ts +19 -0
- package/dist/context-files/templates.d.ts.map +1 -0
- package/dist/context-files/templates.js +266 -0
- package/dist/context-files/templates.js.map +1 -0
- package/dist/db/client.d.ts +95 -0
- package/dist/db/client.d.ts.map +1 -0
- package/dist/db/client.js +343 -0
- package/dist/db/client.js.map +1 -0
- package/dist/db/lock.d.ts +25 -0
- package/dist/db/lock.d.ts.map +1 -0
- package/dist/db/lock.js +56 -0
- package/dist/db/lock.js.map +1 -0
- package/dist/db/lock.test.d.ts +2 -0
- package/dist/db/lock.test.d.ts.map +1 -0
- package/dist/db/lock.test.js +73 -0
- package/dist/db/lock.test.js.map +1 -0
- package/dist/db/queries/agents.d.ts +31 -0
- package/dist/db/queries/agents.d.ts.map +1 -0
- package/dist/db/queries/agents.js +76 -0
- package/dist/db/queries/agents.js.map +1 -0
- package/dist/db/queries/escalations.d.ts +29 -0
- package/dist/db/queries/escalations.d.ts.map +1 -0
- package/dist/db/queries/escalations.js +105 -0
- package/dist/db/queries/escalations.js.map +1 -0
- package/dist/db/queries/heartbeat.d.ts +20 -0
- package/dist/db/queries/heartbeat.d.ts.map +1 -0
- package/dist/db/queries/heartbeat.js +61 -0
- package/dist/db/queries/heartbeat.js.map +1 -0
- package/dist/db/queries/index.d.ts +8 -0
- package/dist/db/queries/index.d.ts.map +1 -0
- package/dist/db/queries/index.js +8 -0
- package/dist/db/queries/index.js.map +1 -0
- package/dist/db/queries/logs.d.ts +21 -0
- package/dist/db/queries/logs.d.ts.map +1 -0
- package/dist/db/queries/logs.js +72 -0
- package/dist/db/queries/logs.js.map +1 -0
- package/dist/db/queries/messages.d.ts +17 -0
- package/dist/db/queries/messages.d.ts.map +1 -0
- package/dist/db/queries/messages.js +22 -0
- package/dist/db/queries/messages.js.map +1 -0
- package/dist/db/queries/pull-requests.d.ts +33 -0
- package/dist/db/queries/pull-requests.d.ts.map +1 -0
- package/dist/db/queries/pull-requests.js +130 -0
- package/dist/db/queries/pull-requests.js.map +1 -0
- package/dist/db/queries/requirements.d.ts +22 -0
- package/dist/db/queries/requirements.d.ts.map +1 -0
- package/dist/db/queries/requirements.js +53 -0
- package/dist/db/queries/requirements.js.map +1 -0
- package/dist/db/queries/stories.d.ts +42 -0
- package/dist/db/queries/stories.d.ts.map +1 -0
- package/dist/db/queries/stories.js +163 -0
- package/dist/db/queries/stories.js.map +1 -0
- package/dist/db/queries/teams.d.ts +14 -0
- package/dist/db/queries/teams.d.ts.map +1 -0
- package/dist/db/queries/teams.js +24 -0
- package/dist/db/queries/teams.js.map +1 -0
- package/dist/git/branches.d.ts +52 -0
- package/dist/git/branches.d.ts.map +1 -0
- package/dist/git/branches.js +133 -0
- package/dist/git/branches.js.map +1 -0
- package/dist/git/github.d.ts +75 -0
- package/dist/git/github.d.ts.map +1 -0
- package/dist/git/github.js +162 -0
- package/dist/git/github.js.map +1 -0
- package/dist/git/index.d.ts +4 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/index.js +4 -0
- package/dist/git/index.js.map +1 -0
- package/dist/git/submodules.d.ts +47 -0
- package/dist/git/submodules.d.ts.map +1 -0
- package/dist/git/submodules.js +115 -0
- package/dist/git/submodules.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/anthropic.d.ts +18 -0
- package/dist/llm/anthropic.d.ts.map +1 -0
- package/dist/llm/anthropic.js +111 -0
- package/dist/llm/anthropic.js.map +1 -0
- package/dist/llm/index.d.ts +6 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +24 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/llm/openai.d.ts +18 -0
- package/dist/llm/openai.d.ts.map +1 -0
- package/dist/llm/openai.js +103 -0
- package/dist/llm/openai.js.map +1 -0
- package/dist/llm/provider.d.ts +38 -0
- package/dist/llm/provider.d.ts.map +1 -0
- package/dist/llm/provider.js +17 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/orchestrator/index.d.ts +4 -0
- package/dist/orchestrator/index.d.ts.map +1 -0
- package/dist/orchestrator/index.js +4 -0
- package/dist/orchestrator/index.js.map +1 -0
- package/dist/orchestrator/scaler.d.ts +42 -0
- package/dist/orchestrator/scaler.d.ts.map +1 -0
- package/dist/orchestrator/scaler.js +154 -0
- package/dist/orchestrator/scaler.js.map +1 -0
- package/dist/orchestrator/scheduler.d.ts +90 -0
- package/dist/orchestrator/scheduler.d.ts.map +1 -0
- package/dist/orchestrator/scheduler.js +1003 -0
- package/dist/orchestrator/scheduler.js.map +1 -0
- package/dist/orchestrator/scheduler.test.d.ts +2 -0
- package/dist/orchestrator/scheduler.test.d.ts.map +1 -0
- package/dist/orchestrator/scheduler.test.js +242 -0
- package/dist/orchestrator/scheduler.test.js.map +1 -0
- package/dist/orchestrator/workflow.d.ts +18 -0
- package/dist/orchestrator/workflow.d.ts.map +1 -0
- package/dist/orchestrator/workflow.js +106 -0
- package/dist/orchestrator/workflow.js.map +1 -0
- package/dist/state-detectors/claude.d.ts +33 -0
- package/dist/state-detectors/claude.d.ts.map +1 -0
- package/dist/state-detectors/claude.js +237 -0
- package/dist/state-detectors/claude.js.map +1 -0
- package/dist/state-detectors/claude.test.d.ts +2 -0
- package/dist/state-detectors/claude.test.d.ts.map +1 -0
- package/dist/state-detectors/claude.test.js +127 -0
- package/dist/state-detectors/claude.test.js.map +1 -0
- package/dist/state-detectors/codex.d.ts +34 -0
- package/dist/state-detectors/codex.d.ts.map +1 -0
- package/dist/state-detectors/codex.js +233 -0
- package/dist/state-detectors/codex.js.map +1 -0
- package/dist/state-detectors/codex.test.d.ts +2 -0
- package/dist/state-detectors/codex.test.d.ts.map +1 -0
- package/dist/state-detectors/codex.test.js +85 -0
- package/dist/state-detectors/codex.test.js.map +1 -0
- package/dist/state-detectors/factory.d.ts +22 -0
- package/dist/state-detectors/factory.d.ts.map +1 -0
- package/dist/state-detectors/factory.js +37 -0
- package/dist/state-detectors/factory.js.map +1 -0
- package/dist/state-detectors/factory.test.d.ts +2 -0
- package/dist/state-detectors/factory.test.d.ts.map +1 -0
- package/dist/state-detectors/factory.test.js +44 -0
- package/dist/state-detectors/factory.test.js.map +1 -0
- package/dist/state-detectors/gemini.d.ts +34 -0
- package/dist/state-detectors/gemini.d.ts.map +1 -0
- package/dist/state-detectors/gemini.js +236 -0
- package/dist/state-detectors/gemini.js.map +1 -0
- package/dist/state-detectors/gemini.test.d.ts +2 -0
- package/dist/state-detectors/gemini.test.d.ts.map +1 -0
- package/dist/state-detectors/gemini.test.js +93 -0
- package/dist/state-detectors/gemini.test.js.map +1 -0
- package/dist/state-detectors/index.d.ts +20 -0
- package/dist/state-detectors/index.d.ts.map +1 -0
- package/dist/state-detectors/index.js +21 -0
- package/dist/state-detectors/index.js.map +1 -0
- package/dist/state-detectors/types.d.ts +67 -0
- package/dist/state-detectors/types.d.ts.map +1 -0
- package/dist/state-detectors/types.js +28 -0
- package/dist/state-detectors/types.js.map +1 -0
- package/dist/tmux/index.d.ts +2 -0
- package/dist/tmux/index.d.ts.map +1 -0
- package/dist/tmux/index.js +2 -0
- package/dist/tmux/index.js.map +1 -0
- package/dist/tmux/manager.d.ts +45 -0
- package/dist/tmux/manager.d.ts.map +1 -0
- package/dist/tmux/manager.js +252 -0
- package/dist/tmux/manager.js.map +1 -0
- package/dist/utils/claude-code-state.d.ts +46 -0
- package/dist/utils/claude-code-state.d.ts.map +1 -0
- package/dist/utils/claude-code-state.js +252 -0
- package/dist/utils/claude-code-state.js.map +1 -0
- package/dist/utils/cli-builder.d.ts +19 -0
- package/dist/utils/cli-builder.d.ts.map +1 -0
- package/dist/utils/cli-builder.js +58 -0
- package/dist/utils/cli-builder.js.map +1 -0
- package/dist/utils/cli-commands.d.ts +27 -0
- package/dist/utils/cli-commands.d.ts.map +1 -0
- package/dist/utils/cli-commands.js +69 -0
- package/dist/utils/cli-commands.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +13 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +77 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/paths.d.ts +17 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +33 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/timeout.d.ts +25 -0
- package/dist/utils/timeout.d.ts.map +1 -0
- package/dist/utils/timeout.js +57 -0
- package/dist/utils/timeout.js.map +1 -0
- package/package.json +78 -0
- package/src/agents/base-agent.ts +255 -0
- package/src/agents/index.ts +6 -0
- package/src/agents/intermediate.ts +161 -0
- package/src/agents/junior.ts +166 -0
- package/src/agents/qa.ts +272 -0
- package/src/agents/senior.ts +307 -0
- package/src/agents/tech-lead.ts +324 -0
- package/src/cli/commands/add-repo.ts +89 -0
- package/src/cli/commands/agents.ts +247 -0
- package/src/cli/commands/assign.ts +86 -0
- package/src/cli/commands/config.ts +121 -0
- package/src/cli/commands/escalations.ts +179 -0
- package/src/cli/commands/index.ts +16 -0
- package/src/cli/commands/init.ts +66 -0
- package/src/cli/commands/manager.test.ts +52 -0
- package/src/cli/commands/manager.ts +916 -0
- package/src/cli/commands/msg.ts +232 -0
- package/src/cli/commands/my-stories.ts +198 -0
- package/src/cli/commands/nuke.ts +223 -0
- package/src/cli/commands/pr.ts +559 -0
- package/src/cli/commands/req.ts +231 -0
- package/src/cli/commands/resume.ts +129 -0
- package/src/cli/commands/status.ts +284 -0
- package/src/cli/commands/stories.ts +131 -0
- package/src/cli/commands/teams.ts +158 -0
- package/src/cli/dashboard/index.ts +141 -0
- package/src/cli/dashboard/panels/activity.ts +77 -0
- package/src/cli/dashboard/panels/agents.ts +244 -0
- package/src/cli/dashboard/panels/escalations.ts +109 -0
- package/src/cli/dashboard/panels/merge-queue.ts +65 -0
- package/src/cli/dashboard/panels/pipeline.ts +65 -0
- package/src/cli/dashboard/panels/stories.ts +87 -0
- package/src/cli-runtimes/claude.ts +31 -0
- package/src/cli-runtimes/codex.ts +31 -0
- package/src/cli-runtimes/gemini.ts +33 -0
- package/src/cli-runtimes/index.test.ts +261 -0
- package/src/cli-runtimes/index.ts +52 -0
- package/src/cli-runtimes/types.ts +30 -0
- package/src/config/index.ts +2 -0
- package/src/config/loader.ts +89 -0
- package/src/config/schema.test.ts +135 -0
- package/src/config/schema.ts +238 -0
- package/src/context-files/generator.ts +132 -0
- package/src/context-files/index.test.ts +323 -0
- package/src/context-files/index.ts +102 -0
- package/src/context-files/templates.ts +279 -0
- package/src/db/client.ts +475 -0
- package/src/db/lock.test.ts +93 -0
- package/src/db/lock.ts +74 -0
- package/src/db/migrations/001-initial.sql +121 -0
- package/src/db/migrations/005-add-agent-heartbeat.sql +4 -0
- package/src/db/queries/agents.ts +113 -0
- package/src/db/queries/escalations.ts +140 -0
- package/src/db/queries/heartbeat.ts +92 -0
- package/src/db/queries/index.ts +7 -0
- package/src/db/queries/logs.ts +136 -0
- package/src/db/queries/messages.ts +38 -0
- package/src/db/queries/pull-requests.ts +170 -0
- package/src/db/queries/requirements.ts +81 -0
- package/src/db/queries/stories.ts +223 -0
- package/src/db/queries/teams.ts +39 -0
- package/src/git/branches.ts +186 -0
- package/src/git/github.ts +247 -0
- package/src/git/index.ts +3 -0
- package/src/git/submodules.ts +141 -0
- package/src/index.ts +93 -0
- package/src/llm/anthropic.ts +134 -0
- package/src/llm/index.ts +26 -0
- package/src/llm/openai.ts +125 -0
- package/src/llm/provider.ts +60 -0
- package/src/orchestrator/index.ts +3 -0
- package/src/orchestrator/scaler.ts +201 -0
- package/src/orchestrator/scheduler.test.ts +288 -0
- package/src/orchestrator/scheduler.ts +1130 -0
- package/src/orchestrator/workflow.ts +137 -0
- package/src/state-detectors/claude.test.ts +149 -0
- package/src/state-detectors/claude.ts +256 -0
- package/src/state-detectors/codex.test.ts +100 -0
- package/src/state-detectors/codex.ts +252 -0
- package/src/state-detectors/factory.test.ts +51 -0
- package/src/state-detectors/factory.ts +40 -0
- package/src/state-detectors/gemini.test.ts +110 -0
- package/src/state-detectors/gemini.ts +255 -0
- package/src/state-detectors/index.ts +25 -0
- package/src/state-detectors/types.ts +80 -0
- package/src/tmux/index.ts +1 -0
- package/src/tmux/manager.ts +310 -0
- package/src/types/sql.js.d.ts +34 -0
- package/src/utils/claude-code-state.ts +281 -0
- package/src/utils/cli-builder.ts +78 -0
- package/src/utils/cli-commands.ts +84 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/logger.ts +93 -0
- package/src/utils/paths.ts +49 -0
- package/src/utils/timeout.ts +84 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { findHiveRoot, getHivePaths } from '../../utils/paths.js';
|
|
4
|
+
import { getDatabase } from '../../db/client.js';
|
|
5
|
+
import { loadConfig } from '../../config/loader.js';
|
|
6
|
+
import { Scheduler } from '../../orchestrator/scheduler.js';
|
|
7
|
+
import { getHiveSessions, sendToTmuxSession, sendEnterToTmuxSession, captureTmuxPane, isManagerRunning, stopManager as stopManagerSession, killTmuxSession } from '../../tmux/manager.js';
|
|
8
|
+
import { getMergeQueue, getPullRequestsByStatus, getApprovedPullRequests, updatePullRequest } from '../../db/queries/pull-requests.js';
|
|
9
|
+
import { getUnreadMessages, markMessageRead, type MessageRow } from '../../db/queries/messages.js';
|
|
10
|
+
import { createEscalation, getPendingEscalations } from '../../db/queries/escalations.js';
|
|
11
|
+
import { getAgentById, updateAgent } from '../../db/queries/agents.js';
|
|
12
|
+
import { createLog } from '../../db/queries/logs.js';
|
|
13
|
+
import { queryAll } from '../../db/client.js';
|
|
14
|
+
import type { StoryRow } from '../../db/client.js';
|
|
15
|
+
import { getAllTeams } from '../../db/queries/teams.js';
|
|
16
|
+
import { updateStory, getStoriesByStatus } from '../../db/queries/stories.js';
|
|
17
|
+
import { execa } from 'execa';
|
|
18
|
+
import { createPullRequest, type PullRequestRow } from '../../db/queries/pull-requests.js';
|
|
19
|
+
import { acquireLock } from '../../db/lock.js';
|
|
20
|
+
import { join } from 'path';
|
|
21
|
+
import { detectClaudeCodeState, getStateDescription, ClaudeCodeState } from '../../utils/claude-code-state.js';
|
|
22
|
+
import { getAvailableCommands, buildAutoRecoveryReminder, type CLITool } from '../../utils/cli-commands.js';
|
|
23
|
+
|
|
24
|
+
// Agent state tracking for nudge logic
|
|
25
|
+
interface AgentStateTracking {
|
|
26
|
+
lastState: ClaudeCodeState;
|
|
27
|
+
lastStateChangeTime: number;
|
|
28
|
+
lastNudgeTime: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// In-memory state tracking per agent session
|
|
32
|
+
const agentStates = new Map<string, AgentStateTracking>();
|
|
33
|
+
|
|
34
|
+
// Constants for nudge behavior
|
|
35
|
+
const STATE_STUCK_THRESHOLD_MS = 120000; // 120 seconds = 2 minutes
|
|
36
|
+
const NUDGE_COOLDOWN_MS = 300000; // 300 seconds = 5 minutes
|
|
37
|
+
|
|
38
|
+
export const managerCommand = new Command('manager')
|
|
39
|
+
.description('Micromanager daemon that keeps agents productive');
|
|
40
|
+
|
|
41
|
+
// Start the manager daemon
|
|
42
|
+
managerCommand
|
|
43
|
+
.command('start')
|
|
44
|
+
.description('Start the manager daemon (runs every 60s)')
|
|
45
|
+
.option('-i, --interval <seconds>', 'Check interval in seconds', '60')
|
|
46
|
+
.option('--once', 'Run once and exit')
|
|
47
|
+
.action(async (options: { interval: string; once?: boolean }) => {
|
|
48
|
+
const root = findHiveRoot();
|
|
49
|
+
if (!root) {
|
|
50
|
+
console.error(chalk.red('Not in a Hive workspace.'));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const paths = getHivePaths(root);
|
|
55
|
+
const lockPath = join(paths.hiveDir, 'manager.lock');
|
|
56
|
+
|
|
57
|
+
// Acquire manager lock to ensure singleton
|
|
58
|
+
let releaseLock: (() => Promise<void>) | null = null;
|
|
59
|
+
try {
|
|
60
|
+
releaseLock = await acquireLock(lockPath, { stale: 120000 }); // 2 min stale threshold
|
|
61
|
+
console.log(chalk.gray('Manager lock acquired'));
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error(chalk.red('Failed to acquire manager lock - another manager instance may be running.'), err);
|
|
64
|
+
console.error(chalk.gray('If you are sure no other manager is running, remove:'), lockPath + '.lock');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Release lock on exit
|
|
69
|
+
const cleanup = async () => {
|
|
70
|
+
if (releaseLock) {
|
|
71
|
+
await releaseLock();
|
|
72
|
+
console.log(chalk.gray('\nManager lock released'));
|
|
73
|
+
}
|
|
74
|
+
process.exit(0);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
process.on('SIGINT', cleanup);
|
|
78
|
+
process.on('SIGTERM', cleanup);
|
|
79
|
+
|
|
80
|
+
// Load config to get polling intervals (for two-tier polling)
|
|
81
|
+
const hivePaths = getHivePaths(root);
|
|
82
|
+
const config = loadConfig(hivePaths.hiveDir);
|
|
83
|
+
|
|
84
|
+
// Support two modes: legacy single-interval and new two-tier polling
|
|
85
|
+
const useTwoTier = options.interval === '60' && config.manager;
|
|
86
|
+
|
|
87
|
+
if (useTwoTier) {
|
|
88
|
+
// Two-tier polling - use slow interval (60s) by default to reduce interruptions
|
|
89
|
+
const slowInterval = config.manager.slow_poll_interval;
|
|
90
|
+
console.log(chalk.cyan(`Manager started (polling every ${slowInterval / 1000}s)`));
|
|
91
|
+
console.log(chalk.gray('Press Ctrl+C to stop\n'));
|
|
92
|
+
|
|
93
|
+
const runCheck = async () => {
|
|
94
|
+
try {
|
|
95
|
+
await managerCheck(root);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error(chalk.red('Manager error:'), err);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
await runCheck();
|
|
102
|
+
|
|
103
|
+
if (!options.once) {
|
|
104
|
+
setInterval(runCheck, slowInterval);
|
|
105
|
+
} else if (releaseLock) {
|
|
106
|
+
await releaseLock();
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
// Legacy mode: single interval
|
|
110
|
+
const interval = parseInt(options.interval, 10) * 1000;
|
|
111
|
+
console.log(chalk.cyan(`Manager started (checking every ${options.interval}s)`));
|
|
112
|
+
console.log(chalk.gray('Press Ctrl+C to stop\n'));
|
|
113
|
+
|
|
114
|
+
const runCheck = async () => {
|
|
115
|
+
try {
|
|
116
|
+
await managerCheck(root);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error(chalk.red('Manager error:'), err);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
await runCheck();
|
|
123
|
+
|
|
124
|
+
if (!options.once) {
|
|
125
|
+
setInterval(runCheck, interval);
|
|
126
|
+
} else if (releaseLock) {
|
|
127
|
+
await releaseLock();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Run a single check
|
|
133
|
+
managerCommand
|
|
134
|
+
.command('check')
|
|
135
|
+
.description('Run a single manager check')
|
|
136
|
+
.action(async () => {
|
|
137
|
+
const root = findHiveRoot();
|
|
138
|
+
if (!root) {
|
|
139
|
+
console.error(chalk.red('Not in a Hive workspace.'));
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await managerCheck(root);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Run health check to sync agents with tmux
|
|
147
|
+
managerCommand
|
|
148
|
+
.command('health')
|
|
149
|
+
.description('Sync agent status with actual tmux sessions')
|
|
150
|
+
.action(async () => {
|
|
151
|
+
const root = findHiveRoot();
|
|
152
|
+
if (!root) {
|
|
153
|
+
console.error(chalk.red('Not in a Hive workspace.'));
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const paths = getHivePaths(root);
|
|
158
|
+
const db = await getDatabase(paths.hiveDir);
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const config = loadConfig(paths.hiveDir);
|
|
162
|
+
const scheduler = new Scheduler(db.db, {
|
|
163
|
+
scaling: config.scaling,
|
|
164
|
+
models: config.models,
|
|
165
|
+
rootDir: root,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
console.log(chalk.cyan('Running health check...'));
|
|
169
|
+
const result = await scheduler.healthCheck();
|
|
170
|
+
db.save();
|
|
171
|
+
|
|
172
|
+
if (result.terminated === 0) {
|
|
173
|
+
console.log(chalk.green('All agents healthy - tmux sessions match database'));
|
|
174
|
+
} else {
|
|
175
|
+
console.log(chalk.yellow(`Cleaned up ${result.terminated} dead agent(s)`));
|
|
176
|
+
if (result.revived.length > 0) {
|
|
177
|
+
console.log(chalk.yellow(`Stories returned to queue: ${result.revived.join(', ')}`));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Also check merge queue
|
|
182
|
+
console.log(chalk.cyan('Checking merge queue...'));
|
|
183
|
+
await scheduler.checkMergeQueue();
|
|
184
|
+
db.save();
|
|
185
|
+
console.log(chalk.green('Done'));
|
|
186
|
+
} finally {
|
|
187
|
+
db.close();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Check manager status
|
|
192
|
+
managerCommand
|
|
193
|
+
.command('status')
|
|
194
|
+
.description('Check if the manager daemon is running')
|
|
195
|
+
.action(async () => {
|
|
196
|
+
const running = await isManagerRunning();
|
|
197
|
+
if (running) {
|
|
198
|
+
console.log(chalk.green('Manager daemon is running (hive-manager tmux session)'));
|
|
199
|
+
console.log(chalk.gray('To view: tmux attach -t hive-manager'));
|
|
200
|
+
console.log(chalk.gray('To stop: hive manager stop'));
|
|
201
|
+
} else {
|
|
202
|
+
console.log(chalk.yellow('Manager daemon is not running'));
|
|
203
|
+
console.log(chalk.gray('To start: hive manager start'));
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Stop the manager daemon
|
|
208
|
+
managerCommand
|
|
209
|
+
.command('stop')
|
|
210
|
+
.description('Stop the manager daemon')
|
|
211
|
+
.action(async () => {
|
|
212
|
+
const stopped = await stopManagerSession();
|
|
213
|
+
if (stopped) {
|
|
214
|
+
console.log(chalk.green('Manager daemon stopped'));
|
|
215
|
+
} else {
|
|
216
|
+
console.log(chalk.yellow('Manager daemon was not running'));
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Nudge a specific agent
|
|
221
|
+
managerCommand
|
|
222
|
+
.command('nudge <session>')
|
|
223
|
+
.description('Nudge an agent to check for work')
|
|
224
|
+
.option('-m, --message <msg>', 'Custom message to send')
|
|
225
|
+
.action(async (session: string, options: { message?: string }) => {
|
|
226
|
+
const root = findHiveRoot();
|
|
227
|
+
if (!root) {
|
|
228
|
+
console.error(chalk.red('Not in a Hive workspace.'));
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const paths = getHivePaths(root);
|
|
233
|
+
const db = await getDatabase(paths.hiveDir);
|
|
234
|
+
try {
|
|
235
|
+
const agent = getAgentById(db.db, session.replace('hive-', ''));
|
|
236
|
+
const cliTool = (agent?.cli_tool || 'claude') as CLITool;
|
|
237
|
+
await nudgeAgent(root, session, options.message, undefined, undefined, cliTool);
|
|
238
|
+
console.log(chalk.green(`Nudged ${session}`));
|
|
239
|
+
} finally {
|
|
240
|
+
db.close();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
async function managerCheck(root: string): Promise<void> {
|
|
245
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
246
|
+
console.log(chalk.gray(`[${timestamp}] Manager checking...`));
|
|
247
|
+
|
|
248
|
+
const paths = getHivePaths(root);
|
|
249
|
+
const db = await getDatabase(paths.hiveDir);
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
// First, run health check to sync agent status with tmux
|
|
253
|
+
const config = loadConfig(paths.hiveDir);
|
|
254
|
+
const scheduler = new Scheduler(db.db, {
|
|
255
|
+
scaling: config.scaling,
|
|
256
|
+
models: config.models,
|
|
257
|
+
rootDir: root,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const healthResult = await scheduler.healthCheck();
|
|
261
|
+
if (healthResult.terminated > 0) {
|
|
262
|
+
console.log(chalk.yellow(` Health check: ${healthResult.terminated} dead agent(s) cleaned up`));
|
|
263
|
+
if (healthResult.revived.length > 0) {
|
|
264
|
+
console.log(chalk.yellow(` Stories returned to queue: ${healthResult.revived.join(', ')}`));
|
|
265
|
+
}
|
|
266
|
+
db.save();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Check merge queue for QA spawning
|
|
270
|
+
await scheduler.checkMergeQueue();
|
|
271
|
+
db.save();
|
|
272
|
+
|
|
273
|
+
// Auto-merge approved PRs
|
|
274
|
+
const autoMerged = await autoMergeApprovedPRs(root, db);
|
|
275
|
+
if (autoMerged > 0) {
|
|
276
|
+
console.log(chalk.green(` Auto-merged ${autoMerged} approved PR(s)`));
|
|
277
|
+
db.save();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Sync merged PRs from GitHub to keep story statuses in sync
|
|
281
|
+
const mergedSynced = await syncMergedPRsFromGitHub(root, db);
|
|
282
|
+
if (mergedSynced > 0) {
|
|
283
|
+
console.log(chalk.green(` Synced ${mergedSynced} merged story(ies) from GitHub`));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Sync GitHub PRs that might not be in queue
|
|
287
|
+
const syncedPRs = await syncGitHubPRs(root, db, paths.hiveDir);
|
|
288
|
+
if (syncedPRs > 0) {
|
|
289
|
+
console.log(chalk.yellow(` Synced ${syncedPRs} GitHub PR(s) into merge queue`));
|
|
290
|
+
// Recheck merge queue after syncing
|
|
291
|
+
await scheduler.checkMergeQueue();
|
|
292
|
+
db.save();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const sessions = await getHiveSessions();
|
|
296
|
+
const hiveSessions = sessions.filter(s =>
|
|
297
|
+
s.name.startsWith('hive-')
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
if (hiveSessions.length === 0) {
|
|
301
|
+
console.log(chalk.gray(' No agent sessions found'));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let nudged = 0;
|
|
306
|
+
let messagesForwarded = 0;
|
|
307
|
+
let escalationsCreated = 0;
|
|
308
|
+
|
|
309
|
+
// Get existing pending escalations to avoid duplicates
|
|
310
|
+
const existingEscalations = getPendingEscalations(db.db);
|
|
311
|
+
const escalatedSessions = new Set(
|
|
312
|
+
existingEscalations
|
|
313
|
+
.filter(e => e.from_agent_id)
|
|
314
|
+
.map(e => e.from_agent_id)
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
for (const session of hiveSessions) {
|
|
318
|
+
// Skip manager itself
|
|
319
|
+
if (session.name === 'hive-manager') continue;
|
|
320
|
+
|
|
321
|
+
// Get agent information for CLI-aware messaging
|
|
322
|
+
const agent = getAgentById(db.db, session.name.replace('hive-', ''));
|
|
323
|
+
const agentCliTool = (agent?.cli_tool || 'claude') as CLITool;
|
|
324
|
+
|
|
325
|
+
// Check if agent has unread messages
|
|
326
|
+
const unread = getUnreadMessages(db.db, session.name);
|
|
327
|
+
if (unread.length > 0) {
|
|
328
|
+
await forwardMessages(session.name, unread, agentCliTool);
|
|
329
|
+
messagesForwarded += unread.length;
|
|
330
|
+
// Mark as read
|
|
331
|
+
for (const msg of unread) {
|
|
332
|
+
markMessageRead(db.db, msg.id);
|
|
333
|
+
}
|
|
334
|
+
db.save();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Check if agent appears stuck (capture last output)
|
|
338
|
+
const output = await captureTmuxPane(session.name, 50);
|
|
339
|
+
const stateResult = detectClaudeCodeState(output);
|
|
340
|
+
|
|
341
|
+
// Track state changes for this agent
|
|
342
|
+
const now = Date.now();
|
|
343
|
+
const trackedState = agentStates.get(session.name);
|
|
344
|
+
|
|
345
|
+
if (!trackedState) {
|
|
346
|
+
// First time seeing this agent - initialize tracking
|
|
347
|
+
agentStates.set(session.name, {
|
|
348
|
+
lastState: stateResult.state,
|
|
349
|
+
lastStateChangeTime: now,
|
|
350
|
+
lastNudgeTime: 0,
|
|
351
|
+
});
|
|
352
|
+
} else if (trackedState.lastState !== stateResult.state) {
|
|
353
|
+
// State changed - update tracking
|
|
354
|
+
trackedState.lastState = stateResult.state;
|
|
355
|
+
trackedState.lastStateChangeTime = now;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Convert to legacy format for escalation logic
|
|
359
|
+
const waitingInfo = {
|
|
360
|
+
isWaiting: stateResult.isWaiting,
|
|
361
|
+
needsHuman: stateResult.needsHuman,
|
|
362
|
+
reason: stateResult.needsHuman ? getStateDescription(stateResult.state) : undefined,
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
if (waitingInfo.needsHuman && !escalatedSessions.has(session.name)) {
|
|
366
|
+
// Create escalation for human attention
|
|
367
|
+
const storyId = agent?.current_story_id || null;
|
|
368
|
+
|
|
369
|
+
createEscalation(db.db, {
|
|
370
|
+
storyId,
|
|
371
|
+
fromAgentId: session.name,
|
|
372
|
+
toAgentId: null, // Escalate to human
|
|
373
|
+
reason: `Agent waiting for input: ${waitingInfo.reason || 'Unknown question'}`,
|
|
374
|
+
});
|
|
375
|
+
db.save();
|
|
376
|
+
escalationsCreated++;
|
|
377
|
+
escalatedSessions.add(session.name);
|
|
378
|
+
|
|
379
|
+
// Also remind the agent to continue autonomously if they can
|
|
380
|
+
const reminder = buildAutoRecoveryReminder(session.name, agentCliTool);
|
|
381
|
+
await sendToTmuxSession(session.name, reminder);
|
|
382
|
+
|
|
383
|
+
console.log(chalk.red(` ESCALATION: ${session.name} needs human input`));
|
|
384
|
+
} else if (waitingInfo.isWaiting && stateResult.state !== ClaudeCodeState.THINKING) {
|
|
385
|
+
// Agent is idle/waiting but doesn't need human
|
|
386
|
+
// Check if we should nudge based on state duration and cooldown
|
|
387
|
+
const currentTrackedState = agentStates.get(session.name);
|
|
388
|
+
if (currentTrackedState) {
|
|
389
|
+
const timeSinceStateChange = now - currentTrackedState.lastStateChangeTime;
|
|
390
|
+
const timeSinceLastNudge = now - currentTrackedState.lastNudgeTime;
|
|
391
|
+
|
|
392
|
+
// Only nudge if:
|
|
393
|
+
// 1. Agent stuck in same state for > 120 seconds
|
|
394
|
+
// 2. Haven't nudged in last 5 minutes
|
|
395
|
+
// 3. Not in THINKING state (extended thinking is normal)
|
|
396
|
+
if (
|
|
397
|
+
timeSinceStateChange > STATE_STUCK_THRESHOLD_MS &&
|
|
398
|
+
timeSinceLastNudge > NUDGE_COOLDOWN_MS
|
|
399
|
+
) {
|
|
400
|
+
// Re-check state immediately before nudging to avoid interrupting active work
|
|
401
|
+
const recheckOutput = await captureTmuxPane(session.name, 50);
|
|
402
|
+
const recheckState = detectClaudeCodeState(recheckOutput);
|
|
403
|
+
|
|
404
|
+
// Only proceed if still in a waiting state and not THINKING
|
|
405
|
+
if (recheckState.isWaiting && !recheckState.needsHuman && recheckState.state !== ClaudeCodeState.THINKING) {
|
|
406
|
+
const agentType = getAgentType(session.name);
|
|
407
|
+
await nudgeAgent(root, session.name, undefined, agentType, waitingInfo.reason, agentCliTool);
|
|
408
|
+
currentTrackedState.lastNudgeTime = now;
|
|
409
|
+
nudged++;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// If not waiting (actively working), do nothing - let them work
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Check for PRs needing QA attention
|
|
418
|
+
const queuedPRs = getMergeQueue(db.db);
|
|
419
|
+
if (queuedPRs.length > 0) {
|
|
420
|
+
const qaSessions = hiveSessions.filter(s => s.name.includes('-qa-'));
|
|
421
|
+
for (const qa of qaSessions) {
|
|
422
|
+
await sendToTmuxSession(qa.name,
|
|
423
|
+
`# ${queuedPRs.length} PR(s) waiting in queue. Run: hive pr queue`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Check for rejected PRs that need developer attention
|
|
429
|
+
// Only notify once by updating status to 'closed' after notification (prevents spam)
|
|
430
|
+
const rejectedPRs = getPullRequestsByStatus(db.db, 'rejected');
|
|
431
|
+
let rejectionNotified = 0;
|
|
432
|
+
for (const pr of rejectedPRs) {
|
|
433
|
+
// Update the story status to qa_failed so developer knows they need to act
|
|
434
|
+
if (pr.story_id) {
|
|
435
|
+
updateStory(db.db, pr.story_id, { status: 'qa_failed' });
|
|
436
|
+
createLog(db.db, {
|
|
437
|
+
agentId: 'manager',
|
|
438
|
+
eventType: 'STORY_QA_FAILED',
|
|
439
|
+
message: `Story ${pr.story_id} QA failed: ${pr.review_notes || 'See review comments'}`,
|
|
440
|
+
storyId: pr.story_id,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (pr.submitted_by) {
|
|
445
|
+
const devSession = hiveSessions.find(s => s.name === pr.submitted_by);
|
|
446
|
+
if (devSession) {
|
|
447
|
+
await sendToTmuxSession(devSession.name,
|
|
448
|
+
`# ⚠️ PR REJECTED - ACTION REQUIRED ⚠️
|
|
449
|
+
# Story: ${pr.story_id || 'Unknown'}
|
|
450
|
+
# Reason: ${pr.review_notes || 'See review comments'}
|
|
451
|
+
#
|
|
452
|
+
# You MUST fix this issue before doing anything else.
|
|
453
|
+
# Fix the issues and resubmit: hive pr submit -b ${pr.branch_name} -s ${pr.story_id || 'STORY-ID'} --from ${devSession.name}`
|
|
454
|
+
);
|
|
455
|
+
await sendEnterToTmuxSession(devSession.name);
|
|
456
|
+
rejectionNotified++;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// Mark as closed to prevent re-notification spam
|
|
460
|
+
// Developer will create a new PR when they resubmit
|
|
461
|
+
db.db.run("UPDATE pull_requests SET status = 'closed' WHERE id = ?", [pr.id]);
|
|
462
|
+
}
|
|
463
|
+
if (rejectedPRs.length > 0) {
|
|
464
|
+
db.save();
|
|
465
|
+
console.log(chalk.yellow(` Notified ${rejectionNotified} developer(s) of PR rejection(s)`));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Nudge developers who have qa_failed stories - they need to fix and resubmit
|
|
469
|
+
// Filter out merged/completed stories to avoid nudging about finished work
|
|
470
|
+
const qaFailedStories = getStoriesByStatus(db.db, 'qa_failed').filter(
|
|
471
|
+
story => !['merged', 'completed'].includes(story.status)
|
|
472
|
+
);
|
|
473
|
+
for (const story of qaFailedStories) {
|
|
474
|
+
if (story.assigned_agent_id) {
|
|
475
|
+
const agent = getAgentById(db.db, story.assigned_agent_id);
|
|
476
|
+
if (agent && agent.status === 'working') {
|
|
477
|
+
const agentSession = hiveSessions.find(s =>
|
|
478
|
+
s.name === agent.tmux_session || s.name.includes(agent.id)
|
|
479
|
+
);
|
|
480
|
+
if (agentSession) {
|
|
481
|
+
// Check if agent is idle before nudging
|
|
482
|
+
const output = await captureTmuxPane(agentSession.name, 30);
|
|
483
|
+
const stateResult = detectClaudeCodeState(output);
|
|
484
|
+
// Only nudge if idle and not thinking (thinking is productive work)
|
|
485
|
+
if (stateResult.isWaiting && !stateResult.needsHuman && stateResult.state !== ClaudeCodeState.THINKING) {
|
|
486
|
+
await sendToTmuxSession(agentSession.name,
|
|
487
|
+
`# REMINDER: Story ${story.id} failed QA review!
|
|
488
|
+
# You must fix the issues and resubmit the PR.
|
|
489
|
+
# Check the QA feedback and address all concerns.
|
|
490
|
+
hive pr queue`
|
|
491
|
+
);
|
|
492
|
+
await sendEnterToTmuxSession(agentSession.name);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Spin down agents whose stories have been merged
|
|
500
|
+
// Find merged stories that still have assigned agents
|
|
501
|
+
const mergedStoriesWithAgents = queryAll<StoryRow>(db.db,
|
|
502
|
+
`SELECT * FROM stories WHERE status = 'merged' AND assigned_agent_id IS NOT NULL`
|
|
503
|
+
);
|
|
504
|
+
let agentsSpunDown = 0;
|
|
505
|
+
for (const story of mergedStoriesWithAgents) {
|
|
506
|
+
if (story.assigned_agent_id) {
|
|
507
|
+
const agent = getAgentById(db.db, story.assigned_agent_id);
|
|
508
|
+
if (agent && agent.status !== 'terminated') {
|
|
509
|
+
// Find and kill the agent's tmux session
|
|
510
|
+
const agentSession = hiveSessions.find(s =>
|
|
511
|
+
s.name === agent.tmux_session || s.name.includes(agent.id)
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
if (agentSession) {
|
|
515
|
+
// Thank the agent and terminate
|
|
516
|
+
await sendToTmuxSession(agentSession.name,
|
|
517
|
+
`# Congratulations! Your story ${story.id} has been merged.
|
|
518
|
+
# Your work is complete. Spinning down...`
|
|
519
|
+
);
|
|
520
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
521
|
+
await killTmuxSession(agentSession.name);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Mark agent as terminated
|
|
525
|
+
updateAgent(db.db, agent.id, { status: 'terminated', currentStoryId: null });
|
|
526
|
+
|
|
527
|
+
// Log the termination
|
|
528
|
+
createLog(db.db, {
|
|
529
|
+
agentId: agent.id,
|
|
530
|
+
storyId: story.id,
|
|
531
|
+
eventType: 'AGENT_TERMINATED',
|
|
532
|
+
message: `Agent spun down after story ${story.id} was merged`,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// Clear the story assignment
|
|
536
|
+
db.db.run("UPDATE stories SET assigned_agent_id = NULL WHERE id = ?", [story.id]);
|
|
537
|
+
|
|
538
|
+
agentsSpunDown++;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (agentsSpunDown > 0) {
|
|
543
|
+
db.save();
|
|
544
|
+
console.log(chalk.green(` Spun down ${agentsSpunDown} agent(s) after successful merge`));
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Spin down all non-tech-lead agents when no work remains in the pipeline
|
|
548
|
+
const activeStories = queryAll<StoryRow>(db.db,
|
|
549
|
+
`SELECT * FROM stories WHERE status IN ('planned', 'in_progress', 'review', 'qa', 'qa_failed', 'pr_submitted')`
|
|
550
|
+
);
|
|
551
|
+
if (activeStories.length === 0) {
|
|
552
|
+
// No active work - spin down all agents except tech lead
|
|
553
|
+
const workingAgents = queryAll<{ id: string; tmux_session: string | null; type: string }>(db.db,
|
|
554
|
+
`SELECT id, tmux_session, type FROM agents WHERE status = 'working' AND type != 'tech_lead'`
|
|
555
|
+
);
|
|
556
|
+
let idleSpunDown = 0;
|
|
557
|
+
for (const agent of workingAgents) {
|
|
558
|
+
const agentSession = hiveSessions.find(s => s.name === agent.tmux_session);
|
|
559
|
+
if (agentSession) {
|
|
560
|
+
await sendToTmuxSession(agentSession.name,
|
|
561
|
+
`# All work complete. No stories in pipeline. Spinning down...`
|
|
562
|
+
);
|
|
563
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
564
|
+
await killTmuxSession(agentSession.name);
|
|
565
|
+
}
|
|
566
|
+
updateAgent(db.db, agent.id, { status: 'terminated', currentStoryId: null });
|
|
567
|
+
createLog(db.db, {
|
|
568
|
+
agentId: agent.id,
|
|
569
|
+
eventType: 'AGENT_TERMINATED',
|
|
570
|
+
message: 'Agent spun down - no work remaining in pipeline',
|
|
571
|
+
});
|
|
572
|
+
idleSpunDown++;
|
|
573
|
+
}
|
|
574
|
+
if (idleSpunDown > 0) {
|
|
575
|
+
db.save();
|
|
576
|
+
console.log(chalk.green(` Spun down ${idleSpunDown} idle agent(s) - pipeline empty`));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Check for stories stuck in "in_progress" for too long (> 30 min without activity)
|
|
581
|
+
// Only nudge stories that haven't been merged or completed
|
|
582
|
+
const stuckStories = queryAll<StoryRow>(db.db,
|
|
583
|
+
`SELECT * FROM stories
|
|
584
|
+
WHERE status = 'in_progress'
|
|
585
|
+
AND updated_at < datetime('now', '-30 minutes')`
|
|
586
|
+
).filter(story => !['merged', 'completed'].includes(story.status));
|
|
587
|
+
for (const story of stuckStories) {
|
|
588
|
+
if (story.assigned_agent_id) {
|
|
589
|
+
const agentSession = hiveSessions.find(s =>
|
|
590
|
+
s.name.includes(story.assigned_agent_id?.replace(/^hive-/, '') || '')
|
|
591
|
+
);
|
|
592
|
+
if (agentSession) {
|
|
593
|
+
await sendToTmuxSession(agentSession.name,
|
|
594
|
+
`# REMINDER: Story ${story.id} has been in progress for a while.
|
|
595
|
+
# If stuck, escalate to your Senior or Tech Lead.
|
|
596
|
+
# If done, submit your PR: hive pr submit -b <branch> -s ${story.id} --from ${agentSession.name}
|
|
597
|
+
# Then mark complete: hive my-stories complete ${story.id}`
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Check for unassigned planned stories
|
|
604
|
+
const plannedStories = queryAll<StoryRow>(db.db,
|
|
605
|
+
"SELECT * FROM stories WHERE status = 'planned' AND assigned_agent_id IS NULL"
|
|
606
|
+
);
|
|
607
|
+
if (plannedStories.length > 0) {
|
|
608
|
+
// Notify seniors about unassigned work
|
|
609
|
+
const seniorSessions = hiveSessions.filter(s => s.name.includes('-senior-'));
|
|
610
|
+
for (const senior of seniorSessions) {
|
|
611
|
+
await sendToTmuxSession(senior.name,
|
|
612
|
+
`# ${plannedStories.length} unassigned story(ies). Run: hive my-stories ${senior.name} --all`
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Summary
|
|
618
|
+
const summary = [];
|
|
619
|
+
if (escalationsCreated > 0) summary.push(`${escalationsCreated} escalations created`);
|
|
620
|
+
if (nudged > 0) summary.push(`${nudged} nudged`);
|
|
621
|
+
if (messagesForwarded > 0) summary.push(`${messagesForwarded} messages forwarded`);
|
|
622
|
+
if (queuedPRs.length > 0) summary.push(`${queuedPRs.length} PRs queued`);
|
|
623
|
+
|
|
624
|
+
if (summary.length > 0) {
|
|
625
|
+
console.log(chalk.yellow(` ${summary.join(', ')}`));
|
|
626
|
+
} else {
|
|
627
|
+
console.log(chalk.green(' All agents productive'));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
} finally {
|
|
631
|
+
db.close();
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
function getAgentType(sessionName: string): 'senior' | 'intermediate' | 'junior' | 'qa' | 'unknown' {
|
|
637
|
+
if (sessionName.includes('-senior-')) return 'senior';
|
|
638
|
+
if (sessionName.includes('-intermediate-')) return 'intermediate';
|
|
639
|
+
if (sessionName.includes('-junior-')) return 'junior';
|
|
640
|
+
if (sessionName.includes('-qa-')) return 'qa';
|
|
641
|
+
return 'unknown';
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function nudgeAgent(
|
|
645
|
+
_root: string,
|
|
646
|
+
sessionName: string,
|
|
647
|
+
customMessage?: string,
|
|
648
|
+
agentType?: string,
|
|
649
|
+
reason?: string,
|
|
650
|
+
agentCliTool?: CLITool
|
|
651
|
+
): Promise<void> {
|
|
652
|
+
if (customMessage) {
|
|
653
|
+
await sendToTmuxSession(sessionName, customMessage);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const type = agentType || getAgentType(sessionName);
|
|
658
|
+
const cliTool = agentCliTool || 'claude' as CLITool;
|
|
659
|
+
const commands = getAvailableCommands(cliTool);
|
|
660
|
+
|
|
661
|
+
// Build contextual nudge message based on agent type and reason
|
|
662
|
+
let nudge: string;
|
|
663
|
+
switch (type) {
|
|
664
|
+
case 'qa':
|
|
665
|
+
nudge = `# You are a QA agent. Check for PRs to review:
|
|
666
|
+
# ${commands.queueCheck()}
|
|
667
|
+
# If there are PRs, review them with: hive pr review <pr-id>`;
|
|
668
|
+
break;
|
|
669
|
+
case 'senior':
|
|
670
|
+
nudge = `# You are a Senior developer. Continue with your assigned stories.
|
|
671
|
+
# Check your work: # ${commands.getMyStories(sessionName)}
|
|
672
|
+
# If no active stories, check for available work: hive stories list --status planned`;
|
|
673
|
+
break;
|
|
674
|
+
case 'intermediate':
|
|
675
|
+
case 'junior':
|
|
676
|
+
nudge = `# Continue with your assigned story. Check status:
|
|
677
|
+
# ${commands.getMyStories(sessionName)}
|
|
678
|
+
# If stuck, ask your Senior for help via: hive msg send hive-senior-<team> "your question"
|
|
679
|
+
# If done, submit PR: hive pr submit -b <branch> -s <story-id> --from ${sessionName}`;
|
|
680
|
+
break;
|
|
681
|
+
default:
|
|
682
|
+
nudge = `# Check current status and continue working:
|
|
683
|
+
hive status`;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Add reason context if provided
|
|
687
|
+
if (reason) {
|
|
688
|
+
nudge = `# Manager detected: ${reason}\n${nudge}`;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
await sendToTmuxSession(sessionName, nudge);
|
|
692
|
+
|
|
693
|
+
// Also send Enter to ensure prompt is activated
|
|
694
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
695
|
+
await sendEnterToTmuxSession(sessionName);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async function forwardMessages(sessionName: string, messages: MessageRow[], cliTool: CLITool = 'claude'): Promise<void> {
|
|
699
|
+
const commands = getAvailableCommands(cliTool);
|
|
700
|
+
for (const msg of messages) {
|
|
701
|
+
const notification = `# New message from ${msg.from_session}${msg.subject ? ` - ${msg.subject}` : ''}
|
|
702
|
+
# ${msg.body}
|
|
703
|
+
# Reply with: # ${commands.msgReply(msg.id, 'your response', sessionName)}`;
|
|
704
|
+
|
|
705
|
+
await sendToTmuxSession(sessionName, notification);
|
|
706
|
+
// Small delay between messages
|
|
707
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
interface DatabaseClient {
|
|
712
|
+
db: import('sql.js').Database;
|
|
713
|
+
save: () => void;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function syncMergedPRsFromGitHub(root: string, db: DatabaseClient): Promise<number> {
|
|
717
|
+
const teams = getAllTeams(db.db);
|
|
718
|
+
if (teams.length === 0) return 0;
|
|
719
|
+
|
|
720
|
+
let storiesUpdated = 0;
|
|
721
|
+
|
|
722
|
+
for (const team of teams) {
|
|
723
|
+
if (!team.repo_path) continue;
|
|
724
|
+
|
|
725
|
+
const repoDir = `${root}/${team.repo_path}`;
|
|
726
|
+
|
|
727
|
+
try {
|
|
728
|
+
// Get recently merged PRs from GitHub
|
|
729
|
+
const result = await execa('gh', ['pr', 'list', '--json', 'number,headRefName,mergedAt', '--state', 'merged', '--limit', '20'], {
|
|
730
|
+
cwd: repoDir,
|
|
731
|
+
});
|
|
732
|
+
const mergedPRs: Array<{ number: number; headRefName: string; mergedAt: string }> = JSON.parse(result.stdout);
|
|
733
|
+
|
|
734
|
+
for (const pr of mergedPRs) {
|
|
735
|
+
// Extract story ID from branch name - match STORY-XXX-NAME pattern
|
|
736
|
+
// Use a more specific pattern to avoid matching extra suffixes
|
|
737
|
+
const storyMatch = pr.headRefName.match(/STORY-\d+-[A-Z]+/i);
|
|
738
|
+
if (!storyMatch) continue;
|
|
739
|
+
|
|
740
|
+
const storyId = storyMatch[0].toUpperCase();
|
|
741
|
+
|
|
742
|
+
// Check if story exists and isn't already merged
|
|
743
|
+
const story = queryAll<StoryRow>(db.db,
|
|
744
|
+
"SELECT * FROM stories WHERE id = ? AND status != 'merged'",
|
|
745
|
+
[storyId]
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
if (story.length > 0) {
|
|
749
|
+
// Update story to merged
|
|
750
|
+
db.db.run("UPDATE stories SET status = 'merged', assigned_agent_id = NULL, updated_at = datetime('now') WHERE id = ?", [storyId]);
|
|
751
|
+
|
|
752
|
+
// Log the sync
|
|
753
|
+
createLog(db.db, {
|
|
754
|
+
agentId: 'manager',
|
|
755
|
+
storyId: storyId,
|
|
756
|
+
eventType: 'STORY_MERGED',
|
|
757
|
+
message: `Story synced to merged from GitHub PR #${pr.number}`,
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
storiesUpdated++;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
} catch {
|
|
764
|
+
// gh CLI might not be authenticated or repo might not have remote
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (storiesUpdated > 0) {
|
|
770
|
+
db.save();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return storiesUpdated;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async function syncGitHubPRs(root: string, db: DatabaseClient, _hiveDir: string): Promise<number> {
|
|
777
|
+
const teams = getAllTeams(db.db);
|
|
778
|
+
if (teams.length === 0) return 0;
|
|
779
|
+
|
|
780
|
+
// Get ALL existing PRs (including merged/closed) to prevent duplicate imports
|
|
781
|
+
const existingPRs = queryAll<PullRequestRow>(db.db,
|
|
782
|
+
"SELECT * FROM pull_requests"
|
|
783
|
+
);
|
|
784
|
+
// Include ALL branch names to prevent duplicate entries for merged/closed PRs
|
|
785
|
+
const existingBranches = new Set(existingPRs.map(pr => pr.branch_name));
|
|
786
|
+
const existingPrNumbers = new Set(existingPRs.map(pr => pr.github_pr_number).filter(Boolean));
|
|
787
|
+
|
|
788
|
+
let synced = 0;
|
|
789
|
+
|
|
790
|
+
for (const team of teams) {
|
|
791
|
+
if (!team.repo_path) continue;
|
|
792
|
+
|
|
793
|
+
const repoDir = `${root}/${team.repo_path}`;
|
|
794
|
+
|
|
795
|
+
try {
|
|
796
|
+
const result = await execa('gh', ['pr', 'list', '--json', 'number,headRefName,url,title', '--state', 'open'], {
|
|
797
|
+
cwd: repoDir,
|
|
798
|
+
});
|
|
799
|
+
const ghPRs: Array<{ number: number; headRefName: string; url: string; title: string }> = JSON.parse(result.stdout);
|
|
800
|
+
|
|
801
|
+
for (const ghPR of ghPRs) {
|
|
802
|
+
// Skip if already in queue
|
|
803
|
+
if (existingBranches.has(ghPR.headRefName) || existingPrNumbers.has(ghPR.number)) {
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Try to match to a story by parsing branch name
|
|
808
|
+
const storyMatch = ghPR.headRefName.match(/STORY-\d+/i);
|
|
809
|
+
const storyId = storyMatch ? storyMatch[0].toUpperCase() : null;
|
|
810
|
+
|
|
811
|
+
createPullRequest(db.db, {
|
|
812
|
+
storyId,
|
|
813
|
+
teamId: team.id,
|
|
814
|
+
branchName: ghPR.headRefName,
|
|
815
|
+
githubPrNumber: ghPR.number,
|
|
816
|
+
githubPrUrl: ghPR.url,
|
|
817
|
+
submittedBy: null,
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
synced++;
|
|
821
|
+
}
|
|
822
|
+
} catch {
|
|
823
|
+
// gh CLI might not be authenticated or repo might not have remote
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (synced > 0) {
|
|
829
|
+
db.save();
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return synced;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
async function autoMergeApprovedPRs(root: string, db: DatabaseClient): Promise<number> {
|
|
836
|
+
const approvedPRs = getApprovedPullRequests(db.db);
|
|
837
|
+
if (approvedPRs.length === 0) return 0;
|
|
838
|
+
|
|
839
|
+
let mergedCount = 0;
|
|
840
|
+
|
|
841
|
+
for (const pr of approvedPRs) {
|
|
842
|
+
// Skip PRs without GitHub PR numbers
|
|
843
|
+
if (!pr.github_pr_number) continue;
|
|
844
|
+
|
|
845
|
+
try {
|
|
846
|
+
// Get team to find repo path
|
|
847
|
+
let teamId = pr.team_id;
|
|
848
|
+
let repoCwd = root;
|
|
849
|
+
|
|
850
|
+
if (teamId) {
|
|
851
|
+
const team = getAllTeams(db.db).find(t => t.id === teamId);
|
|
852
|
+
if (team?.repo_path) {
|
|
853
|
+
repoCwd = join(root, team.repo_path);
|
|
854
|
+
}
|
|
855
|
+
} else if (pr.branch_name) {
|
|
856
|
+
// Try to find team by matching branch name pattern
|
|
857
|
+
const teams = getAllTeams(db.db);
|
|
858
|
+
for (const team of teams) {
|
|
859
|
+
if (team.repo_path) {
|
|
860
|
+
repoCwd = join(root, team.repo_path);
|
|
861
|
+
teamId = team.id;
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Attempt to merge on GitHub
|
|
868
|
+
const { execSync } = await import('child_process');
|
|
869
|
+
try {
|
|
870
|
+
execSync(`gh pr merge ${pr.github_pr_number} --auto --squash --delete-branch`, { stdio: 'pipe', cwd: repoCwd });
|
|
871
|
+
|
|
872
|
+
// Update PR status to merged
|
|
873
|
+
updatePullRequest(db.db, pr.id, { status: 'merged' });
|
|
874
|
+
|
|
875
|
+
// Update story status if linked
|
|
876
|
+
if (pr.story_id) {
|
|
877
|
+
updateStory(db.db, pr.story_id, { status: 'merged' });
|
|
878
|
+
createLog(db.db, {
|
|
879
|
+
agentId: 'manager',
|
|
880
|
+
storyId: pr.story_id,
|
|
881
|
+
eventType: 'STORY_MERGED',
|
|
882
|
+
message: `Story auto-merged from GitHub PR #${pr.github_pr_number}`,
|
|
883
|
+
});
|
|
884
|
+
} else {
|
|
885
|
+
createLog(db.db, {
|
|
886
|
+
agentId: 'manager',
|
|
887
|
+
eventType: 'PR_MERGED',
|
|
888
|
+
message: `PR ${pr.id} auto-merged (GitHub PR #${pr.github_pr_number})`,
|
|
889
|
+
metadata: { pr_id: pr.id },
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
mergedCount++;
|
|
894
|
+
} catch (mergeErr) {
|
|
895
|
+
// Log merge failure but continue with other PRs
|
|
896
|
+
createLog(db.db, {
|
|
897
|
+
agentId: 'manager',
|
|
898
|
+
storyId: pr.story_id || undefined,
|
|
899
|
+
eventType: 'PR_MERGE_FAILED',
|
|
900
|
+
status: 'error',
|
|
901
|
+
message: `Failed to auto-merge PR ${pr.id} (GitHub PR #${pr.github_pr_number}): ${mergeErr instanceof Error ? mergeErr.message : 'Unknown error'}`,
|
|
902
|
+
metadata: { pr_id: pr.id },
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
} catch {
|
|
906
|
+
// Non-fatal - continue with other PRs
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (mergedCount > 0) {
|
|
912
|
+
db.save();
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
return mergedCount;
|
|
916
|
+
}
|