@desplega.ai/agent-swarm 1.20.0 → 1.51.2
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/README.md +271 -169
- package/openapi.json +5015 -0
- package/package.json +40 -7
- 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/src/agentmail/app.ts +65 -0
- package/src/agentmail/handlers.ts +262 -0
- package/src/agentmail/index.ts +9 -0
- package/src/agentmail/templates.ts +111 -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 +3327 -784
- 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/014_prompt_templates.sql +33 -0
- package/src/be/migrations/015_workflow_workspace.sql +3 -0
- package/src/be/migrations/016_active_session_runner_session.sql +4 -0
- package/src/be/migrations/017_channel_activity_cursors.sql +6 -0
- package/src/be/migrations/018_fix_seed_double_version.sql +30 -0
- package/src/be/migrations/runner.ts +188 -0
- package/src/be/seed.ts +62 -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 +1540 -630
- package/src/commands/setup.tsx +23 -38
- package/src/commands/shared/client-config.ts +41 -0
- package/src/commands/templates.ts +172 -0
- package/src/github/app.ts +8 -0
- package/src/github/handlers.ts +384 -151
- package/src/github/index.ts +1 -0
- package/src/github/mentions-aliases.test.ts +73 -0
- package/src/github/mentions.test.ts +3 -3
- package/src/github/mentions.ts +32 -6
- package/src/github/templates.ts +398 -0
- package/src/github/types.ts +1 -0
- package/src/gitlab/auth.ts +63 -0
- package/src/gitlab/handlers.ts +368 -0
- package/src/gitlab/index.ts +19 -0
- package/src/gitlab/reactions.ts +104 -0
- package/src/gitlab/templates.ts +140 -0
- package/src/gitlab/types.ts +130 -0
- package/src/heartbeat/heartbeat.ts +434 -0
- package/src/heartbeat/index.ts +1 -0
- package/src/heartbeat/templates.ts +30 -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 +199 -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 +216 -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 +299 -0
- package/src/http/prompt-templates.ts +412 -0
- package/src/http/repos.ts +195 -0
- package/src/http/route-def.ts +123 -0
- package/src/http/schedules.ts +426 -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 +530 -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 +567 -0
- package/src/linear/templates.ts +47 -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 +150 -265
- package/src/prompts/defaults.ts +196 -0
- package/src/prompts/registry.ts +57 -0
- package/src/prompts/resolver.ts +296 -0
- package/src/prompts/session-templates.ts +604 -0
- package/src/providers/claude-adapter.ts +442 -0
- package/src/providers/index.ts +24 -0
- package/src/providers/pi-mono-adapter.ts +442 -0
- package/src/providers/pi-mono-extension.ts +624 -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 +231 -40
- package/src/server.ts +97 -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 +118 -0
- package/src/slack/blocks.ts +233 -0
- package/src/slack/channel-activity.ts +177 -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 +230 -55
- package/src/slack/responses.ts +120 -67
- package/src/slack/router.ts +17 -99
- package/src/slack/templates.ts +55 -0
- 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/channel-activity.test.ts +363 -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 +364 -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 +406 -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/pool-session-logs.test.ts +199 -0
- package/src/tests/progress-dedup.test.ts +98 -0
- package/src/tests/prompt-template-github.test.ts +682 -0
- package/src/tests/prompt-template-remaining.test.ts +504 -0
- package/src/tests/prompt-template-resolver.test.ts +621 -0
- package/src/tests/prompt-template-session.test.ts +363 -0
- package/src/tests/prompt-templates-db.test.ts +616 -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 +541 -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 +307 -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-agentid.test.ts +248 -0
- package/src/tests/update-profile-api.test.ts +143 -3
- package/src/tests/update-profile-auth.test.ts +195 -0
- 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 +508 -0
- package/src/tests/workflow-convergence.test.ts +541 -0
- package/src/tests/workflow-definition-validation.test.ts +366 -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-schedule-trigger.test.ts +104 -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/workflow-workspace.test.ts +272 -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/prompt-templates/delete.ts +86 -0
- package/src/tools/prompt-templates/get.ts +89 -0
- package/src/tools/prompt-templates/index.ts +5 -0
- package/src/tools/prompt-templates/list.ts +95 -0
- package/src/tools/prompt-templates/preview.ts +84 -0
- package/src/tools/prompt-templates/set.ts +117 -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 +205 -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 +50 -5
- package/src/tools/task-dedup.ts +97 -0
- package/src/tools/templates.ts +53 -0
- package/src/tools/tool-config.ts +124 -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 +172 -17
- package/src/tools/update-service-status.ts +2 -0
- package/src/tools/utils.ts +10 -1
- package/src/tools/workflows/create-workflow.ts +129 -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 +133 -0
- package/src/tracker/types.ts +51 -0
- package/src/types.ts +530 -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 +235 -0
- package/src/workflows/engine.ts +580 -0
- package/src/workflows/event-bus.ts +29 -0
- package/src/workflows/executors/agent-task.ts +103 -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 +229 -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 +124 -0
- package/src/workflows/validation.ts +104 -0
- package/src/workflows/version.ts +44 -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/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/.claude/settings.local.json +0 -115
- package/.dockerignore +0 -61
- package/.editorconfig +0 -15
- package/.env.docker.example +0 -39
- package/.env.example +0 -40
- package/.github/workflows/ci.yml +0 -76
- package/.github/workflows/docker-and-deploy.yml +0 -117
- package/.wts-config.json +0 -4
- package/.wts-setup.ts +0 -102
- package/CLAUDE.md +0 -104
- package/CONTRIBUTING.md +0 -270
- package/DEPLOYMENT.md +0 -605
- package/Dockerfile +0 -57
- package/Dockerfile.worker +0 -157
- package/FAQ.md +0 -19
- package/MCP.md +0 -406
- package/UI.md +0 -40
- package/assets/agent-swarm-logo-orange.png +0 -0
- package/assets/agent-swarm-logo.png +0 -0
- package/assets/agent-swarm.mp4 +0 -0
- package/assets/agent-swarm.png +0 -0
- package/biome.json +0 -39
- package/deploy/DEPLOY.md +0 -60
- package/deploy/agent-swarm.service +0 -17
- package/deploy/docker-push.ts +0 -30
- package/deploy/install.ts +0 -85
- package/deploy/prod-db.ts +0 -42
- package/deploy/uninstall.ts +0 -12
- package/deploy/update.ts +0 -21
- package/docker-compose.example.yml +0 -159
- package/docker-entrypoint.sh +0 -352
- package/ecosystem.config.cjs +0 -66
- package/plugin/README.md +0 -1
- package/plugin/hooks/hooks.json +0 -71
- package/pyproject.toml +0 -9
- package/scripts/generate-mcp-docs.ts +0 -415
- package/slack-manifest.json +0 -71
- 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/thoughts/shared/plans/2025-12-18-slack-integration.md +0 -1195
- package/thoughts/shared/plans/2025-12-19-agent-log-streaming.md +0 -732
- package/thoughts/shared/plans/2025-12-19-role-based-swarm-plugin.md +0 -361
- package/thoughts/shared/plans/2025-12-20-mobile-responsive-ui.md +0 -501
- package/thoughts/shared/plans/2025-12-20-startup-team-swarm.md +0 -560
- package/thoughts/shared/plans/2025-12-23-runner-level-polling.md +0 -934
- package/thoughts/shared/plans/2025-12-23-runner-session-logs.md +0 -1000
- package/thoughts/shared/plans/2025-12-23-worker-lead-spawn-triggers.md +0 -568
- package/thoughts/shared/plans/2026-01-09-inverse-teleport.md +0 -1516
- package/thoughts/shared/plans/2026-01-12-agent-rename-pm2-control.md +0 -1133
- package/thoughts/shared/plans/2026-01-12-github-app-integration.md +0 -380
- package/thoughts/shared/plans/2026-01-12-lead-inbox-model.md +0 -876
- package/thoughts/shared/plans/2026-01-12-ralph-wiggum-integration.md +0 -463
- package/thoughts/shared/plans/2026-01-13-agent-concurrency.md +0 -691
- package/thoughts/shared/plans/2026-01-13-github-assignment-handling.md +0 -690
- package/thoughts/shared/plans/2026-01-13-prevent-duplicate-trigger-processing.md +0 -1071
- package/thoughts/shared/plans/2026-01-14-fix-slack-thread-context.md +0 -507
- package/thoughts/shared/plans/2026-01-15-scheduled-tasks-implementation.md +0 -565
- package/thoughts/shared/plans/2026-01-15-usage-cost-tracking-ui.md +0 -1479
- package/thoughts/shared/plans/2026-01-16-epics-feature-implementation.md +0 -1230
- package/thoughts/shared/research/.gitkeep +0 -0
- package/thoughts/shared/research/2025-01-09-inverse-teleport-plan-review.md +0 -420
- package/thoughts/shared/research/2025-12-18-slack-integration.md +0 -442
- package/thoughts/shared/research/2025-12-19-agent-log-streaming.md +0 -339
- package/thoughts/shared/research/2025-12-19-agent-secrets-cli-research.md +0 -390
- package/thoughts/shared/research/2025-12-21-gemini-cli-integration.md +0 -376
- package/thoughts/shared/research/2025-12-22-runner-loop-architecture.md +0 -582
- package/thoughts/shared/research/2025-12-22-setup-experience-improvements.md +0 -264
- package/thoughts/shared/research/2026-01-13-lead-duplicate-trigger-processing.md +0 -223
- package/thoughts/shared/research/2026-01-14-lead-slack-thread-context.md +0 -277
- package/thoughts/shared/research/2026-01-15-ai-tracker-agent-swarm-integration.md +0 -376
- package/thoughts/shared/research/2026-01-15-auto-starting-processes-in-worker-containers.md +0 -787
- package/thoughts/shared/research/2026-01-15-scheduled-tasks.md +0 -390
- package/thoughts/shared/research/2026-01-16-epics-feature-research.md +0 -437
- package/thoughts/taras/plans/2026-01-22-agent-swarm-schemas.md +0 -98
- package/thoughts/taras/plans/2026-01-28-per-worker-claude-md.md +0 -617
- package/thoughts/taras/plans/2026-01-28-sentry-cli-integration.md +0 -214
- package/thoughts/taras/research/2026-01-22-vercel-cli-integration.md +0 -287
- package/thoughts/taras/research/2026-01-27-excessive-polling-issue.md +0 -311
- package/thoughts/taras/research/2026-01-28-per-worker-claude-md.md +0 -383
- package/thoughts/taras/research/2026-01-28-sentry-cli-integration.md +0 -240
- package/tsconfig.json +0 -37
- 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/public/logo.png +0 -0
- 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/hooks/useAutoScroll.ts +0 -83
- 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/lib/utils.ts +0 -88
- package/ui/src/main.tsx +0 -28
- package/ui/src/types/api.ts +0 -323
- package/ui/src/vite-env.d.ts +0 -1
- package/ui/tailwind.config.js +0 -37
- package/ui/tsconfig.json +0 -31
- package/ui/vite.config.ts +0 -35
- /package/{thoughts/shared/plans → templates/community}/.gitkeep +0 -0
package/src/commands/runner.ts
CHANGED
|
@@ -1,34 +1,27 @@
|
|
|
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 { configureHttpResolver, resolveTemplateAsync } from "../prompts/resolver.ts";
|
|
12
|
+
import {
|
|
13
|
+
type CostData,
|
|
14
|
+
createProviderAdapter,
|
|
15
|
+
type ProviderResult,
|
|
16
|
+
type ProviderSession,
|
|
17
|
+
type ProviderSessionConfig,
|
|
18
|
+
} from "../providers/index.ts";
|
|
19
|
+
import { resolveCredentialPools } from "../utils/credentials.ts";
|
|
3
20
|
import { prettyPrintLine, prettyPrintStderr } from "../utils/pretty-print.ts";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
}
|
|
21
|
+
import { detectVcsProvider } from "../vcs/index.ts";
|
|
22
|
+
import { interpolate } from "../workflows/template.ts";
|
|
23
|
+
// Side-effect import: registers runner trigger/resumption templates
|
|
24
|
+
import "./templates.ts";
|
|
32
25
|
|
|
33
26
|
/** Save PM2 process list for persistence across container restarts */
|
|
34
27
|
async function savePm2State(role: string): Promise<void> {
|
|
@@ -41,6 +34,90 @@ async function savePm2State(role: string): Promise<void> {
|
|
|
41
34
|
}
|
|
42
35
|
}
|
|
43
36
|
|
|
37
|
+
/** Fetch repo config for a task's vcsRepo (e.g., "desplega-ai/agent-swarm") */
|
|
38
|
+
async function fetchRepoConfig(
|
|
39
|
+
apiUrl: string,
|
|
40
|
+
apiKey: string,
|
|
41
|
+
vcsRepo: string,
|
|
42
|
+
): Promise<{ url: string; name: string; clonePath: string; defaultBranch: string } | null> {
|
|
43
|
+
try {
|
|
44
|
+
const repoName = vcsRepo.split("/").pop() || vcsRepo;
|
|
45
|
+
const resp = await fetch(`${apiUrl}/api/repos?name=${encodeURIComponent(repoName)}`, {
|
|
46
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
47
|
+
});
|
|
48
|
+
if (!resp.ok) return null;
|
|
49
|
+
const data = (await resp.json()) as {
|
|
50
|
+
repos: Array<{ url: string; name: string; clonePath: string; defaultBranch: string }>;
|
|
51
|
+
};
|
|
52
|
+
return data.repos.find((r) => r.url.includes(vcsRepo)) ?? data.repos[0] ?? null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Read CLAUDE.md from a repo directory, returning null if not found */
|
|
59
|
+
async function readClaudeMd(clonePath: string, role: string): Promise<string | null> {
|
|
60
|
+
const claudeMdFile = Bun.file(`${clonePath}/CLAUDE.md`);
|
|
61
|
+
if (await claudeMdFile.exists()) {
|
|
62
|
+
const content = await claudeMdFile.text();
|
|
63
|
+
console.log(`[${role}] Read CLAUDE.md from ${clonePath}/CLAUDE.md (${content.length} chars)`);
|
|
64
|
+
return content;
|
|
65
|
+
}
|
|
66
|
+
console.log(`[${role}] No CLAUDE.md found at ${clonePath}/CLAUDE.md`);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Ensure a repo is cloned and up-to-date for a task.
|
|
72
|
+
* Returns { clonePath, claudeMd, warning }.
|
|
73
|
+
*/
|
|
74
|
+
async function ensureRepoForTask(
|
|
75
|
+
repoConfig: { url: string; name: string; clonePath: string; defaultBranch: string },
|
|
76
|
+
role: string,
|
|
77
|
+
): Promise<{ clonePath: string; claudeMd: string | null; warning: string | null }> {
|
|
78
|
+
const { url, name, clonePath, defaultBranch } = repoConfig;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const gitHeadExists = await Bun.file(`${clonePath}/.git/HEAD`).exists();
|
|
82
|
+
|
|
83
|
+
let warning: string | null = null;
|
|
84
|
+
|
|
85
|
+
if (!gitHeadExists) {
|
|
86
|
+
console.log(`[${role}] Cloning ${name} to ${clonePath}...`);
|
|
87
|
+
const provider = detectVcsProvider(url);
|
|
88
|
+
if (provider === "github") {
|
|
89
|
+
await Bun.$`gh repo clone ${url} ${clonePath} -- --branch ${defaultBranch} --single-branch`.quiet();
|
|
90
|
+
} else if (provider === "gitlab") {
|
|
91
|
+
await Bun.$`glab repo clone ${url} ${clonePath} -- --branch ${defaultBranch} --single-branch`.quiet();
|
|
92
|
+
} else {
|
|
93
|
+
await Bun.$`git clone --branch ${defaultBranch} --single-branch ${url} ${clonePath}`.quiet();
|
|
94
|
+
}
|
|
95
|
+
console.log(`[${role}] Cloned ${name}`);
|
|
96
|
+
} else {
|
|
97
|
+
console.log(`[${role}] Repo ${name} already cloned at ${clonePath}`);
|
|
98
|
+
const statusResult = await Bun.$`cd ${clonePath} && git status --porcelain`.quiet();
|
|
99
|
+
const statusOutput = statusResult.text().trim();
|
|
100
|
+
|
|
101
|
+
if (statusOutput === "") {
|
|
102
|
+
console.log(`[${role}] Pulling ${name} (${defaultBranch})...`);
|
|
103
|
+
await Bun.$`cd ${clonePath} && git pull origin ${defaultBranch} --ff-only`.quiet();
|
|
104
|
+
console.log(`[${role}] Pulled ${name}`);
|
|
105
|
+
} else {
|
|
106
|
+
console.warn(`[${role}] Repo ${name} has uncommitted changes, skipping pull`);
|
|
107
|
+
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.`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const claudeMd = await readClaudeMd(clonePath, role);
|
|
112
|
+
return { clonePath, claudeMd, warning };
|
|
113
|
+
} catch (err) {
|
|
114
|
+
const errorMsg = (err as Error).message;
|
|
115
|
+
console.warn(`[${role}] Error setting up repo ${name}: ${errorMsg}`);
|
|
116
|
+
const warning = `Failed to clone/setup repo "${name}" at ${clonePath}: ${errorMsg}. The repo may not be available. You may need to clone it manually.`;
|
|
117
|
+
return { clonePath, claudeMd: null, warning };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
44
121
|
/** API configuration for ping/close */
|
|
45
122
|
interface ApiConfig {
|
|
46
123
|
apiUrl: string;
|
|
@@ -88,6 +165,115 @@ async function closeAgent(config: ApiConfig, role: string): Promise<void> {
|
|
|
88
165
|
}
|
|
89
166
|
}
|
|
90
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Fetch resolved config from the API and merge into a base env object.
|
|
170
|
+
* Falls back to baseEnv on any error (network, parse, etc).
|
|
171
|
+
* Credential env vars with comma-separated values get one randomly selected.
|
|
172
|
+
*/
|
|
173
|
+
async function fetchResolvedEnv(
|
|
174
|
+
apiUrl: string,
|
|
175
|
+
apiKey: string,
|
|
176
|
+
agentId: string,
|
|
177
|
+
baseEnv: Record<string, string | undefined> = process.env,
|
|
178
|
+
): Promise<Record<string, string | undefined>> {
|
|
179
|
+
const env: Record<string, string | undefined> = { ...baseEnv };
|
|
180
|
+
|
|
181
|
+
if (apiUrl && agentId) {
|
|
182
|
+
try {
|
|
183
|
+
const headers: Record<string, string> = { "X-Agent-ID": agentId };
|
|
184
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
185
|
+
|
|
186
|
+
const url = `${apiUrl}/api/config/resolved?agentId=${encodeURIComponent(agentId)}&includeSecrets=true`;
|
|
187
|
+
const response = await fetch(url, { headers });
|
|
188
|
+
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
console.warn(`[env-reload] Failed to fetch config: ${response.status}`);
|
|
191
|
+
} else {
|
|
192
|
+
const data = (await response.json()) as {
|
|
193
|
+
configs: Array<{ key: string; value: string }>;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
if (data.configs?.length) {
|
|
197
|
+
for (const config of data.configs) {
|
|
198
|
+
env[config.key] = config.value;
|
|
199
|
+
}
|
|
200
|
+
console.log(`[env-reload] Loaded ${data.configs.length} config entries from API`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.warn(`[env-reload] Could not fetch config, using current env: ${error}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
resolveCredentialPools(env);
|
|
209
|
+
return env;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Convert a tool call into a human-readable progress description.
|
|
214
|
+
*/
|
|
215
|
+
function toolCallToProgress(toolName: string, args: unknown): string {
|
|
216
|
+
const a = args as Record<string, unknown>;
|
|
217
|
+
const shortPath = (p: unknown) => {
|
|
218
|
+
if (typeof p !== "string") return "";
|
|
219
|
+
// Show last 2 path segments for readability
|
|
220
|
+
const parts = p.split("/");
|
|
221
|
+
return parts.length > 2 ? parts.slice(-2).join("/") : p;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
switch (toolName) {
|
|
225
|
+
case "Read":
|
|
226
|
+
return `Reading ${shortPath(a.file_path)}`;
|
|
227
|
+
case "Edit":
|
|
228
|
+
case "MultiEdit":
|
|
229
|
+
return `Editing ${shortPath(a.file_path)}`;
|
|
230
|
+
case "Write":
|
|
231
|
+
return `Writing ${shortPath(a.file_path)}`;
|
|
232
|
+
case "Bash":
|
|
233
|
+
return a.description ? `${a.description}` : "Running shell command";
|
|
234
|
+
case "Grep":
|
|
235
|
+
return `Searching for "${a.pattern}"`;
|
|
236
|
+
case "Glob":
|
|
237
|
+
return `Finding files matching ${a.pattern}`;
|
|
238
|
+
case "Agent":
|
|
239
|
+
case "Task":
|
|
240
|
+
return a.description ? `${a.description}` : "Delegating sub-task";
|
|
241
|
+
case "Skill":
|
|
242
|
+
return `Running /${a.skill}`;
|
|
243
|
+
default: {
|
|
244
|
+
// MCP tools: mcp__server__tool → "server:tool"
|
|
245
|
+
if (toolName.startsWith("mcp__")) {
|
|
246
|
+
const parts = toolName.split("__");
|
|
247
|
+
return parts.length >= 3 ? `Using ${parts[1]}:${parts[2]}` : `Using ${toolName}`;
|
|
248
|
+
}
|
|
249
|
+
return `Using ${toolName}`;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Report task progress via the API (fire-and-forget).
|
|
256
|
+
*/
|
|
257
|
+
async function updateProgressViaAPI(
|
|
258
|
+
apiUrl: string,
|
|
259
|
+
apiKey: string,
|
|
260
|
+
taskId: string,
|
|
261
|
+
progress: string,
|
|
262
|
+
): Promise<void> {
|
|
263
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
264
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
await fetch(`${apiUrl}/api/tasks/${taskId}/progress`, {
|
|
268
|
+
method: "POST",
|
|
269
|
+
headers,
|
|
270
|
+
body: JSON.stringify({ progress }),
|
|
271
|
+
});
|
|
272
|
+
} catch {
|
|
273
|
+
// Non-blocking — progress update failure is not critical
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
91
277
|
/**
|
|
92
278
|
* Ensure task is marked as completed or failed via the API.
|
|
93
279
|
* This is called when a Claude process exits to ensure task status is updated,
|
|
@@ -96,11 +282,97 @@ async function closeAgent(config: ApiConfig, role: string): Promise<void> {
|
|
|
96
282
|
* The API is idempotent - if the agent already marked the task as completed/failed,
|
|
97
283
|
* this call will succeed without changing anything.
|
|
98
284
|
*/
|
|
285
|
+
/**
|
|
286
|
+
* Attempt to extract structured output from a task's progress history
|
|
287
|
+
* when the agent session ends without calling store-progress with valid output.
|
|
288
|
+
*
|
|
289
|
+
* - Claude adapter: runs a fallback extraction via `claude -p --json-schema`
|
|
290
|
+
* - Pi-mono adapter: returns an error (no fallback available)
|
|
291
|
+
*/
|
|
292
|
+
async function handleStructuredOutputFallback(
|
|
293
|
+
config: ApiConfig,
|
|
294
|
+
taskId: string,
|
|
295
|
+
adapterType: string,
|
|
296
|
+
): Promise<{ output?: string; failReason?: string } | null> {
|
|
297
|
+
const headers: Record<string, string> = {
|
|
298
|
+
"Content-Type": "application/json",
|
|
299
|
+
};
|
|
300
|
+
if (config.apiKey) {
|
|
301
|
+
headers.Authorization = `Bearer ${config.apiKey}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
// Fetch the task to check for outputSchema
|
|
306
|
+
const taskRes = await fetch(`${config.apiUrl}/api/tasks/${taskId}`, { headers });
|
|
307
|
+
if (!taskRes.ok) return null;
|
|
308
|
+
|
|
309
|
+
const taskData = (await taskRes.json()) as {
|
|
310
|
+
task?: {
|
|
311
|
+
task?: string;
|
|
312
|
+
output?: string;
|
|
313
|
+
outputSchema?: Record<string, unknown>;
|
|
314
|
+
};
|
|
315
|
+
logs?: Array<{ eventType: string; newValue?: string; createdAt?: string }>;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const task = taskData.task;
|
|
319
|
+
if (!task?.outputSchema) return null; // No schema — no fallback needed
|
|
320
|
+
if (task.output) return null; // Agent already stored valid output
|
|
321
|
+
|
|
322
|
+
if (adapterType !== "claude") {
|
|
323
|
+
return {
|
|
324
|
+
failReason:
|
|
325
|
+
"Structured output required by outputSchema but not provided via store-progress",
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Claude adapter fallback: extract structured data from progress history
|
|
330
|
+
const progressLogs = (taskData.logs ?? [])
|
|
331
|
+
.filter((l) => l.eventType === "task_progress")
|
|
332
|
+
.sort((a, b) => (a.createdAt ?? "").localeCompare(b.createdAt ?? ""));
|
|
333
|
+
|
|
334
|
+
const progressEntries = progressLogs
|
|
335
|
+
.map((log, i) => `${i + 1}. [${log.createdAt}] ${log.newValue}`)
|
|
336
|
+
.join("\n");
|
|
337
|
+
|
|
338
|
+
const extractionPrompt = `Extract structured data from this task's execution history.
|
|
339
|
+
|
|
340
|
+
## Task Description
|
|
341
|
+
${task.task || "(no description)"}
|
|
342
|
+
|
|
343
|
+
## Progress Updates (chronological)
|
|
344
|
+
${progressEntries || "(no progress recorded)"}
|
|
345
|
+
|
|
346
|
+
## Required Output Schema
|
|
347
|
+
${JSON.stringify(task.outputSchema, null, 2)}
|
|
348
|
+
|
|
349
|
+
Extract the structured data from the progress updates above. Return ONLY valid JSON matching the schema.`;
|
|
350
|
+
|
|
351
|
+
const schemaJson = JSON.stringify(task.outputSchema);
|
|
352
|
+
const result =
|
|
353
|
+
await Bun.$`claude -p ${extractionPrompt} --json-schema ${schemaJson} --output-format json --model sonnet`
|
|
354
|
+
.json()
|
|
355
|
+
.catch(() => null);
|
|
356
|
+
|
|
357
|
+
if (result && typeof result === "object") {
|
|
358
|
+
return { output: JSON.stringify(result) };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
failReason: "Structured output extraction fallback failed — could not produce valid JSON",
|
|
363
|
+
};
|
|
364
|
+
} catch (err) {
|
|
365
|
+
console.warn(`[runner] Structured output fallback failed for task ${taskId}: ${err}`);
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
99
370
|
async function ensureTaskFinished(
|
|
100
371
|
config: ApiConfig,
|
|
101
372
|
role: string,
|
|
102
373
|
taskId: string,
|
|
103
374
|
exitCode: number,
|
|
375
|
+
failureReason?: string,
|
|
104
376
|
): Promise<void> {
|
|
105
377
|
const headers: Record<string, string> = {
|
|
106
378
|
"X-Agent-ID": config.agentId,
|
|
@@ -112,14 +384,26 @@ async function ensureTaskFinished(
|
|
|
112
384
|
|
|
113
385
|
// Determine status and reason based on exit code
|
|
114
386
|
// Exit code 0 = success, non-zero = failure
|
|
115
|
-
|
|
387
|
+
let status = exitCode === 0 ? "completed" : "failed";
|
|
116
388
|
const body: Record<string, string> = { status };
|
|
117
389
|
|
|
118
390
|
if (status === "failed") {
|
|
119
|
-
body.failureReason = `Claude process exited with code ${exitCode}`;
|
|
391
|
+
body.failureReason = failureReason || `Claude process exited with code ${exitCode}`;
|
|
120
392
|
} else {
|
|
121
|
-
|
|
122
|
-
|
|
393
|
+
// Try structured output fallback if the task has an outputSchema
|
|
394
|
+
const adapterType = process.env.HARNESS_PROVIDER || "claude";
|
|
395
|
+
const fallback = await handleStructuredOutputFallback(config, taskId, adapterType);
|
|
396
|
+
|
|
397
|
+
if (fallback?.output) {
|
|
398
|
+
body.output = fallback.output;
|
|
399
|
+
} else if (fallback?.failReason) {
|
|
400
|
+
status = "failed";
|
|
401
|
+
body.status = "failed";
|
|
402
|
+
body.failureReason = fallback.failReason;
|
|
403
|
+
} else {
|
|
404
|
+
body.output =
|
|
405
|
+
"Process completed (runner wrapper fallback - agent may have provided explicit output)";
|
|
406
|
+
}
|
|
123
407
|
}
|
|
124
408
|
|
|
125
409
|
try {
|
|
@@ -143,6 +427,8 @@ async function ensureTaskFinished(
|
|
|
143
427
|
`[${role}] Runner marked task ${taskId.slice(0, 8)} as ${status} (exit code: ${exitCode})`,
|
|
144
428
|
);
|
|
145
429
|
}
|
|
430
|
+
} else if (response.status === 404) {
|
|
431
|
+
console.log(`[${role}] Task ${taskId.slice(0, 8)} already finalized (not found), skipping`);
|
|
146
432
|
} else {
|
|
147
433
|
const error = await response.text();
|
|
148
434
|
console.warn(
|
|
@@ -190,9 +476,20 @@ async function pauseTaskViaAPI(config: ApiConfig, role: string, taskId: string):
|
|
|
190
476
|
}
|
|
191
477
|
|
|
192
478
|
/** Fetch paused tasks from API for this agent */
|
|
193
|
-
async function getPausedTasksFromAPI(
|
|
194
|
-
|
|
195
|
-
|
|
479
|
+
async function getPausedTasksFromAPI(config: ApiConfig): Promise<
|
|
480
|
+
Array<{
|
|
481
|
+
id: string;
|
|
482
|
+
task: string;
|
|
483
|
+
progress?: string;
|
|
484
|
+
claudeSessionId?: string;
|
|
485
|
+
parentTaskId?: string;
|
|
486
|
+
dir?: string;
|
|
487
|
+
vcsRepo?: string;
|
|
488
|
+
finishedAt?: string;
|
|
489
|
+
output?: string;
|
|
490
|
+
status?: string;
|
|
491
|
+
}>
|
|
492
|
+
> {
|
|
196
493
|
const headers: Record<string, string> = {
|
|
197
494
|
"X-Agent-ID": config.agentId,
|
|
198
495
|
};
|
|
@@ -212,7 +509,18 @@ async function getPausedTasksFromAPI(
|
|
|
212
509
|
}
|
|
213
510
|
|
|
214
511
|
const data = (await response.json()) as {
|
|
215
|
-
tasks: Array<{
|
|
512
|
+
tasks: Array<{
|
|
513
|
+
id: string;
|
|
514
|
+
task: string;
|
|
515
|
+
progress?: string;
|
|
516
|
+
claudeSessionId?: string;
|
|
517
|
+
parentTaskId?: string;
|
|
518
|
+
dir?: string;
|
|
519
|
+
vcsRepo?: string;
|
|
520
|
+
finishedAt?: string;
|
|
521
|
+
output?: string;
|
|
522
|
+
status?: string;
|
|
523
|
+
}>;
|
|
216
524
|
};
|
|
217
525
|
return data.tasks || [];
|
|
218
526
|
} catch (error) {
|
|
@@ -244,31 +552,26 @@ async function resumeTaskViaAPI(config: ApiConfig, taskId: string): Promise<bool
|
|
|
244
552
|
}
|
|
245
553
|
|
|
246
554
|
/** Build prompt for a resumed task */
|
|
247
|
-
function buildResumePrompt(
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
Task: "${task.task}"`;
|
|
253
|
-
|
|
555
|
+
async function buildResumePrompt(
|
|
556
|
+
task: { id: string; task: string; progress?: string },
|
|
557
|
+
fmt: (cmd: string) => string = (cmd) => `/${cmd}`,
|
|
558
|
+
): Promise<string> {
|
|
254
559
|
if (task.progress) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
prompt += `
|
|
263
|
-
|
|
264
|
-
No progress was saved before the interruption. Start the task fresh but be aware files may have been partially modified.`;
|
|
560
|
+
const result = await resolveTemplateAsync("task.resumption.with_progress", {
|
|
561
|
+
work_on_task_cmd: fmt("work-on-task"),
|
|
562
|
+
task_id: task.id,
|
|
563
|
+
task_description: task.task,
|
|
564
|
+
progress: task.progress,
|
|
565
|
+
});
|
|
566
|
+
return result.text;
|
|
265
567
|
}
|
|
266
568
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
569
|
+
const result = await resolveTemplateAsync("task.resumption.no_progress", {
|
|
570
|
+
work_on_task_cmd: fmt("work-on-task"),
|
|
571
|
+
task_id: task.id,
|
|
572
|
+
task_description: task.task,
|
|
573
|
+
});
|
|
574
|
+
return result.text;
|
|
272
575
|
}
|
|
273
576
|
|
|
274
577
|
/** Setup signal handlers for graceful shutdown */
|
|
@@ -303,7 +606,7 @@ function setupShutdownHandlers(
|
|
|
303
606
|
);
|
|
304
607
|
for (const [taskId, task] of state.activeTasks) {
|
|
305
608
|
console.log(`[${role}] Pausing task ${taskId.slice(0, 8)}`);
|
|
306
|
-
task.
|
|
609
|
+
task.session.abort().catch(() => {});
|
|
307
610
|
// Mark as paused for graceful resume (instead of failed)
|
|
308
611
|
if (apiConfig) {
|
|
309
612
|
const paused = await pauseTaskViaAPI(apiConfig, role, taskId);
|
|
@@ -352,28 +655,19 @@ export interface RunnerOptions {
|
|
|
352
655
|
aiLoop?: boolean; // Use AI-based loop (old behavior)
|
|
353
656
|
}
|
|
354
657
|
|
|
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
658
|
/** Running task state for parallel execution */
|
|
371
659
|
interface RunningTask {
|
|
372
660
|
taskId: string;
|
|
373
|
-
|
|
661
|
+
session: ProviderSession;
|
|
374
662
|
logFile: string;
|
|
375
663
|
startTime: Date;
|
|
376
|
-
promise: Promise<
|
|
664
|
+
promise: Promise<ProviderResult>;
|
|
665
|
+
/** The trigger type that caused this task to be spawned */
|
|
666
|
+
triggerType?: string;
|
|
667
|
+
/** Set when the promise resolves, enabling non-blocking completion checks */
|
|
668
|
+
result: ProviderResult | null;
|
|
669
|
+
/** Deferred cursor updates for channel_activity triggers — committed after success */
|
|
670
|
+
cursorUpdates?: Array<{ channelId: string; ts: string }>;
|
|
377
671
|
}
|
|
378
672
|
|
|
379
673
|
/** Runner state for tracking concurrent tasks */
|
|
@@ -408,6 +702,12 @@ async function flushLogBuffer(
|
|
|
408
702
|
): Promise<void> {
|
|
409
703
|
if (buffer.lines.length === 0) return;
|
|
410
704
|
|
|
705
|
+
// Snapshot and clear buffer immediately to prevent duplicate flushes
|
|
706
|
+
// (fire-and-forget callers would otherwise race on the same buffer)
|
|
707
|
+
const lines = buffer.lines;
|
|
708
|
+
buffer.lines = [];
|
|
709
|
+
buffer.lastFlush = Date.now();
|
|
710
|
+
|
|
411
711
|
const headers: Record<string, string> = {
|
|
412
712
|
"Content-Type": "application/json",
|
|
413
713
|
"X-Agent-ID": opts.agentId,
|
|
@@ -425,7 +725,7 @@ async function flushLogBuffer(
|
|
|
425
725
|
iteration: opts.iteration,
|
|
426
726
|
taskId: opts.taskId,
|
|
427
727
|
cli: opts.cli || "claude",
|
|
428
|
-
lines
|
|
728
|
+
lines,
|
|
429
729
|
}),
|
|
430
730
|
});
|
|
431
731
|
|
|
@@ -435,26 +735,6 @@ async function flushLogBuffer(
|
|
|
435
735
|
} catch (error) {
|
|
436
736
|
console.warn(`[runner] Error pushing logs: ${error}`);
|
|
437
737
|
}
|
|
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
738
|
}
|
|
459
739
|
|
|
460
740
|
/** Save session cost data to the API */
|
|
@@ -482,6 +762,120 @@ async function saveCostData(cost: CostData, apiUrl: string, apiKey: string): Pro
|
|
|
482
762
|
}
|
|
483
763
|
}
|
|
484
764
|
|
|
765
|
+
/** Save Claude session ID for a task (fire-and-forget) */
|
|
766
|
+
async function saveProviderSessionId(
|
|
767
|
+
apiUrl: string,
|
|
768
|
+
apiKey: string,
|
|
769
|
+
taskId: string,
|
|
770
|
+
claudeSessionId: string,
|
|
771
|
+
): Promise<void> {
|
|
772
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
773
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
774
|
+
await fetch(`${apiUrl}/api/tasks/${taskId}/claude-session`, {
|
|
775
|
+
method: "PUT",
|
|
776
|
+
headers,
|
|
777
|
+
body: JSON.stringify({ claudeSessionId }),
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/** Save provider session ID on the active session (for pool tasks where realTaskId is unknown) */
|
|
782
|
+
async function saveProviderSessionIdOnActiveSession(
|
|
783
|
+
apiUrl: string,
|
|
784
|
+
apiKey: string,
|
|
785
|
+
effectiveTaskId: string,
|
|
786
|
+
providerSessionId: string,
|
|
787
|
+
): Promise<void> {
|
|
788
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
789
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
790
|
+
await fetch(`${apiUrl}/api/active-sessions/provider-session/${effectiveTaskId}`, {
|
|
791
|
+
method: "PUT",
|
|
792
|
+
headers,
|
|
793
|
+
body: JSON.stringify({ providerSessionId }),
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/** Fetch Claude session ID for a task (for --resume) */
|
|
798
|
+
async function fetchProviderSessionId(
|
|
799
|
+
apiUrl: string,
|
|
800
|
+
apiKey: string,
|
|
801
|
+
taskId: string,
|
|
802
|
+
): Promise<string | null> {
|
|
803
|
+
const headers: Record<string, string> = {};
|
|
804
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
805
|
+
try {
|
|
806
|
+
const response = await fetch(`${apiUrl}/api/tasks/${taskId}`, { headers });
|
|
807
|
+
if (!response.ok) return null;
|
|
808
|
+
const data = (await response.json()) as { claudeSessionId?: string };
|
|
809
|
+
return data.claudeSessionId || null;
|
|
810
|
+
} catch {
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/** Register an active session with the API (fire-and-forget) */
|
|
816
|
+
async function registerActiveSession(
|
|
817
|
+
config: ApiConfig,
|
|
818
|
+
session: {
|
|
819
|
+
taskId: string;
|
|
820
|
+
triggerType: string;
|
|
821
|
+
taskDescription?: string;
|
|
822
|
+
runnerSessionId?: string;
|
|
823
|
+
},
|
|
824
|
+
): Promise<void> {
|
|
825
|
+
const headers: Record<string, string> = {
|
|
826
|
+
"Content-Type": "application/json",
|
|
827
|
+
"X-Agent-ID": config.agentId,
|
|
828
|
+
};
|
|
829
|
+
if (config.apiKey) headers.Authorization = `Bearer ${config.apiKey}`;
|
|
830
|
+
try {
|
|
831
|
+
await fetch(`${config.apiUrl}/api/active-sessions`, {
|
|
832
|
+
method: "POST",
|
|
833
|
+
headers,
|
|
834
|
+
body: JSON.stringify({
|
|
835
|
+
agentId: config.agentId,
|
|
836
|
+
taskId: session.taskId,
|
|
837
|
+
triggerType: session.triggerType,
|
|
838
|
+
taskDescription: session.taskDescription,
|
|
839
|
+
runnerSessionId: session.runnerSessionId,
|
|
840
|
+
}),
|
|
841
|
+
});
|
|
842
|
+
} catch {
|
|
843
|
+
// Non-blocking — session tracking is best-effort
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** Remove an active session by taskId (fire-and-forget) */
|
|
848
|
+
async function removeActiveSession(config: ApiConfig, taskId: string): Promise<void> {
|
|
849
|
+
const headers: Record<string, string> = { "X-Agent-ID": config.agentId };
|
|
850
|
+
if (config.apiKey) headers.Authorization = `Bearer ${config.apiKey}`;
|
|
851
|
+
try {
|
|
852
|
+
await fetch(`${config.apiUrl}/api/active-sessions/by-task/${taskId}`, {
|
|
853
|
+
method: "DELETE",
|
|
854
|
+
headers,
|
|
855
|
+
});
|
|
856
|
+
} catch {
|
|
857
|
+
// Non-blocking
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/** Clean up all active sessions for this agent (on startup) */
|
|
862
|
+
async function cleanupActiveSessions(config: ApiConfig): Promise<void> {
|
|
863
|
+
const headers: Record<string, string> = {
|
|
864
|
+
"Content-Type": "application/json",
|
|
865
|
+
"X-Agent-ID": config.agentId,
|
|
866
|
+
};
|
|
867
|
+
if (config.apiKey) headers.Authorization = `Bearer ${config.apiKey}`;
|
|
868
|
+
try {
|
|
869
|
+
await fetch(`${config.apiUrl}/api/active-sessions/cleanup`, {
|
|
870
|
+
method: "POST",
|
|
871
|
+
headers,
|
|
872
|
+
body: JSON.stringify({ agentId: config.agentId }),
|
|
873
|
+
});
|
|
874
|
+
} catch {
|
|
875
|
+
// Non-blocking
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
485
879
|
/** Trigger types returned by the poll API */
|
|
486
880
|
interface Trigger {
|
|
487
881
|
type:
|
|
@@ -489,9 +883,8 @@ interface Trigger {
|
|
|
489
883
|
| "task_offered"
|
|
490
884
|
| "unread_mentions"
|
|
491
885
|
| "pool_tasks_available"
|
|
492
|
-
| "
|
|
493
|
-
| "
|
|
494
|
-
| "epic_progress_changed";
|
|
886
|
+
| "epic_progress_changed"
|
|
887
|
+
| "channel_activity";
|
|
495
888
|
taskId?: string;
|
|
496
889
|
task?: unknown;
|
|
497
890
|
mentionsCount?: number;
|
|
@@ -508,8 +901,14 @@ interface Trigger {
|
|
|
508
901
|
messages?: Array<{
|
|
509
902
|
id: string;
|
|
510
903
|
content: string;
|
|
904
|
+
channelId?: string;
|
|
905
|
+
channelName?: string;
|
|
906
|
+
ts?: string;
|
|
907
|
+
user?: string;
|
|
908
|
+
text?: string;
|
|
511
909
|
}>;
|
|
512
910
|
epics?: unknown; // Epic progress updates for lead
|
|
911
|
+
cursorUpdates?: Array<{ channelId: string; ts: string }>; // Deferred cursor commits for channel_activity
|
|
513
912
|
}
|
|
514
913
|
|
|
515
914
|
/** Options for polling */
|
|
@@ -529,6 +928,7 @@ async function registerAgent(opts: {
|
|
|
529
928
|
agentId: string;
|
|
530
929
|
name: string;
|
|
531
930
|
isLead: boolean;
|
|
931
|
+
role?: string;
|
|
532
932
|
capabilities?: string[];
|
|
533
933
|
maxTasks?: number;
|
|
534
934
|
}): Promise<void> {
|
|
@@ -546,6 +946,7 @@ async function registerAgent(opts: {
|
|
|
546
946
|
body: JSON.stringify({
|
|
547
947
|
name: opts.name,
|
|
548
948
|
isLead: opts.isLead,
|
|
949
|
+
role: opts.role,
|
|
549
950
|
capabilities: opts.capabilities,
|
|
550
951
|
maxTasks: opts.maxTasks,
|
|
551
952
|
}),
|
|
@@ -601,7 +1002,11 @@ async function pollForTrigger(opts: PollOptions): Promise<Trigger | null> {
|
|
|
601
1002
|
}
|
|
602
1003
|
|
|
603
1004
|
/** Build prompt based on trigger type */
|
|
604
|
-
function buildPromptForTrigger(
|
|
1005
|
+
async function buildPromptForTrigger(
|
|
1006
|
+
trigger: Trigger,
|
|
1007
|
+
defaultPrompt: string,
|
|
1008
|
+
fmt: (cmd: string) => string = (cmd) => `/${cmd}`,
|
|
1009
|
+
): Promise<string> {
|
|
605
1010
|
switch (trigger.type) {
|
|
606
1011
|
case "task_assigned": {
|
|
607
1012
|
// Use the work-on-task command with task ID and description
|
|
@@ -609,12 +1014,25 @@ function buildPromptForTrigger(trigger: Trigger, defaultPrompt: string): string
|
|
|
609
1014
|
trigger.task && typeof trigger.task === "object" && "task" in trigger.task
|
|
610
1015
|
? (trigger.task as { task: string }).task
|
|
611
1016
|
: null;
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
1017
|
+
const taskDescSection = taskDesc ? `\n\nTask: "${taskDesc}"` : "";
|
|
1018
|
+
|
|
1019
|
+
// Build output instructions — use outputSchema if present, otherwise generic
|
|
1020
|
+
const taskObj = trigger.task as Record<string, unknown> | undefined;
|
|
1021
|
+
let outputInstructions: string;
|
|
1022
|
+
if (taskObj?.outputSchema && typeof taskObj.outputSchema === "object") {
|
|
1023
|
+
outputInstructions = `\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.`;
|
|
1024
|
+
} else {
|
|
1025
|
+
outputInstructions =
|
|
1026
|
+
'\n\nWhen done, use `store-progress` with status: "completed" and include your output.';
|
|
615
1027
|
}
|
|
616
|
-
|
|
617
|
-
|
|
1028
|
+
|
|
1029
|
+
const result = await resolveTemplateAsync("task.trigger.assigned", {
|
|
1030
|
+
work_on_task_cmd: fmt("work-on-task"),
|
|
1031
|
+
task_id: trigger.taskId,
|
|
1032
|
+
task_desc_section: taskDescSection,
|
|
1033
|
+
output_instructions: outputInstructions,
|
|
1034
|
+
});
|
|
1035
|
+
return result.text;
|
|
618
1036
|
}
|
|
619
1037
|
|
|
620
1038
|
case "task_offered": {
|
|
@@ -623,73 +1041,27 @@ function buildPromptForTrigger(trigger: Trigger, defaultPrompt: string): string
|
|
|
623
1041
|
trigger.task && typeof trigger.task === "object" && "task" in trigger.task
|
|
624
1042
|
? (trigger.task as { task: string }).task
|
|
625
1043
|
: null;
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
1044
|
+
const taskDescSection = taskDesc ? `\n\nA task has been offered to you:\n"${taskDesc}"` : "";
|
|
1045
|
+
const result = await resolveTemplateAsync("task.trigger.offered", {
|
|
1046
|
+
review_offered_task_cmd: fmt("review-offered-task"),
|
|
1047
|
+
task_id: trigger.taskId,
|
|
1048
|
+
task_desc_section: taskDescSection,
|
|
1049
|
+
});
|
|
1050
|
+
return result.text;
|
|
632
1051
|
}
|
|
633
1052
|
|
|
634
|
-
case "unread_mentions":
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
3. If a message requires work, create a task using \`send-task\``;
|
|
641
|
-
|
|
642
|
-
case "pool_tasks_available":
|
|
643
|
-
// Worker: claim a task from the pool - numbered steps for clarity
|
|
644
|
-
return `${trigger.count} task(s) available in the pool.
|
|
645
|
-
|
|
646
|
-
1. Run \`get-tasks\` with unassigned: true to browse
|
|
647
|
-
2. Pick one matching your skills
|
|
648
|
-
3. Run \`task-action\` with action: "claim" and taskId: "<id>"
|
|
649
|
-
|
|
650
|
-
Note: Claims are first-come-first-serve. If claim fails, pick another.`;
|
|
651
|
-
|
|
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
|
-
}
|
|
1053
|
+
case "unread_mentions": {
|
|
1054
|
+
const result = await resolveTemplateAsync("task.trigger.unread_mentions", {
|
|
1055
|
+
mention_count: trigger.count || "unread",
|
|
1056
|
+
});
|
|
1057
|
+
return result.text;
|
|
1058
|
+
}
|
|
691
1059
|
|
|
692
|
-
|
|
1060
|
+
case "pool_tasks_available": {
|
|
1061
|
+
const result = await resolveTemplateAsync("task.trigger.pool_available", {
|
|
1062
|
+
task_count: trigger.count,
|
|
1063
|
+
});
|
|
1064
|
+
return result.text;
|
|
693
1065
|
}
|
|
694
1066
|
|
|
695
1067
|
case "epic_progress_changed": {
|
|
@@ -700,6 +1072,9 @@ Note: Claims are first-come-first-serve. If claim fails, pick another.`;
|
|
|
700
1072
|
id: string;
|
|
701
1073
|
name: string;
|
|
702
1074
|
goal: string;
|
|
1075
|
+
plan?: string;
|
|
1076
|
+
prd?: string;
|
|
1077
|
+
nextSteps?: string;
|
|
703
1078
|
status: string;
|
|
704
1079
|
progress: number;
|
|
705
1080
|
taskStats: {
|
|
@@ -724,92 +1099,83 @@ Note: Claims are first-come-first-serve. If claim fails, pick another.`;
|
|
|
724
1099
|
return "Epic progress was updated but no details available. Use `list-epics` to check status.";
|
|
725
1100
|
}
|
|
726
1101
|
|
|
727
|
-
|
|
728
|
-
|
|
1102
|
+
// Build epics detail section as a pre-computed variable
|
|
1103
|
+
let epicsDetail = "";
|
|
729
1104
|
for (const { epic, finishedTasks } of epics) {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1105
|
+
epicsDetail += `### Epic: "${epic.name}" (${epic.id.slice(0, 8)})\n`;
|
|
1106
|
+
epicsDetail += `**Goal:** ${epic.goal}\n`;
|
|
1107
|
+
epicsDetail += `**Progress:** ${epic.progress}% complete (${epic.taskStats.completed}/${epic.taskStats.total} tasks)\n`;
|
|
1108
|
+
epicsDetail += `**Status:** ${epic.status}\n\n`;
|
|
1109
|
+
|
|
1110
|
+
if (epic.plan) {
|
|
1111
|
+
epicsDetail += `**Plan:**\n${epic.plan.slice(0, 2000)}\n\n`;
|
|
1112
|
+
}
|
|
1113
|
+
if (epic.prd) {
|
|
1114
|
+
epicsDetail += `**PRD:**\n${epic.prd.slice(0, 1000)}\n\n`;
|
|
1115
|
+
}
|
|
734
1116
|
|
|
735
1117
|
// Show finished tasks
|
|
736
1118
|
const completed = finishedTasks.filter((t) => t.status === "completed");
|
|
737
1119
|
const failed = finishedTasks.filter((t) => t.status === "failed");
|
|
738
1120
|
|
|
739
1121
|
if (completed.length > 0) {
|
|
740
|
-
|
|
1122
|
+
epicsDetail += "**Recently Completed:**\n";
|
|
741
1123
|
for (const t of completed) {
|
|
742
1124
|
const agentName = t.agentId ? `Agent ${t.agentId.slice(0, 8)}` : "Unknown";
|
|
743
1125
|
const output = t.output ? t.output.slice(0, 150) : "(no output)";
|
|
744
|
-
|
|
745
|
-
|
|
1126
|
+
epicsDetail += `- Task ${t.id.slice(0, 8)} by ${agentName}: "${t.task.slice(0, 80)}"\n`;
|
|
1127
|
+
epicsDetail += ` Output: ${output}${t.output && t.output.length > 150 ? "..." : ""}\n`;
|
|
746
1128
|
}
|
|
747
1129
|
}
|
|
748
1130
|
|
|
749
1131
|
if (failed.length > 0) {
|
|
750
|
-
|
|
1132
|
+
epicsDetail += "\n**Recently Failed:**\n";
|
|
751
1133
|
for (const t of failed) {
|
|
752
1134
|
const agentName = t.agentId ? `Agent ${t.agentId.slice(0, 8)}` : "Unknown";
|
|
753
|
-
|
|
754
|
-
|
|
1135
|
+
epicsDetail += `- Task ${t.id.slice(0, 8)} by ${agentName}: "${t.task.slice(0, 80)}"\n`;
|
|
1136
|
+
epicsDetail += ` Reason: ${t.failureReason || "(no reason)"}\n`;
|
|
755
1137
|
}
|
|
756
1138
|
}
|
|
757
1139
|
|
|
758
1140
|
// Show remaining work
|
|
759
1141
|
const { inProgress, pending } = epic.taskStats;
|
|
760
1142
|
if (inProgress > 0 || pending > 0) {
|
|
761
|
-
|
|
1143
|
+
epicsDetail += `\n**Remaining:** ${inProgress} in progress, ${pending} pending\n`;
|
|
762
1144
|
}
|
|
763
1145
|
|
|
764
|
-
|
|
1146
|
+
epicsDetail += "\n---\n\n";
|
|
765
1147
|
}
|
|
766
1148
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
3. **If complete:** Use \`update-epic\` to mark status as "completed"
|
|
773
|
-
4. **If not complete:**
|
|
774
|
-
- Retry failed tasks with \`send-task\` (reassign or modify)
|
|
775
|
-
- Create new tasks for remaining work with \`send-task\` (include epicId)
|
|
776
|
-
- Keep the epic progressing until the goal is achieved
|
|
777
|
-
|
|
778
|
-
This is an iterative process - you'll be notified again when more tasks finish.
|
|
779
|
-
The epic should keep progressing until 100% complete and the goal is achieved.`;
|
|
780
|
-
|
|
781
|
-
return prompt;
|
|
1149
|
+
const result = await resolveTemplateAsync("task.trigger.epic_progress", {
|
|
1150
|
+
epic_count: trigger.count,
|
|
1151
|
+
epics_detail: epicsDetail,
|
|
1152
|
+
});
|
|
1153
|
+
return result.text;
|
|
782
1154
|
}
|
|
783
1155
|
|
|
784
|
-
case "
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
const threadHistory = threadHistoryMatch ? threadHistoryMatch[1] : null;
|
|
796
|
-
|
|
797
|
-
let formatted = `### Message ${index + 1} (inboxMessageId: ${m.id})\n`;
|
|
798
|
-
formatted += `**New Message:**\n${newMessage}\n`;
|
|
799
|
-
|
|
800
|
-
if (threadHistory) {
|
|
801
|
-
formatted += `\n**Thread History:**\n${threadHistory}\n`;
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
return formatted;
|
|
805
|
-
})
|
|
806
|
-
.join("\n---\n\n");
|
|
1156
|
+
case "channel_activity": {
|
|
1157
|
+
const msgs = (trigger.messages || []) as Array<{
|
|
1158
|
+
channelId?: string;
|
|
1159
|
+
channelName?: string;
|
|
1160
|
+
ts?: string;
|
|
1161
|
+
user?: string;
|
|
1162
|
+
text?: string;
|
|
1163
|
+
}>;
|
|
1164
|
+
if (msgs.length === 0) {
|
|
1165
|
+
return "New Slack channel activity detected but no message details available. Use `slack-read` to check recent messages.";
|
|
1166
|
+
}
|
|
807
1167
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1168
|
+
let messagesDetail = "";
|
|
1169
|
+
for (const msg of msgs) {
|
|
1170
|
+
const channel = msg.channelName ? `#${msg.channelName}` : msg.channelId || "unknown";
|
|
1171
|
+
messagesDetail += `- **${channel}** (user: ${msg.user || "unknown"}): ${msg.text?.slice(0, 200) || "(no text)"}\n`;
|
|
1172
|
+
}
|
|
811
1173
|
|
|
812
|
-
|
|
1174
|
+
const result = await resolveTemplateAsync("task.trigger.channel_activity", {
|
|
1175
|
+
message_count: trigger.count || msgs.length,
|
|
1176
|
+
messages_detail: messagesDetail,
|
|
1177
|
+
});
|
|
1178
|
+
return result.text;
|
|
813
1179
|
}
|
|
814
1180
|
|
|
815
1181
|
default:
|
|
@@ -817,388 +1183,244 @@ Do not leave messages unanswered.`;
|
|
|
817
1183
|
}
|
|
818
1184
|
}
|
|
819
1185
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
"-p",
|
|
834
|
-
opts.prompt,
|
|
835
|
-
];
|
|
836
|
-
|
|
837
|
-
if (opts.additionalArgs && opts.additionalArgs.length > 0) {
|
|
838
|
-
Cmd.push(...opts.additionalArgs);
|
|
839
|
-
}
|
|
1186
|
+
/** Search agent memories relevant to a task description via the API */
|
|
1187
|
+
async function fetchRelevantMemories(
|
|
1188
|
+
apiUrl: string,
|
|
1189
|
+
apiKey: string,
|
|
1190
|
+
agentId: string,
|
|
1191
|
+
taskDescription: string,
|
|
1192
|
+
): Promise<string | null> {
|
|
1193
|
+
try {
|
|
1194
|
+
const headers: Record<string, string> = {
|
|
1195
|
+
"Content-Type": "application/json",
|
|
1196
|
+
"X-Agent-ID": agentId,
|
|
1197
|
+
};
|
|
1198
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
840
1199
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1200
|
+
const response = await fetch(`${apiUrl}/api/memory/search`, {
|
|
1201
|
+
method: "POST",
|
|
1202
|
+
headers,
|
|
1203
|
+
body: JSON.stringify({ query: taskDescription, limit: 5 }),
|
|
1204
|
+
});
|
|
844
1205
|
|
|
845
|
-
|
|
1206
|
+
if (!response.ok) return null;
|
|
846
1207
|
|
|
847
|
-
|
|
848
|
-
|
|
1208
|
+
const data = (await response.json()) as {
|
|
1209
|
+
results: Array<{ id: string; name: string; content: string; similarity: number }>;
|
|
1210
|
+
};
|
|
849
1211
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
stdout: "pipe",
|
|
853
|
-
stderr: "pipe",
|
|
854
|
-
});
|
|
1212
|
+
const useful = (data.results || []).filter((m) => m.similarity > 0.4);
|
|
1213
|
+
if (useful.length === 0) return null;
|
|
855
1214
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
const stdoutPromise = (async () => {
|
|
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
|
-
}
|
|
1215
|
+
const memoryContext = useful
|
|
1216
|
+
.map((m) => `- **${m.name}** (id: ${m.id}): ${m.content.substring(0, 300)}`)
|
|
1217
|
+
.join("\n");
|
|
904
1218
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
logBuffer.partialLine = "";
|
|
912
|
-
}
|
|
1219
|
+
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`;
|
|
1220
|
+
} catch {
|
|
1221
|
+
// Non-blocking — don't fail task start because of memory search
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
913
1225
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
taskId: opts.taskId,
|
|
923
|
-
cli: "claude",
|
|
924
|
-
});
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
})();
|
|
928
|
-
|
|
929
|
-
const stderrPromise = (async () => {
|
|
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
|
-
})();
|
|
1226
|
+
async function fetchEpicNameAndGoal(
|
|
1227
|
+
apiUrl: string,
|
|
1228
|
+
apiKey: string,
|
|
1229
|
+
epicId: string,
|
|
1230
|
+
): Promise<{ name: string; goal: string } | null> {
|
|
1231
|
+
try {
|
|
1232
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
1233
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
942
1234
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
const exitCode = await proc.exited;
|
|
1235
|
+
const response = await fetch(`${apiUrl}/api/epics/${epicId}`, { headers });
|
|
1236
|
+
if (!response.ok) return null;
|
|
946
1237
|
|
|
947
|
-
|
|
948
|
-
|
|
1238
|
+
const data = (await response.json()) as { name: string; goal: string };
|
|
1239
|
+
return { name: data.name, goal: data.goal };
|
|
1240
|
+
} catch {
|
|
1241
|
+
return null;
|
|
949
1242
|
}
|
|
1243
|
+
}
|
|
950
1244
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1245
|
+
async function fetchEpicTaskContext(
|
|
1246
|
+
apiUrl: string,
|
|
1247
|
+
apiKey: string,
|
|
1248
|
+
epicId: string,
|
|
1249
|
+
currentTaskId: string,
|
|
1250
|
+
): Promise<string | null> {
|
|
1251
|
+
try {
|
|
1252
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
1253
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
954
1254
|
|
|
955
|
-
|
|
1255
|
+
const response = await fetch(`${apiUrl}/api/tasks?epicId=${epicId}&status=completed&limit=5`, {
|
|
1256
|
+
headers,
|
|
1257
|
+
});
|
|
1258
|
+
if (!response.ok) return null;
|
|
1259
|
+
|
|
1260
|
+
const data = (await response.json()) as {
|
|
1261
|
+
tasks: Array<{ id: string; task: string; output?: string }>;
|
|
1262
|
+
};
|
|
1263
|
+
const tasks = data.tasks || [];
|
|
1264
|
+
|
|
1265
|
+
const relevant = tasks.filter((t) => t.id !== currentTaskId);
|
|
1266
|
+
if (relevant.length === 0) return null;
|
|
1267
|
+
|
|
1268
|
+
let context = "\n\n### Recent Epic Task Completions\n\n";
|
|
1269
|
+
context += "These tasks were recently completed in the same epic:\n\n";
|
|
1270
|
+
for (const t of relevant.slice(0, 5)) {
|
|
1271
|
+
context += `- **${t.task.slice(0, 100)}**: ${(t.output || "no output").slice(0, 200)}\n`;
|
|
1272
|
+
}
|
|
1273
|
+
return context;
|
|
1274
|
+
} catch {
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
956
1277
|
}
|
|
957
1278
|
|
|
958
|
-
/** Spawn a
|
|
959
|
-
async function
|
|
960
|
-
|
|
1279
|
+
/** Spawn a provider session without blocking - returns immediately with tracking info */
|
|
1280
|
+
async function spawnProviderProcess(
|
|
1281
|
+
adapter: ReturnType<typeof createProviderAdapter>,
|
|
1282
|
+
opts: {
|
|
1283
|
+
prompt: string;
|
|
1284
|
+
logFile: string;
|
|
1285
|
+
systemPrompt?: string;
|
|
1286
|
+
additionalArgs?: string[];
|
|
1287
|
+
role: string;
|
|
1288
|
+
apiUrl: string;
|
|
1289
|
+
apiKey: string;
|
|
1290
|
+
agentId: string;
|
|
1291
|
+
runnerSessionId: string;
|
|
1292
|
+
iteration: number;
|
|
1293
|
+
taskId?: string;
|
|
1294
|
+
model?: string;
|
|
1295
|
+
cwd?: string;
|
|
1296
|
+
},
|
|
961
1297
|
logDir: string,
|
|
962
|
-
_metadataType: string,
|
|
963
|
-
_sessionId: string,
|
|
964
1298
|
isYolo: boolean,
|
|
965
1299
|
): 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);
|
|
1300
|
+
// Real task ID from DB (may be undefined for pool_tasks_available triggers)
|
|
1301
|
+
const realTaskId = opts.taskId;
|
|
1302
|
+
// Correlation ID for logs/display — always defined
|
|
1303
|
+
const effectiveTaskId = realTaskId || crypto.randomUUID();
|
|
1304
|
+
|
|
1305
|
+
// Resolve env first so we can use MODEL_OVERRIDE from config
|
|
1306
|
+
const freshEnv = await fetchResolvedEnv(opts.apiUrl, opts.apiKey, opts.agentId);
|
|
1307
|
+
|
|
1308
|
+
// Propagate agent-fs config to process.env so getBasePrompt() can read them
|
|
1309
|
+
// (fetchResolvedEnv returns a new object, doesn't update process.env)
|
|
1310
|
+
if (freshEnv.AGENT_FS_SHARED_ORG_ID) {
|
|
1311
|
+
process.env.AGENT_FS_SHARED_ORG_ID = freshEnv.AGENT_FS_SHARED_ORG_ID as string;
|
|
984
1312
|
}
|
|
985
1313
|
|
|
986
|
-
|
|
987
|
-
Cmd.push("--append-system-prompt", opts.systemPrompt);
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
const effectiveTaskId = taskId || crypto.randomUUID();
|
|
991
|
-
|
|
992
|
-
console.log(
|
|
993
|
-
`\x1b[2m[${role}]\x1b[0m \x1b[36m▸\x1b[0m Spawning Claude for task ${effectiveTaskId.slice(0, 8)}`,
|
|
994
|
-
);
|
|
995
|
-
|
|
996
|
-
const logFileHandle = Bun.file(opts.logFile).writer();
|
|
1314
|
+
const model = opts.model || (freshEnv.MODEL_OVERRIDE as string) || "";
|
|
997
1315
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1316
|
+
const config: ProviderSessionConfig = {
|
|
1317
|
+
prompt: opts.prompt,
|
|
1318
|
+
systemPrompt: opts.systemPrompt || "",
|
|
1319
|
+
model,
|
|
1320
|
+
role: opts.role,
|
|
1321
|
+
agentId: opts.agentId,
|
|
1002
1322
|
taskId: effectiveTaskId,
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1323
|
+
apiUrl: opts.apiUrl,
|
|
1324
|
+
apiKey: opts.apiKey,
|
|
1325
|
+
cwd: opts.cwd || process.cwd(),
|
|
1326
|
+
logFile: opts.logFile,
|
|
1327
|
+
additionalArgs: opts.additionalArgs,
|
|
1328
|
+
iteration: opts.iteration,
|
|
1329
|
+
env: freshEnv as Record<string, string>,
|
|
1330
|
+
};
|
|
1006
1331
|
|
|
1007
|
-
|
|
1332
|
+
const session = await adapter.createSession(config);
|
|
1008
1333
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
TASK_FILE: taskFilePath,
|
|
1013
|
-
},
|
|
1014
|
-
stdout: "pipe",
|
|
1015
|
-
stderr: "pipe",
|
|
1016
|
-
});
|
|
1334
|
+
// Set up log streaming
|
|
1335
|
+
const logBuffer: LogBuffer = { lines: [], lastFlush: Date.now(), partialLine: "" };
|
|
1336
|
+
const shouldStream = opts.apiUrl && opts.runnerSessionId && opts.iteration;
|
|
1017
1337
|
|
|
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
|
-
}
|
|
1338
|
+
// Auto-progress throttle: don't update more than once per 3 seconds
|
|
1339
|
+
let lastProgressTime = 0;
|
|
1340
|
+
const PROGRESS_THROTTLE_MS = 3000;
|
|
1085
1341
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1342
|
+
session.onEvent((event) => {
|
|
1343
|
+
switch (event.type) {
|
|
1344
|
+
case "session_init":
|
|
1345
|
+
if (realTaskId) {
|
|
1346
|
+
saveProviderSessionId(opts.apiUrl, opts.apiKey, realTaskId, event.sessionId).catch(
|
|
1347
|
+
(err) => console.warn(`[runner] Failed to save session ID: ${err}`),
|
|
1348
|
+
);
|
|
1349
|
+
} else {
|
|
1350
|
+
// Pool task: save provider session ID on active session so it can be
|
|
1351
|
+
// propagated to the real task when the agent claims one
|
|
1352
|
+
saveProviderSessionIdOnActiveSession(
|
|
1353
|
+
opts.apiUrl,
|
|
1354
|
+
opts.apiKey,
|
|
1355
|
+
effectiveTaskId,
|
|
1356
|
+
event.sessionId,
|
|
1357
|
+
).catch((err) =>
|
|
1358
|
+
console.warn(`[runner] Failed to save provider session on active session: ${err}`),
|
|
1359
|
+
);
|
|
1106
1360
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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 = "";
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
// Final flush for remaining buffered logs
|
|
1152
|
-
if (shouldStream && logBuffer.lines.length > 0) {
|
|
1153
|
-
await flushLogBuffer(logBuffer, {
|
|
1154
|
-
apiUrl: opts.apiUrl!,
|
|
1155
|
-
apiKey: opts.apiKey || "",
|
|
1156
|
-
agentId: opts.agentId || "",
|
|
1157
|
-
sessionId: opts.sessionId!,
|
|
1158
|
-
iteration: opts.iteration!,
|
|
1159
|
-
taskId: opts.taskId,
|
|
1160
|
-
cli: "claude",
|
|
1161
|
-
});
|
|
1361
|
+
break;
|
|
1362
|
+
case "tool_start": {
|
|
1363
|
+
// Auto-progress: report tool activity as task progress (throttled)
|
|
1364
|
+
const now = Date.now();
|
|
1365
|
+
if (effectiveTaskId && opts.apiUrl && now - lastProgressTime >= PROGRESS_THROTTLE_MS) {
|
|
1366
|
+
lastProgressTime = now;
|
|
1367
|
+
const progress = toolCallToProgress(event.toolName, event.args);
|
|
1368
|
+
updateProgressViaAPI(opts.apiUrl, opts.apiKey, effectiveTaskId, progress).catch(() => {});
|
|
1162
1369
|
}
|
|
1370
|
+
break;
|
|
1163
1371
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1372
|
+
case "result":
|
|
1373
|
+
// Cost save is handled in waitForCompletion().then() to ensure
|
|
1374
|
+
// it completes before the process exits (fire-and-forget here
|
|
1375
|
+
// races with container shutdown).
|
|
1376
|
+
break;
|
|
1377
|
+
case "raw_log":
|
|
1378
|
+
prettyPrintLine(event.content, opts.role);
|
|
1379
|
+
if (shouldStream) {
|
|
1380
|
+
logBuffer.lines.push(event.content);
|
|
1381
|
+
const shouldFlush =
|
|
1382
|
+
logBuffer.lines.length >= LOG_BUFFER_SIZE ||
|
|
1383
|
+
Date.now() - logBuffer.lastFlush >= LOG_FLUSH_INTERVAL_MS;
|
|
1384
|
+
if (shouldFlush) {
|
|
1385
|
+
flushLogBuffer(logBuffer, {
|
|
1386
|
+
apiUrl: opts.apiUrl,
|
|
1387
|
+
apiKey: opts.apiKey,
|
|
1388
|
+
agentId: opts.agentId,
|
|
1389
|
+
sessionId: opts.runnerSessionId,
|
|
1390
|
+
iteration: opts.iteration,
|
|
1391
|
+
taskId: effectiveTaskId,
|
|
1392
|
+
cli: adapter.name,
|
|
1393
|
+
}).catch(() => {});
|
|
1394
|
+
}
|
|
1176
1395
|
}
|
|
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
|
-
);
|
|
1396
|
+
break;
|
|
1397
|
+
case "raw_stderr":
|
|
1398
|
+
prettyPrintStderr(event.content, opts.role);
|
|
1399
|
+
break;
|
|
1188
1400
|
}
|
|
1401
|
+
});
|
|
1189
1402
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1403
|
+
// Create promise that handles completion
|
|
1404
|
+
const promise: Promise<ProviderResult> = session.waitForCompletion().then(async (result) => {
|
|
1405
|
+
// Final log flush
|
|
1406
|
+
if (shouldStream && logBuffer.lines.length > 0) {
|
|
1407
|
+
await flushLogBuffer(logBuffer, {
|
|
1408
|
+
apiUrl: opts.apiUrl,
|
|
1409
|
+
apiKey: opts.apiKey,
|
|
1410
|
+
agentId: opts.agentId,
|
|
1411
|
+
sessionId: opts.runnerSessionId,
|
|
1412
|
+
iteration: opts.iteration,
|
|
1413
|
+
taskId: effectiveTaskId,
|
|
1414
|
+
cli: adapter.name,
|
|
1415
|
+
});
|
|
1194
1416
|
}
|
|
1195
1417
|
|
|
1196
|
-
//
|
|
1197
|
-
if (exitCode !== 0) {
|
|
1418
|
+
// Error logging for non-zero exit
|
|
1419
|
+
if (result.exitCode !== 0) {
|
|
1198
1420
|
const errorLog = {
|
|
1199
1421
|
timestamp: new Date().toISOString(),
|
|
1200
1422
|
iteration: opts.iteration,
|
|
1201
|
-
exitCode,
|
|
1423
|
+
exitCode: result.exitCode,
|
|
1202
1424
|
taskId: effectiveTaskId,
|
|
1203
1425
|
error: true,
|
|
1204
1426
|
};
|
|
@@ -1210,29 +1432,99 @@ async function spawnClaudeProcess(
|
|
|
1210
1432
|
|
|
1211
1433
|
if (!isYolo) {
|
|
1212
1434
|
console.error(
|
|
1213
|
-
`[${role}] Task ${effectiveTaskId.slice(0, 8)} exited with code ${exitCode}.`,
|
|
1435
|
+
`[${opts.role}] Task ${effectiveTaskId.slice(0, 8)} exited with code ${result.exitCode}.`,
|
|
1214
1436
|
);
|
|
1215
1437
|
} else {
|
|
1216
1438
|
console.warn(
|
|
1217
|
-
`[${role}] Task ${effectiveTaskId.slice(0, 8)} exited with code ${exitCode}. YOLO mode - continuing...`,
|
|
1439
|
+
`[${opts.role}] Task ${effectiveTaskId.slice(0, 8)} exited with code ${result.exitCode}. YOLO mode - continuing...`,
|
|
1218
1440
|
);
|
|
1219
1441
|
}
|
|
1220
1442
|
}
|
|
1221
1443
|
|
|
1222
|
-
//
|
|
1223
|
-
|
|
1224
|
-
|
|
1444
|
+
// Save cost data (awaited to ensure it completes before container exits)
|
|
1445
|
+
if (result.cost) {
|
|
1446
|
+
try {
|
|
1447
|
+
await saveCostData(
|
|
1448
|
+
{ ...result.cost, taskId: realTaskId, sessionId: opts.runnerSessionId },
|
|
1449
|
+
opts.apiUrl,
|
|
1450
|
+
opts.apiKey,
|
|
1451
|
+
);
|
|
1452
|
+
} catch (err) {
|
|
1453
|
+
console.warn(`[runner] Failed to save cost: ${err}`);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1225
1456
|
|
|
1226
|
-
return
|
|
1227
|
-
})
|
|
1457
|
+
return result;
|
|
1458
|
+
});
|
|
1228
1459
|
|
|
1229
|
-
|
|
1460
|
+
const runningTask: RunningTask = {
|
|
1230
1461
|
taskId: effectiveTaskId,
|
|
1231
|
-
|
|
1462
|
+
session,
|
|
1232
1463
|
logFile: opts.logFile,
|
|
1233
1464
|
startTime: new Date(),
|
|
1234
1465
|
promise,
|
|
1466
|
+
result: null,
|
|
1235
1467
|
};
|
|
1468
|
+
|
|
1469
|
+
// Non-blocking completion tracking
|
|
1470
|
+
promise
|
|
1471
|
+
.then((r) => {
|
|
1472
|
+
runningTask.result = r;
|
|
1473
|
+
})
|
|
1474
|
+
.catch(() => {
|
|
1475
|
+
runningTask.result = { exitCode: 1, isError: true };
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
return runningTask;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/** Run a single provider iteration (blocking) - used for AI-loop mode */
|
|
1482
|
+
async function runProviderIteration(
|
|
1483
|
+
adapter: ReturnType<typeof createProviderAdapter>,
|
|
1484
|
+
opts: {
|
|
1485
|
+
prompt: string;
|
|
1486
|
+
logFile: string;
|
|
1487
|
+
systemPrompt?: string;
|
|
1488
|
+
additionalArgs?: string[];
|
|
1489
|
+
role: string;
|
|
1490
|
+
apiUrl: string;
|
|
1491
|
+
apiKey: string;
|
|
1492
|
+
agentId: string;
|
|
1493
|
+
taskId?: string;
|
|
1494
|
+
cwd?: string;
|
|
1495
|
+
},
|
|
1496
|
+
): Promise<ProviderResult> {
|
|
1497
|
+
const freshEnv = await fetchResolvedEnv(opts.apiUrl, opts.apiKey, opts.agentId);
|
|
1498
|
+
const model = (freshEnv.MODEL_OVERRIDE as string) || "";
|
|
1499
|
+
|
|
1500
|
+
const config: ProviderSessionConfig = {
|
|
1501
|
+
prompt: opts.prompt,
|
|
1502
|
+
systemPrompt: opts.systemPrompt || "",
|
|
1503
|
+
model,
|
|
1504
|
+
role: opts.role,
|
|
1505
|
+
agentId: opts.agentId,
|
|
1506
|
+
taskId: opts.taskId || crypto.randomUUID(),
|
|
1507
|
+
apiUrl: opts.apiUrl,
|
|
1508
|
+
apiKey: opts.apiKey,
|
|
1509
|
+
cwd: opts.cwd || process.cwd(),
|
|
1510
|
+
logFile: opts.logFile,
|
|
1511
|
+
additionalArgs: opts.additionalArgs,
|
|
1512
|
+
env: freshEnv as Record<string, string>,
|
|
1513
|
+
};
|
|
1514
|
+
|
|
1515
|
+
const session = await adapter.createSession(config);
|
|
1516
|
+
|
|
1517
|
+
session.onEvent((event) => {
|
|
1518
|
+
if (event.type === "raw_log") prettyPrintLine(event.content, opts.role);
|
|
1519
|
+
if (event.type === "raw_stderr") prettyPrintStderr(event.content, opts.role);
|
|
1520
|
+
if (event.type === "session_init" && opts.taskId) {
|
|
1521
|
+
saveProviderSessionId(opts.apiUrl, opts.apiKey, opts.taskId, event.sessionId).catch((err) =>
|
|
1522
|
+
console.warn(`[runner] Failed to save session ID: ${err}`),
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
return session.waitForCompletion();
|
|
1236
1528
|
}
|
|
1237
1529
|
|
|
1238
1530
|
/** Check for completed processes and remove them from active tasks */
|
|
@@ -1241,32 +1533,134 @@ async function checkCompletedProcesses(
|
|
|
1241
1533
|
role: string,
|
|
1242
1534
|
apiConfig?: ApiConfig,
|
|
1243
1535
|
): Promise<void> {
|
|
1244
|
-
const completedTasks: Array<{
|
|
1536
|
+
const completedTasks: Array<{
|
|
1537
|
+
taskId: string;
|
|
1538
|
+
result: ProviderResult;
|
|
1539
|
+
triggerType?: string;
|
|
1540
|
+
cursorUpdates?: Array<{ channelId: string; ts: string }>;
|
|
1541
|
+
}> = [];
|
|
1245
1542
|
|
|
1246
1543
|
for (const [taskId, task] of state.activeTasks) {
|
|
1247
|
-
//
|
|
1248
|
-
if (task.
|
|
1544
|
+
// Non-blocking check: result is set by a .then() callback when the promise resolves
|
|
1545
|
+
if (task.result !== null) {
|
|
1249
1546
|
console.log(
|
|
1250
|
-
`[${role}] Task ${taskId.slice(0, 8)} completed with exit code ${task.
|
|
1547
|
+
`[${role}] Task ${taskId.slice(0, 8)} completed with exit code ${task.result.exitCode} (trigger: ${task.triggerType || "unknown"})`,
|
|
1251
1548
|
);
|
|
1252
|
-
completedTasks.push({
|
|
1549
|
+
completedTasks.push({
|
|
1550
|
+
taskId,
|
|
1551
|
+
result: task.result,
|
|
1552
|
+
triggerType: task.triggerType,
|
|
1553
|
+
cursorUpdates: task.cursorUpdates,
|
|
1554
|
+
});
|
|
1253
1555
|
}
|
|
1254
1556
|
}
|
|
1255
1557
|
|
|
1256
1558
|
// Remove completed tasks from the map and ensure they're marked as finished
|
|
1257
|
-
for (const { taskId,
|
|
1559
|
+
for (const { taskId, result, cursorUpdates } of completedTasks) {
|
|
1258
1560
|
state.activeTasks.delete(taskId);
|
|
1259
1561
|
|
|
1562
|
+
if (apiConfig) {
|
|
1563
|
+
removeActiveSession(apiConfig, taskId);
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1260
1566
|
// Call the finish API to ensure task status is updated
|
|
1261
1567
|
// This is idempotent - if the agent already marked it, this is a no-op
|
|
1262
1568
|
if (apiConfig) {
|
|
1263
|
-
|
|
1569
|
+
let failureReason: string | undefined;
|
|
1570
|
+
if (result.exitCode !== 0 && result.failureReason) {
|
|
1571
|
+
failureReason = result.failureReason;
|
|
1572
|
+
console.log(`[${role}] Detected error for task ${taskId.slice(0, 8)}: ${failureReason}`);
|
|
1573
|
+
}
|
|
1574
|
+
await ensureTaskFinished(apiConfig, role, taskId, result.exitCode, failureReason);
|
|
1575
|
+
|
|
1576
|
+
// Commit channel activity cursors after successful processing
|
|
1577
|
+
// If the task failed, cursors stay uncommitted so messages are re-seen on next poll
|
|
1578
|
+
if (cursorUpdates && cursorUpdates.length > 0 && result.exitCode === 0) {
|
|
1579
|
+
try {
|
|
1580
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
1581
|
+
if (apiConfig.apiKey) headers.Authorization = `Bearer ${apiConfig.apiKey}`;
|
|
1582
|
+
await fetch(`${apiConfig.apiUrl}/api/channel-activity/commit-cursors`, {
|
|
1583
|
+
method: "POST",
|
|
1584
|
+
headers,
|
|
1585
|
+
body: JSON.stringify({ cursorUpdates }),
|
|
1586
|
+
});
|
|
1587
|
+
console.log(
|
|
1588
|
+
`[${role}] Committed ${cursorUpdates.length} channel activity cursor(s) for task ${taskId.slice(0, 8)}`,
|
|
1589
|
+
);
|
|
1590
|
+
} catch (err) {
|
|
1591
|
+
console.warn(`[${role}] Failed to commit channel activity cursors: ${err}`);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
const TEMPLATE_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
1599
|
+
|
|
1600
|
+
async function fetchTemplate(
|
|
1601
|
+
templateId: string,
|
|
1602
|
+
registryUrl: string,
|
|
1603
|
+
cacheDir: string,
|
|
1604
|
+
): Promise<TemplateResponse | null> {
|
|
1605
|
+
const safeId = templateId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
1606
|
+
const cachePath = `${cacheDir}/${safeId}.json`;
|
|
1607
|
+
|
|
1608
|
+
// Check local cache
|
|
1609
|
+
try {
|
|
1610
|
+
const info = await stat(cachePath);
|
|
1611
|
+
if (Date.now() - info.mtimeMs < TEMPLATE_CACHE_TTL_MS) {
|
|
1612
|
+
const cached = await readFile(cachePath, "utf-8");
|
|
1613
|
+
return JSON.parse(cached) as TemplateResponse;
|
|
1614
|
+
}
|
|
1615
|
+
} catch {
|
|
1616
|
+
// No cache or expired, continue to fetch
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// Fetch from registry
|
|
1620
|
+
try {
|
|
1621
|
+
const resp = await fetch(`${registryUrl}/api/templates/${templateId}`);
|
|
1622
|
+
if (!resp.ok) {
|
|
1623
|
+
console.warn(`[template] Registry returned ${resp.status} for ${templateId}`);
|
|
1624
|
+
// Fall back to expired cache if available
|
|
1625
|
+
try {
|
|
1626
|
+
const cached = await readFile(cachePath, "utf-8");
|
|
1627
|
+
console.log(`[template] Using expired cache for ${templateId}`);
|
|
1628
|
+
return JSON.parse(cached) as TemplateResponse;
|
|
1629
|
+
} catch {
|
|
1630
|
+
return null;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
const template = (await resp.json()) as TemplateResponse;
|
|
1635
|
+
|
|
1636
|
+
// Cache the response
|
|
1637
|
+
try {
|
|
1638
|
+
await mkdir(cacheDir, { recursive: true });
|
|
1639
|
+
await writeFile(cachePath, JSON.stringify(template), "utf-8");
|
|
1640
|
+
} catch {
|
|
1641
|
+
console.warn(`[template] Could not cache template to ${cachePath}`);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
return template;
|
|
1645
|
+
} catch (err) {
|
|
1646
|
+
console.warn(`[template] Failed to fetch from registry: ${err}`);
|
|
1647
|
+
// Fall back to expired cache
|
|
1648
|
+
try {
|
|
1649
|
+
const cached = await readFile(cachePath, "utf-8");
|
|
1650
|
+
console.log(`[template] Using expired cache for ${templateId}`);
|
|
1651
|
+
return JSON.parse(cached) as TemplateResponse;
|
|
1652
|
+
} catch {
|
|
1653
|
+
return null;
|
|
1264
1654
|
}
|
|
1265
1655
|
}
|
|
1266
1656
|
}
|
|
1267
1657
|
|
|
1268
1658
|
export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
1269
|
-
const {
|
|
1659
|
+
const { defaultPrompt, metadataType } = config;
|
|
1660
|
+
let role = config.role;
|
|
1661
|
+
|
|
1662
|
+
// Create provider adapter based on HARNESS_PROVIDER env var (default: claude)
|
|
1663
|
+
const adapter = createProviderAdapter(process.env.HARNESS_PROVIDER || "claude");
|
|
1270
1664
|
|
|
1271
1665
|
const sessionId = process.env.SESSION_ID || crypto.randomUUID().slice(0, 8);
|
|
1272
1666
|
const baseLogDir = opts.logsDir || process.env.LOG_DIR || "/logs";
|
|
@@ -1280,13 +1674,46 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1280
1674
|
// Get agent identity and swarm URL for base prompt
|
|
1281
1675
|
const agentId = process.env.AGENT_ID || "unknown";
|
|
1282
1676
|
|
|
1283
|
-
const apiUrl = process.env.MCP_BASE_URL ||
|
|
1677
|
+
const apiUrl = process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
|
|
1284
1678
|
const swarmUrl = process.env.SWARM_URL || "localhost";
|
|
1285
1679
|
|
|
1286
|
-
|
|
1680
|
+
// Configure HTTP-based template resolution (workers resolve via API, not local DB)
|
|
1681
|
+
if (process.env.API_KEY) {
|
|
1682
|
+
configureHttpResolver(apiUrl, process.env.API_KEY);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
let capabilities = config.capabilities;
|
|
1686
|
+
|
|
1687
|
+
// Agent identity fields — populated after registration by fetching full profile
|
|
1688
|
+
let agentSoulMd: string | undefined;
|
|
1689
|
+
let agentIdentityMd: string | undefined;
|
|
1690
|
+
let agentSetupScript: string | undefined;
|
|
1691
|
+
let agentToolsMd: string | undefined;
|
|
1692
|
+
let agentClaudeMd: string | undefined;
|
|
1693
|
+
let agentProfileName: string | undefined;
|
|
1694
|
+
let agentDescription: string | undefined;
|
|
1695
|
+
|
|
1696
|
+
// Per-task repo context — set when processing a task with githubRepo
|
|
1697
|
+
let currentRepoContext: BasePromptArgs["repoContext"] | undefined;
|
|
1698
|
+
|
|
1699
|
+
// Generate base prompt (identity fields injected after profile fetch below)
|
|
1700
|
+
const buildSystemPrompt = async () => {
|
|
1701
|
+
return getBasePrompt({
|
|
1702
|
+
role,
|
|
1703
|
+
agentId,
|
|
1704
|
+
swarmUrl,
|
|
1705
|
+
capabilities,
|
|
1706
|
+
name: agentProfileName,
|
|
1707
|
+
description: agentDescription,
|
|
1708
|
+
soulMd: agentSoulMd,
|
|
1709
|
+
identityMd: agentIdentityMd,
|
|
1710
|
+
toolsMd: agentToolsMd,
|
|
1711
|
+
claudeMd: agentClaudeMd,
|
|
1712
|
+
repoContext: currentRepoContext,
|
|
1713
|
+
});
|
|
1714
|
+
};
|
|
1287
1715
|
|
|
1288
|
-
|
|
1289
|
-
const basePrompt = getBasePrompt({ role, agentId, swarmUrl, capabilities });
|
|
1716
|
+
let basePrompt = await buildSystemPrompt();
|
|
1290
1717
|
|
|
1291
1718
|
// Resolve additional system prompt: CLI flag > env var
|
|
1292
1719
|
let additionalSystemPrompt: string | undefined;
|
|
@@ -1318,7 +1745,8 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1318
1745
|
}
|
|
1319
1746
|
|
|
1320
1747
|
// Combine base prompt with any additional system prompt
|
|
1321
|
-
|
|
1748
|
+
// Note: resolvedSystemPrompt is rebuilt after profile fetch when identity is available
|
|
1749
|
+
let resolvedSystemPrompt = additionalSystemPrompt
|
|
1322
1750
|
? `${basePrompt}\n\n${additionalSystemPrompt}`
|
|
1323
1751
|
: basePrompt;
|
|
1324
1752
|
|
|
@@ -1346,8 +1774,38 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1346
1774
|
let iteration = 0;
|
|
1347
1775
|
|
|
1348
1776
|
if (!isAiLoop) {
|
|
1777
|
+
// Fetch template early (before registration) so defaults can be applied
|
|
1778
|
+
const templateId = process.env.TEMPLATE_ID;
|
|
1779
|
+
const registryUrl = process.env.TEMPLATE_REGISTRY_URL || "https://templates.agent-swarm.dev";
|
|
1780
|
+
let cachedTemplate: TemplateResponse | null = null;
|
|
1781
|
+
|
|
1782
|
+
if (templateId) {
|
|
1783
|
+
try {
|
|
1784
|
+
cachedTemplate = await fetchTemplate(templateId, registryUrl, "/workspace/.template-cache");
|
|
1785
|
+
if (cachedTemplate) {
|
|
1786
|
+
console.log(`[${role}] Fetched template: ${templateId}`);
|
|
1787
|
+
|
|
1788
|
+
// Apply agentDefaults as fallbacks (env/config takes precedence)
|
|
1789
|
+
const defaults = cachedTemplate.config.agentDefaults;
|
|
1790
|
+
if (config.role === "worker" && defaults.role) {
|
|
1791
|
+
role = defaults.role;
|
|
1792
|
+
}
|
|
1793
|
+
if (!capabilities?.length && defaults.capabilities?.length) {
|
|
1794
|
+
capabilities = defaults.capabilities;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
} catch (err) {
|
|
1798
|
+
console.warn(`[${role}] Failed to fetch template ${templateId}: ${err}`);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1349
1802
|
// Runner-level polling mode with parallel execution support
|
|
1350
|
-
const
|
|
1803
|
+
const isLeadFromConfig = config.role === "lead";
|
|
1804
|
+
const isLead = isLeadFromConfig || (cachedTemplate?.config.agentDefaults?.isLead ?? false);
|
|
1805
|
+
const defaultMaxTasks = isLead ? 2 : 1;
|
|
1806
|
+
const maxConcurrent = process.env.MAX_CONCURRENT_TASKS
|
|
1807
|
+
? parseInt(process.env.MAX_CONCURRENT_TASKS, 10)
|
|
1808
|
+
: (cachedTemplate?.config.agentDefaults?.maxTasks ?? defaultMaxTasks);
|
|
1351
1809
|
console.log(`[${role}] Mode: runner-level polling (use --ai-loop for AI-based polling)`);
|
|
1352
1810
|
console.log(`[${role}] Max concurrent tasks: ${maxConcurrent}`);
|
|
1353
1811
|
|
|
@@ -1357,6 +1815,9 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1357
1815
|
maxConcurrent,
|
|
1358
1816
|
};
|
|
1359
1817
|
|
|
1818
|
+
// Track tasks already signaled for cancellation to avoid repeated SIGTERM
|
|
1819
|
+
const cancelledSignaled = new Set<string>();
|
|
1820
|
+
|
|
1360
1821
|
// Create API config for ping/close
|
|
1361
1822
|
const apiConfig: ApiConfig = { apiUrl, apiKey, agentId };
|
|
1362
1823
|
|
|
@@ -1364,15 +1825,19 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1364
1825
|
setupShutdownHandlers(role, apiConfig, () => state);
|
|
1365
1826
|
|
|
1366
1827
|
// Register agent before starting
|
|
1367
|
-
const agentName =
|
|
1828
|
+
const agentName =
|
|
1829
|
+
process.env.AGENT_NAME ||
|
|
1830
|
+
cachedTemplate?.config.displayName ||
|
|
1831
|
+
`${role}-${agentId.slice(0, 8)}`;
|
|
1368
1832
|
try {
|
|
1369
1833
|
await registerAgent({
|
|
1370
1834
|
apiUrl,
|
|
1371
1835
|
apiKey,
|
|
1372
1836
|
agentId,
|
|
1373
1837
|
name: agentName,
|
|
1374
|
-
|
|
1375
|
-
|
|
1838
|
+
role,
|
|
1839
|
+
isLead,
|
|
1840
|
+
capabilities,
|
|
1376
1841
|
maxTasks: maxConcurrent,
|
|
1377
1842
|
});
|
|
1378
1843
|
console.log(`[${role}] Registered as "${agentName}" (ID: ${agentId})`);
|
|
@@ -1381,6 +1846,165 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1381
1846
|
process.exit(1);
|
|
1382
1847
|
}
|
|
1383
1848
|
|
|
1849
|
+
// Clean up any stale active sessions from previous runs (crash recovery)
|
|
1850
|
+
await cleanupActiveSessions(apiConfig);
|
|
1851
|
+
console.log(`[${role}] Cleaned up stale active sessions`);
|
|
1852
|
+
|
|
1853
|
+
// Fetch full agent profile to get soul/identity content
|
|
1854
|
+
try {
|
|
1855
|
+
const resp = await fetch(`${apiUrl}/me`, {
|
|
1856
|
+
headers: {
|
|
1857
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1858
|
+
"X-Agent-ID": agentId,
|
|
1859
|
+
},
|
|
1860
|
+
});
|
|
1861
|
+
if (resp.ok) {
|
|
1862
|
+
const profile = (await resp.json()) as {
|
|
1863
|
+
soulMd?: string;
|
|
1864
|
+
identityMd?: string;
|
|
1865
|
+
claudeMd?: string;
|
|
1866
|
+
setupScript?: string;
|
|
1867
|
+
toolsMd?: string;
|
|
1868
|
+
name?: string;
|
|
1869
|
+
description?: string;
|
|
1870
|
+
};
|
|
1871
|
+
agentSoulMd = profile.soulMd;
|
|
1872
|
+
agentIdentityMd = profile.identityMd;
|
|
1873
|
+
agentSetupScript = profile.setupScript;
|
|
1874
|
+
agentToolsMd = profile.toolsMd;
|
|
1875
|
+
agentClaudeMd = profile.claudeMd;
|
|
1876
|
+
agentProfileName = profile.name;
|
|
1877
|
+
agentDescription = profile.description;
|
|
1878
|
+
|
|
1879
|
+
// Generate default templates if missing (runner registers via POST /api/agents
|
|
1880
|
+
// which doesn't generate templates like join-swarm does)
|
|
1881
|
+
if (!agentSoulMd || !agentIdentityMd || !agentToolsMd || !agentClaudeMd) {
|
|
1882
|
+
// Use already-fetched template (from pre-registration step)
|
|
1883
|
+
if (cachedTemplate) {
|
|
1884
|
+
const ctx = {
|
|
1885
|
+
agent: {
|
|
1886
|
+
name: agentProfileName || agentName,
|
|
1887
|
+
role: role,
|
|
1888
|
+
description: agentDescription || "",
|
|
1889
|
+
capabilities: (capabilities || []).join(", "),
|
|
1890
|
+
},
|
|
1891
|
+
};
|
|
1892
|
+
if (!agentSoulMd) agentSoulMd = interpolate(cachedTemplate.files.soulMd, ctx).result;
|
|
1893
|
+
if (!agentIdentityMd)
|
|
1894
|
+
agentIdentityMd = interpolate(cachedTemplate.files.identityMd, ctx).result;
|
|
1895
|
+
if (!agentToolsMd) agentToolsMd = interpolate(cachedTemplate.files.toolsMd, ctx).result;
|
|
1896
|
+
if (!agentClaudeMd)
|
|
1897
|
+
agentClaudeMd = interpolate(cachedTemplate.files.claudeMd, ctx).result;
|
|
1898
|
+
if (!agentSetupScript)
|
|
1899
|
+
agentSetupScript = interpolate(cachedTemplate.files.setupScript, ctx).result;
|
|
1900
|
+
console.log(`[${role}] Applied template: ${templateId}`);
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// Fallback to generic defaults for any still-missing fields
|
|
1904
|
+
const agentInfo = {
|
|
1905
|
+
name: agentProfileName || agentName,
|
|
1906
|
+
role: role,
|
|
1907
|
+
description: agentDescription,
|
|
1908
|
+
capabilities: config.capabilities,
|
|
1909
|
+
};
|
|
1910
|
+
if (!agentSoulMd) agentSoulMd = generateDefaultSoulMd(agentInfo);
|
|
1911
|
+
if (!agentIdentityMd) agentIdentityMd = generateDefaultIdentityMd(agentInfo);
|
|
1912
|
+
if (!agentToolsMd) agentToolsMd = generateDefaultToolsMd(agentInfo);
|
|
1913
|
+
if (!agentClaudeMd) agentClaudeMd = generateDefaultClaudeMd(agentInfo);
|
|
1914
|
+
|
|
1915
|
+
// Push generated templates to server
|
|
1916
|
+
try {
|
|
1917
|
+
const profileUpdate: Record<string, string> = {};
|
|
1918
|
+
if (!profile.soulMd) profileUpdate.soulMd = agentSoulMd;
|
|
1919
|
+
if (!profile.identityMd) profileUpdate.identityMd = agentIdentityMd;
|
|
1920
|
+
if (!profile.toolsMd) profileUpdate.toolsMd = agentToolsMd;
|
|
1921
|
+
if (!profile.claudeMd && agentClaudeMd) profileUpdate.claudeMd = agentClaudeMd;
|
|
1922
|
+
if (!profile.setupScript && agentSetupScript)
|
|
1923
|
+
profileUpdate.setupScript = agentSetupScript;
|
|
1924
|
+
|
|
1925
|
+
await fetch(`${apiUrl}/api/agents/${agentId}/profile`, {
|
|
1926
|
+
method: "PUT",
|
|
1927
|
+
headers: {
|
|
1928
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1929
|
+
"X-Agent-ID": agentId,
|
|
1930
|
+
"Content-Type": "application/json",
|
|
1931
|
+
},
|
|
1932
|
+
body: JSON.stringify(profileUpdate),
|
|
1933
|
+
});
|
|
1934
|
+
console.log(`[${role}] Generated and saved default identity templates`);
|
|
1935
|
+
} catch {
|
|
1936
|
+
console.warn(`[${role}] Could not save generated templates to server`);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// Rebuild system prompt with identity
|
|
1941
|
+
basePrompt = await buildSystemPrompt();
|
|
1942
|
+
resolvedSystemPrompt = additionalSystemPrompt
|
|
1943
|
+
? `${basePrompt}\n\n${additionalSystemPrompt}`
|
|
1944
|
+
: basePrompt;
|
|
1945
|
+
console.log(
|
|
1946
|
+
`[${role}] Loaded agent identity (soul: ${agentSoulMd ? "yes" : "no"}, identity: ${agentIdentityMd ? "yes" : "no"}, tools: ${agentToolsMd ? "yes" : "no"}, claude: ${agentClaudeMd ? "yes" : "no"})`,
|
|
1947
|
+
);
|
|
1948
|
+
console.log(`[${role}] Updated system prompt length: ${resolvedSystemPrompt.length} chars`);
|
|
1949
|
+
}
|
|
1950
|
+
} catch {
|
|
1951
|
+
console.warn(`[${role}] Could not fetch agent profile for identity — proceeding without`);
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
// Write SOUL.md and IDENTITY.md to workspace before spawning Claude
|
|
1955
|
+
const SOUL_MD_PATH = "/workspace/SOUL.md";
|
|
1956
|
+
const IDENTITY_MD_PATH = "/workspace/IDENTITY.md";
|
|
1957
|
+
|
|
1958
|
+
if (agentSoulMd) {
|
|
1959
|
+
try {
|
|
1960
|
+
await Bun.write(SOUL_MD_PATH, agentSoulMd);
|
|
1961
|
+
console.log(`[${role}] Wrote SOUL.md to workspace`);
|
|
1962
|
+
} catch (err) {
|
|
1963
|
+
console.warn(`[${role}] Could not write SOUL.md: ${(err as Error).message}`);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
if (agentIdentityMd) {
|
|
1967
|
+
try {
|
|
1968
|
+
await Bun.write(IDENTITY_MD_PATH, agentIdentityMd);
|
|
1969
|
+
console.log(`[${role}] Wrote IDENTITY.md to workspace`);
|
|
1970
|
+
} catch (err) {
|
|
1971
|
+
console.warn(`[${role}] Could not write IDENTITY.md: ${(err as Error).message}`);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
// Write setup script to workspace (agent can edit during session)
|
|
1976
|
+
// Only create if it doesn't exist — the entrypoint already composed/prepended it at container start
|
|
1977
|
+
if (agentSetupScript) {
|
|
1978
|
+
try {
|
|
1979
|
+
if (!(await Bun.file("/workspace/start-up.sh").exists())) {
|
|
1980
|
+
await Bun.write("/workspace/start-up.sh", `#!/bin/bash\n${agentSetupScript}\n`);
|
|
1981
|
+
console.log(`[${role}] Wrote start-up.sh to workspace`);
|
|
1982
|
+
}
|
|
1983
|
+
} catch (err) {
|
|
1984
|
+
console.warn(`[${role}] Could not write start-up.sh: ${(err as Error).message}`);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// Write TOOLS.md to workspace (agent can edit during session)
|
|
1989
|
+
if (agentToolsMd) {
|
|
1990
|
+
try {
|
|
1991
|
+
await Bun.write("/workspace/TOOLS.md", agentToolsMd);
|
|
1992
|
+
console.log(`[${role}] Wrote TOOLS.md to workspace`);
|
|
1993
|
+
} catch (err) {
|
|
1994
|
+
console.warn(`[${role}] Could not write TOOLS.md: ${(err as Error).message}`);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// Write CLAUDE.md to workspace (agent-level instructions)
|
|
1999
|
+
if (agentClaudeMd) {
|
|
2000
|
+
try {
|
|
2001
|
+
await Bun.write("/workspace/CLAUDE.md", agentClaudeMd);
|
|
2002
|
+
console.log(`[${role}] Wrote CLAUDE.md to workspace`);
|
|
2003
|
+
} catch (err) {
|
|
2004
|
+
console.warn(`[${role}] Could not write CLAUDE.md: ${(err as Error).message}`);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
|
|
1384
2008
|
// ========== Resume paused tasks with PRIORITY ==========
|
|
1385
2009
|
// Check for paused tasks from previous shutdown and resume them before normal polling
|
|
1386
2010
|
try {
|
|
@@ -1391,6 +2015,14 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1391
2015
|
console.log(`[${role}] Found ${pausedTasks.length} paused task(s) to resume`);
|
|
1392
2016
|
|
|
1393
2017
|
for (const task of pausedTasks) {
|
|
2018
|
+
// Defensive: skip tasks that already have completion data (zombie prevention)
|
|
2019
|
+
if (task.finishedAt || task.output) {
|
|
2020
|
+
console.warn(
|
|
2021
|
+
`[${role}] Skipping zombie task ${task.id.slice(0, 8)} — already has completion data (finishedAt: ${!!task.finishedAt}, output: ${!!task.output})`,
|
|
2022
|
+
);
|
|
2023
|
+
continue;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1394
2026
|
// Wait if at capacity (though unlikely on fresh startup)
|
|
1395
2027
|
while (state.activeTasks.size >= state.maxConcurrent) {
|
|
1396
2028
|
await checkCompletedProcesses(state, role, apiConfig);
|
|
@@ -1410,8 +2042,35 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1410
2042
|
continue;
|
|
1411
2043
|
}
|
|
1412
2044
|
|
|
1413
|
-
// Build prompt with resume context
|
|
1414
|
-
|
|
2045
|
+
// Build prompt with resume context + memory injection
|
|
2046
|
+
let resumePrompt = await buildResumePrompt(task, adapter.formatCommand.bind(adapter));
|
|
2047
|
+
|
|
2048
|
+
// Inject relevant memories for resumed tasks
|
|
2049
|
+
const resumeMemoryContext = await fetchRelevantMemories(
|
|
2050
|
+
apiUrl,
|
|
2051
|
+
apiKey,
|
|
2052
|
+
agentId,
|
|
2053
|
+
task.task,
|
|
2054
|
+
);
|
|
2055
|
+
if (resumeMemoryContext) {
|
|
2056
|
+
resumePrompt += resumeMemoryContext;
|
|
2057
|
+
console.log(`[${role}] Injected relevant memories into resumed task prompt`);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// Resolve --resume: prefer own session ID, then parent's
|
|
2061
|
+
let resumeAdditionalArgs = opts.additionalArgs || [];
|
|
2062
|
+
if (task.claudeSessionId) {
|
|
2063
|
+
resumeAdditionalArgs = [...resumeAdditionalArgs, "--resume", task.claudeSessionId];
|
|
2064
|
+
console.log(
|
|
2065
|
+
`[${role}] Resuming task's own session ${task.claudeSessionId.slice(0, 8)}`,
|
|
2066
|
+
);
|
|
2067
|
+
} else if (task.parentTaskId) {
|
|
2068
|
+
const parentSessionId = await fetchProviderSessionId(apiUrl, apiKey, task.parentTaskId);
|
|
2069
|
+
if (parentSessionId) {
|
|
2070
|
+
resumeAdditionalArgs = [...resumeAdditionalArgs, "--resume", parentSessionId];
|
|
2071
|
+
console.log(`[${role}] Resuming parent session ${parentSessionId.slice(0, 8)}`);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
1415
2074
|
|
|
1416
2075
|
// Spawn Claude process for resumed task
|
|
1417
2076
|
iteration++;
|
|
@@ -1434,27 +2093,69 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1434
2093
|
};
|
|
1435
2094
|
await Bun.write(logFile, `${JSON.stringify(metadata)}\n`);
|
|
1436
2095
|
|
|
1437
|
-
|
|
2096
|
+
// Resolve cwd for resumed task (mirrors normal task path: task.dir > vcsRepo clonePath)
|
|
2097
|
+
let resumeCwd: string | undefined;
|
|
2098
|
+
if (task.dir) {
|
|
2099
|
+
try {
|
|
2100
|
+
if (existsSync(task.dir) && statSync(task.dir).isDirectory()) {
|
|
2101
|
+
resumeCwd = task.dir;
|
|
2102
|
+
} else {
|
|
2103
|
+
console.warn(
|
|
2104
|
+
`[${role}] Resume task dir "${task.dir}" does not exist or is not a directory, falling back to default cwd`,
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
} catch {
|
|
2108
|
+
console.warn(
|
|
2109
|
+
`[${role}] Failed to check resume task dir "${task.dir}", falling back to default cwd`,
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
if (!resumeCwd && task.vcsRepo && apiUrl) {
|
|
2115
|
+
const repoConfig = await fetchRepoConfig(apiUrl, apiKey, task.vcsRepo);
|
|
2116
|
+
const effectiveConfig = repoConfig ?? {
|
|
2117
|
+
url: task.vcsRepo,
|
|
2118
|
+
name: task.vcsRepo.split("/").pop() || task.vcsRepo,
|
|
2119
|
+
clonePath: `/workspace/repos/${task.vcsRepo.split("/").pop() || task.vcsRepo}`,
|
|
2120
|
+
defaultBranch: "main",
|
|
2121
|
+
};
|
|
2122
|
+
const repoContext = await ensureRepoForTask(effectiveConfig, role);
|
|
2123
|
+
if (repoContext?.clonePath) {
|
|
2124
|
+
resumeCwd = repoContext.clonePath;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// Per-task runner session ID so session logs are scoped to this task
|
|
2129
|
+
const resumeRunnerSessionId = crypto.randomUUID();
|
|
2130
|
+
|
|
2131
|
+
const runningTask = await spawnProviderProcess(
|
|
2132
|
+
adapter,
|
|
1438
2133
|
{
|
|
1439
2134
|
prompt: resumePrompt,
|
|
1440
2135
|
logFile,
|
|
1441
2136
|
systemPrompt: resolvedSystemPrompt,
|
|
1442
|
-
additionalArgs:
|
|
2137
|
+
additionalArgs: resumeAdditionalArgs,
|
|
1443
2138
|
role,
|
|
1444
2139
|
apiUrl,
|
|
1445
2140
|
apiKey,
|
|
1446
2141
|
agentId,
|
|
1447
|
-
|
|
2142
|
+
runnerSessionId: resumeRunnerSessionId,
|
|
1448
2143
|
iteration,
|
|
1449
2144
|
taskId: task.id,
|
|
2145
|
+
model: (task as { model?: string }).model,
|
|
2146
|
+
cwd: resumeCwd,
|
|
1450
2147
|
},
|
|
1451
2148
|
logDir,
|
|
1452
|
-
metadataType,
|
|
1453
|
-
sessionId,
|
|
1454
2149
|
isYolo,
|
|
1455
2150
|
);
|
|
1456
2151
|
|
|
1457
2152
|
state.activeTasks.set(task.id, runningTask);
|
|
2153
|
+
registerActiveSession(apiConfig, {
|
|
2154
|
+
taskId: task.id,
|
|
2155
|
+
triggerType: "task_resumed",
|
|
2156
|
+
taskDescription: task.task?.slice(0, 200),
|
|
2157
|
+
runnerSessionId: resumeRunnerSessionId,
|
|
2158
|
+
});
|
|
1458
2159
|
console.log(
|
|
1459
2160
|
`[${role}] Resumed task ${task.id.slice(0, 8)} (${state.activeTasks.size}/${state.maxConcurrent} active)`,
|
|
1460
2161
|
);
|
|
@@ -1471,8 +2172,6 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1471
2172
|
// ========== END: Resume paused tasks ==========
|
|
1472
2173
|
|
|
1473
2174
|
// Track last finished task check for leads (to avoid re-processing)
|
|
1474
|
-
let lastFinishedTaskCheck: string | undefined;
|
|
1475
|
-
|
|
1476
2175
|
while (true) {
|
|
1477
2176
|
// Ping server on each iteration to keep status updated
|
|
1478
2177
|
await pingServer(apiConfig, role);
|
|
@@ -1480,6 +2179,38 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1480
2179
|
// Check for completed processes first and ensure tasks are marked as finished
|
|
1481
2180
|
await checkCompletedProcesses(state, role, apiConfig);
|
|
1482
2181
|
|
|
2182
|
+
// Check for cancelled tasks and signal their subprocesses
|
|
2183
|
+
if (state.activeTasks.size > 0) {
|
|
2184
|
+
for (const [taskId, task] of state.activeTasks) {
|
|
2185
|
+
if (cancelledSignaled.has(taskId)) continue; // Already sent SIGTERM
|
|
2186
|
+
try {
|
|
2187
|
+
const cancelResp = await fetch(
|
|
2188
|
+
`${apiUrl}/cancelled-tasks?taskId=${encodeURIComponent(taskId)}`,
|
|
2189
|
+
{
|
|
2190
|
+
headers: {
|
|
2191
|
+
Authorization: `Bearer ${apiKey}`,
|
|
2192
|
+
"X-Agent-ID": agentId,
|
|
2193
|
+
},
|
|
2194
|
+
},
|
|
2195
|
+
);
|
|
2196
|
+
if (cancelResp.ok) {
|
|
2197
|
+
const cancelData = (await cancelResp.json()) as {
|
|
2198
|
+
cancelled: Array<{ id: string }>;
|
|
2199
|
+
};
|
|
2200
|
+
if (cancelData.cancelled?.some((t) => t.id === taskId)) {
|
|
2201
|
+
console.log(
|
|
2202
|
+
`[${role}] Task ${taskId.slice(0, 8)} was cancelled — sending SIGTERM to subprocess`,
|
|
2203
|
+
);
|
|
2204
|
+
task.session.abort().catch(() => {});
|
|
2205
|
+
cancelledSignaled.add(taskId);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
} catch {
|
|
2209
|
+
// Non-blocking — cancellation check is best-effort
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
|
|
1483
2214
|
// Only poll if we have capacity
|
|
1484
2215
|
if (state.activeTasks.size < state.maxConcurrent) {
|
|
1485
2216
|
console.log(
|
|
@@ -1495,19 +2226,160 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1495
2226
|
agentId,
|
|
1496
2227
|
pollInterval: PollIntervalMs,
|
|
1497
2228
|
pollTimeout: effectiveTimeout,
|
|
1498
|
-
since: lastFinishedTaskCheck,
|
|
1499
2229
|
});
|
|
1500
2230
|
|
|
1501
2231
|
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
2232
|
console.log(`[${role}] Trigger received: ${trigger.type}`);
|
|
1508
2233
|
|
|
1509
2234
|
// Build prompt based on trigger
|
|
1510
|
-
|
|
2235
|
+
let triggerPrompt = await buildPromptForTrigger(
|
|
2236
|
+
trigger,
|
|
2237
|
+
prompt,
|
|
2238
|
+
adapter.formatCommand.bind(adapter),
|
|
2239
|
+
);
|
|
2240
|
+
|
|
2241
|
+
// Enrich prompt with relevant memories from past sessions
|
|
2242
|
+
if (trigger.type === "task_assigned" || trigger.type === "task_offered") {
|
|
2243
|
+
const task =
|
|
2244
|
+
trigger.task && typeof trigger.task === "object" && "task" in trigger.task
|
|
2245
|
+
? (trigger.task as { task: string; epicId?: string; id?: string })
|
|
2246
|
+
: null;
|
|
2247
|
+
if (task?.task) {
|
|
2248
|
+
// Enrich search query with epic context for better memory retrieval
|
|
2249
|
+
let searchQuery = task.task;
|
|
2250
|
+
if (task.epicId) {
|
|
2251
|
+
const epicContext = await fetchEpicNameAndGoal(apiUrl, apiKey, task.epicId);
|
|
2252
|
+
if (epicContext) {
|
|
2253
|
+
searchQuery = `[Epic: ${epicContext.name}] ${epicContext.goal}\n\n${task.task}`;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
const memoryContext = await fetchRelevantMemories(
|
|
2258
|
+
apiUrl,
|
|
2259
|
+
apiKey,
|
|
2260
|
+
agentId,
|
|
2261
|
+
searchQuery,
|
|
2262
|
+
);
|
|
2263
|
+
if (memoryContext) {
|
|
2264
|
+
triggerPrompt += memoryContext;
|
|
2265
|
+
console.log(`[${role}] Injected relevant memories into task prompt`);
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// Inject recent completed task summaries from the same epic
|
|
2269
|
+
if (task.epicId && task.id) {
|
|
2270
|
+
const epicTaskContext = await fetchEpicTaskContext(
|
|
2271
|
+
apiUrl,
|
|
2272
|
+
apiKey,
|
|
2273
|
+
task.epicId,
|
|
2274
|
+
task.id,
|
|
2275
|
+
);
|
|
2276
|
+
if (epicTaskContext) {
|
|
2277
|
+
triggerPrompt += epicTaskContext;
|
|
2278
|
+
console.log(`[${role}] Injected epic task context into prompt`);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// For epic progress triggers, search memories related to the epic goals
|
|
2285
|
+
if (trigger.type === "epic_progress_changed" && trigger.epics) {
|
|
2286
|
+
const epics = trigger.epics as Array<{
|
|
2287
|
+
epic: { name: string; goal: string };
|
|
2288
|
+
}>;
|
|
2289
|
+
const epicQueries = epics.map((e) => `${e.epic.name}: ${e.epic.goal}`).join("\n");
|
|
2290
|
+
const memoryContext = await fetchRelevantMemories(apiUrl, apiKey, agentId, epicQueries);
|
|
2291
|
+
if (memoryContext) {
|
|
2292
|
+
triggerPrompt += memoryContext;
|
|
2293
|
+
console.log(`[${role}] Injected memories into epic progress prompt`);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
// Resolve --resume for child tasks with parentTaskId
|
|
2298
|
+
let effectiveAdditionalArgs = opts.additionalArgs || [];
|
|
2299
|
+
const taskObj = trigger.task as { parentTaskId?: string } | undefined;
|
|
2300
|
+
if (taskObj?.parentTaskId) {
|
|
2301
|
+
const parentSessionId = await fetchProviderSessionId(
|
|
2302
|
+
apiUrl,
|
|
2303
|
+
apiKey,
|
|
2304
|
+
taskObj.parentTaskId,
|
|
2305
|
+
);
|
|
2306
|
+
if (parentSessionId) {
|
|
2307
|
+
effectiveAdditionalArgs = [...effectiveAdditionalArgs, "--resume", parentSessionId];
|
|
2308
|
+
console.log(
|
|
2309
|
+
`[${role}] Child task — resuming parent session ${parentSessionId.slice(0, 8)}`,
|
|
2310
|
+
);
|
|
2311
|
+
} else {
|
|
2312
|
+
console.log(`[${role}] Child task — parent session ID not found, starting fresh`);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
// Extract model from task data for per-task model selection
|
|
2317
|
+
const taskModel = (trigger.task as { model?: string } | undefined)?.model;
|
|
2318
|
+
|
|
2319
|
+
// Handle repo context for tasks with vcsRepo (GitHub/GitLab)
|
|
2320
|
+
const taskVcsRepo = (trigger.task as { vcsRepo?: string } | undefined)?.vcsRepo;
|
|
2321
|
+
if (taskVcsRepo && apiUrl) {
|
|
2322
|
+
const repoConfig = await fetchRepoConfig(apiUrl, apiKey, taskVcsRepo);
|
|
2323
|
+
// Fall back to convention-based config if repo is not registered
|
|
2324
|
+
const effectiveConfig = repoConfig ?? {
|
|
2325
|
+
url: taskVcsRepo,
|
|
2326
|
+
name: taskVcsRepo.split("/").pop() || taskVcsRepo,
|
|
2327
|
+
clonePath: `/workspace/repos/${taskVcsRepo.split("/").pop() || taskVcsRepo}`,
|
|
2328
|
+
defaultBranch: "main",
|
|
2329
|
+
};
|
|
2330
|
+
currentRepoContext = await ensureRepoForTask(effectiveConfig, role);
|
|
2331
|
+
} else {
|
|
2332
|
+
currentRepoContext = undefined;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
// Resolve effective working directory (priority: task.dir > repoContext.clonePath > process.cwd())
|
|
2336
|
+
const taskDir = (trigger.task as { dir?: string } | undefined)?.dir;
|
|
2337
|
+
let effectiveCwd: string | undefined;
|
|
2338
|
+
|
|
2339
|
+
if (taskDir) {
|
|
2340
|
+
try {
|
|
2341
|
+
if (existsSync(taskDir) && statSync(taskDir).isDirectory()) {
|
|
2342
|
+
effectiveCwd = taskDir;
|
|
2343
|
+
} else {
|
|
2344
|
+
console.warn(
|
|
2345
|
+
`[${role}] Task dir "${taskDir}" does not exist or is not a directory, falling back to default cwd`,
|
|
2346
|
+
);
|
|
2347
|
+
}
|
|
2348
|
+
} catch {
|
|
2349
|
+
console.warn(
|
|
2350
|
+
`[${role}] Failed to check task dir "${taskDir}", falling back to default cwd`,
|
|
2351
|
+
);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
if (!effectiveCwd && currentRepoContext?.clonePath) {
|
|
2356
|
+
effectiveCwd = currentRepoContext.clonePath;
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// Annotate prompt with working directory context
|
|
2360
|
+
if (effectiveCwd && effectiveCwd !== process.cwd()) {
|
|
2361
|
+
triggerPrompt += `\n\n---\n**Working Directory**: You are starting in \`${effectiveCwd}\`. `;
|
|
2362
|
+
if (taskDir) {
|
|
2363
|
+
triggerPrompt += "This was explicitly set on the task.";
|
|
2364
|
+
} else if (currentRepoContext?.clonePath) {
|
|
2365
|
+
triggerPrompt += "This is the repository clone path for this task's VCS repo.";
|
|
2366
|
+
}
|
|
2367
|
+
triggerPrompt +=
|
|
2368
|
+
" You can still access any path on the filesystem — this is just your starting directory.";
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
// Warn in system prompt when task dir was specified but doesn't exist
|
|
2372
|
+
let cwdWarning = "";
|
|
2373
|
+
if (taskDir && !effectiveCwd) {
|
|
2374
|
+
cwdWarning = `\n\nNote: The task requested working directory "${taskDir}" but it does not exist. Falling back to default directory.`;
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
// Rebuild system prompt with per-task repo context
|
|
2378
|
+
const taskBasePrompt = await buildSystemPrompt();
|
|
2379
|
+
const taskSystemPrompt =
|
|
2380
|
+
(additionalSystemPrompt
|
|
2381
|
+
? `${taskBasePrompt}\n\n${additionalSystemPrompt}`
|
|
2382
|
+
: taskBasePrompt) + cwdWarning;
|
|
1511
2383
|
|
|
1512
2384
|
iteration++;
|
|
1513
2385
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
@@ -1517,6 +2389,9 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1517
2389
|
console.log(`\n[${role}] === Iteration ${iteration} ===`);
|
|
1518
2390
|
console.log(`[${role}] Logging to: ${logFile}`);
|
|
1519
2391
|
console.log(`[${role}] Prompt: ${triggerPrompt.slice(0, 100)}...`);
|
|
2392
|
+
if (effectiveCwd) {
|
|
2393
|
+
console.log(`[${role}] Working directory: ${effectiveCwd}`);
|
|
2394
|
+
}
|
|
1520
2395
|
|
|
1521
2396
|
const metadata = {
|
|
1522
2397
|
type: metadataType,
|
|
@@ -1529,30 +2404,58 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1529
2404
|
};
|
|
1530
2405
|
await Bun.write(logFile, `${JSON.stringify(metadata)}\n`);
|
|
1531
2406
|
|
|
1532
|
-
//
|
|
1533
|
-
const
|
|
2407
|
+
// Per-task runner session ID so session logs are scoped to this task
|
|
2408
|
+
const taskRunnerSessionId = crypto.randomUUID();
|
|
2409
|
+
|
|
2410
|
+
// Spawn without blocking (await to set up session, but process runs async)
|
|
2411
|
+
const runningTask = await spawnProviderProcess(
|
|
2412
|
+
adapter,
|
|
1534
2413
|
{
|
|
1535
2414
|
prompt: triggerPrompt,
|
|
1536
2415
|
logFile,
|
|
1537
|
-
systemPrompt:
|
|
1538
|
-
additionalArgs:
|
|
2416
|
+
systemPrompt: taskSystemPrompt,
|
|
2417
|
+
additionalArgs: effectiveAdditionalArgs,
|
|
1539
2418
|
role,
|
|
1540
2419
|
apiUrl,
|
|
1541
2420
|
apiKey,
|
|
1542
2421
|
agentId,
|
|
1543
|
-
|
|
2422
|
+
runnerSessionId: taskRunnerSessionId,
|
|
1544
2423
|
iteration,
|
|
1545
2424
|
taskId: trigger.taskId,
|
|
2425
|
+
model: taskModel,
|
|
2426
|
+
cwd: effectiveCwd,
|
|
1546
2427
|
},
|
|
1547
2428
|
logDir,
|
|
1548
|
-
metadataType,
|
|
1549
|
-
sessionId,
|
|
1550
2429
|
isYolo,
|
|
1551
2430
|
);
|
|
1552
2431
|
|
|
2432
|
+
// Attach trigger metadata for logging
|
|
2433
|
+
runningTask.triggerType = trigger.type;
|
|
2434
|
+
|
|
2435
|
+
// Attach deferred cursor updates for channel_activity triggers
|
|
2436
|
+
if (trigger.type === "channel_activity" && trigger.cursorUpdates) {
|
|
2437
|
+
runningTask.cursorUpdates = trigger.cursorUpdates as Array<{
|
|
2438
|
+
channelId: string;
|
|
2439
|
+
ts: string;
|
|
2440
|
+
}>;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
1553
2443
|
state.activeTasks.set(runningTask.taskId, runningTask);
|
|
2444
|
+
|
|
2445
|
+
// Register active session for concurrency awareness
|
|
2446
|
+
const taskDesc =
|
|
2447
|
+
trigger.task && typeof trigger.task === "object" && "task" in trigger.task
|
|
2448
|
+
? String((trigger.task as { task: string }).task).slice(0, 200)
|
|
2449
|
+
: undefined;
|
|
2450
|
+
registerActiveSession(apiConfig, {
|
|
2451
|
+
taskId: runningTask.taskId,
|
|
2452
|
+
triggerType: trigger.type,
|
|
2453
|
+
taskDescription: taskDesc,
|
|
2454
|
+
runnerSessionId: taskRunnerSessionId,
|
|
2455
|
+
});
|
|
2456
|
+
|
|
1554
2457
|
console.log(
|
|
1555
|
-
`[${role}] Started task ${runningTask.taskId.slice(0, 8)} (${state.activeTasks.size}/${state.maxConcurrent} active)`,
|
|
2458
|
+
`[${role}] Started task ${runningTask.taskId.slice(0, 8)} (${state.activeTasks.size}/${state.maxConcurrent} active, trigger: ${trigger.type})`,
|
|
1556
2459
|
);
|
|
1557
2460
|
}
|
|
1558
2461
|
} else {
|
|
@@ -1593,19 +2496,26 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1593
2496
|
};
|
|
1594
2497
|
await Bun.write(logFile, `${JSON.stringify(metadata)}\n`);
|
|
1595
2498
|
|
|
1596
|
-
const
|
|
2499
|
+
const iterationResult = await runProviderIteration(adapter, {
|
|
1597
2500
|
prompt,
|
|
1598
2501
|
logFile,
|
|
1599
2502
|
systemPrompt: resolvedSystemPrompt,
|
|
1600
2503
|
additionalArgs: opts.additionalArgs,
|
|
1601
2504
|
role,
|
|
2505
|
+
apiUrl,
|
|
2506
|
+
apiKey,
|
|
2507
|
+
agentId,
|
|
1602
2508
|
});
|
|
1603
2509
|
|
|
1604
|
-
if (exitCode !== 0) {
|
|
2510
|
+
if (iterationResult.exitCode !== 0) {
|
|
2511
|
+
const failureReason =
|
|
2512
|
+
iterationResult.failureReason || `Process exited with code ${iterationResult.exitCode}`;
|
|
2513
|
+
|
|
1605
2514
|
const errorLog = {
|
|
1606
2515
|
timestamp: new Date().toISOString(),
|
|
1607
2516
|
iteration,
|
|
1608
|
-
exitCode,
|
|
2517
|
+
exitCode: iterationResult.exitCode,
|
|
2518
|
+
failureReason,
|
|
1609
2519
|
error: true,
|
|
1610
2520
|
};
|
|
1611
2521
|
|
|
@@ -1615,12 +2525,12 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1615
2525
|
await Bun.write(errorsFile, `${existingErrors}${JSON.stringify(errorLog)}\n`);
|
|
1616
2526
|
|
|
1617
2527
|
if (!isYolo) {
|
|
1618
|
-
console.error(`[${role}]
|
|
2528
|
+
console.error(`[${role}] ${failureReason}. Stopping.`);
|
|
1619
2529
|
console.error(`[${role}] Error logged to: ${errorsFile}`);
|
|
1620
|
-
process.exit(exitCode);
|
|
2530
|
+
process.exit(iterationResult.exitCode);
|
|
1621
2531
|
}
|
|
1622
2532
|
|
|
1623
|
-
console.warn(`[${role}]
|
|
2533
|
+
console.warn(`[${role}] ${failureReason}. YOLO mode - continuing...`);
|
|
1624
2534
|
}
|
|
1625
2535
|
|
|
1626
2536
|
console.log(`[${role}] Iteration ${iteration} complete. Starting next iteration...`);
|