@desplega.ai/agent-swarm 1.20.0 → 1.49.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/.claude/settings.json +84 -0
- package/.claude/settings.local.json +3 -1
- package/.entire/settings.json +4 -0
- package/.env.docker.example +40 -23
- package/.env.example +39 -1
- package/.github/ISSUE_TEMPLATE/bug_report.yml +78 -0
- package/.github/ISSUE_TEMPLATE/community-template.yml +77 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +60 -0
- package/.github/PULL_REQUEST_TEMPLATE/community-template.md +29 -0
- package/.github/workflows/ci.yml +3 -27
- package/.github/workflows/docker-and-deploy.yml +20 -5
- package/.github/workflows/merge-gate.yml +233 -0
- package/.opencode/plugins/entire.ts +133 -0
- package/.superset/config.json +6 -0
- package/.wts-setup.ts +103 -34
- package/CHANGELOG.md +447 -0
- package/CLAUDE.md +422 -5
- package/CONTRIBUTING.md +46 -1
- package/DEPLOYMENT.md +26 -9
- package/Dockerfile +15 -7
- package/Dockerfile.worker +99 -67
- package/MCP.md +452 -17
- package/README.md +271 -169
- package/api-entrypoint.sh +56 -0
- package/depot.json +1 -0
- package/docker-compose.example.yml +194 -3
- package/docker-compose.local.yml +119 -0
- package/docker-entrypoint.sh +393 -113
- package/docs-site/app/api/search/route.ts +4 -0
- package/docs-site/app/docs/[[...slug]]/page.tsx +87 -0
- package/docs-site/app/docs/layout.tsx +12 -0
- package/docs-site/app/globals.css +24 -0
- package/docs-site/app/layout.config.tsx +34 -0
- package/docs-site/app/layout.tsx +119 -0
- package/docs-site/app/llms-full.txt/route.ts +11 -0
- package/docs-site/app/llms.mdx/docs/[[...slug]]/route.ts +24 -0
- package/docs-site/app/llms.txt/route.ts +8 -0
- package/docs-site/app/page.tsx +5 -0
- package/docs-site/app/robots.ts +13 -0
- package/docs-site/app/sitemap.ts +37 -0
- package/docs-site/components/api-page.client.tsx +4 -0
- package/docs-site/components/api-page.tsx +7 -0
- package/docs-site/components/mdx/mermaid.tsx +55 -0
- package/docs-site/content/docs/(documentation)/architecture/agents.mdx +117 -0
- package/docs-site/content/docs/(documentation)/architecture/hooks.mdx +77 -0
- package/docs-site/content/docs/(documentation)/architecture/memory.mdx +96 -0
- package/docs-site/content/docs/(documentation)/architecture/meta.json +4 -0
- package/docs-site/content/docs/(documentation)/architecture/overview.mdx +172 -0
- package/docs-site/content/docs/(documentation)/concepts/epics.mdx +98 -0
- package/docs-site/content/docs/(documentation)/concepts/meta.json +4 -0
- package/docs-site/content/docs/(documentation)/concepts/scheduling.mdx +136 -0
- package/docs-site/content/docs/(documentation)/concepts/services.mdx +104 -0
- package/docs-site/content/docs/(documentation)/concepts/task-lifecycle.mdx +148 -0
- package/docs-site/content/docs/(documentation)/concepts/workflows.mdx +209 -0
- package/docs-site/content/docs/(documentation)/contributing.mdx +158 -0
- package/docs-site/content/docs/(documentation)/getting-started.mdx +157 -0
- package/docs-site/content/docs/(documentation)/guides/agentmail-integration.mdx +79 -0
- package/docs-site/content/docs/(documentation)/guides/deployment.mdx +171 -0
- package/docs-site/content/docs/(documentation)/guides/github-integration.mdx +81 -0
- package/docs-site/content/docs/(documentation)/guides/gitlab-integration.mdx +93 -0
- package/docs-site/content/docs/(documentation)/guides/linear-integration.mdx +98 -0
- package/docs-site/content/docs/(documentation)/guides/meta.json +13 -0
- package/docs-site/content/docs/(documentation)/guides/sentry-integration.mdx +52 -0
- package/docs-site/content/docs/(documentation)/guides/slack-integration.mdx +179 -0
- package/docs-site/content/docs/(documentation)/guides/x402-payments.mdx +154 -0
- package/docs-site/content/docs/(documentation)/index.mdx +65 -0
- package/docs-site/content/docs/(documentation)/meta.json +19 -0
- package/docs-site/content/docs/(documentation)/reference/cli.mdx +241 -0
- package/docs-site/content/docs/(documentation)/reference/environment-variables.mdx +205 -0
- package/docs-site/content/docs/(documentation)/reference/mcp-tools.mdx +449 -0
- package/docs-site/content/docs/(documentation)/reference/meta.json +4 -0
- package/docs-site/content/docs/api-reference/active-sessions.mdx +9 -0
- package/docs-site/content/docs/api-reference/agents.mdx +9 -0
- package/docs-site/content/docs/api-reference/channels.mdx +9 -0
- package/docs-site/content/docs/api-reference/config.mdx +9 -0
- package/docs-site/content/docs/api-reference/debug.mdx +9 -0
- package/docs-site/content/docs/api-reference/ecosystem.mdx +9 -0
- package/docs-site/content/docs/api-reference/epics.mdx +9 -0
- package/docs-site/content/docs/api-reference/index.mdx +32 -0
- package/docs-site/content/docs/api-reference/memory.mdx +9 -0
- package/docs-site/content/docs/api-reference/meta.json +25 -0
- package/docs-site/content/docs/api-reference/poll.mdx +9 -0
- package/docs-site/content/docs/api-reference/repos.mdx +9 -0
- package/docs-site/content/docs/api-reference/schedules.mdx +9 -0
- package/docs-site/content/docs/api-reference/session-data.mdx +9 -0
- package/docs-site/content/docs/api-reference/stats.mdx +9 -0
- package/docs-site/content/docs/api-reference/tasks.mdx +9 -0
- package/docs-site/content/docs/api-reference/trackers.mdx +9 -0
- package/docs-site/content/docs/api-reference/webhooks.mdx +9 -0
- package/docs-site/content/docs/api-reference/workflows.mdx +9 -0
- package/docs-site/content/docs/meta.json +3 -0
- package/docs-site/lib/get-llm-text.ts +10 -0
- package/docs-site/lib/openapi.ts +23 -0
- package/docs-site/lib/source.ts +8 -0
- package/docs-site/mdx-components.tsx +13 -0
- package/docs-site/next.config.mjs +29 -0
- package/docs-site/package.json +35 -0
- package/docs-site/pnpm-lock.yaml +5407 -0
- package/docs-site/postcss.config.mjs +8 -0
- package/docs-site/scripts/generate-docs.ts +171 -0
- package/docs-site/source.config.ts +17 -0
- package/docs-site/tsconfig.json +46 -0
- package/ecosystem.config.cjs +4 -4
- package/landing/next.config.ts +14 -0
- package/landing/package.json +31 -0
- package/landing/pnpm-lock.yaml +1091 -0
- package/landing/postcss.config.mjs +8 -0
- package/landing/public/apple-touch-icon.png +0 -0
- package/landing/public/favicon.ico +0 -0
- package/landing/public/logo.png +0 -0
- package/landing/public/og-image.png +0 -0
- package/landing/public/omghost-desplega.svg +30 -0
- package/landing/public/omghost-openfort.svg +9 -0
- package/landing/src/app/actions/waitlist.ts +25 -0
- package/landing/src/app/blog/openfort-hackathon/page.tsx +863 -0
- package/landing/src/app/blog/page.tsx +162 -0
- package/landing/src/app/blog/swarm-metrics/page.tsx +685 -0
- package/landing/src/app/examples/page.tsx +174 -0
- package/landing/src/app/examples/x402/page.tsx +456 -0
- package/landing/src/app/globals.css +122 -0
- package/landing/src/app/layout.tsx +134 -0
- package/landing/src/app/page.tsx +27 -0
- package/landing/src/app/robots.ts +13 -0
- package/landing/src/app/sitemap.ts +44 -0
- package/landing/src/components/architecture.tsx +163 -0
- package/landing/src/components/cta.tsx +52 -0
- package/landing/src/components/features.tsx +160 -0
- package/landing/src/components/footer.tsx +100 -0
- package/landing/src/components/hero.tsx +217 -0
- package/landing/src/components/how-it-works.tsx +165 -0
- package/landing/src/components/navbar.tsx +147 -0
- package/landing/src/components/waitlist.tsx +110 -0
- package/landing/src/components/why-choose.tsx +149 -0
- package/landing/src/components/workshops.tsx +328 -0
- package/landing/src/lib/utils.ts +6 -0
- package/landing/tsconfig.json +41 -0
- package/misc/transcripts/2026-03-09-pi-mono-e2e-verification.md +154 -0
- package/new-ui/CLAUDE.md +92 -0
- package/new-ui/README.md +73 -0
- package/new-ui/biome.json +42 -0
- package/new-ui/components.json +21 -0
- package/new-ui/index.html +25 -0
- package/new-ui/package.json +49 -0
- package/new-ui/pnpm-lock.yaml +4845 -0
- package/new-ui/public/logo.png +0 -0
- package/new-ui/src/api/client.ts +814 -0
- package/new-ui/src/api/hooks/index.ts +64 -0
- package/new-ui/src/api/hooks/use-agents.ts +58 -0
- package/new-ui/src/api/hooks/use-channels.ts +115 -0
- package/new-ui/src/api/hooks/use-config-api.ts +46 -0
- package/new-ui/src/api/hooks/use-costs.ts +122 -0
- package/new-ui/src/api/hooks/use-db-query.ts +29 -0
- package/new-ui/src/api/hooks/use-epics.ts +75 -0
- package/new-ui/src/api/hooks/use-repos.ts +61 -0
- package/new-ui/src/api/hooks/use-schedules.ts +81 -0
- package/new-ui/src/api/hooks/use-services.ts +16 -0
- package/new-ui/src/api/hooks/use-stats.ts +27 -0
- package/new-ui/src/api/hooks/use-tasks.ts +89 -0
- package/new-ui/src/api/hooks/use-workflows.ts +109 -0
- package/{ui/src/types/api.ts → new-ui/src/api/types.ts} +236 -10
- package/new-ui/src/app/App.tsx +13 -0
- package/new-ui/src/app/providers.tsx +32 -0
- package/new-ui/src/app/router.tsx +52 -0
- package/new-ui/src/components/layout/app-header.tsx +47 -0
- package/new-ui/src/components/layout/app-sidebar.tsx +128 -0
- package/new-ui/src/components/layout/breadcrumbs.tsx +57 -0
- package/new-ui/src/components/layout/config-guard.tsx +22 -0
- package/new-ui/src/components/layout/root-layout.tsx +40 -0
- package/new-ui/src/components/layout/swarm-switcher.tsx +85 -0
- package/new-ui/src/components/shared/command-menu.tsx +131 -0
- package/new-ui/src/components/shared/data-grid.tsx +141 -0
- package/new-ui/src/components/shared/empty-state.tsx +24 -0
- package/new-ui/src/components/shared/error-boundary.tsx +72 -0
- package/new-ui/src/components/shared/json-viewer.tsx +47 -0
- package/new-ui/src/components/shared/name-connection-modal.tsx +99 -0
- package/new-ui/src/components/shared/page-skeleton.tsx +16 -0
- package/new-ui/src/components/shared/session-log-viewer.tsx +364 -0
- package/new-ui/src/components/shared/stats-bar.tsx +132 -0
- package/new-ui/src/components/shared/status-badge.tsx +131 -0
- package/new-ui/src/components/shared/usage-summary.tsx +179 -0
- package/new-ui/src/components/ui/alert-dialog.tsx +176 -0
- package/new-ui/src/components/ui/alert.tsx +60 -0
- package/new-ui/src/components/ui/avatar.tsx +96 -0
- package/new-ui/src/components/ui/badge.tsx +46 -0
- package/new-ui/src/components/ui/button.tsx +62 -0
- package/new-ui/src/components/ui/card.tsx +75 -0
- package/new-ui/src/components/ui/command.tsx +160 -0
- package/new-ui/src/components/ui/dialog.tsx +143 -0
- package/new-ui/src/components/ui/dropdown-menu.tsx +226 -0
- package/new-ui/src/components/ui/input.tsx +21 -0
- package/new-ui/src/components/ui/label.tsx +19 -0
- package/new-ui/src/components/ui/progress.tsx +26 -0
- package/new-ui/src/components/ui/scroll-area.tsx +54 -0
- package/new-ui/src/components/ui/select.tsx +175 -0
- package/new-ui/src/components/ui/separator.tsx +28 -0
- package/new-ui/src/components/ui/sheet.tsx +132 -0
- package/new-ui/src/components/ui/sidebar.tsx +691 -0
- package/new-ui/src/components/ui/skeleton.tsx +13 -0
- package/new-ui/src/components/ui/sonner.tsx +35 -0
- package/new-ui/src/components/ui/switch.tsx +33 -0
- package/new-ui/src/components/ui/table.tsx +92 -0
- package/new-ui/src/components/ui/tabs.tsx +79 -0
- package/new-ui/src/components/ui/textarea.tsx +18 -0
- package/new-ui/src/components/ui/tooltip.tsx +51 -0
- package/new-ui/src/components/workflows/action-node.tsx +53 -0
- package/new-ui/src/components/workflows/condition-node.tsx +50 -0
- package/new-ui/src/components/workflows/graph-utils.ts +124 -0
- package/new-ui/src/components/workflows/json-tree.tsx +189 -0
- package/new-ui/src/components/workflows/node-styles.ts +10 -0
- package/new-ui/src/components/workflows/step-detail-sheet.tsx +87 -0
- package/new-ui/src/components/workflows/trigger-node.tsx +41 -0
- package/new-ui/src/components/workflows/workflow-graph.tsx +65 -0
- package/{ui/src/hooks/useAutoScroll.ts → new-ui/src/hooks/use-auto-scroll.ts} +0 -1
- package/new-ui/src/hooks/use-config.ts +203 -0
- package/new-ui/src/hooks/use-keyboard-shortcuts.ts +41 -0
- package/new-ui/src/hooks/use-mobile.ts +19 -0
- package/new-ui/src/hooks/use-theme.ts +60 -0
- package/new-ui/src/lib/config.ts +188 -0
- package/new-ui/src/lib/slugs.ts +71 -0
- package/{ui → new-ui}/src/lib/utils.ts +32 -0
- package/new-ui/src/main.tsx +11 -0
- package/new-ui/src/pages/agents/[id]/page.tsx +492 -0
- package/new-ui/src/pages/agents/page.tsx +134 -0
- package/new-ui/src/pages/chat/page.tsx +674 -0
- package/new-ui/src/pages/config/page.tsx +1109 -0
- package/new-ui/src/pages/dashboard/page.tsx +454 -0
- package/new-ui/src/pages/debug/page.tsx +275 -0
- package/new-ui/src/pages/epics/[id]/page.tsx +809 -0
- package/new-ui/src/pages/epics/page.tsx +321 -0
- package/new-ui/src/pages/not-found/page.tsx +18 -0
- package/new-ui/src/pages/repos/page.tsx +369 -0
- package/new-ui/src/pages/schedules/[id]/page.tsx +664 -0
- package/new-ui/src/pages/schedules/page.tsx +477 -0
- package/new-ui/src/pages/services/page.tsx +128 -0
- package/new-ui/src/pages/tasks/[id]/page.tsx +670 -0
- package/new-ui/src/pages/tasks/page.tsx +592 -0
- package/new-ui/src/pages/usage/page.tsx +195 -0
- package/new-ui/src/pages/workflow-runs/[id]/page.tsx +363 -0
- package/new-ui/src/pages/workflows/[id]/page.tsx +417 -0
- package/new-ui/src/pages/workflows/page.tsx +266 -0
- package/new-ui/src/styles/ag-grid.css +36 -0
- package/new-ui/src/styles/globals.css +213 -0
- package/new-ui/test-results/.last-run.json +4 -0
- package/{ui/tsconfig.json → new-ui/tsconfig.app.json} +7 -4
- package/new-ui/tsconfig.json +4 -0
- package/new-ui/tsconfig.node.json +26 -0
- package/new-ui/vercel.json +4 -0
- package/new-ui/vite.config.ts +28 -0
- package/openapi.json +4436 -0
- package/package.json +31 -7
- package/plugin/build-pi-skills.ts +233 -0
- package/plugin/commands/close-issue.md +7 -3
- package/plugin/commands/create-pr.md +18 -12
- package/plugin/commands/implement-issue.md +7 -3
- package/plugin/commands/respond-github.md +8 -4
- package/plugin/commands/review-pr.md +44 -10
- package/plugin/commands/start-leader.md +1 -3
- package/plugin/commands/start-worker.md +1 -3
- package/plugin/commands/work-on-task.md +22 -3
- package/plugin/pi-skills/close-issue/SKILL.md +90 -0
- package/plugin/pi-skills/create-pr/SKILL.md +99 -0
- package/plugin/pi-skills/implement-issue/SKILL.md +135 -0
- package/plugin/pi-skills/investigate-sentry-issue/SKILL.md +138 -0
- package/plugin/pi-skills/respond-github/SKILL.md +98 -0
- package/plugin/pi-skills/review-offered-task/SKILL.md +45 -0
- package/plugin/pi-skills/review-pr/SKILL.md +261 -0
- package/plugin/pi-skills/start-leader/SKILL.md +121 -0
- package/plugin/pi-skills/start-worker/SKILL.md +60 -0
- package/plugin/pi-skills/swarm-chat/SKILL.md +82 -0
- package/plugin/pi-skills/todos/SKILL.md +66 -0
- package/plugin/pi-skills/work-on-task/SKILL.md +65 -0
- package/plugin/skills/artifacts/examples/approval-flow.ts +34 -0
- package/plugin/skills/artifacts/examples/hono-dashboard.ts +31 -0
- package/plugin/skills/artifacts/examples/multi-artifact.ts +20 -0
- package/plugin/skills/artifacts/examples/static-report.sh +17 -0
- package/plugin/skills/artifacts/skill.md +71 -0
- package/prek.toml +75 -0
- package/scripts/check-db-boundary.sh +60 -0
- package/scripts/e2e-docker-provider.ts +820 -0
- package/scripts/e2e-io-schemas-test.ts +807 -0
- package/scripts/e2e-provider-test.ts +220 -0
- package/scripts/e2e-workflow-redesign.sh +229 -0
- package/scripts/e2e-workflow-test.sh +285 -0
- package/scripts/e2e-workflow-test.ts +857 -0
- package/scripts/generate-openapi.ts +26 -0
- package/scripts/measure-tool-tokens.ts +118 -0
- package/scripts/x402-e2e-test.ts +195 -0
- package/scripts/x402-test-server.ts +236 -0
- package/scripts/x402-testnet-e2e.ts +668 -0
- package/slack-manifest.json +17 -0
- package/src/agentmail/app.ts +65 -0
- package/src/agentmail/handlers.ts +207 -0
- package/src/agentmail/index.ts +9 -0
- package/src/agentmail/types.ts +51 -0
- package/src/artifact-sdk/browser-sdk.ts +30 -0
- package/src/artifact-sdk/index.ts +2 -0
- package/src/artifact-sdk/localtunnel.d.ts +20 -0
- package/src/artifact-sdk/port.ts +12 -0
- package/src/artifact-sdk/server.ts +156 -0
- package/src/artifact-sdk/tunnel.ts +19 -0
- package/src/be/chunking.ts +193 -0
- package/src/be/db-queries/oauth.ts +90 -0
- package/src/be/db-queries/tracker.ts +182 -0
- package/src/be/db.ts +2729 -770
- package/src/be/embedding.ts +80 -0
- package/src/be/migrations/001_initial.sql +409 -0
- package/src/be/migrations/002_one_time_schedules.sql +59 -0
- package/src/be/migrations/003_workflows.sql +51 -0
- package/src/be/migrations/004_workflow_source.sql +81 -0
- package/src/be/migrations/005_epic_next_steps.sql +2 -0
- package/src/be/migrations/006_vcs_provider.sql +94 -0
- package/src/be/migrations/007_task_dir.sql +2 -0
- package/src/be/migrations/008_workflow_redesign.sql +85 -0
- package/src/be/migrations/009_tracker_integration.sql +144 -0
- package/src/be/migrations/010_step_diagnostics.sql +1 -0
- package/src/be/migrations/011_step_next_port.sql +1 -0
- package/src/be/migrations/012_trigger_schema.sql +1 -0
- package/src/be/migrations/013_task_output_schema.sql +2 -0
- package/src/be/migrations/runner.ts +188 -0
- package/src/cli.tsx +231 -299
- package/src/commands/artifact.ts +241 -0
- package/src/commands/onboard/compose-generator.ts +169 -0
- package/src/commands/onboard/env-generator.ts +79 -0
- package/src/commands/onboard/manifest.ts +37 -0
- package/src/commands/onboard/presets.ts +85 -0
- package/src/commands/onboard/service-names.ts +47 -0
- package/src/commands/onboard/steps/core-credentials.tsx +111 -0
- package/src/commands/onboard/steps/custom-templates.tsx +168 -0
- package/src/commands/onboard/steps/generate.tsx +154 -0
- package/src/commands/onboard/steps/harness-credentials.tsx +195 -0
- package/src/commands/onboard/steps/harness.tsx +21 -0
- package/src/commands/onboard/steps/health-check.tsx +171 -0
- package/src/commands/onboard/steps/integration-github.tsx +105 -0
- package/src/commands/onboard/steps/integration-gitlab.tsx +79 -0
- package/src/commands/onboard/steps/integration-menu.tsx +58 -0
- package/src/commands/onboard/steps/integration-sentry.tsx +79 -0
- package/src/commands/onboard/steps/integration-slack.tsx +165 -0
- package/src/commands/onboard/steps/post-connect.tsx +145 -0
- package/src/commands/onboard/steps/post-dashboard.tsx +34 -0
- package/src/commands/onboard/steps/post-task.tsx +103 -0
- package/src/commands/onboard/steps/prereq-check.tsx +178 -0
- package/src/commands/onboard/steps/review.tsx +82 -0
- package/src/commands/onboard/steps/start.tsx +97 -0
- package/src/commands/onboard/templates.ts +34 -0
- package/src/commands/onboard/types.ts +259 -0
- package/src/commands/onboard.tsx +425 -0
- package/src/commands/runner.ts +1376 -559
- package/src/commands/setup.tsx +23 -38
- package/src/commands/shared/client-config.ts +41 -0
- package/src/github/app.ts +8 -0
- package/src/github/handlers.ts +93 -75
- package/src/github/index.ts +1 -0
- package/src/github/types.ts +1 -0
- package/src/gitlab/auth.ts +63 -0
- package/src/gitlab/handlers.ts +327 -0
- package/src/gitlab/index.ts +19 -0
- package/src/gitlab/reactions.ts +104 -0
- package/src/gitlab/types.ts +130 -0
- package/src/heartbeat/heartbeat.ts +425 -0
- package/src/heartbeat/index.ts +1 -0
- package/src/hooks/hook.ts +555 -4
- package/src/hooks/tool-loop-detection.test.ts +158 -0
- package/src/hooks/tool-loop-detection.ts +167 -0
- package/src/http/active-sessions.ts +172 -0
- package/src/http/agents.ts +328 -0
- package/src/http/config.ts +191 -0
- package/src/http/core.ts +309 -0
- package/src/http/db-query.ts +91 -0
- package/src/http/ecosystem.ts +63 -0
- package/src/http/epics.ts +460 -0
- package/src/http/index.ts +213 -0
- package/src/http/mcp.ts +77 -0
- package/src/http/memory.ts +168 -0
- package/src/http/openapi.ts +109 -0
- package/src/http/poll.ts +178 -0
- package/src/http/repos.ts +195 -0
- package/src/http/route-def.ts +123 -0
- package/src/http/schedules.ts +391 -0
- package/src/http/session-data.ts +241 -0
- package/src/http/stats.ts +174 -0
- package/src/http/tasks.ts +468 -0
- package/src/http/trackers/index.ts +10 -0
- package/src/http/trackers/linear.ts +187 -0
- package/src/http/types.ts +12 -0
- package/src/http/utils.ts +87 -0
- package/src/http/webhooks.ts +432 -0
- package/src/http/workflows.ts +522 -0
- package/src/http.ts +1 -1890
- package/src/linear/README.md +65 -0
- package/src/linear/app.ts +48 -0
- package/src/linear/client.ts +18 -0
- package/src/linear/index.ts +1 -0
- package/src/linear/oauth.ts +35 -0
- package/src/linear/outbound.ts +212 -0
- package/src/linear/sync.ts +543 -0
- package/src/linear/types.ts +7 -0
- package/src/linear/webhook.ts +104 -0
- package/src/oauth/README.md +66 -0
- package/src/oauth/index.ts +6 -0
- package/src/oauth/wrapper.ts +204 -0
- package/src/prompts/base-prompt.ts +366 -24
- package/src/prompts/defaults.ts +196 -0
- package/src/providers/claude-adapter.ts +429 -0
- package/src/providers/index.ts +24 -0
- package/src/providers/pi-mono-adapter.ts +442 -0
- package/src/providers/pi-mono-extension.ts +620 -0
- package/src/providers/pi-mono-mcp-client.ts +124 -0
- package/src/providers/types.ts +75 -0
- package/src/scheduler/scheduler.test.ts +2 -0
- package/src/scheduler/scheduler.ts +186 -29
- package/src/server.ts +82 -6
- package/src/slack/HEURISTICS.md +105 -0
- package/src/slack/actions.ts +133 -0
- package/src/slack/app.ts +7 -0
- package/src/slack/assistant.ts +115 -0
- package/src/slack/blocks.ts +233 -0
- package/src/slack/commands.ts +31 -17
- package/src/slack/files.ts +1 -1
- package/src/slack/handlers.test.ts +114 -1
- package/src/slack/handlers.ts +212 -52
- package/src/slack/responses.ts +120 -67
- package/src/slack/router.ts +17 -99
- package/src/slack/thread-buffer.ts +213 -0
- package/src/slack/watcher.ts +119 -4
- package/src/tests/agent-activity.test.ts +247 -0
- package/src/tests/agentmail-filters.test.ts +97 -0
- package/src/tests/artifact-sdk.test.ts +800 -0
- package/src/tests/base-prompt.test.ts +264 -0
- package/src/tests/build-pi-skills.test.ts +127 -0
- package/src/tests/claude-adapter.test.ts +126 -0
- package/src/tests/context-versioning.test.ts +425 -0
- package/src/tests/db-queries-oauth.test.ts +197 -0
- package/src/tests/db-queries-tracker.test.ts +230 -0
- package/src/tests/epics.test.ts +3 -3
- package/src/tests/error-tracker.test.ts +368 -0
- package/src/tests/fetch-resolved-env.test.ts +167 -0
- package/src/tests/generate-default-claude-md.test.ts +9 -1
- package/src/tests/generate-identity-templates.test.ts +124 -0
- package/src/tests/gitlab-auth.test.ts +109 -0
- package/src/tests/gitlab-handlers.test.ts +691 -0
- package/src/tests/gitlab-vcs-db.test.ts +177 -0
- package/src/tests/heartbeat.test.ts +363 -0
- package/src/tests/http-api-integration.test.ts +1698 -0
- package/src/tests/linear-outbound-sync.test.ts +200 -0
- package/src/tests/linear-webhook.test.ts +402 -0
- package/src/tests/match-route.test.ts +187 -0
- package/src/tests/memory.test.ts +737 -0
- package/src/tests/migration-runner-regressions.test.ts +86 -0
- package/src/tests/model-control.test.ts +338 -0
- package/src/tests/oauth-wrapper.test.ts +147 -0
- package/src/tests/onboard-compose.test.ts +138 -0
- package/src/tests/onboard-env.test.ts +174 -0
- package/src/tests/onboard-manifest.test.ts +137 -0
- package/src/tests/pi-mono-adapter.test.ts +234 -0
- package/src/tests/progress-dedup.test.ts +98 -0
- package/src/tests/provider-adapter.test.ts +122 -0
- package/src/tests/provider-command-format.test.ts +98 -0
- package/src/tests/reload-config.test.ts +170 -0
- package/src/tests/runner-polling-api.test.ts +25 -20
- package/src/tests/scheduled-tasks.test.ts +104 -0
- package/src/tests/scheduler-backoff.test.ts +166 -0
- package/src/tests/self-improvement.test.ts +540 -0
- package/src/tests/session-attach.test.ts +536 -0
- package/src/tests/session-costs.test.ts +267 -1
- package/src/tests/slack-actions.test.ts +133 -0
- package/src/tests/slack-assistant.test.ts +136 -0
- package/src/tests/slack-blocks.test.ts +246 -0
- package/src/tests/slack-metadata-inheritance.test.ts +243 -0
- package/src/tests/slack-queue-offline.test.ts +174 -0
- package/src/tests/slack-router.test.ts +181 -0
- package/src/tests/slack-thread-buffer.test.ts +305 -0
- package/src/tests/slack-thread-followups.test.ts +298 -0
- package/src/tests/slack-watcher.test.ts +101 -0
- package/src/tests/structured-output.test.ts +311 -0
- package/src/tests/swarm-repos.test.ts +198 -0
- package/src/tests/task-cancellation.test.ts +6 -4
- package/src/tests/task-working-dir.test.ts +176 -0
- package/src/tests/template-fetch.test.ts +490 -0
- package/src/tests/tool-annotations.test.ts +371 -0
- package/src/tests/tracker-tools.test.ts +184 -0
- package/src/tests/update-profile-api.test.ts +143 -3
- package/src/tests/validation-adapters.test.ts +86 -0
- package/src/tests/vcs-provider.test.ts +27 -0
- package/src/tests/workflow-agent-task.test.ts +196 -0
- package/src/tests/workflow-async-v2.test.ts +386 -0
- package/src/tests/workflow-convergence.test.ts +541 -0
- package/src/tests/workflow-definition-validation.test.ts +290 -0
- package/src/tests/workflow-engine-v2.test.ts +691 -0
- package/src/tests/workflow-executors.test.ts +736 -0
- package/src/tests/workflow-http-v2.test.ts +599 -0
- package/src/tests/workflow-integration-io.test.ts +902 -0
- package/src/tests/workflow-io-schemas.test.ts +624 -0
- package/src/tests/workflow-registry.test.ts +592 -0
- package/src/tests/workflow-retry-v2.test.ts +401 -0
- package/src/tests/workflow-retry-validation.test.ts +282 -0
- package/src/tests/workflow-template.test.ts +288 -0
- package/src/tests/workflow-trigger-schema.test.ts +359 -0
- package/src/tests/workflow-triggers-v2.test.ts +264 -0
- package/src/tests/workflow-versions.test.ts +208 -0
- package/src/tests/x402-client.test.ts +117 -0
- package/src/tests/x402-config.test.ts +182 -0
- package/src/tests/x402-spending-tracker.test.ts +185 -0
- package/src/tools/cancel-task.ts +2 -0
- package/src/tools/context-diff.ts +171 -0
- package/src/tools/context-history.ts +138 -0
- package/src/tools/create-channel.ts +1 -0
- package/src/tools/db-query.ts +78 -0
- package/src/tools/delete-channel.ts +132 -0
- package/src/tools/epics/assign-task-to-epic.ts +1 -0
- package/src/tools/epics/create-epic.ts +3 -2
- package/src/tools/epics/delete-epic.ts +2 -0
- package/src/tools/epics/get-epic-details.ts +2 -0
- package/src/tools/epics/list-epics.ts +2 -0
- package/src/tools/epics/unassign-task-from-epic.ts +1 -0
- package/src/tools/epics/update-epic.ts +7 -4
- package/src/tools/get-swarm.ts +2 -0
- package/src/tools/get-task-details.ts +2 -0
- package/src/tools/get-tasks.ts +27 -1
- package/src/tools/inject-learning.ts +106 -0
- package/src/tools/join-swarm.ts +17 -7
- package/src/tools/list-channels.ts +2 -0
- package/src/tools/list-services.ts +2 -0
- package/src/tools/memory-get.ts +56 -0
- package/src/tools/memory-search.ts +131 -0
- package/src/tools/my-agent-info.ts +2 -0
- package/src/tools/poll-task.ts +2 -20
- package/src/tools/post-message.ts +1 -0
- package/src/tools/read-messages.ts +2 -0
- package/src/tools/register-agentmail-inbox.ts +166 -0
- package/src/tools/register-service.ts +2 -0
- package/src/tools/schedules/create-schedule.ts +134 -24
- package/src/tools/schedules/delete-schedule.ts +2 -0
- package/src/tools/schedules/list-schedules.ts +20 -4
- package/src/tools/schedules/run-schedule-now.ts +1 -0
- package/src/tools/schedules/update-schedule.ts +49 -17
- package/src/tools/send-task.ts +132 -10
- package/src/tools/slack-download-file.ts +4 -2
- package/src/tools/slack-list-channels.ts +2 -0
- package/src/tools/slack-post.ts +2 -0
- package/src/tools/slack-read.ts +2 -0
- package/src/tools/slack-reply.ts +2 -0
- package/src/tools/slack-upload-file.ts +2 -0
- package/src/tools/store-progress.ts +187 -4
- package/src/tools/swarm-config/delete-config.ts +87 -0
- package/src/tools/swarm-config/get-config.ts +108 -0
- package/src/tools/swarm-config/index.ts +4 -0
- package/src/tools/swarm-config/list-config.ts +99 -0
- package/src/tools/swarm-config/set-config.ts +118 -0
- package/src/tools/task-action.ts +30 -5
- package/src/tools/task-dedup.ts +97 -0
- package/src/tools/tool-config.ts +117 -0
- package/src/tools/tracker/index.ts +6 -0
- package/src/tools/tracker/tracker-link-epic.ts +64 -0
- package/src/tools/tracker/tracker-link-task.ts +64 -0
- package/src/tools/tracker/tracker-map-agent.ts +57 -0
- package/src/tools/tracker/tracker-status.ts +56 -0
- package/src/tools/tracker/tracker-sync-status.ts +42 -0
- package/src/tools/tracker/tracker-unlink.ts +41 -0
- package/src/tools/unregister-service.ts +2 -0
- package/src/tools/update-profile.ts +93 -10
- package/src/tools/update-service-status.ts +2 -0
- package/src/tools/utils.ts +10 -1
- package/src/tools/workflows/create-workflow.ts +111 -0
- package/src/tools/workflows/delete-workflow.ts +42 -0
- package/src/tools/workflows/get-workflow-run.ts +59 -0
- package/src/tools/workflows/get-workflow.ts +53 -0
- package/src/tools/workflows/index.ts +9 -0
- package/src/tools/workflows/list-workflow-runs.ts +48 -0
- package/src/tools/workflows/list-workflows.ts +42 -0
- package/src/tools/workflows/retry-workflow-run.ts +40 -0
- package/src/tools/workflows/trigger-workflow.ts +96 -0
- package/src/tools/workflows/update-workflow.ts +118 -0
- package/src/tracker/types.ts +51 -0
- package/src/types.ts +477 -14
- package/src/utils/credentials.test.ts +156 -0
- package/src/utils/credentials.ts +50 -0
- package/src/utils/error-tracker.ts +190 -0
- package/src/vcs/index.ts +15 -0
- package/src/vcs/types.ts +5 -0
- package/src/workflows/checkpoint.ts +121 -0
- package/src/workflows/cooldown.ts +28 -0
- package/src/workflows/definition.ts +217 -0
- package/src/workflows/engine.ts +554 -0
- package/src/workflows/event-bus.ts +29 -0
- package/src/workflows/executors/agent-task.ts +92 -0
- package/src/workflows/executors/base.ts +86 -0
- package/src/workflows/executors/code-match.ts +88 -0
- package/src/workflows/executors/index.ts +16 -0
- package/src/workflows/executors/notify.ts +93 -0
- package/src/workflows/executors/property-match.ts +104 -0
- package/src/workflows/executors/raw-llm.ts +83 -0
- package/src/workflows/executors/registry.ts +76 -0
- package/src/workflows/executors/script.ts +103 -0
- package/src/workflows/executors/validate.ts +215 -0
- package/src/workflows/executors/vcs.ts +58 -0
- package/src/workflows/index.ts +61 -0
- package/src/workflows/input.ts +46 -0
- package/src/workflows/json-schema-validator.ts +118 -0
- package/src/workflows/recovery.ts +139 -0
- package/src/workflows/resume.ts +143 -0
- package/src/workflows/retry-poller.ts +216 -0
- package/src/workflows/template.ts +74 -0
- package/src/workflows/templates.ts +86 -0
- package/src/workflows/triggers.ts +95 -0
- package/src/workflows/validation.ts +104 -0
- package/src/workflows/version.ts +42 -0
- package/src/x402/cli.ts +140 -0
- package/src/x402/client.ts +192 -0
- package/src/x402/config.ts +131 -0
- package/src/x402/index.ts +37 -0
- package/src/x402/openfort-signer.ts +83 -0
- package/src/x402/spending-tracker.ts +109 -0
- package/templates/community/.gitkeep +0 -0
- package/templates/official/coder/CLAUDE.md +49 -0
- package/templates/official/coder/IDENTITY.md +28 -0
- package/templates/official/coder/SOUL.md +43 -0
- package/templates/official/coder/TOOLS.md +40 -0
- package/templates/official/coder/config.json +23 -0
- package/templates/official/coder/start-up.sh +23 -0
- package/templates/official/content-reviewer/CLAUDE.md +68 -0
- package/templates/official/content-reviewer/IDENTITY.md +28 -0
- package/templates/official/content-reviewer/SOUL.md +44 -0
- package/templates/official/content-reviewer/TOOLS.md +37 -0
- package/templates/official/content-reviewer/config.json +23 -0
- package/templates/official/content-reviewer/start-up.sh +23 -0
- package/templates/official/content-strategist/CLAUDE.md +63 -0
- package/templates/official/content-strategist/IDENTITY.md +33 -0
- package/templates/official/content-strategist/SOUL.md +48 -0
- package/templates/official/content-strategist/TOOLS.md +47 -0
- package/templates/official/content-strategist/config.json +23 -0
- package/templates/official/content-strategist/start-up.sh +23 -0
- package/templates/official/content-writer/CLAUDE.md +72 -0
- package/templates/official/content-writer/IDENTITY.md +30 -0
- package/templates/official/content-writer/SOUL.md +46 -0
- package/templates/official/content-writer/TOOLS.md +44 -0
- package/templates/official/content-writer/config.json +23 -0
- package/templates/official/content-writer/start-up.sh +23 -0
- package/templates/official/forward-deployed-engineer/CLAUDE.md +54 -0
- package/templates/official/forward-deployed-engineer/IDENTITY.md +37 -0
- package/templates/official/forward-deployed-engineer/SOUL.md +55 -0
- package/templates/official/forward-deployed-engineer/config.json +21 -0
- package/templates/official/lead/CLAUDE.md +33 -0
- package/templates/official/lead/IDENTITY.md +36 -0
- package/templates/official/lead/SOUL.md +51 -0
- package/templates/official/lead/config.json +22 -0
- package/templates/official/researcher/CLAUDE.md +46 -0
- package/templates/official/researcher/IDENTITY.md +28 -0
- package/templates/official/researcher/SOUL.md +43 -0
- package/templates/official/researcher/config.json +21 -0
- package/templates/official/reviewer/CLAUDE.md +63 -0
- package/templates/official/reviewer/IDENTITY.md +28 -0
- package/templates/official/reviewer/SOUL.md +45 -0
- package/templates/official/reviewer/config.json +21 -0
- package/templates/official/tester/CLAUDE.md +53 -0
- package/templates/official/tester/IDENTITY.md +28 -0
- package/templates/official/tester/SOUL.md +55 -0
- package/templates/official/tester/config.json +21 -0
- package/templates/schema.ts +35 -0
- package/templates-ui/README.md +46 -0
- package/templates-ui/components.json +17 -0
- package/templates-ui/eslint.config.mjs +18 -0
- package/templates-ui/next.config.ts +7 -0
- package/templates-ui/package.json +35 -0
- package/templates-ui/pnpm-lock.yaml +4571 -0
- package/templates-ui/postcss.config.mjs +7 -0
- package/templates-ui/public/file.svg +1 -0
- package/templates-ui/public/globe.svg +1 -0
- package/templates-ui/public/logo.png +0 -0
- package/templates-ui/public/next.svg +1 -0
- package/templates-ui/public/vercel.svg +1 -0
- package/templates-ui/public/window.svg +1 -0
- package/templates-ui/src/app/[category]/[name]/page.tsx +89 -0
- package/templates-ui/src/app/api/templates/[...slug]/route.ts +52 -0
- package/templates-ui/src/app/api/templates/route.ts +18 -0
- package/templates-ui/src/app/builder/page.tsx +37 -0
- package/templates-ui/src/app/globals.css +94 -0
- package/templates-ui/src/app/layout.tsx +79 -0
- package/templates-ui/src/app/page.tsx +38 -0
- package/templates-ui/src/app/robots.ts +11 -0
- package/templates-ui/src/app/sitemap.ts +31 -0
- package/templates-ui/src/components/compose-builder.tsx +442 -0
- package/templates-ui/src/components/compose-preview.tsx +117 -0
- package/templates-ui/src/components/file-preview.tsx +77 -0
- package/templates-ui/src/components/footer.tsx +40 -0
- package/templates-ui/src/components/header.tsx +41 -0
- package/templates-ui/src/components/template-card.tsx +87 -0
- package/templates-ui/src/components/template-detail.tsx +125 -0
- package/templates-ui/src/components/template-gallery.tsx +263 -0
- package/templates-ui/src/components/ui/badge.tsx +36 -0
- package/templates-ui/src/components/ui/button.tsx +57 -0
- package/templates-ui/src/components/ui/card.tsx +76 -0
- package/templates-ui/src/components/ui/separator.tsx +31 -0
- package/templates-ui/src/components/ui/tooltip.tsx +32 -0
- package/templates-ui/src/lib/compose-generator.ts +241 -0
- package/templates-ui/src/lib/templates.ts +137 -0
- package/templates-ui/src/lib/utils.ts +6 -0
- package/templates-ui/tsconfig.json +34 -0
- package/thoughts/research/2026-02-28-openfort-viem-x402-research.md +679 -0
- package/thoughts/research/2026-02-28-x402-payments-research.md +686 -0
- package/thoughts/researcher/plans/2026-02-20-agent-self-improvement-plan.md +282 -0
- package/thoughts/researcher/research/2026-02-20-agent-self-improvement.md +492 -0
- package/thoughts/shared/plans/2026-02-26-mcp-tool-context-reduction.md +282 -0
- package/thoughts/shared/plans/2026-03-02-claude-context-mode-integration.md +328 -0
- package/thoughts/shared/plans/2026-03-02-code-level-heartbeat.md +224 -0
- package/thoughts/shared/research/2026-02-26-cliffy-mcp-tools.md +159 -0
- package/thoughts/shared/research/2026-03-03-database-migration-system-refactor.md +337 -0
- package/thoughts/swarm-researcher/plans/2026-02-23-openclaw-improvements-plan.md +778 -0
- package/thoughts/swarm-researcher/plans/2026-02-26-artifacts-localtunnel-plan.md +1269 -0
- package/thoughts/swarm-researcher/research/2026-02-23-openclaw-vs-agent-swarm-comparison.md +411 -0
- package/thoughts/swarm-researcher/research/2026-02-26-artifacts-localtunnel.md +724 -0
- package/thoughts/taras/brainstorms/2026-03-20-prompt-template-registry.md +443 -0
- package/thoughts/taras/brainstorms/2026-03-20-setup-cli-onboarding.md +307 -0
- package/thoughts/taras/plans/2026-02-20-auto-improvement.md +803 -0
- package/thoughts/taras/plans/2026-02-20-env-management.md +538 -0
- package/thoughts/taras/plans/2026-02-20-memory-system.md +882 -0
- package/thoughts/taras/plans/2026-02-20-repos-knowledge.md +806 -0
- package/thoughts/taras/plans/2026-02-20-session-attach.md +647 -0
- package/thoughts/taras/plans/2026-02-20-worker-identity.md +820 -0
- package/thoughts/taras/plans/2026-02-25-feat-new-ui-visual-redesign-plan.md +768 -0
- package/thoughts/taras/plans/2026-03-04-fix-buildSystemPrompt-missing-fields.md +77 -0
- package/thoughts/taras/plans/2026-03-04-new-ui-missing-actions.md +543 -0
- package/thoughts/taras/plans/2026-03-06-one-time-scheduled-tasks.md +373 -0
- package/thoughts/taras/plans/2026-03-08-memory-self-improvement-enhancements.md +512 -0
- package/thoughts/taras/plans/2026-03-08-pi-mono-provider-implementation.md +919 -0
- package/thoughts/taras/plans/2026-03-09-templates-registry.md +723 -0
- package/thoughts/taras/plans/2026-03-10-task-working-directory.md +371 -0
- package/thoughts/taras/plans/2026-03-11-archil-per-agent-write-strategy.md +621 -0
- package/thoughts/taras/plans/2026-03-12-eliminate-inbox-route-to-tasks.md +61 -0
- package/thoughts/taras/plans/2026-03-12-slack-thread-followup-additive.md +488 -0
- package/thoughts/taras/plans/2026-03-13-slack-ai-improvements.md +644 -0
- package/thoughts/taras/plans/2026-03-16-route-wrapper-openapi.md +636 -0
- package/thoughts/taras/plans/2026-03-17-multi-api-config.md +444 -0
- package/thoughts/taras/plans/2026-03-18-agent-fs-integration.md +591 -0
- package/thoughts/taras/plans/2026-03-18-debug-db-explorer.md +446 -0
- package/thoughts/taras/plans/2026-03-18-workflow-redesign.md +987 -0
- package/thoughts/taras/plans/2026-03-19-compound-learnings.md +403 -0
- package/thoughts/taras/plans/2026-03-19-ticket-tracker-linear-integration.md +860 -0
- package/thoughts/taras/plans/2026-03-19-workflow-io-schemas-and-bugs.md +899 -0
- package/thoughts/taras/plans/2026-03-20-setup-cli-onboarding.md +874 -0
- package/thoughts/taras/plans/2026-03-20-workflow-structured-output-validation-workspace.md +723 -0
- package/thoughts/taras/research/2026-02-19-agent-native-swarm-architecture.md +390 -0
- package/thoughts/taras/research/2026-02-19-swarm-gaps-implementation.md +594 -0
- package/thoughts/taras/research/2026-02-25-dashboard-ui-design-best-practices.md +825 -0
- package/thoughts/taras/research/2026-02-26-task-detail-page-redesign.md +393 -0
- package/thoughts/taras/research/2026-03-03-new-ui-missing-actions.md +168 -0
- package/thoughts/taras/research/2026-03-05-pi-mono-provider-research.md +230 -0
- package/thoughts/taras/research/2026-03-06-workflow-engine-design.md +445 -0
- package/thoughts/taras/research/2026-03-08-drive-loop-concept.md +375 -0
- package/thoughts/taras/research/2026-03-08-pi-mono-deep-dive.md +869 -0
- package/thoughts/taras/research/2026-03-09-templates-registry.md +373 -0
- package/thoughts/taras/research/2026-03-10-agent-working-directory.md +223 -0
- package/thoughts/taras/research/2026-03-10-configurable-event-prompts.md +339 -0
- package/thoughts/taras/research/2026-03-11-archil-production-setup.md +181 -0
- package/thoughts/taras/research/2026-03-11-archil-shared-disk-write-strategies.md +437 -0
- package/thoughts/taras/research/2026-03-13-slack-ai-features.md +258 -0
- package/thoughts/taras/research/2026-03-16-openapi-docs-generation.md +335 -0
- package/thoughts/taras/research/2026-03-16-route-wrapper-openapi.md +670 -0
- package/thoughts/taras/research/2026-03-16-slack-thread-followups-e2e.md +54 -0
- package/thoughts/taras/research/2026-03-18-agent-fs-integration.md +558 -0
- package/thoughts/taras/research/2026-03-18-linear-integration-finalization.md +526 -0
- package/thoughts/taras/research/2026-03-18-workflow-redesign.md +797 -0
- package/thoughts/taras/research/2026-03-19-workflow-node-io-schemas-and-bugs.md +563 -0
- package/thoughts/taras/research/2026-03-19-workflow-structured-output-validation-workspace.md +486 -0
- package/thoughts/taras/research/2026-03-20-prompt-template-registry.md +469 -0
- package/tsconfig.json +1 -1
- package/FAQ.md +0 -19
- package/src/tests/get-inbox-message.test.ts +0 -145
- package/src/tools/get-inbox-message.ts +0 -89
- package/src/tools/inbox-delegate.ts +0 -113
- package/ui/CLAUDE.md +0 -49
- package/ui/bun.lock +0 -771
- package/ui/index.html +0 -22
- package/ui/package-lock.json +0 -5290
- package/ui/package.json +0 -33
- package/ui/pnpm-lock.yaml +0 -3341
- package/ui/postcss.config.js +0 -6
- package/ui/src/App.tsx +0 -63
- package/ui/src/components/ActivityFeed.tsx +0 -440
- package/ui/src/components/AgentDetailPanel.tsx +0 -733
- package/ui/src/components/AgentsPanel.tsx +0 -815
- package/ui/src/components/ChatPanel.tsx +0 -1920
- package/ui/src/components/ConfigModal.tsx +0 -253
- package/ui/src/components/Dashboard.tsx +0 -832
- package/ui/src/components/EditAgentProfileModal.tsx +0 -433
- package/ui/src/components/EpicDetailPage.tsx +0 -741
- package/ui/src/components/EpicsPanel.tsx +0 -566
- package/ui/src/components/Header.tsx +0 -160
- package/ui/src/components/JsonViewer.tsx +0 -171
- package/ui/src/components/ScheduledTaskDetailPanel.tsx +0 -517
- package/ui/src/components/ScheduledTasksPanel.tsx +0 -639
- package/ui/src/components/ServicesPanel.tsx +0 -622
- package/ui/src/components/SessionLogPanel.tsx +0 -1219
- package/ui/src/components/StatsBar.tsx +0 -321
- package/ui/src/components/StatusBadge.tsx +0 -168
- package/ui/src/components/TaskDetailPanel.tsx +0 -903
- package/ui/src/components/TasksPanel.tsx +0 -614
- package/ui/src/components/UsageCharts.tsx +0 -216
- package/ui/src/components/UsageTab.tsx +0 -394
- package/ui/src/hooks/queries.ts +0 -353
- package/ui/src/index.css +0 -257
- package/ui/src/lib/api.ts +0 -268
- package/ui/src/lib/config.ts +0 -35
- package/ui/src/lib/contentPreview.ts +0 -208
- package/ui/src/lib/theme.ts +0 -214
- package/ui/src/main.tsx +0 -28
- package/ui/src/vite-env.d.ts +0 -1
- package/ui/tailwind.config.js +0 -37
- package/ui/vite.config.ts +0 -35
- /package/{ui → docs-site}/public/logo.png +0 -0
package/src/commands/runner.ts
CHANGED
|
@@ -1,34 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import type { TemplateResponse } from "../../templates/schema.ts";
|
|
4
|
+
import { type BasePromptArgs, getBasePrompt } from "../prompts/base-prompt.ts";
|
|
5
|
+
import {
|
|
6
|
+
generateDefaultClaudeMd,
|
|
7
|
+
generateDefaultIdentityMd,
|
|
8
|
+
generateDefaultSoulMd,
|
|
9
|
+
generateDefaultToolsMd,
|
|
10
|
+
} from "../prompts/defaults.ts";
|
|
11
|
+
import {
|
|
12
|
+
type CostData,
|
|
13
|
+
createProviderAdapter,
|
|
14
|
+
type ProviderResult,
|
|
15
|
+
type ProviderSession,
|
|
16
|
+
type ProviderSessionConfig,
|
|
17
|
+
} from "../providers/index.ts";
|
|
18
|
+
import { resolveCredentialPools } from "../utils/credentials.ts";
|
|
3
19
|
import { prettyPrintLine, prettyPrintStderr } from "../utils/pretty-print.ts";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
interface TaskFileData {
|
|
7
|
-
taskId: string;
|
|
8
|
-
agentId: string;
|
|
9
|
-
startedAt: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Get the task file path for a given PID */
|
|
13
|
-
function getTaskFilePath(pid: number): string {
|
|
14
|
-
return `/tmp/agent-swarm-task-${pid}.json`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Write task file before spawning Claude process */
|
|
18
|
-
async function writeTaskFile(pid: number, data: TaskFileData): Promise<string> {
|
|
19
|
-
const filePath = getTaskFilePath(pid);
|
|
20
|
-
await writeFile(filePath, JSON.stringify(data, null, 2));
|
|
21
|
-
return filePath;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Clean up task file after process exits */
|
|
25
|
-
async function cleanupTaskFile(pid: number): Promise<void> {
|
|
26
|
-
try {
|
|
27
|
-
await unlink(getTaskFilePath(pid));
|
|
28
|
-
} catch {
|
|
29
|
-
// File might already be deleted or never created - ignore
|
|
30
|
-
}
|
|
31
|
-
}
|
|
20
|
+
import { detectVcsProvider } from "../vcs/index.ts";
|
|
21
|
+
import { interpolate } from "../workflows/template.ts";
|
|
32
22
|
|
|
33
23
|
/** Save PM2 process list for persistence across container restarts */
|
|
34
24
|
async function savePm2State(role: string): Promise<void> {
|
|
@@ -41,6 +31,90 @@ async function savePm2State(role: string): Promise<void> {
|
|
|
41
31
|
}
|
|
42
32
|
}
|
|
43
33
|
|
|
34
|
+
/** Fetch repo config for a task's vcsRepo (e.g., "desplega-ai/agent-swarm") */
|
|
35
|
+
async function fetchRepoConfig(
|
|
36
|
+
apiUrl: string,
|
|
37
|
+
apiKey: string,
|
|
38
|
+
vcsRepo: string,
|
|
39
|
+
): Promise<{ url: string; name: string; clonePath: string; defaultBranch: string } | null> {
|
|
40
|
+
try {
|
|
41
|
+
const repoName = vcsRepo.split("/").pop() || vcsRepo;
|
|
42
|
+
const resp = await fetch(`${apiUrl}/api/repos?name=${encodeURIComponent(repoName)}`, {
|
|
43
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
44
|
+
});
|
|
45
|
+
if (!resp.ok) return null;
|
|
46
|
+
const data = (await resp.json()) as {
|
|
47
|
+
repos: Array<{ url: string; name: string; clonePath: string; defaultBranch: string }>;
|
|
48
|
+
};
|
|
49
|
+
return data.repos.find((r) => r.url.includes(vcsRepo)) ?? data.repos[0] ?? null;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Read CLAUDE.md from a repo directory, returning null if not found */
|
|
56
|
+
async function readClaudeMd(clonePath: string, role: string): Promise<string | null> {
|
|
57
|
+
const claudeMdFile = Bun.file(`${clonePath}/CLAUDE.md`);
|
|
58
|
+
if (await claudeMdFile.exists()) {
|
|
59
|
+
const content = await claudeMdFile.text();
|
|
60
|
+
console.log(`[${role}] Read CLAUDE.md from ${clonePath}/CLAUDE.md (${content.length} chars)`);
|
|
61
|
+
return content;
|
|
62
|
+
}
|
|
63
|
+
console.log(`[${role}] No CLAUDE.md found at ${clonePath}/CLAUDE.md`);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Ensure a repo is cloned and up-to-date for a task.
|
|
69
|
+
* Returns { clonePath, claudeMd, warning }.
|
|
70
|
+
*/
|
|
71
|
+
async function ensureRepoForTask(
|
|
72
|
+
repoConfig: { url: string; name: string; clonePath: string; defaultBranch: string },
|
|
73
|
+
role: string,
|
|
74
|
+
): Promise<{ clonePath: string; claudeMd: string | null; warning: string | null }> {
|
|
75
|
+
const { url, name, clonePath, defaultBranch } = repoConfig;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const gitHeadExists = await Bun.file(`${clonePath}/.git/HEAD`).exists();
|
|
79
|
+
|
|
80
|
+
let warning: string | null = null;
|
|
81
|
+
|
|
82
|
+
if (!gitHeadExists) {
|
|
83
|
+
console.log(`[${role}] Cloning ${name} to ${clonePath}...`);
|
|
84
|
+
const provider = detectVcsProvider(url);
|
|
85
|
+
if (provider === "github") {
|
|
86
|
+
await Bun.$`gh repo clone ${url} ${clonePath} -- --branch ${defaultBranch} --single-branch`.quiet();
|
|
87
|
+
} else if (provider === "gitlab") {
|
|
88
|
+
await Bun.$`glab repo clone ${url} ${clonePath} -- --branch ${defaultBranch} --single-branch`.quiet();
|
|
89
|
+
} else {
|
|
90
|
+
await Bun.$`git clone --branch ${defaultBranch} --single-branch ${url} ${clonePath}`.quiet();
|
|
91
|
+
}
|
|
92
|
+
console.log(`[${role}] Cloned ${name}`);
|
|
93
|
+
} else {
|
|
94
|
+
console.log(`[${role}] Repo ${name} already cloned at ${clonePath}`);
|
|
95
|
+
const statusResult = await Bun.$`cd ${clonePath} && git status --porcelain`.quiet();
|
|
96
|
+
const statusOutput = statusResult.text().trim();
|
|
97
|
+
|
|
98
|
+
if (statusOutput === "") {
|
|
99
|
+
console.log(`[${role}] Pulling ${name} (${defaultBranch})...`);
|
|
100
|
+
await Bun.$`cd ${clonePath} && git pull origin ${defaultBranch} --ff-only`.quiet();
|
|
101
|
+
console.log(`[${role}] Pulled ${name}`);
|
|
102
|
+
} else {
|
|
103
|
+
console.warn(`[${role}] Repo ${name} has uncommitted changes, skipping pull`);
|
|
104
|
+
warning = `The repo "${name}" at ${clonePath} has uncommitted changes. A git pull was skipped to avoid losing work. You may need to commit or stash changes before pulling updates.`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const claudeMd = await readClaudeMd(clonePath, role);
|
|
109
|
+
return { clonePath, claudeMd, warning };
|
|
110
|
+
} catch (err) {
|
|
111
|
+
const errorMsg = (err as Error).message;
|
|
112
|
+
console.warn(`[${role}] Error setting up repo ${name}: ${errorMsg}`);
|
|
113
|
+
const warning = `Failed to clone/setup repo "${name}" at ${clonePath}: ${errorMsg}. The repo may not be available. You may need to clone it manually.`;
|
|
114
|
+
return { clonePath, claudeMd: null, warning };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
44
118
|
/** API configuration for ping/close */
|
|
45
119
|
interface ApiConfig {
|
|
46
120
|
apiUrl: string;
|
|
@@ -88,6 +162,115 @@ async function closeAgent(config: ApiConfig, role: string): Promise<void> {
|
|
|
88
162
|
}
|
|
89
163
|
}
|
|
90
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Fetch resolved config from the API and merge into a base env object.
|
|
167
|
+
* Falls back to baseEnv on any error (network, parse, etc).
|
|
168
|
+
* Credential env vars with comma-separated values get one randomly selected.
|
|
169
|
+
*/
|
|
170
|
+
async function fetchResolvedEnv(
|
|
171
|
+
apiUrl: string,
|
|
172
|
+
apiKey: string,
|
|
173
|
+
agentId: string,
|
|
174
|
+
baseEnv: Record<string, string | undefined> = process.env,
|
|
175
|
+
): Promise<Record<string, string | undefined>> {
|
|
176
|
+
const env: Record<string, string | undefined> = { ...baseEnv };
|
|
177
|
+
|
|
178
|
+
if (apiUrl && agentId) {
|
|
179
|
+
try {
|
|
180
|
+
const headers: Record<string, string> = { "X-Agent-ID": agentId };
|
|
181
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
182
|
+
|
|
183
|
+
const url = `${apiUrl}/api/config/resolved?agentId=${encodeURIComponent(agentId)}&includeSecrets=true`;
|
|
184
|
+
const response = await fetch(url, { headers });
|
|
185
|
+
|
|
186
|
+
if (!response.ok) {
|
|
187
|
+
console.warn(`[env-reload] Failed to fetch config: ${response.status}`);
|
|
188
|
+
} else {
|
|
189
|
+
const data = (await response.json()) as {
|
|
190
|
+
configs: Array<{ key: string; value: string }>;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (data.configs?.length) {
|
|
194
|
+
for (const config of data.configs) {
|
|
195
|
+
env[config.key] = config.value;
|
|
196
|
+
}
|
|
197
|
+
console.log(`[env-reload] Loaded ${data.configs.length} config entries from API`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.warn(`[env-reload] Could not fetch config, using current env: ${error}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
resolveCredentialPools(env);
|
|
206
|
+
return env;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Convert a tool call into a human-readable progress description.
|
|
211
|
+
*/
|
|
212
|
+
function toolCallToProgress(toolName: string, args: unknown): string {
|
|
213
|
+
const a = args as Record<string, unknown>;
|
|
214
|
+
const shortPath = (p: unknown) => {
|
|
215
|
+
if (typeof p !== "string") return "";
|
|
216
|
+
// Show last 2 path segments for readability
|
|
217
|
+
const parts = p.split("/");
|
|
218
|
+
return parts.length > 2 ? parts.slice(-2).join("/") : p;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
switch (toolName) {
|
|
222
|
+
case "Read":
|
|
223
|
+
return `Reading ${shortPath(a.file_path)}`;
|
|
224
|
+
case "Edit":
|
|
225
|
+
case "MultiEdit":
|
|
226
|
+
return `Editing ${shortPath(a.file_path)}`;
|
|
227
|
+
case "Write":
|
|
228
|
+
return `Writing ${shortPath(a.file_path)}`;
|
|
229
|
+
case "Bash":
|
|
230
|
+
return a.description ? `${a.description}` : "Running shell command";
|
|
231
|
+
case "Grep":
|
|
232
|
+
return `Searching for "${a.pattern}"`;
|
|
233
|
+
case "Glob":
|
|
234
|
+
return `Finding files matching ${a.pattern}`;
|
|
235
|
+
case "Agent":
|
|
236
|
+
case "Task":
|
|
237
|
+
return a.description ? `${a.description}` : "Delegating sub-task";
|
|
238
|
+
case "Skill":
|
|
239
|
+
return `Running /${a.skill}`;
|
|
240
|
+
default: {
|
|
241
|
+
// MCP tools: mcp__server__tool → "server:tool"
|
|
242
|
+
if (toolName.startsWith("mcp__")) {
|
|
243
|
+
const parts = toolName.split("__");
|
|
244
|
+
return parts.length >= 3 ? `Using ${parts[1]}:${parts[2]}` : `Using ${toolName}`;
|
|
245
|
+
}
|
|
246
|
+
return `Using ${toolName}`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Report task progress via the API (fire-and-forget).
|
|
253
|
+
*/
|
|
254
|
+
async function updateProgressViaAPI(
|
|
255
|
+
apiUrl: string,
|
|
256
|
+
apiKey: string,
|
|
257
|
+
taskId: string,
|
|
258
|
+
progress: string,
|
|
259
|
+
): Promise<void> {
|
|
260
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
261
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
await fetch(`${apiUrl}/api/tasks/${taskId}/progress`, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers,
|
|
267
|
+
body: JSON.stringify({ progress }),
|
|
268
|
+
});
|
|
269
|
+
} catch {
|
|
270
|
+
// Non-blocking — progress update failure is not critical
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
91
274
|
/**
|
|
92
275
|
* Ensure task is marked as completed or failed via the API.
|
|
93
276
|
* This is called when a Claude process exits to ensure task status is updated,
|
|
@@ -96,11 +279,97 @@ async function closeAgent(config: ApiConfig, role: string): Promise<void> {
|
|
|
96
279
|
* The API is idempotent - if the agent already marked the task as completed/failed,
|
|
97
280
|
* this call will succeed without changing anything.
|
|
98
281
|
*/
|
|
282
|
+
/**
|
|
283
|
+
* Attempt to extract structured output from a task's progress history
|
|
284
|
+
* when the agent session ends without calling store-progress with valid output.
|
|
285
|
+
*
|
|
286
|
+
* - Claude adapter: runs a fallback extraction via `claude -p --json-schema`
|
|
287
|
+
* - Pi-mono adapter: returns an error (no fallback available)
|
|
288
|
+
*/
|
|
289
|
+
async function handleStructuredOutputFallback(
|
|
290
|
+
config: ApiConfig,
|
|
291
|
+
taskId: string,
|
|
292
|
+
adapterType: string,
|
|
293
|
+
): Promise<{ output?: string; failReason?: string } | null> {
|
|
294
|
+
const headers: Record<string, string> = {
|
|
295
|
+
"Content-Type": "application/json",
|
|
296
|
+
};
|
|
297
|
+
if (config.apiKey) {
|
|
298
|
+
headers.Authorization = `Bearer ${config.apiKey}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
// Fetch the task to check for outputSchema
|
|
303
|
+
const taskRes = await fetch(`${config.apiUrl}/api/tasks/${taskId}`, { headers });
|
|
304
|
+
if (!taskRes.ok) return null;
|
|
305
|
+
|
|
306
|
+
const taskData = (await taskRes.json()) as {
|
|
307
|
+
task?: {
|
|
308
|
+
task?: string;
|
|
309
|
+
output?: string;
|
|
310
|
+
outputSchema?: Record<string, unknown>;
|
|
311
|
+
};
|
|
312
|
+
logs?: Array<{ eventType: string; newValue?: string; createdAt?: string }>;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const task = taskData.task;
|
|
316
|
+
if (!task?.outputSchema) return null; // No schema — no fallback needed
|
|
317
|
+
if (task.output) return null; // Agent already stored valid output
|
|
318
|
+
|
|
319
|
+
if (adapterType !== "claude") {
|
|
320
|
+
return {
|
|
321
|
+
failReason:
|
|
322
|
+
"Structured output required by outputSchema but not provided via store-progress",
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Claude adapter fallback: extract structured data from progress history
|
|
327
|
+
const progressLogs = (taskData.logs ?? [])
|
|
328
|
+
.filter((l) => l.eventType === "task_progress")
|
|
329
|
+
.sort((a, b) => (a.createdAt ?? "").localeCompare(b.createdAt ?? ""));
|
|
330
|
+
|
|
331
|
+
const progressEntries = progressLogs
|
|
332
|
+
.map((log, i) => `${i + 1}. [${log.createdAt}] ${log.newValue}`)
|
|
333
|
+
.join("\n");
|
|
334
|
+
|
|
335
|
+
const extractionPrompt = `Extract structured data from this task's execution history.
|
|
336
|
+
|
|
337
|
+
## Task Description
|
|
338
|
+
${task.task || "(no description)"}
|
|
339
|
+
|
|
340
|
+
## Progress Updates (chronological)
|
|
341
|
+
${progressEntries || "(no progress recorded)"}
|
|
342
|
+
|
|
343
|
+
## Required Output Schema
|
|
344
|
+
${JSON.stringify(task.outputSchema, null, 2)}
|
|
345
|
+
|
|
346
|
+
Extract the structured data from the progress updates above. Return ONLY valid JSON matching the schema.`;
|
|
347
|
+
|
|
348
|
+
const schemaJson = JSON.stringify(task.outputSchema);
|
|
349
|
+
const result =
|
|
350
|
+
await Bun.$`claude -p ${extractionPrompt} --json-schema ${schemaJson} --output-format json --model sonnet`
|
|
351
|
+
.json()
|
|
352
|
+
.catch(() => null);
|
|
353
|
+
|
|
354
|
+
if (result && typeof result === "object") {
|
|
355
|
+
return { output: JSON.stringify(result) };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
failReason: "Structured output extraction fallback failed — could not produce valid JSON",
|
|
360
|
+
};
|
|
361
|
+
} catch (err) {
|
|
362
|
+
console.warn(`[runner] Structured output fallback failed for task ${taskId}: ${err}`);
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
99
367
|
async function ensureTaskFinished(
|
|
100
368
|
config: ApiConfig,
|
|
101
369
|
role: string,
|
|
102
370
|
taskId: string,
|
|
103
371
|
exitCode: number,
|
|
372
|
+
failureReason?: string,
|
|
104
373
|
): Promise<void> {
|
|
105
374
|
const headers: Record<string, string> = {
|
|
106
375
|
"X-Agent-ID": config.agentId,
|
|
@@ -112,14 +381,26 @@ async function ensureTaskFinished(
|
|
|
112
381
|
|
|
113
382
|
// Determine status and reason based on exit code
|
|
114
383
|
// Exit code 0 = success, non-zero = failure
|
|
115
|
-
|
|
384
|
+
let status = exitCode === 0 ? "completed" : "failed";
|
|
116
385
|
const body: Record<string, string> = { status };
|
|
117
386
|
|
|
118
387
|
if (status === "failed") {
|
|
119
|
-
body.failureReason = `Claude process exited with code ${exitCode}`;
|
|
388
|
+
body.failureReason = failureReason || `Claude process exited with code ${exitCode}`;
|
|
120
389
|
} else {
|
|
121
|
-
|
|
122
|
-
|
|
390
|
+
// Try structured output fallback if the task has an outputSchema
|
|
391
|
+
const adapterType = process.env.HARNESS_PROVIDER || "claude";
|
|
392
|
+
const fallback = await handleStructuredOutputFallback(config, taskId, adapterType);
|
|
393
|
+
|
|
394
|
+
if (fallback?.output) {
|
|
395
|
+
body.output = fallback.output;
|
|
396
|
+
} else if (fallback?.failReason) {
|
|
397
|
+
status = "failed";
|
|
398
|
+
body.status = "failed";
|
|
399
|
+
body.failureReason = fallback.failReason;
|
|
400
|
+
} else {
|
|
401
|
+
body.output =
|
|
402
|
+
"Process completed (runner wrapper fallback - agent may have provided explicit output)";
|
|
403
|
+
}
|
|
123
404
|
}
|
|
124
405
|
|
|
125
406
|
try {
|
|
@@ -143,6 +424,8 @@ async function ensureTaskFinished(
|
|
|
143
424
|
`[${role}] Runner marked task ${taskId.slice(0, 8)} as ${status} (exit code: ${exitCode})`,
|
|
144
425
|
);
|
|
145
426
|
}
|
|
427
|
+
} else if (response.status === 404) {
|
|
428
|
+
console.log(`[${role}] Task ${taskId.slice(0, 8)} already finalized (not found), skipping`);
|
|
146
429
|
} else {
|
|
147
430
|
const error = await response.text();
|
|
148
431
|
console.warn(
|
|
@@ -190,9 +473,20 @@ async function pauseTaskViaAPI(config: ApiConfig, role: string, taskId: string):
|
|
|
190
473
|
}
|
|
191
474
|
|
|
192
475
|
/** Fetch paused tasks from API for this agent */
|
|
193
|
-
async function getPausedTasksFromAPI(
|
|
194
|
-
|
|
195
|
-
|
|
476
|
+
async function getPausedTasksFromAPI(config: ApiConfig): Promise<
|
|
477
|
+
Array<{
|
|
478
|
+
id: string;
|
|
479
|
+
task: string;
|
|
480
|
+
progress?: string;
|
|
481
|
+
claudeSessionId?: string;
|
|
482
|
+
parentTaskId?: string;
|
|
483
|
+
dir?: string;
|
|
484
|
+
vcsRepo?: string;
|
|
485
|
+
finishedAt?: string;
|
|
486
|
+
output?: string;
|
|
487
|
+
status?: string;
|
|
488
|
+
}>
|
|
489
|
+
> {
|
|
196
490
|
const headers: Record<string, string> = {
|
|
197
491
|
"X-Agent-ID": config.agentId,
|
|
198
492
|
};
|
|
@@ -212,7 +506,18 @@ async function getPausedTasksFromAPI(
|
|
|
212
506
|
}
|
|
213
507
|
|
|
214
508
|
const data = (await response.json()) as {
|
|
215
|
-
tasks: Array<{
|
|
509
|
+
tasks: Array<{
|
|
510
|
+
id: string;
|
|
511
|
+
task: string;
|
|
512
|
+
progress?: string;
|
|
513
|
+
claudeSessionId?: string;
|
|
514
|
+
parentTaskId?: string;
|
|
515
|
+
dir?: string;
|
|
516
|
+
vcsRepo?: string;
|
|
517
|
+
finishedAt?: string;
|
|
518
|
+
output?: string;
|
|
519
|
+
status?: string;
|
|
520
|
+
}>;
|
|
216
521
|
};
|
|
217
522
|
return data.tasks || [];
|
|
218
523
|
} catch (error) {
|
|
@@ -244,8 +549,11 @@ async function resumeTaskViaAPI(config: ApiConfig, taskId: string): Promise<bool
|
|
|
244
549
|
}
|
|
245
550
|
|
|
246
551
|
/** Build prompt for a resumed task */
|
|
247
|
-
function buildResumePrompt(
|
|
248
|
-
|
|
552
|
+
function buildResumePrompt(
|
|
553
|
+
task: { id: string; task: string; progress?: string },
|
|
554
|
+
fmt: (cmd: string) => string = (cmd) => `/${cmd}`,
|
|
555
|
+
): string {
|
|
556
|
+
let prompt = `${fmt("work-on-task")} ${task.id}
|
|
249
557
|
|
|
250
558
|
**RESUMED TASK** - This task was interrupted during a deployment and is being resumed.
|
|
251
559
|
|
|
@@ -303,7 +611,7 @@ function setupShutdownHandlers(
|
|
|
303
611
|
);
|
|
304
612
|
for (const [taskId, task] of state.activeTasks) {
|
|
305
613
|
console.log(`[${role}] Pausing task ${taskId.slice(0, 8)}`);
|
|
306
|
-
task.
|
|
614
|
+
task.session.abort().catch(() => {});
|
|
307
615
|
// Mark as paused for graceful resume (instead of failed)
|
|
308
616
|
if (apiConfig) {
|
|
309
617
|
const paused = await pauseTaskViaAPI(apiConfig, role, taskId);
|
|
@@ -352,28 +660,17 @@ export interface RunnerOptions {
|
|
|
352
660
|
aiLoop?: boolean; // Use AI-based loop (old behavior)
|
|
353
661
|
}
|
|
354
662
|
|
|
355
|
-
interface RunClaudeIterationOptions {
|
|
356
|
-
prompt: string;
|
|
357
|
-
logFile: string;
|
|
358
|
-
systemPrompt?: string;
|
|
359
|
-
additionalArgs?: string[];
|
|
360
|
-
role: string;
|
|
361
|
-
// New fields for log streaming
|
|
362
|
-
apiUrl?: string;
|
|
363
|
-
apiKey?: string;
|
|
364
|
-
agentId?: string;
|
|
365
|
-
sessionId?: string;
|
|
366
|
-
iteration?: number;
|
|
367
|
-
taskId?: string;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
663
|
/** Running task state for parallel execution */
|
|
371
664
|
interface RunningTask {
|
|
372
665
|
taskId: string;
|
|
373
|
-
|
|
666
|
+
session: ProviderSession;
|
|
374
667
|
logFile: string;
|
|
375
668
|
startTime: Date;
|
|
376
|
-
promise: Promise<
|
|
669
|
+
promise: Promise<ProviderResult>;
|
|
670
|
+
/** The trigger type that caused this task to be spawned */
|
|
671
|
+
triggerType?: string;
|
|
672
|
+
/** Set when the promise resolves, enabling non-blocking completion checks */
|
|
673
|
+
result: ProviderResult | null;
|
|
377
674
|
}
|
|
378
675
|
|
|
379
676
|
/** Runner state for tracking concurrent tasks */
|
|
@@ -408,6 +705,12 @@ async function flushLogBuffer(
|
|
|
408
705
|
): Promise<void> {
|
|
409
706
|
if (buffer.lines.length === 0) return;
|
|
410
707
|
|
|
708
|
+
// Snapshot and clear buffer immediately to prevent duplicate flushes
|
|
709
|
+
// (fire-and-forget callers would otherwise race on the same buffer)
|
|
710
|
+
const lines = buffer.lines;
|
|
711
|
+
buffer.lines = [];
|
|
712
|
+
buffer.lastFlush = Date.now();
|
|
713
|
+
|
|
411
714
|
const headers: Record<string, string> = {
|
|
412
715
|
"Content-Type": "application/json",
|
|
413
716
|
"X-Agent-ID": opts.agentId,
|
|
@@ -425,7 +728,7 @@ async function flushLogBuffer(
|
|
|
425
728
|
iteration: opts.iteration,
|
|
426
729
|
taskId: opts.taskId,
|
|
427
730
|
cli: opts.cli || "claude",
|
|
428
|
-
lines
|
|
731
|
+
lines,
|
|
429
732
|
}),
|
|
430
733
|
});
|
|
431
734
|
|
|
@@ -435,26 +738,6 @@ async function flushLogBuffer(
|
|
|
435
738
|
} catch (error) {
|
|
436
739
|
console.warn(`[runner] Error pushing logs: ${error}`);
|
|
437
740
|
}
|
|
438
|
-
|
|
439
|
-
// Clear buffer after flush
|
|
440
|
-
buffer.lines = [];
|
|
441
|
-
buffer.lastFlush = Date.now();
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/** Data for session cost tracking */
|
|
445
|
-
interface CostData {
|
|
446
|
-
sessionId: string;
|
|
447
|
-
taskId?: string;
|
|
448
|
-
agentId: string;
|
|
449
|
-
totalCostUsd: number;
|
|
450
|
-
inputTokens?: number;
|
|
451
|
-
outputTokens?: number;
|
|
452
|
-
cacheReadTokens?: number;
|
|
453
|
-
cacheWriteTokens?: number;
|
|
454
|
-
durationMs: number;
|
|
455
|
-
numTurns: number;
|
|
456
|
-
model: string;
|
|
457
|
-
isError: boolean;
|
|
458
741
|
}
|
|
459
742
|
|
|
460
743
|
/** Save session cost data to the API */
|
|
@@ -482,6 +765,102 @@ async function saveCostData(cost: CostData, apiUrl: string, apiKey: string): Pro
|
|
|
482
765
|
}
|
|
483
766
|
}
|
|
484
767
|
|
|
768
|
+
/** Save Claude session ID for a task (fire-and-forget) */
|
|
769
|
+
async function saveProviderSessionId(
|
|
770
|
+
apiUrl: string,
|
|
771
|
+
apiKey: string,
|
|
772
|
+
taskId: string,
|
|
773
|
+
claudeSessionId: string,
|
|
774
|
+
): Promise<void> {
|
|
775
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
776
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
777
|
+
await fetch(`${apiUrl}/api/tasks/${taskId}/claude-session`, {
|
|
778
|
+
method: "PUT",
|
|
779
|
+
headers,
|
|
780
|
+
body: JSON.stringify({ claudeSessionId }),
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** Fetch Claude session ID for a task (for --resume) */
|
|
785
|
+
async function fetchProviderSessionId(
|
|
786
|
+
apiUrl: string,
|
|
787
|
+
apiKey: string,
|
|
788
|
+
taskId: string,
|
|
789
|
+
): Promise<string | null> {
|
|
790
|
+
const headers: Record<string, string> = {};
|
|
791
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
792
|
+
try {
|
|
793
|
+
const response = await fetch(`${apiUrl}/api/tasks/${taskId}`, { headers });
|
|
794
|
+
if (!response.ok) return null;
|
|
795
|
+
const data = (await response.json()) as { claudeSessionId?: string };
|
|
796
|
+
return data.claudeSessionId || null;
|
|
797
|
+
} catch {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** Register an active session with the API (fire-and-forget) */
|
|
803
|
+
async function registerActiveSession(
|
|
804
|
+
config: ApiConfig,
|
|
805
|
+
session: {
|
|
806
|
+
taskId: string;
|
|
807
|
+
triggerType: string;
|
|
808
|
+
taskDescription?: string;
|
|
809
|
+
},
|
|
810
|
+
): Promise<void> {
|
|
811
|
+
const headers: Record<string, string> = {
|
|
812
|
+
"Content-Type": "application/json",
|
|
813
|
+
"X-Agent-ID": config.agentId,
|
|
814
|
+
};
|
|
815
|
+
if (config.apiKey) headers.Authorization = `Bearer ${config.apiKey}`;
|
|
816
|
+
try {
|
|
817
|
+
await fetch(`${config.apiUrl}/api/active-sessions`, {
|
|
818
|
+
method: "POST",
|
|
819
|
+
headers,
|
|
820
|
+
body: JSON.stringify({
|
|
821
|
+
agentId: config.agentId,
|
|
822
|
+
taskId: session.taskId,
|
|
823
|
+
triggerType: session.triggerType,
|
|
824
|
+
taskDescription: session.taskDescription,
|
|
825
|
+
}),
|
|
826
|
+
});
|
|
827
|
+
} catch {
|
|
828
|
+
// Non-blocking — session tracking is best-effort
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/** Remove an active session by taskId (fire-and-forget) */
|
|
833
|
+
async function removeActiveSession(config: ApiConfig, taskId: string): Promise<void> {
|
|
834
|
+
const headers: Record<string, string> = { "X-Agent-ID": config.agentId };
|
|
835
|
+
if (config.apiKey) headers.Authorization = `Bearer ${config.apiKey}`;
|
|
836
|
+
try {
|
|
837
|
+
await fetch(`${config.apiUrl}/api/active-sessions/by-task/${taskId}`, {
|
|
838
|
+
method: "DELETE",
|
|
839
|
+
headers,
|
|
840
|
+
});
|
|
841
|
+
} catch {
|
|
842
|
+
// Non-blocking
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/** Clean up all active sessions for this agent (on startup) */
|
|
847
|
+
async function cleanupActiveSessions(config: ApiConfig): Promise<void> {
|
|
848
|
+
const headers: Record<string, string> = {
|
|
849
|
+
"Content-Type": "application/json",
|
|
850
|
+
"X-Agent-ID": config.agentId,
|
|
851
|
+
};
|
|
852
|
+
if (config.apiKey) headers.Authorization = `Bearer ${config.apiKey}`;
|
|
853
|
+
try {
|
|
854
|
+
await fetch(`${config.apiUrl}/api/active-sessions/cleanup`, {
|
|
855
|
+
method: "POST",
|
|
856
|
+
headers,
|
|
857
|
+
body: JSON.stringify({ agentId: config.agentId }),
|
|
858
|
+
});
|
|
859
|
+
} catch {
|
|
860
|
+
// Non-blocking
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
485
864
|
/** Trigger types returned by the poll API */
|
|
486
865
|
interface Trigger {
|
|
487
866
|
type:
|
|
@@ -489,8 +868,6 @@ interface Trigger {
|
|
|
489
868
|
| "task_offered"
|
|
490
869
|
| "unread_mentions"
|
|
491
870
|
| "pool_tasks_available"
|
|
492
|
-
| "tasks_finished"
|
|
493
|
-
| "slack_inbox_message"
|
|
494
871
|
| "epic_progress_changed";
|
|
495
872
|
taskId?: string;
|
|
496
873
|
task?: unknown;
|
|
@@ -529,6 +906,7 @@ async function registerAgent(opts: {
|
|
|
529
906
|
agentId: string;
|
|
530
907
|
name: string;
|
|
531
908
|
isLead: boolean;
|
|
909
|
+
role?: string;
|
|
532
910
|
capabilities?: string[];
|
|
533
911
|
maxTasks?: number;
|
|
534
912
|
}): Promise<void> {
|
|
@@ -546,6 +924,7 @@ async function registerAgent(opts: {
|
|
|
546
924
|
body: JSON.stringify({
|
|
547
925
|
name: opts.name,
|
|
548
926
|
isLead: opts.isLead,
|
|
927
|
+
role: opts.role,
|
|
549
928
|
capabilities: opts.capabilities,
|
|
550
929
|
maxTasks: opts.maxTasks,
|
|
551
930
|
}),
|
|
@@ -601,7 +980,11 @@ async function pollForTrigger(opts: PollOptions): Promise<Trigger | null> {
|
|
|
601
980
|
}
|
|
602
981
|
|
|
603
982
|
/** Build prompt based on trigger type */
|
|
604
|
-
function buildPromptForTrigger(
|
|
983
|
+
function buildPromptForTrigger(
|
|
984
|
+
trigger: Trigger,
|
|
985
|
+
defaultPrompt: string,
|
|
986
|
+
fmt: (cmd: string) => string = (cmd) => `/${cmd}`,
|
|
987
|
+
): string {
|
|
605
988
|
switch (trigger.type) {
|
|
606
989
|
case "task_assigned": {
|
|
607
990
|
// Use the work-on-task command with task ID and description
|
|
@@ -609,11 +992,18 @@ function buildPromptForTrigger(trigger: Trigger, defaultPrompt: string): string
|
|
|
609
992
|
trigger.task && typeof trigger.task === "object" && "task" in trigger.task
|
|
610
993
|
? (trigger.task as { task: string }).task
|
|
611
994
|
: null;
|
|
612
|
-
let prompt =
|
|
995
|
+
let prompt = `${fmt("work-on-task")} ${trigger.taskId}`;
|
|
613
996
|
if (taskDesc) {
|
|
614
997
|
prompt += `\n\nTask: "${taskDesc}"`;
|
|
615
998
|
}
|
|
616
|
-
|
|
999
|
+
|
|
1000
|
+
// Inject outputSchema instructions if present
|
|
1001
|
+
const taskObj = trigger.task as Record<string, unknown> | undefined;
|
|
1002
|
+
if (taskObj?.outputSchema && typeof taskObj.outputSchema === "object") {
|
|
1003
|
+
prompt += `\n\n**Required Output Format**: When completing this task, you MUST call store-progress with output that is valid JSON conforming to this schema:\n\`\`\`json\n${JSON.stringify(taskObj.outputSchema, null, 2)}\n\`\`\`\nCall store-progress with status "completed" and your JSON output. If your output doesn't match the schema, the tool call will fail and you should fix and retry.`;
|
|
1004
|
+
} else {
|
|
1005
|
+
prompt += `\n\nWhen done, use \`store-progress\` with status: "completed" and include your output.`;
|
|
1006
|
+
}
|
|
617
1007
|
return prompt;
|
|
618
1008
|
}
|
|
619
1009
|
|
|
@@ -623,7 +1013,7 @@ function buildPromptForTrigger(trigger: Trigger, defaultPrompt: string): string
|
|
|
623
1013
|
trigger.task && typeof trigger.task === "object" && "task" in trigger.task
|
|
624
1014
|
? (trigger.task as { task: string }).task
|
|
625
1015
|
: null;
|
|
626
|
-
let prompt =
|
|
1016
|
+
let prompt = `${fmt("review-offered-task")} ${trigger.taskId}`;
|
|
627
1017
|
if (taskDesc) {
|
|
628
1018
|
prompt += `\n\nA task has been offered to you:\n"${taskDesc}"`;
|
|
629
1019
|
}
|
|
@@ -649,49 +1039,6 @@ function buildPromptForTrigger(trigger: Trigger, defaultPrompt: string): string
|
|
|
649
1039
|
|
|
650
1040
|
Note: Claims are first-come-first-serve. If claim fails, pick another.`;
|
|
651
1041
|
|
|
652
|
-
case "tasks_finished": {
|
|
653
|
-
// Lead: notification about finished tasks with inline details
|
|
654
|
-
if (trigger.tasks && Array.isArray(trigger.tasks) && trigger.tasks.length > 0) {
|
|
655
|
-
const completed = trigger.tasks.filter((t) => t.status === "completed");
|
|
656
|
-
const failed = trigger.tasks.filter((t) => t.status === "failed");
|
|
657
|
-
|
|
658
|
-
let prompt = `${trigger.count} task(s) finished:\n`;
|
|
659
|
-
|
|
660
|
-
if (completed.length > 0) {
|
|
661
|
-
prompt += "\n### Completed:\n";
|
|
662
|
-
for (const t of completed) {
|
|
663
|
-
const agentName = t.agentId ? `Agent ${t.agentId.slice(0, 8)}` : "Unknown";
|
|
664
|
-
const output = t.output ? t.output.slice(0, 200) : "(no output)";
|
|
665
|
-
const hasSlack = t.slackChannelId ? " [Slack - user expects reply]" : "";
|
|
666
|
-
prompt += `- **Task ${t.id.slice(0, 8)}** by ${agentName}${hasSlack}\n`;
|
|
667
|
-
prompt += ` Description: "${t.task?.slice(0, 100)}"\n`;
|
|
668
|
-
prompt += ` Output: ${output}${t.output && t.output.length > 200 ? "..." : ""}\n`;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
if (failed.length > 0) {
|
|
673
|
-
prompt += "\n### Failed:\n";
|
|
674
|
-
for (const t of failed) {
|
|
675
|
-
const agentName = t.agentId ? `Agent ${t.agentId.slice(0, 8)}` : "Unknown";
|
|
676
|
-
const reason = t.failureReason || "(no reason given)";
|
|
677
|
-
const hasSlack = t.slackChannelId ? " [Slack - user expects reply]" : "";
|
|
678
|
-
prompt += `- **Task ${t.id.slice(0, 8)}** by ${agentName}${hasSlack}\n`;
|
|
679
|
-
prompt += ` Description: "${t.task?.slice(0, 100)}"\n`;
|
|
680
|
-
prompt += ` Reason: ${reason}\n`;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
prompt += `\nFor each task:
|
|
685
|
-
1. Completed: Verify output meets requirements
|
|
686
|
-
2. Failed: Reassign to another worker, or handle the issue
|
|
687
|
-
3. If Slack context: Use \`slack-reply\` with taskId to update the user`;
|
|
688
|
-
|
|
689
|
-
return prompt;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
return `Workers have finished ${trigger.count} task(s). Use \`get-tasks\` with status: "completed" or "failed" to review them.`;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
1042
|
case "epic_progress_changed": {
|
|
696
1043
|
// Lead: Epic progress updated - tasks completed or failed for an active epic
|
|
697
1044
|
// This is similar to ralph loop - keep the epic progressing until done
|
|
@@ -700,6 +1047,9 @@ Note: Claims are first-come-first-serve. If claim fails, pick another.`;
|
|
|
700
1047
|
id: string;
|
|
701
1048
|
name: string;
|
|
702
1049
|
goal: string;
|
|
1050
|
+
plan?: string;
|
|
1051
|
+
prd?: string;
|
|
1052
|
+
nextSteps?: string;
|
|
703
1053
|
status: string;
|
|
704
1054
|
progress: number;
|
|
705
1055
|
taskStats: {
|
|
@@ -732,6 +1082,13 @@ Note: Claims are first-come-first-serve. If claim fails, pick another.`;
|
|
|
732
1082
|
prompt += `**Progress:** ${epic.progress}% complete (${epic.taskStats.completed}/${epic.taskStats.total} tasks)\n`;
|
|
733
1083
|
prompt += `**Status:** ${epic.status}\n\n`;
|
|
734
1084
|
|
|
1085
|
+
if (epic.plan) {
|
|
1086
|
+
prompt += `**Plan:**\n${epic.plan.slice(0, 2000)}\n\n`;
|
|
1087
|
+
}
|
|
1088
|
+
if (epic.prd) {
|
|
1089
|
+
prompt += `**PRD:**\n${epic.prd.slice(0, 1000)}\n\n`;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
735
1092
|
// Show finished tasks
|
|
736
1093
|
const completed = finishedTasks.filter((t) => t.status === "completed");
|
|
737
1094
|
const failed = finishedTasks.filter((t) => t.status === "failed");
|
|
@@ -781,424 +1138,238 @@ The epic should keep progressing until 100% complete and the goal is achieved.`;
|
|
|
781
1138
|
return prompt;
|
|
782
1139
|
}
|
|
783
1140
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
// Parse structured content if present
|
|
789
|
-
const newMessageMatch = m.content.match(/<new_message>\n([\s\S]*?)\n<\/new_message>/);
|
|
790
|
-
const threadHistoryMatch = m.content.match(
|
|
791
|
-
/<thread_history>\n([\s\S]*?)\n<\/thread_history>/,
|
|
792
|
-
);
|
|
1141
|
+
default:
|
|
1142
|
+
return defaultPrompt;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
793
1145
|
|
|
794
|
-
|
|
795
|
-
|
|
1146
|
+
/** Search agent memories relevant to a task description via the API */
|
|
1147
|
+
async function fetchRelevantMemories(
|
|
1148
|
+
apiUrl: string,
|
|
1149
|
+
apiKey: string,
|
|
1150
|
+
agentId: string,
|
|
1151
|
+
taskDescription: string,
|
|
1152
|
+
): Promise<string | null> {
|
|
1153
|
+
try {
|
|
1154
|
+
const headers: Record<string, string> = {
|
|
1155
|
+
"Content-Type": "application/json",
|
|
1156
|
+
"X-Agent-ID": agentId,
|
|
1157
|
+
};
|
|
1158
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
796
1159
|
|
|
797
|
-
|
|
798
|
-
|
|
1160
|
+
const response = await fetch(`${apiUrl}/api/memory/search`, {
|
|
1161
|
+
method: "POST",
|
|
1162
|
+
headers,
|
|
1163
|
+
body: JSON.stringify({ query: taskDescription, limit: 5 }),
|
|
1164
|
+
});
|
|
799
1165
|
|
|
800
|
-
|
|
801
|
-
formatted += `\n**Thread History:**\n${threadHistory}\n`;
|
|
802
|
-
}
|
|
1166
|
+
if (!response.ok) return null;
|
|
803
1167
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1168
|
+
const data = (await response.json()) as {
|
|
1169
|
+
results: Array<{ id: string; name: string; content: string; similarity: number }>;
|
|
1170
|
+
};
|
|
807
1171
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
- **Delegate to worker**: Use \`inbox-delegate\` with inboxMessageId and agentId if it requires work
|
|
1172
|
+
const useful = (data.results || []).filter((m) => m.similarity > 0.4);
|
|
1173
|
+
if (useful.length === 0) return null;
|
|
811
1174
|
|
|
812
|
-
|
|
813
|
-
|
|
1175
|
+
const memoryContext = useful
|
|
1176
|
+
.map((m) => `- **${m.name}** (id: ${m.id}): ${m.content.substring(0, 300)}`)
|
|
1177
|
+
.join("\n");
|
|
814
1178
|
|
|
815
|
-
|
|
816
|
-
|
|
1179
|
+
return `\n\n### Relevant Past Knowledge\n\nThese memories from your previous sessions may be useful. Use \`memory-get\` with the memory ID to retrieve full details.\n\n${memoryContext}\n`;
|
|
1180
|
+
} catch {
|
|
1181
|
+
// Non-blocking — don't fail task start because of memory search
|
|
1182
|
+
return null;
|
|
817
1183
|
}
|
|
818
1184
|
}
|
|
819
1185
|
|
|
820
|
-
async function
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
"
|
|
827
|
-
|
|
828
|
-
"stream-json",
|
|
829
|
-
"--dangerously-skip-permissions",
|
|
830
|
-
"--allow-dangerously-skip-permissions",
|
|
831
|
-
"--permission-mode",
|
|
832
|
-
"bypassPermissions",
|
|
833
|
-
"-p",
|
|
834
|
-
opts.prompt,
|
|
835
|
-
];
|
|
836
|
-
|
|
837
|
-
if (opts.additionalArgs && opts.additionalArgs.length > 0) {
|
|
838
|
-
Cmd.push(...opts.additionalArgs);
|
|
839
|
-
}
|
|
1186
|
+
async function fetchEpicNameAndGoal(
|
|
1187
|
+
apiUrl: string,
|
|
1188
|
+
apiKey: string,
|
|
1189
|
+
epicId: string,
|
|
1190
|
+
): Promise<{ name: string; goal: string } | null> {
|
|
1191
|
+
try {
|
|
1192
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
1193
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
840
1194
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
}
|
|
1195
|
+
const response = await fetch(`${apiUrl}/api/epics/${epicId}`, { headers });
|
|
1196
|
+
if (!response.ok) return null;
|
|
844
1197
|
|
|
845
|
-
|
|
1198
|
+
const data = (await response.json()) as { name: string; goal: string };
|
|
1199
|
+
return { name: data.name, goal: data.goal };
|
|
1200
|
+
} catch {
|
|
1201
|
+
return null;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
846
1204
|
|
|
847
|
-
|
|
848
|
-
|
|
1205
|
+
async function fetchEpicTaskContext(
|
|
1206
|
+
apiUrl: string,
|
|
1207
|
+
apiKey: string,
|
|
1208
|
+
epicId: string,
|
|
1209
|
+
currentTaskId: string,
|
|
1210
|
+
): Promise<string | null> {
|
|
1211
|
+
try {
|
|
1212
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
1213
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
849
1214
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
});
|
|
1215
|
+
const response = await fetch(`${apiUrl}/api/tasks?epicId=${epicId}&status=completed&limit=5`, {
|
|
1216
|
+
headers,
|
|
1217
|
+
});
|
|
1218
|
+
if (!response.ok) return null;
|
|
855
1219
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
if (proc.stdout) {
|
|
861
|
-
// Initialize log buffer for API streaming
|
|
862
|
-
const logBuffer: LogBuffer = { lines: [], lastFlush: Date.now(), partialLine: "" };
|
|
863
|
-
const shouldStream = opts.apiUrl && opts.sessionId && opts.iteration;
|
|
864
|
-
|
|
865
|
-
for await (const chunk of proc.stdout) {
|
|
866
|
-
stdoutChunks++;
|
|
867
|
-
const text = new TextDecoder().decode(chunk);
|
|
868
|
-
logFileHandle.write(text);
|
|
869
|
-
|
|
870
|
-
// Prepend any partial line from previous chunk
|
|
871
|
-
const combined = logBuffer.partialLine + text;
|
|
872
|
-
const parts = combined.split("\n");
|
|
873
|
-
|
|
874
|
-
// Last element may be incomplete - save for next chunk
|
|
875
|
-
logBuffer.partialLine = parts.pop() || "";
|
|
876
|
-
|
|
877
|
-
// Process only complete lines (those that ended with \n)
|
|
878
|
-
for (const line of parts) {
|
|
879
|
-
prettyPrintLine(line, role);
|
|
880
|
-
|
|
881
|
-
// Buffer non-empty lines for API streaming
|
|
882
|
-
if (shouldStream && line.trim()) {
|
|
883
|
-
logBuffer.lines.push(line.trim());
|
|
884
|
-
|
|
885
|
-
// Check if we should flush (buffer full or time elapsed)
|
|
886
|
-
const shouldFlush =
|
|
887
|
-
logBuffer.lines.length >= LOG_BUFFER_SIZE ||
|
|
888
|
-
Date.now() - logBuffer.lastFlush >= LOG_FLUSH_INTERVAL_MS;
|
|
889
|
-
|
|
890
|
-
if (shouldFlush) {
|
|
891
|
-
await flushLogBuffer(logBuffer, {
|
|
892
|
-
apiUrl: opts.apiUrl!,
|
|
893
|
-
apiKey: opts.apiKey || "",
|
|
894
|
-
agentId: opts.agentId || "",
|
|
895
|
-
sessionId: opts.sessionId!,
|
|
896
|
-
iteration: opts.iteration!,
|
|
897
|
-
taskId: opts.taskId,
|
|
898
|
-
cli: "claude",
|
|
899
|
-
});
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
}
|
|
1220
|
+
const data = (await response.json()) as {
|
|
1221
|
+
tasks: Array<{ id: string; task: string; output?: string }>;
|
|
1222
|
+
};
|
|
1223
|
+
const tasks = data.tasks || [];
|
|
904
1224
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
prettyPrintLine(logBuffer.partialLine, role);
|
|
908
|
-
if (shouldStream) {
|
|
909
|
-
logBuffer.lines.push(logBuffer.partialLine.trim());
|
|
910
|
-
}
|
|
911
|
-
logBuffer.partialLine = "";
|
|
912
|
-
}
|
|
1225
|
+
const relevant = tasks.filter((t) => t.id !== currentTaskId);
|
|
1226
|
+
if (relevant.length === 0) return null;
|
|
913
1227
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
apiKey: opts.apiKey || "",
|
|
919
|
-
agentId: opts.agentId || "",
|
|
920
|
-
sessionId: opts.sessionId!,
|
|
921
|
-
iteration: opts.iteration!,
|
|
922
|
-
taskId: opts.taskId,
|
|
923
|
-
cli: "claude",
|
|
924
|
-
});
|
|
925
|
-
}
|
|
1228
|
+
let context = "\n\n### Recent Epic Task Completions\n\n";
|
|
1229
|
+
context += "These tasks were recently completed in the same epic:\n\n";
|
|
1230
|
+
for (const t of relevant.slice(0, 5)) {
|
|
1231
|
+
context += `- **${t.task.slice(0, 100)}**: ${(t.output || "no output").slice(0, 200)}\n`;
|
|
926
1232
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
if (proc.stderr) {
|
|
931
|
-
for await (const chunk of proc.stderr) {
|
|
932
|
-
stderrChunks++;
|
|
933
|
-
const text = new TextDecoder().decode(chunk);
|
|
934
|
-
stderrOutput += text;
|
|
935
|
-
prettyPrintStderr(text, role);
|
|
936
|
-
logFileHandle.write(
|
|
937
|
-
`${JSON.stringify({ type: "stderr", content: text, timestamp: new Date().toISOString() })}\n`,
|
|
938
|
-
);
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
})();
|
|
942
|
-
|
|
943
|
-
await Promise.all([stdoutPromise, stderrPromise]);
|
|
944
|
-
await logFileHandle.end();
|
|
945
|
-
const exitCode = await proc.exited;
|
|
946
|
-
|
|
947
|
-
if (exitCode !== 0 && stderrOutput) {
|
|
948
|
-
console.error(`\x1b[31m[${role}] Full stderr:\x1b[0m\n${stderrOutput}`);
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
if (stdoutChunks === 0 && stderrChunks === 0) {
|
|
952
|
-
console.warn(`\x1b[33m[${role}] WARNING: No output from Claude - check auth/startup\x1b[0m`);
|
|
1233
|
+
return context;
|
|
1234
|
+
} catch {
|
|
1235
|
+
return null;
|
|
953
1236
|
}
|
|
954
|
-
|
|
955
|
-
return exitCode ?? 1;
|
|
956
1237
|
}
|
|
957
1238
|
|
|
958
|
-
/** Spawn a
|
|
959
|
-
async function
|
|
960
|
-
|
|
1239
|
+
/** Spawn a provider session without blocking - returns immediately with tracking info */
|
|
1240
|
+
async function spawnProviderProcess(
|
|
1241
|
+
adapter: ReturnType<typeof createProviderAdapter>,
|
|
1242
|
+
opts: {
|
|
1243
|
+
prompt: string;
|
|
1244
|
+
logFile: string;
|
|
1245
|
+
systemPrompt?: string;
|
|
1246
|
+
additionalArgs?: string[];
|
|
1247
|
+
role: string;
|
|
1248
|
+
apiUrl: string;
|
|
1249
|
+
apiKey: string;
|
|
1250
|
+
agentId: string;
|
|
1251
|
+
runnerSessionId: string;
|
|
1252
|
+
iteration: number;
|
|
1253
|
+
taskId?: string;
|
|
1254
|
+
model?: string;
|
|
1255
|
+
cwd?: string;
|
|
1256
|
+
},
|
|
961
1257
|
logDir: string,
|
|
962
|
-
_metadataType: string,
|
|
963
|
-
_sessionId: string,
|
|
964
1258
|
isYolo: boolean,
|
|
965
1259
|
): Promise<RunningTask> {
|
|
966
|
-
|
|
967
|
-
const
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
"-p",
|
|
979
|
-
opts.prompt,
|
|
980
|
-
];
|
|
981
|
-
|
|
982
|
-
if (opts.additionalArgs && opts.additionalArgs.length > 0) {
|
|
983
|
-
Cmd.push(...opts.additionalArgs);
|
|
1260
|
+
// Real task ID from DB (may be undefined for pool_tasks_available triggers)
|
|
1261
|
+
const realTaskId = opts.taskId;
|
|
1262
|
+
// Correlation ID for logs/display — always defined
|
|
1263
|
+
const effectiveTaskId = realTaskId || crypto.randomUUID();
|
|
1264
|
+
|
|
1265
|
+
// Resolve env first so we can use MODEL_OVERRIDE from config
|
|
1266
|
+
const freshEnv = await fetchResolvedEnv(opts.apiUrl, opts.apiKey, opts.agentId);
|
|
1267
|
+
|
|
1268
|
+
// Propagate agent-fs config to process.env so getBasePrompt() can read them
|
|
1269
|
+
// (fetchResolvedEnv returns a new object, doesn't update process.env)
|
|
1270
|
+
if (freshEnv.AGENT_FS_SHARED_ORG_ID) {
|
|
1271
|
+
process.env.AGENT_FS_SHARED_ORG_ID = freshEnv.AGENT_FS_SHARED_ORG_ID as string;
|
|
984
1272
|
}
|
|
985
1273
|
|
|
986
|
-
|
|
987
|
-
Cmd.push("--append-system-prompt", opts.systemPrompt);
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
const effectiveTaskId = taskId || crypto.randomUUID();
|
|
1274
|
+
const model = opts.model || (freshEnv.MODEL_OVERRIDE as string) || "";
|
|
991
1275
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
// Write task file before spawning so hook can read the current taskId
|
|
999
|
-
// We use the parent process PID since we need to write before spawn
|
|
1000
|
-
const taskFilePid = process.pid;
|
|
1001
|
-
const taskFilePath = await writeTaskFile(taskFilePid, {
|
|
1276
|
+
const config: ProviderSessionConfig = {
|
|
1277
|
+
prompt: opts.prompt,
|
|
1278
|
+
systemPrompt: opts.systemPrompt || "",
|
|
1279
|
+
model,
|
|
1280
|
+
role: opts.role,
|
|
1281
|
+
agentId: opts.agentId,
|
|
1002
1282
|
taskId: effectiveTaskId,
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1283
|
+
apiUrl: opts.apiUrl,
|
|
1284
|
+
apiKey: opts.apiKey,
|
|
1285
|
+
cwd: opts.cwd || process.cwd(),
|
|
1286
|
+
logFile: opts.logFile,
|
|
1287
|
+
additionalArgs: opts.additionalArgs,
|
|
1288
|
+
iteration: opts.iteration,
|
|
1289
|
+
env: freshEnv as Record<string, string>,
|
|
1290
|
+
};
|
|
1006
1291
|
|
|
1007
|
-
|
|
1292
|
+
const session = await adapter.createSession(config);
|
|
1008
1293
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
TASK_FILE: taskFilePath,
|
|
1013
|
-
},
|
|
1014
|
-
stdout: "pipe",
|
|
1015
|
-
stderr: "pipe",
|
|
1016
|
-
});
|
|
1294
|
+
// Set up log streaming
|
|
1295
|
+
const logBuffer: LogBuffer = { lines: [], lastFlush: Date.now(), partialLine: "" };
|
|
1296
|
+
const shouldStream = opts.apiUrl && opts.runnerSessionId && opts.iteration;
|
|
1017
1297
|
|
|
1018
|
-
//
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
let stdoutChunks = 0;
|
|
1022
|
-
let stderrChunks = 0;
|
|
1023
|
-
|
|
1024
|
-
// Initialize log buffer for API streaming
|
|
1025
|
-
const logBuffer: LogBuffer = { lines: [], lastFlush: Date.now(), partialLine: "" };
|
|
1026
|
-
const shouldStream = opts.apiUrl && opts.sessionId && opts.iteration;
|
|
1027
|
-
|
|
1028
|
-
const stdoutPromise = (async () => {
|
|
1029
|
-
if (proc.stdout) {
|
|
1030
|
-
for await (const chunk of proc.stdout) {
|
|
1031
|
-
stdoutChunks++;
|
|
1032
|
-
const text = new TextDecoder().decode(chunk);
|
|
1033
|
-
logFileHandle.write(text);
|
|
1034
|
-
|
|
1035
|
-
// Prepend any partial line from previous chunk
|
|
1036
|
-
const combined = logBuffer.partialLine + text;
|
|
1037
|
-
const parts = combined.split("\n");
|
|
1038
|
-
|
|
1039
|
-
// Last element may be incomplete - save for next chunk
|
|
1040
|
-
logBuffer.partialLine = parts.pop() || "";
|
|
1041
|
-
|
|
1042
|
-
// Process only complete lines (those that ended with \n)
|
|
1043
|
-
for (const line of parts) {
|
|
1044
|
-
prettyPrintLine(line, role);
|
|
1045
|
-
|
|
1046
|
-
// Extract cost data from result messages
|
|
1047
|
-
if (shouldStream && line.trim()) {
|
|
1048
|
-
try {
|
|
1049
|
-
const json = JSON.parse(line.trim());
|
|
1050
|
-
if (json.type === "result" && json.total_cost_usd !== undefined) {
|
|
1051
|
-
// Extract token data from the usage object
|
|
1052
|
-
// Claude's result JSON has: usage.input_tokens, usage.output_tokens, usage.cache_read_input_tokens, usage.cache_creation_input_tokens
|
|
1053
|
-
const usage = json.usage as
|
|
1054
|
-
| {
|
|
1055
|
-
input_tokens?: number;
|
|
1056
|
-
output_tokens?: number;
|
|
1057
|
-
cache_read_input_tokens?: number;
|
|
1058
|
-
cache_creation_input_tokens?: number;
|
|
1059
|
-
}
|
|
1060
|
-
| undefined;
|
|
1061
|
-
|
|
1062
|
-
// Fire and forget - don't block the stream
|
|
1063
|
-
saveCostData(
|
|
1064
|
-
{
|
|
1065
|
-
sessionId: opts.sessionId!,
|
|
1066
|
-
taskId: opts.taskId,
|
|
1067
|
-
agentId: opts.agentId || "",
|
|
1068
|
-
totalCostUsd: json.total_cost_usd || 0,
|
|
1069
|
-
inputTokens: usage?.input_tokens ?? 0,
|
|
1070
|
-
outputTokens: usage?.output_tokens ?? 0,
|
|
1071
|
-
cacheReadTokens: usage?.cache_read_input_tokens ?? 0,
|
|
1072
|
-
cacheWriteTokens: usage?.cache_creation_input_tokens ?? 0,
|
|
1073
|
-
durationMs: json.duration_ms || 0,
|
|
1074
|
-
numTurns: json.num_turns || 1,
|
|
1075
|
-
model: "opus",
|
|
1076
|
-
isError: json.is_error || false,
|
|
1077
|
-
},
|
|
1078
|
-
opts.apiUrl!,
|
|
1079
|
-
opts.apiKey || "",
|
|
1080
|
-
).catch((err) => console.warn(`[runner] Failed to save cost: ${err}`));
|
|
1081
|
-
}
|
|
1082
|
-
} catch {
|
|
1083
|
-
// Ignore parse errors - not all lines are JSON
|
|
1084
|
-
}
|
|
1298
|
+
// Auto-progress throttle: don't update more than once per 3 seconds
|
|
1299
|
+
let lastProgressTime = 0;
|
|
1300
|
+
const PROGRESS_THROTTLE_MS = 3000;
|
|
1085
1301
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
if (shouldFlush) {
|
|
1094
|
-
await flushLogBuffer(logBuffer, {
|
|
1095
|
-
apiUrl: opts.apiUrl!,
|
|
1096
|
-
apiKey: opts.apiKey || "",
|
|
1097
|
-
agentId: opts.agentId || "",
|
|
1098
|
-
sessionId: opts.sessionId!,
|
|
1099
|
-
iteration: opts.iteration!,
|
|
1100
|
-
taskId: opts.taskId,
|
|
1101
|
-
cli: "claude",
|
|
1102
|
-
});
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
// Handle any remaining partial line at stream end
|
|
1109
|
-
if (logBuffer.partialLine.trim()) {
|
|
1110
|
-
prettyPrintLine(logBuffer.partialLine, role);
|
|
1111
|
-
if (shouldStream) {
|
|
1112
|
-
// Try to extract cost data from final partial line
|
|
1113
|
-
try {
|
|
1114
|
-
const json = JSON.parse(logBuffer.partialLine.trim());
|
|
1115
|
-
if (json.type === "result" && json.total_cost_usd !== undefined) {
|
|
1116
|
-
const usage = json.usage as
|
|
1117
|
-
| {
|
|
1118
|
-
input_tokens?: number;
|
|
1119
|
-
output_tokens?: number;
|
|
1120
|
-
cache_read_input_tokens?: number;
|
|
1121
|
-
cache_creation_input_tokens?: number;
|
|
1122
|
-
}
|
|
1123
|
-
| undefined;
|
|
1124
|
-
saveCostData(
|
|
1125
|
-
{
|
|
1126
|
-
sessionId: opts.sessionId!,
|
|
1127
|
-
taskId: opts.taskId,
|
|
1128
|
-
agentId: opts.agentId || "",
|
|
1129
|
-
totalCostUsd: json.total_cost_usd || 0,
|
|
1130
|
-
inputTokens: usage?.input_tokens ?? 0,
|
|
1131
|
-
outputTokens: usage?.output_tokens ?? 0,
|
|
1132
|
-
cacheReadTokens: usage?.cache_read_input_tokens ?? 0,
|
|
1133
|
-
cacheWriteTokens: usage?.cache_creation_input_tokens ?? 0,
|
|
1134
|
-
durationMs: json.duration_ms || 0,
|
|
1135
|
-
numTurns: json.num_turns || 1,
|
|
1136
|
-
model: "opus",
|
|
1137
|
-
isError: json.is_error || false,
|
|
1138
|
-
},
|
|
1139
|
-
opts.apiUrl!,
|
|
1140
|
-
opts.apiKey || "",
|
|
1141
|
-
).catch((err) => console.warn(`[runner] Failed to save cost: ${err}`));
|
|
1142
|
-
}
|
|
1143
|
-
} catch {
|
|
1144
|
-
// Ignore parse errors
|
|
1145
|
-
}
|
|
1146
|
-
logBuffer.lines.push(logBuffer.partialLine.trim());
|
|
1147
|
-
}
|
|
1148
|
-
logBuffer.partialLine = "";
|
|
1302
|
+
session.onEvent((event) => {
|
|
1303
|
+
switch (event.type) {
|
|
1304
|
+
case "session_init":
|
|
1305
|
+
if (realTaskId) {
|
|
1306
|
+
saveProviderSessionId(opts.apiUrl, opts.apiKey, realTaskId, event.sessionId).catch(
|
|
1307
|
+
(err) => console.warn(`[runner] Failed to save session ID: ${err}`),
|
|
1308
|
+
);
|
|
1149
1309
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
iteration: opts.iteration!,
|
|
1159
|
-
taskId: opts.taskId,
|
|
1160
|
-
cli: "claude",
|
|
1161
|
-
});
|
|
1310
|
+
break;
|
|
1311
|
+
case "tool_start": {
|
|
1312
|
+
// Auto-progress: report tool activity as task progress (throttled)
|
|
1313
|
+
const now = Date.now();
|
|
1314
|
+
if (effectiveTaskId && opts.apiUrl && now - lastProgressTime >= PROGRESS_THROTTLE_MS) {
|
|
1315
|
+
lastProgressTime = now;
|
|
1316
|
+
const progress = toolCallToProgress(event.toolName, event.args);
|
|
1317
|
+
updateProgressViaAPI(opts.apiUrl, opts.apiKey, effectiveTaskId, progress).catch(() => {});
|
|
1162
1318
|
}
|
|
1319
|
+
break;
|
|
1163
1320
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1321
|
+
case "result":
|
|
1322
|
+
// Cost save is handled in waitForCompletion().then() to ensure
|
|
1323
|
+
// it completes before the process exits (fire-and-forget here
|
|
1324
|
+
// races with container shutdown).
|
|
1325
|
+
break;
|
|
1326
|
+
case "raw_log":
|
|
1327
|
+
prettyPrintLine(event.content, opts.role);
|
|
1328
|
+
if (shouldStream) {
|
|
1329
|
+
logBuffer.lines.push(event.content);
|
|
1330
|
+
const shouldFlush =
|
|
1331
|
+
logBuffer.lines.length >= LOG_BUFFER_SIZE ||
|
|
1332
|
+
Date.now() - logBuffer.lastFlush >= LOG_FLUSH_INTERVAL_MS;
|
|
1333
|
+
if (shouldFlush) {
|
|
1334
|
+
flushLogBuffer(logBuffer, {
|
|
1335
|
+
apiUrl: opts.apiUrl,
|
|
1336
|
+
apiKey: opts.apiKey,
|
|
1337
|
+
agentId: opts.agentId,
|
|
1338
|
+
sessionId: opts.runnerSessionId,
|
|
1339
|
+
iteration: opts.iteration,
|
|
1340
|
+
taskId: effectiveTaskId,
|
|
1341
|
+
cli: adapter.name,
|
|
1342
|
+
}).catch(() => {});
|
|
1343
|
+
}
|
|
1176
1344
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
await logFileHandle.end();
|
|
1182
|
-
const exitCode = await proc.exited;
|
|
1183
|
-
|
|
1184
|
-
if (exitCode !== 0 && stderrOutput) {
|
|
1185
|
-
console.error(
|
|
1186
|
-
`\x1b[31m[${role}] Full stderr for task ${effectiveTaskId.slice(0, 8)}:\x1b[0m\n${stderrOutput}`,
|
|
1187
|
-
);
|
|
1345
|
+
break;
|
|
1346
|
+
case "raw_stderr":
|
|
1347
|
+
prettyPrintStderr(event.content, opts.role);
|
|
1348
|
+
break;
|
|
1188
1349
|
}
|
|
1350
|
+
});
|
|
1189
1351
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1352
|
+
// Create promise that handles completion
|
|
1353
|
+
const promise: Promise<ProviderResult> = session.waitForCompletion().then(async (result) => {
|
|
1354
|
+
// Final log flush
|
|
1355
|
+
if (shouldStream && logBuffer.lines.length > 0) {
|
|
1356
|
+
await flushLogBuffer(logBuffer, {
|
|
1357
|
+
apiUrl: opts.apiUrl,
|
|
1358
|
+
apiKey: opts.apiKey,
|
|
1359
|
+
agentId: opts.agentId,
|
|
1360
|
+
sessionId: opts.runnerSessionId,
|
|
1361
|
+
iteration: opts.iteration,
|
|
1362
|
+
taskId: effectiveTaskId,
|
|
1363
|
+
cli: adapter.name,
|
|
1364
|
+
});
|
|
1194
1365
|
}
|
|
1195
1366
|
|
|
1196
|
-
//
|
|
1197
|
-
if (exitCode !== 0) {
|
|
1367
|
+
// Error logging for non-zero exit
|
|
1368
|
+
if (result.exitCode !== 0) {
|
|
1198
1369
|
const errorLog = {
|
|
1199
1370
|
timestamp: new Date().toISOString(),
|
|
1200
1371
|
iteration: opts.iteration,
|
|
1201
|
-
exitCode,
|
|
1372
|
+
exitCode: result.exitCode,
|
|
1202
1373
|
taskId: effectiveTaskId,
|
|
1203
1374
|
error: true,
|
|
1204
1375
|
};
|
|
@@ -1210,29 +1381,99 @@ async function spawnClaudeProcess(
|
|
|
1210
1381
|
|
|
1211
1382
|
if (!isYolo) {
|
|
1212
1383
|
console.error(
|
|
1213
|
-
`[${role}] Task ${effectiveTaskId.slice(0, 8)} exited with code ${exitCode}.`,
|
|
1384
|
+
`[${opts.role}] Task ${effectiveTaskId.slice(0, 8)} exited with code ${result.exitCode}.`,
|
|
1214
1385
|
);
|
|
1215
1386
|
} else {
|
|
1216
1387
|
console.warn(
|
|
1217
|
-
`[${role}] Task ${effectiveTaskId.slice(0, 8)} exited with code ${exitCode}. YOLO mode - continuing...`,
|
|
1388
|
+
`[${opts.role}] Task ${effectiveTaskId.slice(0, 8)} exited with code ${result.exitCode}. YOLO mode - continuing...`,
|
|
1218
1389
|
);
|
|
1219
1390
|
}
|
|
1220
1391
|
}
|
|
1221
1392
|
|
|
1222
|
-
//
|
|
1223
|
-
|
|
1224
|
-
|
|
1393
|
+
// Save cost data (awaited to ensure it completes before container exits)
|
|
1394
|
+
if (result.cost) {
|
|
1395
|
+
try {
|
|
1396
|
+
await saveCostData(
|
|
1397
|
+
{ ...result.cost, taskId: realTaskId, sessionId: opts.runnerSessionId },
|
|
1398
|
+
opts.apiUrl,
|
|
1399
|
+
opts.apiKey,
|
|
1400
|
+
);
|
|
1401
|
+
} catch (err) {
|
|
1402
|
+
console.warn(`[runner] Failed to save cost: ${err}`);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1225
1405
|
|
|
1226
|
-
return
|
|
1227
|
-
})
|
|
1406
|
+
return result;
|
|
1407
|
+
});
|
|
1228
1408
|
|
|
1229
|
-
|
|
1409
|
+
const runningTask: RunningTask = {
|
|
1230
1410
|
taskId: effectiveTaskId,
|
|
1231
|
-
|
|
1411
|
+
session,
|
|
1232
1412
|
logFile: opts.logFile,
|
|
1233
1413
|
startTime: new Date(),
|
|
1234
1414
|
promise,
|
|
1415
|
+
result: null,
|
|
1416
|
+
};
|
|
1417
|
+
|
|
1418
|
+
// Non-blocking completion tracking
|
|
1419
|
+
promise
|
|
1420
|
+
.then((r) => {
|
|
1421
|
+
runningTask.result = r;
|
|
1422
|
+
})
|
|
1423
|
+
.catch(() => {
|
|
1424
|
+
runningTask.result = { exitCode: 1, isError: true };
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
return runningTask;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
/** Run a single provider iteration (blocking) - used for AI-loop mode */
|
|
1431
|
+
async function runProviderIteration(
|
|
1432
|
+
adapter: ReturnType<typeof createProviderAdapter>,
|
|
1433
|
+
opts: {
|
|
1434
|
+
prompt: string;
|
|
1435
|
+
logFile: string;
|
|
1436
|
+
systemPrompt?: string;
|
|
1437
|
+
additionalArgs?: string[];
|
|
1438
|
+
role: string;
|
|
1439
|
+
apiUrl: string;
|
|
1440
|
+
apiKey: string;
|
|
1441
|
+
agentId: string;
|
|
1442
|
+
taskId?: string;
|
|
1443
|
+
cwd?: string;
|
|
1444
|
+
},
|
|
1445
|
+
): Promise<ProviderResult> {
|
|
1446
|
+
const freshEnv = await fetchResolvedEnv(opts.apiUrl, opts.apiKey, opts.agentId);
|
|
1447
|
+
const model = (freshEnv.MODEL_OVERRIDE as string) || "";
|
|
1448
|
+
|
|
1449
|
+
const config: ProviderSessionConfig = {
|
|
1450
|
+
prompt: opts.prompt,
|
|
1451
|
+
systemPrompt: opts.systemPrompt || "",
|
|
1452
|
+
model,
|
|
1453
|
+
role: opts.role,
|
|
1454
|
+
agentId: opts.agentId,
|
|
1455
|
+
taskId: opts.taskId || crypto.randomUUID(),
|
|
1456
|
+
apiUrl: opts.apiUrl,
|
|
1457
|
+
apiKey: opts.apiKey,
|
|
1458
|
+
cwd: opts.cwd || process.cwd(),
|
|
1459
|
+
logFile: opts.logFile,
|
|
1460
|
+
additionalArgs: opts.additionalArgs,
|
|
1461
|
+
env: freshEnv as Record<string, string>,
|
|
1235
1462
|
};
|
|
1463
|
+
|
|
1464
|
+
const session = await adapter.createSession(config);
|
|
1465
|
+
|
|
1466
|
+
session.onEvent((event) => {
|
|
1467
|
+
if (event.type === "raw_log") prettyPrintLine(event.content, opts.role);
|
|
1468
|
+
if (event.type === "raw_stderr") prettyPrintStderr(event.content, opts.role);
|
|
1469
|
+
if (event.type === "session_init" && opts.taskId) {
|
|
1470
|
+
saveProviderSessionId(opts.apiUrl, opts.apiKey, opts.taskId, event.sessionId).catch((err) =>
|
|
1471
|
+
console.warn(`[runner] Failed to save session ID: ${err}`),
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
return session.waitForCompletion();
|
|
1236
1477
|
}
|
|
1237
1478
|
|
|
1238
1479
|
/** Check for completed processes and remove them from active tasks */
|
|
@@ -1241,32 +1482,113 @@ async function checkCompletedProcesses(
|
|
|
1241
1482
|
role: string,
|
|
1242
1483
|
apiConfig?: ApiConfig,
|
|
1243
1484
|
): Promise<void> {
|
|
1244
|
-
const completedTasks: Array<{
|
|
1485
|
+
const completedTasks: Array<{
|
|
1486
|
+
taskId: string;
|
|
1487
|
+
result: ProviderResult;
|
|
1488
|
+
triggerType?: string;
|
|
1489
|
+
}> = [];
|
|
1245
1490
|
|
|
1246
1491
|
for (const [taskId, task] of state.activeTasks) {
|
|
1247
|
-
//
|
|
1248
|
-
if (task.
|
|
1492
|
+
// Non-blocking check: result is set by a .then() callback when the promise resolves
|
|
1493
|
+
if (task.result !== null) {
|
|
1249
1494
|
console.log(
|
|
1250
|
-
`[${role}] Task ${taskId.slice(0, 8)} completed with exit code ${task.
|
|
1495
|
+
`[${role}] Task ${taskId.slice(0, 8)} completed with exit code ${task.result.exitCode} (trigger: ${task.triggerType || "unknown"})`,
|
|
1251
1496
|
);
|
|
1252
|
-
completedTasks.push({
|
|
1497
|
+
completedTasks.push({
|
|
1498
|
+
taskId,
|
|
1499
|
+
result: task.result,
|
|
1500
|
+
triggerType: task.triggerType,
|
|
1501
|
+
});
|
|
1253
1502
|
}
|
|
1254
1503
|
}
|
|
1255
1504
|
|
|
1256
1505
|
// Remove completed tasks from the map and ensure they're marked as finished
|
|
1257
|
-
for (const { taskId,
|
|
1506
|
+
for (const { taskId, result } of completedTasks) {
|
|
1258
1507
|
state.activeTasks.delete(taskId);
|
|
1259
1508
|
|
|
1509
|
+
if (apiConfig) {
|
|
1510
|
+
removeActiveSession(apiConfig, taskId);
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1260
1513
|
// Call the finish API to ensure task status is updated
|
|
1261
1514
|
// This is idempotent - if the agent already marked it, this is a no-op
|
|
1262
1515
|
if (apiConfig) {
|
|
1263
|
-
|
|
1516
|
+
let failureReason: string | undefined;
|
|
1517
|
+
if (result.exitCode !== 0 && result.failureReason) {
|
|
1518
|
+
failureReason = result.failureReason;
|
|
1519
|
+
console.log(`[${role}] Detected error for task ${taskId.slice(0, 8)}: ${failureReason}`);
|
|
1520
|
+
}
|
|
1521
|
+
await ensureTaskFinished(apiConfig, role, taskId, result.exitCode, failureReason);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
const TEMPLATE_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
1527
|
+
|
|
1528
|
+
async function fetchTemplate(
|
|
1529
|
+
templateId: string,
|
|
1530
|
+
registryUrl: string,
|
|
1531
|
+
cacheDir: string,
|
|
1532
|
+
): Promise<TemplateResponse | null> {
|
|
1533
|
+
const safeId = templateId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
1534
|
+
const cachePath = `${cacheDir}/${safeId}.json`;
|
|
1535
|
+
|
|
1536
|
+
// Check local cache
|
|
1537
|
+
try {
|
|
1538
|
+
const info = await stat(cachePath);
|
|
1539
|
+
if (Date.now() - info.mtimeMs < TEMPLATE_CACHE_TTL_MS) {
|
|
1540
|
+
const cached = await readFile(cachePath, "utf-8");
|
|
1541
|
+
return JSON.parse(cached) as TemplateResponse;
|
|
1542
|
+
}
|
|
1543
|
+
} catch {
|
|
1544
|
+
// No cache or expired, continue to fetch
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// Fetch from registry
|
|
1548
|
+
try {
|
|
1549
|
+
const resp = await fetch(`${registryUrl}/api/templates/${templateId}`);
|
|
1550
|
+
if (!resp.ok) {
|
|
1551
|
+
console.warn(`[template] Registry returned ${resp.status} for ${templateId}`);
|
|
1552
|
+
// Fall back to expired cache if available
|
|
1553
|
+
try {
|
|
1554
|
+
const cached = await readFile(cachePath, "utf-8");
|
|
1555
|
+
console.log(`[template] Using expired cache for ${templateId}`);
|
|
1556
|
+
return JSON.parse(cached) as TemplateResponse;
|
|
1557
|
+
} catch {
|
|
1558
|
+
return null;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
const template = (await resp.json()) as TemplateResponse;
|
|
1563
|
+
|
|
1564
|
+
// Cache the response
|
|
1565
|
+
try {
|
|
1566
|
+
await mkdir(cacheDir, { recursive: true });
|
|
1567
|
+
await writeFile(cachePath, JSON.stringify(template), "utf-8");
|
|
1568
|
+
} catch {
|
|
1569
|
+
console.warn(`[template] Could not cache template to ${cachePath}`);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
return template;
|
|
1573
|
+
} catch (err) {
|
|
1574
|
+
console.warn(`[template] Failed to fetch from registry: ${err}`);
|
|
1575
|
+
// Fall back to expired cache
|
|
1576
|
+
try {
|
|
1577
|
+
const cached = await readFile(cachePath, "utf-8");
|
|
1578
|
+
console.log(`[template] Using expired cache for ${templateId}`);
|
|
1579
|
+
return JSON.parse(cached) as TemplateResponse;
|
|
1580
|
+
} catch {
|
|
1581
|
+
return null;
|
|
1264
1582
|
}
|
|
1265
1583
|
}
|
|
1266
1584
|
}
|
|
1267
1585
|
|
|
1268
1586
|
export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
1269
|
-
const {
|
|
1587
|
+
const { defaultPrompt, metadataType } = config;
|
|
1588
|
+
let role = config.role;
|
|
1589
|
+
|
|
1590
|
+
// Create provider adapter based on HARNESS_PROVIDER env var (default: claude)
|
|
1591
|
+
const adapter = createProviderAdapter(process.env.HARNESS_PROVIDER || "claude");
|
|
1270
1592
|
|
|
1271
1593
|
const sessionId = process.env.SESSION_ID || crypto.randomUUID().slice(0, 8);
|
|
1272
1594
|
const baseLogDir = opts.logsDir || process.env.LOG_DIR || "/logs";
|
|
@@ -1280,13 +1602,41 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1280
1602
|
// Get agent identity and swarm URL for base prompt
|
|
1281
1603
|
const agentId = process.env.AGENT_ID || "unknown";
|
|
1282
1604
|
|
|
1283
|
-
const apiUrl = process.env.MCP_BASE_URL ||
|
|
1605
|
+
const apiUrl = process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
|
|
1284
1606
|
const swarmUrl = process.env.SWARM_URL || "localhost";
|
|
1285
1607
|
|
|
1286
|
-
|
|
1608
|
+
let capabilities = config.capabilities;
|
|
1609
|
+
|
|
1610
|
+
// Agent identity fields — populated after registration by fetching full profile
|
|
1611
|
+
let agentSoulMd: string | undefined;
|
|
1612
|
+
let agentIdentityMd: string | undefined;
|
|
1613
|
+
let agentSetupScript: string | undefined;
|
|
1614
|
+
let agentToolsMd: string | undefined;
|
|
1615
|
+
let agentClaudeMd: string | undefined;
|
|
1616
|
+
let agentProfileName: string | undefined;
|
|
1617
|
+
let agentDescription: string | undefined;
|
|
1618
|
+
|
|
1619
|
+
// Per-task repo context — set when processing a task with githubRepo
|
|
1620
|
+
let currentRepoContext: BasePromptArgs["repoContext"] | undefined;
|
|
1621
|
+
|
|
1622
|
+
// Generate base prompt (identity fields injected after profile fetch below)
|
|
1623
|
+
const buildSystemPrompt = () => {
|
|
1624
|
+
return getBasePrompt({
|
|
1625
|
+
role,
|
|
1626
|
+
agentId,
|
|
1627
|
+
swarmUrl,
|
|
1628
|
+
capabilities,
|
|
1629
|
+
name: agentProfileName,
|
|
1630
|
+
description: agentDescription,
|
|
1631
|
+
soulMd: agentSoulMd,
|
|
1632
|
+
identityMd: agentIdentityMd,
|
|
1633
|
+
toolsMd: agentToolsMd,
|
|
1634
|
+
claudeMd: agentClaudeMd,
|
|
1635
|
+
repoContext: currentRepoContext,
|
|
1636
|
+
});
|
|
1637
|
+
};
|
|
1287
1638
|
|
|
1288
|
-
|
|
1289
|
-
const basePrompt = getBasePrompt({ role, agentId, swarmUrl, capabilities });
|
|
1639
|
+
let basePrompt = buildSystemPrompt();
|
|
1290
1640
|
|
|
1291
1641
|
// Resolve additional system prompt: CLI flag > env var
|
|
1292
1642
|
let additionalSystemPrompt: string | undefined;
|
|
@@ -1318,7 +1668,8 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1318
1668
|
}
|
|
1319
1669
|
|
|
1320
1670
|
// Combine base prompt with any additional system prompt
|
|
1321
|
-
|
|
1671
|
+
// Note: resolvedSystemPrompt is rebuilt after profile fetch when identity is available
|
|
1672
|
+
let resolvedSystemPrompt = additionalSystemPrompt
|
|
1322
1673
|
? `${basePrompt}\n\n${additionalSystemPrompt}`
|
|
1323
1674
|
: basePrompt;
|
|
1324
1675
|
|
|
@@ -1346,8 +1697,38 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1346
1697
|
let iteration = 0;
|
|
1347
1698
|
|
|
1348
1699
|
if (!isAiLoop) {
|
|
1700
|
+
// Fetch template early (before registration) so defaults can be applied
|
|
1701
|
+
const templateId = process.env.TEMPLATE_ID;
|
|
1702
|
+
const registryUrl = process.env.TEMPLATE_REGISTRY_URL || "https://templates.agent-swarm.dev";
|
|
1703
|
+
let cachedTemplate: TemplateResponse | null = null;
|
|
1704
|
+
|
|
1705
|
+
if (templateId) {
|
|
1706
|
+
try {
|
|
1707
|
+
cachedTemplate = await fetchTemplate(templateId, registryUrl, "/workspace/.template-cache");
|
|
1708
|
+
if (cachedTemplate) {
|
|
1709
|
+
console.log(`[${role}] Fetched template: ${templateId}`);
|
|
1710
|
+
|
|
1711
|
+
// Apply agentDefaults as fallbacks (env/config takes precedence)
|
|
1712
|
+
const defaults = cachedTemplate.config.agentDefaults;
|
|
1713
|
+
if (config.role === "worker" && defaults.role) {
|
|
1714
|
+
role = defaults.role;
|
|
1715
|
+
}
|
|
1716
|
+
if (!capabilities?.length && defaults.capabilities?.length) {
|
|
1717
|
+
capabilities = defaults.capabilities;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
} catch (err) {
|
|
1721
|
+
console.warn(`[${role}] Failed to fetch template ${templateId}: ${err}`);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1349
1725
|
// Runner-level polling mode with parallel execution support
|
|
1350
|
-
const
|
|
1726
|
+
const isLeadFromConfig = config.role === "lead";
|
|
1727
|
+
const isLead = isLeadFromConfig || (cachedTemplate?.config.agentDefaults?.isLead ?? false);
|
|
1728
|
+
const defaultMaxTasks = isLead ? 2 : 1;
|
|
1729
|
+
const maxConcurrent = process.env.MAX_CONCURRENT_TASKS
|
|
1730
|
+
? parseInt(process.env.MAX_CONCURRENT_TASKS, 10)
|
|
1731
|
+
: (cachedTemplate?.config.agentDefaults?.maxTasks ?? defaultMaxTasks);
|
|
1351
1732
|
console.log(`[${role}] Mode: runner-level polling (use --ai-loop for AI-based polling)`);
|
|
1352
1733
|
console.log(`[${role}] Max concurrent tasks: ${maxConcurrent}`);
|
|
1353
1734
|
|
|
@@ -1357,6 +1738,9 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1357
1738
|
maxConcurrent,
|
|
1358
1739
|
};
|
|
1359
1740
|
|
|
1741
|
+
// Track tasks already signaled for cancellation to avoid repeated SIGTERM
|
|
1742
|
+
const cancelledSignaled = new Set<string>();
|
|
1743
|
+
|
|
1360
1744
|
// Create API config for ping/close
|
|
1361
1745
|
const apiConfig: ApiConfig = { apiUrl, apiKey, agentId };
|
|
1362
1746
|
|
|
@@ -1364,15 +1748,19 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1364
1748
|
setupShutdownHandlers(role, apiConfig, () => state);
|
|
1365
1749
|
|
|
1366
1750
|
// Register agent before starting
|
|
1367
|
-
const agentName =
|
|
1751
|
+
const agentName =
|
|
1752
|
+
process.env.AGENT_NAME ||
|
|
1753
|
+
cachedTemplate?.config.displayName ||
|
|
1754
|
+
`${role}-${agentId.slice(0, 8)}`;
|
|
1368
1755
|
try {
|
|
1369
1756
|
await registerAgent({
|
|
1370
1757
|
apiUrl,
|
|
1371
1758
|
apiKey,
|
|
1372
1759
|
agentId,
|
|
1373
1760
|
name: agentName,
|
|
1374
|
-
|
|
1375
|
-
|
|
1761
|
+
role,
|
|
1762
|
+
isLead,
|
|
1763
|
+
capabilities,
|
|
1376
1764
|
maxTasks: maxConcurrent,
|
|
1377
1765
|
});
|
|
1378
1766
|
console.log(`[${role}] Registered as "${agentName}" (ID: ${agentId})`);
|
|
@@ -1381,6 +1769,165 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1381
1769
|
process.exit(1);
|
|
1382
1770
|
}
|
|
1383
1771
|
|
|
1772
|
+
// Clean up any stale active sessions from previous runs (crash recovery)
|
|
1773
|
+
await cleanupActiveSessions(apiConfig);
|
|
1774
|
+
console.log(`[${role}] Cleaned up stale active sessions`);
|
|
1775
|
+
|
|
1776
|
+
// Fetch full agent profile to get soul/identity content
|
|
1777
|
+
try {
|
|
1778
|
+
const resp = await fetch(`${apiUrl}/me`, {
|
|
1779
|
+
headers: {
|
|
1780
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1781
|
+
"X-Agent-ID": agentId,
|
|
1782
|
+
},
|
|
1783
|
+
});
|
|
1784
|
+
if (resp.ok) {
|
|
1785
|
+
const profile = (await resp.json()) as {
|
|
1786
|
+
soulMd?: string;
|
|
1787
|
+
identityMd?: string;
|
|
1788
|
+
claudeMd?: string;
|
|
1789
|
+
setupScript?: string;
|
|
1790
|
+
toolsMd?: string;
|
|
1791
|
+
name?: string;
|
|
1792
|
+
description?: string;
|
|
1793
|
+
};
|
|
1794
|
+
agentSoulMd = profile.soulMd;
|
|
1795
|
+
agentIdentityMd = profile.identityMd;
|
|
1796
|
+
agentSetupScript = profile.setupScript;
|
|
1797
|
+
agentToolsMd = profile.toolsMd;
|
|
1798
|
+
agentClaudeMd = profile.claudeMd;
|
|
1799
|
+
agentProfileName = profile.name;
|
|
1800
|
+
agentDescription = profile.description;
|
|
1801
|
+
|
|
1802
|
+
// Generate default templates if missing (runner registers via POST /api/agents
|
|
1803
|
+
// which doesn't generate templates like join-swarm does)
|
|
1804
|
+
if (!agentSoulMd || !agentIdentityMd || !agentToolsMd || !agentClaudeMd) {
|
|
1805
|
+
// Use already-fetched template (from pre-registration step)
|
|
1806
|
+
if (cachedTemplate) {
|
|
1807
|
+
const ctx = {
|
|
1808
|
+
agent: {
|
|
1809
|
+
name: agentProfileName || agentName,
|
|
1810
|
+
role: role,
|
|
1811
|
+
description: agentDescription || "",
|
|
1812
|
+
capabilities: (capabilities || []).join(", "),
|
|
1813
|
+
},
|
|
1814
|
+
};
|
|
1815
|
+
if (!agentSoulMd) agentSoulMd = interpolate(cachedTemplate.files.soulMd, ctx).result;
|
|
1816
|
+
if (!agentIdentityMd)
|
|
1817
|
+
agentIdentityMd = interpolate(cachedTemplate.files.identityMd, ctx).result;
|
|
1818
|
+
if (!agentToolsMd) agentToolsMd = interpolate(cachedTemplate.files.toolsMd, ctx).result;
|
|
1819
|
+
if (!agentClaudeMd)
|
|
1820
|
+
agentClaudeMd = interpolate(cachedTemplate.files.claudeMd, ctx).result;
|
|
1821
|
+
if (!agentSetupScript)
|
|
1822
|
+
agentSetupScript = interpolate(cachedTemplate.files.setupScript, ctx).result;
|
|
1823
|
+
console.log(`[${role}] Applied template: ${templateId}`);
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// Fallback to generic defaults for any still-missing fields
|
|
1827
|
+
const agentInfo = {
|
|
1828
|
+
name: agentProfileName || agentName,
|
|
1829
|
+
role: role,
|
|
1830
|
+
description: agentDescription,
|
|
1831
|
+
capabilities: config.capabilities,
|
|
1832
|
+
};
|
|
1833
|
+
if (!agentSoulMd) agentSoulMd = generateDefaultSoulMd(agentInfo);
|
|
1834
|
+
if (!agentIdentityMd) agentIdentityMd = generateDefaultIdentityMd(agentInfo);
|
|
1835
|
+
if (!agentToolsMd) agentToolsMd = generateDefaultToolsMd(agentInfo);
|
|
1836
|
+
if (!agentClaudeMd) agentClaudeMd = generateDefaultClaudeMd(agentInfo);
|
|
1837
|
+
|
|
1838
|
+
// Push generated templates to server
|
|
1839
|
+
try {
|
|
1840
|
+
const profileUpdate: Record<string, string> = {};
|
|
1841
|
+
if (!profile.soulMd) profileUpdate.soulMd = agentSoulMd;
|
|
1842
|
+
if (!profile.identityMd) profileUpdate.identityMd = agentIdentityMd;
|
|
1843
|
+
if (!profile.toolsMd) profileUpdate.toolsMd = agentToolsMd;
|
|
1844
|
+
if (!profile.claudeMd && agentClaudeMd) profileUpdate.claudeMd = agentClaudeMd;
|
|
1845
|
+
if (!profile.setupScript && agentSetupScript)
|
|
1846
|
+
profileUpdate.setupScript = agentSetupScript;
|
|
1847
|
+
|
|
1848
|
+
await fetch(`${apiUrl}/api/agents/${agentId}/profile`, {
|
|
1849
|
+
method: "PUT",
|
|
1850
|
+
headers: {
|
|
1851
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1852
|
+
"X-Agent-ID": agentId,
|
|
1853
|
+
"Content-Type": "application/json",
|
|
1854
|
+
},
|
|
1855
|
+
body: JSON.stringify(profileUpdate),
|
|
1856
|
+
});
|
|
1857
|
+
console.log(`[${role}] Generated and saved default identity templates`);
|
|
1858
|
+
} catch {
|
|
1859
|
+
console.warn(`[${role}] Could not save generated templates to server`);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
// Rebuild system prompt with identity
|
|
1864
|
+
basePrompt = buildSystemPrompt();
|
|
1865
|
+
resolvedSystemPrompt = additionalSystemPrompt
|
|
1866
|
+
? `${basePrompt}\n\n${additionalSystemPrompt}`
|
|
1867
|
+
: basePrompt;
|
|
1868
|
+
console.log(
|
|
1869
|
+
`[${role}] Loaded agent identity (soul: ${agentSoulMd ? "yes" : "no"}, identity: ${agentIdentityMd ? "yes" : "no"}, tools: ${agentToolsMd ? "yes" : "no"}, claude: ${agentClaudeMd ? "yes" : "no"})`,
|
|
1870
|
+
);
|
|
1871
|
+
console.log(`[${role}] Updated system prompt length: ${resolvedSystemPrompt.length} chars`);
|
|
1872
|
+
}
|
|
1873
|
+
} catch {
|
|
1874
|
+
console.warn(`[${role}] Could not fetch agent profile for identity — proceeding without`);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// Write SOUL.md and IDENTITY.md to workspace before spawning Claude
|
|
1878
|
+
const SOUL_MD_PATH = "/workspace/SOUL.md";
|
|
1879
|
+
const IDENTITY_MD_PATH = "/workspace/IDENTITY.md";
|
|
1880
|
+
|
|
1881
|
+
if (agentSoulMd) {
|
|
1882
|
+
try {
|
|
1883
|
+
await Bun.write(SOUL_MD_PATH, agentSoulMd);
|
|
1884
|
+
console.log(`[${role}] Wrote SOUL.md to workspace`);
|
|
1885
|
+
} catch (err) {
|
|
1886
|
+
console.warn(`[${role}] Could not write SOUL.md: ${(err as Error).message}`);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
if (agentIdentityMd) {
|
|
1890
|
+
try {
|
|
1891
|
+
await Bun.write(IDENTITY_MD_PATH, agentIdentityMd);
|
|
1892
|
+
console.log(`[${role}] Wrote IDENTITY.md to workspace`);
|
|
1893
|
+
} catch (err) {
|
|
1894
|
+
console.warn(`[${role}] Could not write IDENTITY.md: ${(err as Error).message}`);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// Write setup script to workspace (agent can edit during session)
|
|
1899
|
+
// Only create if it doesn't exist — the entrypoint already composed/prepended it at container start
|
|
1900
|
+
if (agentSetupScript) {
|
|
1901
|
+
try {
|
|
1902
|
+
if (!(await Bun.file("/workspace/start-up.sh").exists())) {
|
|
1903
|
+
await Bun.write("/workspace/start-up.sh", `#!/bin/bash\n${agentSetupScript}\n`);
|
|
1904
|
+
console.log(`[${role}] Wrote start-up.sh to workspace`);
|
|
1905
|
+
}
|
|
1906
|
+
} catch (err) {
|
|
1907
|
+
console.warn(`[${role}] Could not write start-up.sh: ${(err as Error).message}`);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// Write TOOLS.md to workspace (agent can edit during session)
|
|
1912
|
+
if (agentToolsMd) {
|
|
1913
|
+
try {
|
|
1914
|
+
await Bun.write("/workspace/TOOLS.md", agentToolsMd);
|
|
1915
|
+
console.log(`[${role}] Wrote TOOLS.md to workspace`);
|
|
1916
|
+
} catch (err) {
|
|
1917
|
+
console.warn(`[${role}] Could not write TOOLS.md: ${(err as Error).message}`);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// Write CLAUDE.md to workspace (agent-level instructions)
|
|
1922
|
+
if (agentClaudeMd) {
|
|
1923
|
+
try {
|
|
1924
|
+
await Bun.write("/workspace/CLAUDE.md", agentClaudeMd);
|
|
1925
|
+
console.log(`[${role}] Wrote CLAUDE.md to workspace`);
|
|
1926
|
+
} catch (err) {
|
|
1927
|
+
console.warn(`[${role}] Could not write CLAUDE.md: ${(err as Error).message}`);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1384
1931
|
// ========== Resume paused tasks with PRIORITY ==========
|
|
1385
1932
|
// Check for paused tasks from previous shutdown and resume them before normal polling
|
|
1386
1933
|
try {
|
|
@@ -1391,6 +1938,14 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1391
1938
|
console.log(`[${role}] Found ${pausedTasks.length} paused task(s) to resume`);
|
|
1392
1939
|
|
|
1393
1940
|
for (const task of pausedTasks) {
|
|
1941
|
+
// Defensive: skip tasks that already have completion data (zombie prevention)
|
|
1942
|
+
if (task.finishedAt || task.output) {
|
|
1943
|
+
console.warn(
|
|
1944
|
+
`[${role}] Skipping zombie task ${task.id.slice(0, 8)} — already has completion data (finishedAt: ${!!task.finishedAt}, output: ${!!task.output})`,
|
|
1945
|
+
);
|
|
1946
|
+
continue;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1394
1949
|
// Wait if at capacity (though unlikely on fresh startup)
|
|
1395
1950
|
while (state.activeTasks.size >= state.maxConcurrent) {
|
|
1396
1951
|
await checkCompletedProcesses(state, role, apiConfig);
|
|
@@ -1410,8 +1965,35 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1410
1965
|
continue;
|
|
1411
1966
|
}
|
|
1412
1967
|
|
|
1413
|
-
// Build prompt with resume context
|
|
1414
|
-
|
|
1968
|
+
// Build prompt with resume context + memory injection
|
|
1969
|
+
let resumePrompt = buildResumePrompt(task, adapter.formatCommand.bind(adapter));
|
|
1970
|
+
|
|
1971
|
+
// Inject relevant memories for resumed tasks
|
|
1972
|
+
const resumeMemoryContext = await fetchRelevantMemories(
|
|
1973
|
+
apiUrl,
|
|
1974
|
+
apiKey,
|
|
1975
|
+
agentId,
|
|
1976
|
+
task.task,
|
|
1977
|
+
);
|
|
1978
|
+
if (resumeMemoryContext) {
|
|
1979
|
+
resumePrompt += resumeMemoryContext;
|
|
1980
|
+
console.log(`[${role}] Injected relevant memories into resumed task prompt`);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// Resolve --resume: prefer own session ID, then parent's
|
|
1984
|
+
let resumeAdditionalArgs = opts.additionalArgs || [];
|
|
1985
|
+
if (task.claudeSessionId) {
|
|
1986
|
+
resumeAdditionalArgs = [...resumeAdditionalArgs, "--resume", task.claudeSessionId];
|
|
1987
|
+
console.log(
|
|
1988
|
+
`[${role}] Resuming task's own session ${task.claudeSessionId.slice(0, 8)}`,
|
|
1989
|
+
);
|
|
1990
|
+
} else if (task.parentTaskId) {
|
|
1991
|
+
const parentSessionId = await fetchProviderSessionId(apiUrl, apiKey, task.parentTaskId);
|
|
1992
|
+
if (parentSessionId) {
|
|
1993
|
+
resumeAdditionalArgs = [...resumeAdditionalArgs, "--resume", parentSessionId];
|
|
1994
|
+
console.log(`[${role}] Resuming parent session ${parentSessionId.slice(0, 8)}`);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1415
1997
|
|
|
1416
1998
|
// Spawn Claude process for resumed task
|
|
1417
1999
|
iteration++;
|
|
@@ -1434,27 +2016,65 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1434
2016
|
};
|
|
1435
2017
|
await Bun.write(logFile, `${JSON.stringify(metadata)}\n`);
|
|
1436
2018
|
|
|
1437
|
-
|
|
2019
|
+
// Resolve cwd for resumed task (mirrors normal task path: task.dir > vcsRepo clonePath)
|
|
2020
|
+
let resumeCwd: string | undefined;
|
|
2021
|
+
if (task.dir) {
|
|
2022
|
+
try {
|
|
2023
|
+
if (existsSync(task.dir) && statSync(task.dir).isDirectory()) {
|
|
2024
|
+
resumeCwd = task.dir;
|
|
2025
|
+
} else {
|
|
2026
|
+
console.warn(
|
|
2027
|
+
`[${role}] Resume task dir "${task.dir}" does not exist or is not a directory, falling back to default cwd`,
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
} catch {
|
|
2031
|
+
console.warn(
|
|
2032
|
+
`[${role}] Failed to check resume task dir "${task.dir}", falling back to default cwd`,
|
|
2033
|
+
);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
if (!resumeCwd && task.vcsRepo && apiUrl) {
|
|
2038
|
+
const repoConfig = await fetchRepoConfig(apiUrl, apiKey, task.vcsRepo);
|
|
2039
|
+
const effectiveConfig = repoConfig ?? {
|
|
2040
|
+
url: task.vcsRepo,
|
|
2041
|
+
name: task.vcsRepo.split("/").pop() || task.vcsRepo,
|
|
2042
|
+
clonePath: `/workspace/repos/${task.vcsRepo.split("/").pop() || task.vcsRepo}`,
|
|
2043
|
+
defaultBranch: "main",
|
|
2044
|
+
};
|
|
2045
|
+
const repoContext = await ensureRepoForTask(effectiveConfig, role);
|
|
2046
|
+
if (repoContext?.clonePath) {
|
|
2047
|
+
resumeCwd = repoContext.clonePath;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
const runningTask = await spawnProviderProcess(
|
|
2052
|
+
adapter,
|
|
1438
2053
|
{
|
|
1439
2054
|
prompt: resumePrompt,
|
|
1440
2055
|
logFile,
|
|
1441
2056
|
systemPrompt: resolvedSystemPrompt,
|
|
1442
|
-
additionalArgs:
|
|
2057
|
+
additionalArgs: resumeAdditionalArgs,
|
|
1443
2058
|
role,
|
|
1444
2059
|
apiUrl,
|
|
1445
2060
|
apiKey,
|
|
1446
2061
|
agentId,
|
|
1447
|
-
sessionId,
|
|
2062
|
+
runnerSessionId: sessionId,
|
|
1448
2063
|
iteration,
|
|
1449
2064
|
taskId: task.id,
|
|
2065
|
+
model: (task as { model?: string }).model,
|
|
2066
|
+
cwd: resumeCwd,
|
|
1450
2067
|
},
|
|
1451
2068
|
logDir,
|
|
1452
|
-
metadataType,
|
|
1453
|
-
sessionId,
|
|
1454
2069
|
isYolo,
|
|
1455
2070
|
);
|
|
1456
2071
|
|
|
1457
2072
|
state.activeTasks.set(task.id, runningTask);
|
|
2073
|
+
registerActiveSession(apiConfig, {
|
|
2074
|
+
taskId: task.id,
|
|
2075
|
+
triggerType: "task_resumed",
|
|
2076
|
+
taskDescription: task.task?.slice(0, 200),
|
|
2077
|
+
});
|
|
1458
2078
|
console.log(
|
|
1459
2079
|
`[${role}] Resumed task ${task.id.slice(0, 8)} (${state.activeTasks.size}/${state.maxConcurrent} active)`,
|
|
1460
2080
|
);
|
|
@@ -1471,8 +2091,6 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1471
2091
|
// ========== END: Resume paused tasks ==========
|
|
1472
2092
|
|
|
1473
2093
|
// Track last finished task check for leads (to avoid re-processing)
|
|
1474
|
-
let lastFinishedTaskCheck: string | undefined;
|
|
1475
|
-
|
|
1476
2094
|
while (true) {
|
|
1477
2095
|
// Ping server on each iteration to keep status updated
|
|
1478
2096
|
await pingServer(apiConfig, role);
|
|
@@ -1480,6 +2098,38 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1480
2098
|
// Check for completed processes first and ensure tasks are marked as finished
|
|
1481
2099
|
await checkCompletedProcesses(state, role, apiConfig);
|
|
1482
2100
|
|
|
2101
|
+
// Check for cancelled tasks and signal their subprocesses
|
|
2102
|
+
if (state.activeTasks.size > 0) {
|
|
2103
|
+
for (const [taskId, task] of state.activeTasks) {
|
|
2104
|
+
if (cancelledSignaled.has(taskId)) continue; // Already sent SIGTERM
|
|
2105
|
+
try {
|
|
2106
|
+
const cancelResp = await fetch(
|
|
2107
|
+
`${apiUrl}/cancelled-tasks?taskId=${encodeURIComponent(taskId)}`,
|
|
2108
|
+
{
|
|
2109
|
+
headers: {
|
|
2110
|
+
Authorization: `Bearer ${apiKey}`,
|
|
2111
|
+
"X-Agent-ID": agentId,
|
|
2112
|
+
},
|
|
2113
|
+
},
|
|
2114
|
+
);
|
|
2115
|
+
if (cancelResp.ok) {
|
|
2116
|
+
const cancelData = (await cancelResp.json()) as {
|
|
2117
|
+
cancelled: Array<{ id: string }>;
|
|
2118
|
+
};
|
|
2119
|
+
if (cancelData.cancelled?.some((t) => t.id === taskId)) {
|
|
2120
|
+
console.log(
|
|
2121
|
+
`[${role}] Task ${taskId.slice(0, 8)} was cancelled — sending SIGTERM to subprocess`,
|
|
2122
|
+
);
|
|
2123
|
+
task.session.abort().catch(() => {});
|
|
2124
|
+
cancelledSignaled.add(taskId);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
} catch {
|
|
2128
|
+
// Non-blocking — cancellation check is best-effort
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
|
|
1483
2133
|
// Only poll if we have capacity
|
|
1484
2134
|
if (state.activeTasks.size < state.maxConcurrent) {
|
|
1485
2135
|
console.log(
|
|
@@ -1495,19 +2145,160 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1495
2145
|
agentId,
|
|
1496
2146
|
pollInterval: PollIntervalMs,
|
|
1497
2147
|
pollTimeout: effectiveTimeout,
|
|
1498
|
-
since: lastFinishedTaskCheck,
|
|
1499
2148
|
});
|
|
1500
2149
|
|
|
1501
2150
|
if (trigger) {
|
|
1502
|
-
// After getting a tasks_finished trigger, update the timestamp
|
|
1503
|
-
if (trigger.type === "tasks_finished") {
|
|
1504
|
-
lastFinishedTaskCheck = new Date().toISOString();
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
2151
|
console.log(`[${role}] Trigger received: ${trigger.type}`);
|
|
1508
2152
|
|
|
1509
2153
|
// Build prompt based on trigger
|
|
1510
|
-
|
|
2154
|
+
let triggerPrompt = buildPromptForTrigger(
|
|
2155
|
+
trigger,
|
|
2156
|
+
prompt,
|
|
2157
|
+
adapter.formatCommand.bind(adapter),
|
|
2158
|
+
);
|
|
2159
|
+
|
|
2160
|
+
// Enrich prompt with relevant memories from past sessions
|
|
2161
|
+
if (trigger.type === "task_assigned" || trigger.type === "task_offered") {
|
|
2162
|
+
const task =
|
|
2163
|
+
trigger.task && typeof trigger.task === "object" && "task" in trigger.task
|
|
2164
|
+
? (trigger.task as { task: string; epicId?: string; id?: string })
|
|
2165
|
+
: null;
|
|
2166
|
+
if (task?.task) {
|
|
2167
|
+
// Enrich search query with epic context for better memory retrieval
|
|
2168
|
+
let searchQuery = task.task;
|
|
2169
|
+
if (task.epicId) {
|
|
2170
|
+
const epicContext = await fetchEpicNameAndGoal(apiUrl, apiKey, task.epicId);
|
|
2171
|
+
if (epicContext) {
|
|
2172
|
+
searchQuery = `[Epic: ${epicContext.name}] ${epicContext.goal}\n\n${task.task}`;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
const memoryContext = await fetchRelevantMemories(
|
|
2177
|
+
apiUrl,
|
|
2178
|
+
apiKey,
|
|
2179
|
+
agentId,
|
|
2180
|
+
searchQuery,
|
|
2181
|
+
);
|
|
2182
|
+
if (memoryContext) {
|
|
2183
|
+
triggerPrompt += memoryContext;
|
|
2184
|
+
console.log(`[${role}] Injected relevant memories into task prompt`);
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
// Inject recent completed task summaries from the same epic
|
|
2188
|
+
if (task.epicId && task.id) {
|
|
2189
|
+
const epicTaskContext = await fetchEpicTaskContext(
|
|
2190
|
+
apiUrl,
|
|
2191
|
+
apiKey,
|
|
2192
|
+
task.epicId,
|
|
2193
|
+
task.id,
|
|
2194
|
+
);
|
|
2195
|
+
if (epicTaskContext) {
|
|
2196
|
+
triggerPrompt += epicTaskContext;
|
|
2197
|
+
console.log(`[${role}] Injected epic task context into prompt`);
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
// For epic progress triggers, search memories related to the epic goals
|
|
2204
|
+
if (trigger.type === "epic_progress_changed" && trigger.epics) {
|
|
2205
|
+
const epics = trigger.epics as Array<{
|
|
2206
|
+
epic: { name: string; goal: string };
|
|
2207
|
+
}>;
|
|
2208
|
+
const epicQueries = epics.map((e) => `${e.epic.name}: ${e.epic.goal}`).join("\n");
|
|
2209
|
+
const memoryContext = await fetchRelevantMemories(apiUrl, apiKey, agentId, epicQueries);
|
|
2210
|
+
if (memoryContext) {
|
|
2211
|
+
triggerPrompt += memoryContext;
|
|
2212
|
+
console.log(`[${role}] Injected memories into epic progress prompt`);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// Resolve --resume for child tasks with parentTaskId
|
|
2217
|
+
let effectiveAdditionalArgs = opts.additionalArgs || [];
|
|
2218
|
+
const taskObj = trigger.task as { parentTaskId?: string } | undefined;
|
|
2219
|
+
if (taskObj?.parentTaskId) {
|
|
2220
|
+
const parentSessionId = await fetchProviderSessionId(
|
|
2221
|
+
apiUrl,
|
|
2222
|
+
apiKey,
|
|
2223
|
+
taskObj.parentTaskId,
|
|
2224
|
+
);
|
|
2225
|
+
if (parentSessionId) {
|
|
2226
|
+
effectiveAdditionalArgs = [...effectiveAdditionalArgs, "--resume", parentSessionId];
|
|
2227
|
+
console.log(
|
|
2228
|
+
`[${role}] Child task — resuming parent session ${parentSessionId.slice(0, 8)}`,
|
|
2229
|
+
);
|
|
2230
|
+
} else {
|
|
2231
|
+
console.log(`[${role}] Child task — parent session ID not found, starting fresh`);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
// Extract model from task data for per-task model selection
|
|
2236
|
+
const taskModel = (trigger.task as { model?: string } | undefined)?.model;
|
|
2237
|
+
|
|
2238
|
+
// Handle repo context for tasks with vcsRepo (GitHub/GitLab)
|
|
2239
|
+
const taskVcsRepo = (trigger.task as { vcsRepo?: string } | undefined)?.vcsRepo;
|
|
2240
|
+
if (taskVcsRepo && apiUrl) {
|
|
2241
|
+
const repoConfig = await fetchRepoConfig(apiUrl, apiKey, taskVcsRepo);
|
|
2242
|
+
// Fall back to convention-based config if repo is not registered
|
|
2243
|
+
const effectiveConfig = repoConfig ?? {
|
|
2244
|
+
url: taskVcsRepo,
|
|
2245
|
+
name: taskVcsRepo.split("/").pop() || taskVcsRepo,
|
|
2246
|
+
clonePath: `/workspace/repos/${taskVcsRepo.split("/").pop() || taskVcsRepo}`,
|
|
2247
|
+
defaultBranch: "main",
|
|
2248
|
+
};
|
|
2249
|
+
currentRepoContext = await ensureRepoForTask(effectiveConfig, role);
|
|
2250
|
+
} else {
|
|
2251
|
+
currentRepoContext = undefined;
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
// Resolve effective working directory (priority: task.dir > repoContext.clonePath > process.cwd())
|
|
2255
|
+
const taskDir = (trigger.task as { dir?: string } | undefined)?.dir;
|
|
2256
|
+
let effectiveCwd: string | undefined;
|
|
2257
|
+
|
|
2258
|
+
if (taskDir) {
|
|
2259
|
+
try {
|
|
2260
|
+
if (existsSync(taskDir) && statSync(taskDir).isDirectory()) {
|
|
2261
|
+
effectiveCwd = taskDir;
|
|
2262
|
+
} else {
|
|
2263
|
+
console.warn(
|
|
2264
|
+
`[${role}] Task dir "${taskDir}" does not exist or is not a directory, falling back to default cwd`,
|
|
2265
|
+
);
|
|
2266
|
+
}
|
|
2267
|
+
} catch {
|
|
2268
|
+
console.warn(
|
|
2269
|
+
`[${role}] Failed to check task dir "${taskDir}", falling back to default cwd`,
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
if (!effectiveCwd && currentRepoContext?.clonePath) {
|
|
2275
|
+
effectiveCwd = currentRepoContext.clonePath;
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
// Annotate prompt with working directory context
|
|
2279
|
+
if (effectiveCwd && effectiveCwd !== process.cwd()) {
|
|
2280
|
+
triggerPrompt += `\n\n---\n**Working Directory**: You are starting in \`${effectiveCwd}\`. `;
|
|
2281
|
+
if (taskDir) {
|
|
2282
|
+
triggerPrompt += "This was explicitly set on the task.";
|
|
2283
|
+
} else if (currentRepoContext?.clonePath) {
|
|
2284
|
+
triggerPrompt += "This is the repository clone path for this task's VCS repo.";
|
|
2285
|
+
}
|
|
2286
|
+
triggerPrompt +=
|
|
2287
|
+
" You can still access any path on the filesystem — this is just your starting directory.";
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
// Warn in system prompt when task dir was specified but doesn't exist
|
|
2291
|
+
let cwdWarning = "";
|
|
2292
|
+
if (taskDir && !effectiveCwd) {
|
|
2293
|
+
cwdWarning = `\n\nNote: The task requested working directory "${taskDir}" but it does not exist. Falling back to default directory.`;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
// Rebuild system prompt with per-task repo context
|
|
2297
|
+
const taskBasePrompt = buildSystemPrompt();
|
|
2298
|
+
const taskSystemPrompt =
|
|
2299
|
+
(additionalSystemPrompt
|
|
2300
|
+
? `${taskBasePrompt}\n\n${additionalSystemPrompt}`
|
|
2301
|
+
: taskBasePrompt) + cwdWarning;
|
|
1511
2302
|
|
|
1512
2303
|
iteration++;
|
|
1513
2304
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
@@ -1517,6 +2308,9 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1517
2308
|
console.log(`\n[${role}] === Iteration ${iteration} ===`);
|
|
1518
2309
|
console.log(`[${role}] Logging to: ${logFile}`);
|
|
1519
2310
|
console.log(`[${role}] Prompt: ${triggerPrompt.slice(0, 100)}...`);
|
|
2311
|
+
if (effectiveCwd) {
|
|
2312
|
+
console.log(`[${role}] Working directory: ${effectiveCwd}`);
|
|
2313
|
+
}
|
|
1520
2314
|
|
|
1521
2315
|
const metadata = {
|
|
1522
2316
|
type: metadataType,
|
|
@@ -1529,30 +2323,46 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1529
2323
|
};
|
|
1530
2324
|
await Bun.write(logFile, `${JSON.stringify(metadata)}\n`);
|
|
1531
2325
|
|
|
1532
|
-
// Spawn without blocking (await to
|
|
1533
|
-
const runningTask = await
|
|
2326
|
+
// Spawn without blocking (await to set up session, but process runs async)
|
|
2327
|
+
const runningTask = await spawnProviderProcess(
|
|
2328
|
+
adapter,
|
|
1534
2329
|
{
|
|
1535
2330
|
prompt: triggerPrompt,
|
|
1536
2331
|
logFile,
|
|
1537
|
-
systemPrompt:
|
|
1538
|
-
additionalArgs:
|
|
2332
|
+
systemPrompt: taskSystemPrompt,
|
|
2333
|
+
additionalArgs: effectiveAdditionalArgs,
|
|
1539
2334
|
role,
|
|
1540
2335
|
apiUrl,
|
|
1541
2336
|
apiKey,
|
|
1542
2337
|
agentId,
|
|
1543
|
-
sessionId,
|
|
2338
|
+
runnerSessionId: sessionId,
|
|
1544
2339
|
iteration,
|
|
1545
2340
|
taskId: trigger.taskId,
|
|
2341
|
+
model: taskModel,
|
|
2342
|
+
cwd: effectiveCwd,
|
|
1546
2343
|
},
|
|
1547
2344
|
logDir,
|
|
1548
|
-
metadataType,
|
|
1549
|
-
sessionId,
|
|
1550
2345
|
isYolo,
|
|
1551
2346
|
);
|
|
1552
2347
|
|
|
2348
|
+
// Attach trigger metadata for logging
|
|
2349
|
+
runningTask.triggerType = trigger.type;
|
|
2350
|
+
|
|
1553
2351
|
state.activeTasks.set(runningTask.taskId, runningTask);
|
|
2352
|
+
|
|
2353
|
+
// Register active session for concurrency awareness
|
|
2354
|
+
const taskDesc =
|
|
2355
|
+
trigger.task && typeof trigger.task === "object" && "task" in trigger.task
|
|
2356
|
+
? String((trigger.task as { task: string }).task).slice(0, 200)
|
|
2357
|
+
: undefined;
|
|
2358
|
+
registerActiveSession(apiConfig, {
|
|
2359
|
+
taskId: runningTask.taskId,
|
|
2360
|
+
triggerType: trigger.type,
|
|
2361
|
+
taskDescription: taskDesc,
|
|
2362
|
+
});
|
|
2363
|
+
|
|
1554
2364
|
console.log(
|
|
1555
|
-
`[${role}] Started task ${runningTask.taskId.slice(0, 8)} (${state.activeTasks.size}/${state.maxConcurrent} active)`,
|
|
2365
|
+
`[${role}] Started task ${runningTask.taskId.slice(0, 8)} (${state.activeTasks.size}/${state.maxConcurrent} active, trigger: ${trigger.type})`,
|
|
1556
2366
|
);
|
|
1557
2367
|
}
|
|
1558
2368
|
} else {
|
|
@@ -1593,19 +2403,26 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1593
2403
|
};
|
|
1594
2404
|
await Bun.write(logFile, `${JSON.stringify(metadata)}\n`);
|
|
1595
2405
|
|
|
1596
|
-
const
|
|
2406
|
+
const iterationResult = await runProviderIteration(adapter, {
|
|
1597
2407
|
prompt,
|
|
1598
2408
|
logFile,
|
|
1599
2409
|
systemPrompt: resolvedSystemPrompt,
|
|
1600
2410
|
additionalArgs: opts.additionalArgs,
|
|
1601
2411
|
role,
|
|
2412
|
+
apiUrl,
|
|
2413
|
+
apiKey,
|
|
2414
|
+
agentId,
|
|
1602
2415
|
});
|
|
1603
2416
|
|
|
1604
|
-
if (exitCode !== 0) {
|
|
2417
|
+
if (iterationResult.exitCode !== 0) {
|
|
2418
|
+
const failureReason =
|
|
2419
|
+
iterationResult.failureReason || `Process exited with code ${iterationResult.exitCode}`;
|
|
2420
|
+
|
|
1605
2421
|
const errorLog = {
|
|
1606
2422
|
timestamp: new Date().toISOString(),
|
|
1607
2423
|
iteration,
|
|
1608
|
-
exitCode,
|
|
2424
|
+
exitCode: iterationResult.exitCode,
|
|
2425
|
+
failureReason,
|
|
1609
2426
|
error: true,
|
|
1610
2427
|
};
|
|
1611
2428
|
|
|
@@ -1615,12 +2432,12 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1615
2432
|
await Bun.write(errorsFile, `${existingErrors}${JSON.stringify(errorLog)}\n`);
|
|
1616
2433
|
|
|
1617
2434
|
if (!isYolo) {
|
|
1618
|
-
console.error(`[${role}]
|
|
2435
|
+
console.error(`[${role}] ${failureReason}. Stopping.`);
|
|
1619
2436
|
console.error(`[${role}] Error logged to: ${errorsFile}`);
|
|
1620
|
-
process.exit(exitCode);
|
|
2437
|
+
process.exit(iterationResult.exitCode);
|
|
1621
2438
|
}
|
|
1622
2439
|
|
|
1623
|
-
console.warn(`[${role}]
|
|
2440
|
+
console.warn(`[${role}] ${failureReason}. YOLO mode - continuing...`);
|
|
1624
2441
|
}
|
|
1625
2442
|
|
|
1626
2443
|
console.log(`[${role}] Iteration ${iteration} complete. Starting next iteration...`);
|