@ag-eco/agentplate-cli 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +462 -0
- package/agents/ap-co-creation.md +90 -0
- package/agents/builder.md +144 -0
- package/agents/coordinator.md +377 -0
- package/agents/lead.md +435 -0
- package/agents/merger.md +164 -0
- package/agents/monitor.md +214 -0
- package/agents/orchestrator.md +239 -0
- package/agents/reviewer.md +140 -0
- package/agents/scout.md +125 -0
- package/agents/supervisor.md +427 -0
- package/package.json +66 -0
- package/src/agents/capabilities.test.ts +85 -0
- package/src/agents/capabilities.ts +125 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +101 -0
- package/src/agents/copilot-hooks-deployer.test.ts +162 -0
- package/src/agents/copilot-hooks-deployer.ts +93 -0
- package/src/agents/guard-rules.test.ts +372 -0
- package/src/agents/guard-rules.ts +97 -0
- package/src/agents/headless-mail-injector.test.ts +709 -0
- package/src/agents/headless-mail-injector.ts +377 -0
- package/src/agents/headless-prompt.test.ts +102 -0
- package/src/agents/headless-prompt.ts +68 -0
- package/src/agents/hooks-deployer.test.ts +3119 -0
- package/src/agents/hooks-deployer.ts +804 -0
- package/src/agents/identity.test.ts +604 -0
- package/src/agents/identity.ts +384 -0
- package/src/agents/lifecycle.test.ts +196 -0
- package/src/agents/lifecycle.ts +183 -0
- package/src/agents/mail-poll-detect.test.ts +153 -0
- package/src/agents/mail-poll-detect.ts +73 -0
- package/src/agents/manifest.test.ts +1026 -0
- package/src/agents/manifest.ts +376 -0
- package/src/agents/overlay.test.ts +1058 -0
- package/src/agents/overlay.ts +490 -0
- package/src/agents/scope-detect.test.ts +190 -0
- package/src/agents/scope-detect.ts +146 -0
- package/src/agents/turn-lock.test.ts +181 -0
- package/src/agents/turn-lock.ts +235 -0
- package/src/agents/turn-runner-dispatch.test.ts +182 -0
- package/src/agents/turn-runner-dispatch.ts +105 -0
- package/src/agents/turn-runner.test.ts +2312 -0
- package/src/agents/turn-runner.ts +1383 -0
- package/src/beads/client.test.ts +217 -0
- package/src/beads/client.ts +230 -0
- package/src/beads/molecules.test.ts +338 -0
- package/src/beads/molecules.ts +198 -0
- package/src/commands/agents.test.ts +328 -0
- package/src/commands/agents.ts +299 -0
- package/src/commands/clean.test.ts +797 -0
- package/src/commands/clean.ts +791 -0
- package/src/commands/completions.test.ts +348 -0
- package/src/commands/completions.ts +981 -0
- package/src/commands/coordinator.test.ts +2975 -0
- package/src/commands/coordinator.ts +1841 -0
- package/src/commands/costs.test.ts +1183 -0
- package/src/commands/costs.ts +599 -0
- package/src/commands/dashboard.test.ts +954 -0
- package/src/commands/dashboard.ts +1212 -0
- package/src/commands/discover.test.ts +288 -0
- package/src/commands/discover.ts +202 -0
- package/src/commands/doctor.test.ts +303 -0
- package/src/commands/doctor.ts +311 -0
- package/src/commands/ecosystem.test.ts +226 -0
- package/src/commands/ecosystem.ts +248 -0
- package/src/commands/errors.test.ts +654 -0
- package/src/commands/errors.ts +197 -0
- package/src/commands/feed.test.ts +709 -0
- package/src/commands/feed.ts +260 -0
- package/src/commands/group.test.ts +475 -0
- package/src/commands/group.ts +546 -0
- package/src/commands/hooks.test.ts +458 -0
- package/src/commands/hooks.ts +263 -0
- package/src/commands/init.test.ts +1011 -0
- package/src/commands/init.ts +967 -0
- package/src/commands/inspect.test.ts +1239 -0
- package/src/commands/inspect.ts +648 -0
- package/src/commands/log.test.ts +1913 -0
- package/src/commands/log.ts +958 -0
- package/src/commands/logs.test.ts +801 -0
- package/src/commands/logs.ts +483 -0
- package/src/commands/mail.test.ts +1501 -0
- package/src/commands/mail.ts +848 -0
- package/src/commands/merge.test.ts +864 -0
- package/src/commands/merge.ts +381 -0
- package/src/commands/metrics.test.ts +458 -0
- package/src/commands/metrics.ts +129 -0
- package/src/commands/monitor.test.ts +191 -0
- package/src/commands/monitor.ts +409 -0
- package/src/commands/nudge.test.ts +579 -0
- package/src/commands/nudge.ts +646 -0
- package/src/commands/orchestrator.ts +42 -0
- package/src/commands/prime.test.ts +612 -0
- package/src/commands/prime.ts +359 -0
- package/src/commands/replay.test.ts +757 -0
- package/src/commands/replay.ts +231 -0
- package/src/commands/run.test.ts +469 -0
- package/src/commands/run.ts +353 -0
- package/src/commands/serve/agent-actions.test.ts +210 -0
- package/src/commands/serve/agent-actions.ts +192 -0
- package/src/commands/serve/build.test.ts +202 -0
- package/src/commands/serve/build.ts +206 -0
- package/src/commands/serve/coordinator-actions.test.ts +339 -0
- package/src/commands/serve/coordinator-actions.ts +410 -0
- package/src/commands/serve/dev.test.ts +168 -0
- package/src/commands/serve/dev.ts +117 -0
- package/src/commands/serve/mail-actions.test.ts +312 -0
- package/src/commands/serve/mail-actions.ts +167 -0
- package/src/commands/serve/rest.test.ts +1680 -0
- package/src/commands/serve/rest.ts +1130 -0
- package/src/commands/serve/static.ts +51 -0
- package/src/commands/serve/ws.test.ts +361 -0
- package/src/commands/serve/ws.ts +332 -0
- package/src/commands/serve.test.ts +459 -0
- package/src/commands/serve.ts +654 -0
- package/src/commands/sling.test.ts +1583 -0
- package/src/commands/sling.ts +1351 -0
- package/src/commands/spec.test.ts +179 -0
- package/src/commands/spec.ts +105 -0
- package/src/commands/status.test.ts +614 -0
- package/src/commands/status.ts +403 -0
- package/src/commands/stop.test.ts +964 -0
- package/src/commands/stop.ts +319 -0
- package/src/commands/supervisor.test.ts +185 -0
- package/src/commands/supervisor.ts +537 -0
- package/src/commands/trace.test.ts +762 -0
- package/src/commands/trace.ts +205 -0
- package/src/commands/update.test.ts +466 -0
- package/src/commands/update.ts +263 -0
- package/src/commands/upgrade.test.ts +48 -0
- package/src/commands/upgrade.ts +240 -0
- package/src/commands/watch.test.ts +257 -0
- package/src/commands/watch.ts +308 -0
- package/src/commands/worktree.test.ts +1297 -0
- package/src/commands/worktree.ts +451 -0
- package/src/config.test.ts +1535 -0
- package/src/config.ts +1064 -0
- package/src/doctor/agents.test.ts +523 -0
- package/src/doctor/agents.ts +399 -0
- package/src/doctor/config-check.test.ts +191 -0
- package/src/doctor/config-check.ts +183 -0
- package/src/doctor/consistency.test.ts +807 -0
- package/src/doctor/consistency.ts +347 -0
- package/src/doctor/databases.test.ts +350 -0
- package/src/doctor/databases.ts +243 -0
- package/src/doctor/dependencies.test.ts +296 -0
- package/src/doctor/dependencies.ts +272 -0
- package/src/doctor/ecosystem.test.ts +308 -0
- package/src/doctor/ecosystem.ts +156 -0
- package/src/doctor/logs.test.ts +253 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +315 -0
- package/src/doctor/merge-queue.ts +167 -0
- package/src/doctor/providers.test.ts +409 -0
- package/src/doctor/providers.ts +250 -0
- package/src/doctor/serve.test.ts +95 -0
- package/src/doctor/serve.ts +86 -0
- package/src/doctor/structure.test.ts +423 -0
- package/src/doctor/structure.ts +285 -0
- package/src/doctor/types.ts +43 -0
- package/src/doctor/version.test.ts +241 -0
- package/src/doctor/version.ts +132 -0
- package/src/doctor/watchdog.test.ts +167 -0
- package/src/doctor/watchdog.ts +214 -0
- package/src/e2e/init-sling-lifecycle.test.ts +283 -0
- package/src/errors.test.ts +350 -0
- package/src/errors.ts +217 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +369 -0
- package/src/events/tailer.test.ts +719 -0
- package/src/events/tailer.ts +332 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/index.ts +533 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/insights/quality-gates.test.ts +141 -0
- package/src/insights/quality-gates.ts +156 -0
- package/src/json.test.ts +72 -0
- package/src/json.ts +53 -0
- package/src/loam/client.test.ts +752 -0
- package/src/loam/client.ts +664 -0
- package/src/logging/color.test.ts +252 -0
- package/src/logging/color.ts +105 -0
- package/src/logging/format.test.ts +110 -0
- package/src/logging/format.ts +255 -0
- package/src/logging/logger.test.ts +814 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +259 -0
- package/src/logging/reporter.ts +110 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/logging/theme.ts +140 -0
- package/src/mail/broadcast.test.ts +204 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +774 -0
- package/src/mail/client.ts +236 -0
- package/src/mail/store.test.ts +898 -0
- package/src/mail/store.ts +425 -0
- package/src/merge/lock.test.ts +149 -0
- package/src/merge/lock.ts +140 -0
- package/src/merge/predict.test.ts +387 -0
- package/src/merge/predict.ts +249 -0
- package/src/merge/queue.test.ts +426 -0
- package/src/merge/queue.ts +246 -0
- package/src/merge/resolver.test.ts +1993 -0
- package/src/merge/resolver.ts +926 -0
- package/src/metrics/pricing.test.ts +258 -0
- package/src/metrics/pricing.ts +135 -0
- package/src/metrics/store.test.ts +978 -0
- package/src/metrics/store.ts +501 -0
- package/src/metrics/summary.test.ts +398 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +483 -0
- package/src/metrics/transcript.ts +114 -0
- package/src/runtimes/__fixtures__/claude-stream-fixture.ts +22 -0
- package/src/runtimes/aider.test.ts +124 -0
- package/src/runtimes/aider.ts +147 -0
- package/src/runtimes/amp.test.ts +164 -0
- package/src/runtimes/amp.ts +154 -0
- package/src/runtimes/claude.test.ts +1474 -0
- package/src/runtimes/claude.ts +579 -0
- package/src/runtimes/codex.test.ts +805 -0
- package/src/runtimes/codex.ts +273 -0
- package/src/runtimes/connections.test.ts +214 -0
- package/src/runtimes/connections.ts +103 -0
- package/src/runtimes/copilot.test.ts +707 -0
- package/src/runtimes/copilot.ts +316 -0
- package/src/runtimes/cursor.test.ts +497 -0
- package/src/runtimes/cursor.ts +205 -0
- package/src/runtimes/gemini.test.ts +537 -0
- package/src/runtimes/gemini.ts +243 -0
- package/src/runtimes/goose.test.ts +133 -0
- package/src/runtimes/goose.ts +157 -0
- package/src/runtimes/headless-connection.test.ts +264 -0
- package/src/runtimes/headless-connection.ts +158 -0
- package/src/runtimes/opencode.test.ts +325 -0
- package/src/runtimes/opencode.ts +188 -0
- package/src/runtimes/pi-guards.test.ts +486 -0
- package/src/runtimes/pi-guards.ts +367 -0
- package/src/runtimes/pi.test.ts +789 -0
- package/src/runtimes/pi.ts +305 -0
- package/src/runtimes/registry.test.ts +196 -0
- package/src/runtimes/registry.ts +99 -0
- package/src/runtimes/sapling.test.ts +1267 -0
- package/src/runtimes/sapling.ts +710 -0
- package/src/runtimes/types.ts +266 -0
- package/src/schema-consistency.test.ts +246 -0
- package/src/sessions/compat.test.ts +281 -0
- package/src/sessions/compat.ts +105 -0
- package/src/sessions/store.test.ts +1748 -0
- package/src/sessions/store.ts +858 -0
- package/src/test-helpers.test.ts +124 -0
- package/src/test-helpers.ts +145 -0
- package/src/test-setup.test.ts +31 -0
- package/src/test-setup.ts +28 -0
- package/src/tools/loam/api.ts +368 -0
- package/src/tools/loam/cli.ts +278 -0
- package/src/tools/loam/commands/add.ts +52 -0
- package/src/tools/loam/commands/archive.ts +214 -0
- package/src/tools/loam/commands/audit.ts +276 -0
- package/src/tools/loam/commands/compact.ts +1062 -0
- package/src/tools/loam/commands/completions.ts +79 -0
- package/src/tools/loam/commands/config.ts +381 -0
- package/src/tools/loam/commands/delete-domain.ts +121 -0
- package/src/tools/loam/commands/delete.ts +316 -0
- package/src/tools/loam/commands/diff.ts +200 -0
- package/src/tools/loam/commands/doctor.ts +1113 -0
- package/src/tools/loam/commands/edit.ts +226 -0
- package/src/tools/loam/commands/init.ts +31 -0
- package/src/tools/loam/commands/learn.ts +179 -0
- package/src/tools/loam/commands/move.ts +323 -0
- package/src/tools/loam/commands/onboard.ts +374 -0
- package/src/tools/loam/commands/outcome.ts +185 -0
- package/src/tools/loam/commands/prime.ts +688 -0
- package/src/tools/loam/commands/prune.ts +614 -0
- package/src/tools/loam/commands/query.ts +218 -0
- package/src/tools/loam/commands/rank.ts +180 -0
- package/src/tools/loam/commands/ready.ts +189 -0
- package/src/tools/loam/commands/record.ts +1210 -0
- package/src/tools/loam/commands/restore.ts +166 -0
- package/src/tools/loam/commands/search.ts +327 -0
- package/src/tools/loam/commands/setup.ts +887 -0
- package/src/tools/loam/commands/status.ts +103 -0
- package/src/tools/loam/commands/sync.ts +298 -0
- package/src/tools/loam/commands/update.ts +19 -0
- package/src/tools/loam/commands/upgrade.ts +93 -0
- package/src/tools/loam/commands/validate.ts +190 -0
- package/src/tools/loam/index.ts +62 -0
- package/src/tools/loam/log.ts +127 -0
- package/src/tools/loam/registry/builtins.ts +409 -0
- package/src/tools/loam/registry/custom.ts +431 -0
- package/src/tools/loam/registry/init.ts +55 -0
- package/src/tools/loam/registry/template.ts +40 -0
- package/src/tools/loam/registry/type-registry.ts +113 -0
- package/src/tools/loam/schemas/config-schema.ts +489 -0
- package/src/tools/loam/schemas/config.ts +245 -0
- package/src/tools/loam/schemas/index.ts +18 -0
- package/src/tools/loam/schemas/record-schema.ts +191 -0
- package/src/tools/loam/schemas/record.ts +115 -0
- package/src/tools/loam/utils/active-work.ts +205 -0
- package/src/tools/loam/utils/anchor-validity.ts +80 -0
- package/src/tools/loam/utils/archive.ts +146 -0
- package/src/tools/loam/utils/audit.ts +667 -0
- package/src/tools/loam/utils/bm25.ts +238 -0
- package/src/tools/loam/utils/budget.ts +142 -0
- package/src/tools/loam/utils/config.ts +344 -0
- package/src/tools/loam/utils/dir-anchors.ts +62 -0
- package/src/tools/loam/utils/domain-rules.ts +114 -0
- package/src/tools/loam/utils/expertise.ts +393 -0
- package/src/tools/loam/utils/format-helpers.ts +96 -0
- package/src/tools/loam/utils/format.ts +1234 -0
- package/src/tools/loam/utils/git-context.ts +50 -0
- package/src/tools/loam/utils/git.ts +183 -0
- package/src/tools/loam/utils/hooks.ts +299 -0
- package/src/tools/loam/utils/index.ts +52 -0
- package/src/tools/loam/utils/json-output.ts +13 -0
- package/src/tools/loam/utils/lock.ts +76 -0
- package/src/tools/loam/utils/markers.ts +48 -0
- package/src/tools/loam/utils/numeric-flags.ts +20 -0
- package/src/tools/loam/utils/palette.ts +44 -0
- package/src/tools/loam/utils/prime-ranking.ts +135 -0
- package/src/tools/loam/utils/recipe-discovery.ts +195 -0
- package/src/tools/loam/utils/runtime-flags.ts +28 -0
- package/src/tools/loam/utils/scoring.ts +94 -0
- package/src/tools/loam/utils/version.ts +116 -0
- package/src/tools/sprout/commands/block.ts +64 -0
- package/src/tools/sprout/commands/blocked.ts +86 -0
- package/src/tools/sprout/commands/close.ts +129 -0
- package/src/tools/sprout/commands/completions.ts +198 -0
- package/src/tools/sprout/commands/config.ts +238 -0
- package/src/tools/sprout/commands/create.ts +164 -0
- package/src/tools/sprout/commands/dep.ts +148 -0
- package/src/tools/sprout/commands/doctor.ts +979 -0
- package/src/tools/sprout/commands/init.ts +83 -0
- package/src/tools/sprout/commands/label.ts +178 -0
- package/src/tools/sprout/commands/list.ts +210 -0
- package/src/tools/sprout/commands/migrate.ts +133 -0
- package/src/tools/sprout/commands/onboard.ts +207 -0
- package/src/tools/sprout/commands/plan-show.ts +278 -0
- package/src/tools/sprout/commands/plan.ts +2526 -0
- package/src/tools/sprout/commands/prime.ts +399 -0
- package/src/tools/sprout/commands/ready.ts +245 -0
- package/src/tools/sprout/commands/search.ts +221 -0
- package/src/tools/sprout/commands/show.ts +277 -0
- package/src/tools/sprout/commands/stats.ts +146 -0
- package/src/tools/sprout/commands/sync.ts +134 -0
- package/src/tools/sprout/commands/tpl.ts +364 -0
- package/src/tools/sprout/commands/unblock.ts +115 -0
- package/src/tools/sprout/commands/update.ts +257 -0
- package/src/tools/sprout/commands/upgrade.ts +91 -0
- package/src/tools/sprout/config-schema.ts +152 -0
- package/src/tools/sprout/config.ts +355 -0
- package/src/tools/sprout/filter.ts +107 -0
- package/src/tools/sprout/format.ts +43 -0
- package/src/tools/sprout/id.ts +22 -0
- package/src/tools/sprout/index.ts +204 -0
- package/src/tools/sprout/log.ts +76 -0
- package/src/tools/sprout/markers.ts +22 -0
- package/src/tools/sprout/output.ts +121 -0
- package/src/tools/sprout/plan-backref.ts +93 -0
- package/src/tools/sprout/plan-context.ts +81 -0
- package/src/tools/sprout/plan-domain.ts +139 -0
- package/src/tools/sprout/plan-lifecycle.ts +65 -0
- package/src/tools/sprout/plan-loam.ts +207 -0
- package/src/tools/sprout/plan-schema.ts +209 -0
- package/src/tools/sprout/sort.ts +31 -0
- package/src/tools/sprout/store.ts +172 -0
- package/src/tools/sprout/types.ts +118 -0
- package/src/tools/sprout/validation.ts +119 -0
- package/src/tools/sprout/version.ts +1 -0
- package/src/tools/sprout/yaml.ts +387 -0
- package/src/tools/trellis/commands/archive.ts +87 -0
- package/src/tools/trellis/commands/completions.ts +610 -0
- package/src/tools/trellis/commands/config.ts +382 -0
- package/src/tools/trellis/commands/create.ts +252 -0
- package/src/tools/trellis/commands/diff.ts +150 -0
- package/src/tools/trellis/commands/doctor.ts +771 -0
- package/src/tools/trellis/commands/emit.ts +365 -0
- package/src/tools/trellis/commands/history.ts +83 -0
- package/src/tools/trellis/commands/import.ts +198 -0
- package/src/tools/trellis/commands/init.ts +81 -0
- package/src/tools/trellis/commands/list.ts +103 -0
- package/src/tools/trellis/commands/onboard.ts +156 -0
- package/src/tools/trellis/commands/pin.ts +172 -0
- package/src/tools/trellis/commands/prime.ts +193 -0
- package/src/tools/trellis/commands/render.ts +122 -0
- package/src/tools/trellis/commands/schema.ts +353 -0
- package/src/tools/trellis/commands/show.ts +115 -0
- package/src/tools/trellis/commands/stats.ts +65 -0
- package/src/tools/trellis/commands/sync.ts +112 -0
- package/src/tools/trellis/commands/tree.ts +123 -0
- package/src/tools/trellis/commands/update.ts +330 -0
- package/src/tools/trellis/commands/upgrade.ts +95 -0
- package/src/tools/trellis/commands/validate.ts +166 -0
- package/src/tools/trellis/config-schema.ts +81 -0
- package/src/tools/trellis/config.ts +108 -0
- package/src/tools/trellis/frontmatter.ts +348 -0
- package/src/tools/trellis/id.ts +24 -0
- package/src/tools/trellis/index.ts +209 -0
- package/src/tools/trellis/markers.ts +28 -0
- package/src/tools/trellis/output.ts +84 -0
- package/src/tools/trellis/render.ts +212 -0
- package/src/tools/trellis/store.ts +144 -0
- package/src/tools/trellis/types.ts +82 -0
- package/src/tools/trellis/validate.ts +199 -0
- package/src/tools/trellis/yaml.ts +309 -0
- package/src/tracker/beads.test.ts +454 -0
- package/src/tracker/beads.ts +56 -0
- package/src/tracker/factory.test.ts +90 -0
- package/src/tracker/factory.ts +65 -0
- package/src/tracker/sprout.test.ts +461 -0
- package/src/tracker/sprout.ts +182 -0
- package/src/tracker/types.ts +52 -0
- package/src/trellis/client.test.ts +107 -0
- package/src/trellis/client.ts +179 -0
- package/src/types.ts +970 -0
- package/src/utils/bin.test.ts +10 -0
- package/src/utils/bin.ts +37 -0
- package/src/utils/browser.test.ts +49 -0
- package/src/utils/browser.ts +48 -0
- package/src/utils/fs.test.ts +119 -0
- package/src/utils/fs.ts +62 -0
- package/src/utils/pid.test.ts +152 -0
- package/src/utils/pid.ts +130 -0
- package/src/utils/process-scan.test.ts +53 -0
- package/src/utils/process-scan.ts +76 -0
- package/src/utils/time.test.ts +43 -0
- package/src/utils/time.ts +37 -0
- package/src/utils/version.test.ts +33 -0
- package/src/utils/version.ts +70 -0
- package/src/version.ts +5 -0
- package/src/watchdog/daemon.test.ts +3721 -0
- package/src/watchdog/daemon.ts +1257 -0
- package/src/watchdog/health.test.ts +830 -0
- package/src/watchdog/health.ts +434 -0
- package/src/watchdog/triage.test.ts +205 -0
- package/src/watchdog/triage.ts +205 -0
- package/src/worktree/manager.test.ts +720 -0
- package/src/worktree/manager.ts +405 -0
- package/src/worktree/process.test.ts +172 -0
- package/src/worktree/process.ts +131 -0
- package/src/worktree/tmux.test.ts +1616 -0
- package/src/worktree/tmux.ts +721 -0
- package/templates/CLAUDE.md.tmpl +100 -0
- package/templates/copilot-hooks.json.tmpl +13 -0
- package/templates/hooks.json.tmpl +109 -0
- package/templates/overlay.md.tmpl +88 -0
- package/ui/dist/apple-touch-icon-bdy6teep.png +0 -0
- package/ui/dist/chunk-8s31f05k.css +1 -0
- package/ui/dist/chunk-vm5rz679.js +300 -0
- package/ui/dist/favicon-nzb39vza.svg +4 -0
- package/ui/dist/index.html +17 -0
|
@@ -0,0 +1,1351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: ap sling <task-id>
|
|
3
|
+
*
|
|
4
|
+
* CRITICAL PATH. Orchestrates a full agent spawn:
|
|
5
|
+
* 1. Load config + manifest
|
|
6
|
+
* 2. Validate (depth limit, hierarchy)
|
|
7
|
+
* 3. Load manifest + validate capability
|
|
8
|
+
* 4. Resolve or create run_id (current-run.txt)
|
|
9
|
+
* 5. Check name uniqueness + concurrency limit
|
|
10
|
+
* 6. Validate task exists
|
|
11
|
+
* 7. Create worktree
|
|
12
|
+
* 8. Generate + write overlay CLAUDE.md
|
|
13
|
+
* 9. Deploy hooks config
|
|
14
|
+
* 10. Claim task issue
|
|
15
|
+
* 11. Create agent identity
|
|
16
|
+
* 12. Create tmux session running claude
|
|
17
|
+
* 13. Record session in SessionStore + increment run agent count
|
|
18
|
+
* 14. Return AgentSession
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { mkdir } from "node:fs/promises";
|
|
22
|
+
import { join, resolve } from "node:path";
|
|
23
|
+
import { buildInitialHeadlessPrompt, formatMailSection } from "../agents/headless-prompt.ts";
|
|
24
|
+
import { createIdentity, loadIdentity } from "../agents/identity.ts";
|
|
25
|
+
import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
|
|
26
|
+
import { writeOverlay } from "../agents/overlay.ts";
|
|
27
|
+
import { runTurn } from "../agents/turn-runner.ts";
|
|
28
|
+
import { loadConfig } from "../config.ts";
|
|
29
|
+
import { AgentError, HierarchyError, ValidationError } from "../errors.ts";
|
|
30
|
+
import { inferDomain } from "../insights/analyzer.ts";
|
|
31
|
+
import { jsonOutput } from "../json.ts";
|
|
32
|
+
import { createLoamClient } from "../loam/client.ts";
|
|
33
|
+
import { printSuccess } from "../logging/color.ts";
|
|
34
|
+
import { createMailClient } from "../mail/client.ts";
|
|
35
|
+
import { createMailStore } from "../mail/store.ts";
|
|
36
|
+
import { getRuntime } from "../runtimes/registry.ts";
|
|
37
|
+
import { openSessionStore } from "../sessions/compat.ts";
|
|
38
|
+
import { createRunStore } from "../sessions/store.ts";
|
|
39
|
+
import type { TrackerIssue } from "../tracker/factory.ts";
|
|
40
|
+
import { createTrackerClient, resolveBackend, trackerCliName } from "../tracker/factory.ts";
|
|
41
|
+
import { createTrellisClient } from "../trellis/client.ts";
|
|
42
|
+
import type { AgentplateConfig, AgentSession, OverlayConfig } from "../types.ts";
|
|
43
|
+
import { createWorktree, rollbackWorktree } from "../worktree/manager.ts";
|
|
44
|
+
import {
|
|
45
|
+
capturePaneContent,
|
|
46
|
+
checkSessionState,
|
|
47
|
+
createSession,
|
|
48
|
+
ensureTmuxAvailable,
|
|
49
|
+
isSessionAlive,
|
|
50
|
+
killSession,
|
|
51
|
+
sanitizeTmuxName,
|
|
52
|
+
sendKeys,
|
|
53
|
+
waitForTuiReady,
|
|
54
|
+
} from "../worktree/tmux.ts";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Calculate how many milliseconds to sleep before spawning a new agent,
|
|
58
|
+
* based on the configured stagger delay and when the most recent active
|
|
59
|
+
* session was started.
|
|
60
|
+
*
|
|
61
|
+
* Returns 0 if no sleep is needed (no active sessions, delay is 0, or
|
|
62
|
+
* enough time has already elapsed).
|
|
63
|
+
*
|
|
64
|
+
* @param staggerDelayMs - The configured minimum delay between spawns
|
|
65
|
+
* @param activeSessions - Currently active (non-zombie) sessions
|
|
66
|
+
* @param now - Current timestamp in ms (defaults to Date.now(), injectable for testing)
|
|
67
|
+
*/
|
|
68
|
+
export function calculateStaggerDelay(
|
|
69
|
+
staggerDelayMs: number,
|
|
70
|
+
activeSessions: ReadonlyArray<{ startedAt: string }>,
|
|
71
|
+
now: number = Date.now(),
|
|
72
|
+
): number {
|
|
73
|
+
if (staggerDelayMs <= 0 || activeSessions.length === 0) {
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const mostRecent = activeSessions.reduce((latest, s) => {
|
|
78
|
+
return new Date(s.startedAt).getTime() > new Date(latest.startedAt).getTime() ? s : latest;
|
|
79
|
+
});
|
|
80
|
+
const elapsed = now - new Date(mostRecent.startedAt).getTime();
|
|
81
|
+
const remaining = staggerDelayMs - elapsed;
|
|
82
|
+
return remaining > 0 ? remaining : 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate a unique agent name from capability and taskId.
|
|
87
|
+
* Base: capability-taskId. If that collides with takenNames,
|
|
88
|
+
* appends -2, -3, etc. up to 100. Falls back to -Date.now() for guaranteed uniqueness.
|
|
89
|
+
*/
|
|
90
|
+
export function generateAgentName(
|
|
91
|
+
capability: string,
|
|
92
|
+
taskId: string,
|
|
93
|
+
takenNames: readonly string[],
|
|
94
|
+
): string {
|
|
95
|
+
const base = `${capability}-${taskId}`;
|
|
96
|
+
if (!takenNames.includes(base)) {
|
|
97
|
+
return base;
|
|
98
|
+
}
|
|
99
|
+
for (let i = 2; i <= 100; i++) {
|
|
100
|
+
const candidate = `${base}-${i}`;
|
|
101
|
+
if (!takenNames.includes(candidate)) {
|
|
102
|
+
return candidate;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return `${base}-${Date.now()}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if the current process is running as root (UID 0).
|
|
110
|
+
* Returns true if running as root, false otherwise.
|
|
111
|
+
* Returns false on platforms that don't support getuid (e.g., Windows).
|
|
112
|
+
*
|
|
113
|
+
* The getuid parameter is injectable for testability without mocking process.getuid.
|
|
114
|
+
*/
|
|
115
|
+
export function isRunningAsRoot(getuid: (() => number) | undefined = process.getuid): boolean {
|
|
116
|
+
return getuid?.() === 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Infer loam domains from a list of file paths.
|
|
121
|
+
* Returns unique domains sorted alphabetically, falling back to
|
|
122
|
+
* configured defaults if no domains could be inferred.
|
|
123
|
+
*/
|
|
124
|
+
export function inferDomainsFromFiles(
|
|
125
|
+
files: readonly string[],
|
|
126
|
+
configDomains: readonly string[],
|
|
127
|
+
): string[] {
|
|
128
|
+
const inferred = new Set<string>();
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
const domain = inferDomain(file);
|
|
131
|
+
if (domain !== null) {
|
|
132
|
+
inferred.add(domain);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (inferred.size === 0) {
|
|
136
|
+
return [...configDomains];
|
|
137
|
+
}
|
|
138
|
+
return [...inferred].sort();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface SlingOptions {
|
|
142
|
+
capability?: string;
|
|
143
|
+
name?: string;
|
|
144
|
+
spec?: string;
|
|
145
|
+
files?: string;
|
|
146
|
+
parent?: string;
|
|
147
|
+
depth?: string;
|
|
148
|
+
skipScout?: boolean;
|
|
149
|
+
skipTaskCheck?: boolean;
|
|
150
|
+
forceHierarchy?: boolean;
|
|
151
|
+
json?: boolean;
|
|
152
|
+
maxAgents?: string;
|
|
153
|
+
skipReview?: boolean;
|
|
154
|
+
dispatchMaxAgents?: string;
|
|
155
|
+
runtime?: string;
|
|
156
|
+
noScoutCheck?: boolean;
|
|
157
|
+
baseBranch?: string;
|
|
158
|
+
profile?: string;
|
|
159
|
+
headless?: boolean;
|
|
160
|
+
recover?: boolean;
|
|
161
|
+
/**
|
|
162
|
+
* Comma-separated list of sibling agent names dispatched in parallel that
|
|
163
|
+
* may share file scope with this agent (agentplate-f76a). Plumbed through
|
|
164
|
+
* to `OverlayConfig.siblings` so the overlay renders rebase-before-merge_ready
|
|
165
|
+
* guidance.
|
|
166
|
+
*/
|
|
167
|
+
siblings?: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Parse the `--siblings <names>` argument into a normalized string array.
|
|
172
|
+
* Trims whitespace, drops empty entries. Empty / undefined input → `[]`.
|
|
173
|
+
*
|
|
174
|
+
* Exported for unit-testing.
|
|
175
|
+
*/
|
|
176
|
+
export function parseSiblings(raw: string | undefined): string[] {
|
|
177
|
+
if (!raw) return [];
|
|
178
|
+
return raw
|
|
179
|
+
.split(",")
|
|
180
|
+
.map((s) => s.trim())
|
|
181
|
+
.filter((s) => s.length > 0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const WORKABLE_STATUSES = ["open", "in_progress"] as const;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Decide whether a task with the given tracker status can accept a fresh
|
|
188
|
+
* sling. Normal dispatch requires an `open` or `in_progress` task; passing
|
|
189
|
+
* `recover` accepts any status so a coordinator can re-dispatch against a
|
|
190
|
+
* task whose previous owner exited (e.g. closed by a dead lead). (agentplate-629f)
|
|
191
|
+
*/
|
|
192
|
+
export function isTaskWorkable(status: string, recover: boolean): boolean {
|
|
193
|
+
if (recover) return true;
|
|
194
|
+
return (WORKABLE_STATUSES as readonly string[]).includes(status);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Resolve the effective `parentAgent` for a sling invocation, preserving the
|
|
199
|
+
* prior session's link on a re-spawn (`--recover`) when `--parent` was not
|
|
200
|
+
* explicitly passed.
|
|
201
|
+
*
|
|
202
|
+
* Pre-fix, sling always read `opts.parent ?? null` and upserted that into the
|
|
203
|
+
* session row, overwriting the prior `parent_agent` with null whenever a
|
|
204
|
+
* coordinator/lead invoked `ap sling --recover --name <existing>` without
|
|
205
|
+
* threading `--parent`. The runner then read `parentAgent === null` and
|
|
206
|
+
* skipped its in-band `worker_died` notify on a resumed-turn parser stall —
|
|
207
|
+
* the lead waited forever on a signal that never came (agentplate-de3c).
|
|
208
|
+
*
|
|
209
|
+
* Resolution rules:
|
|
210
|
+
* - **Explicit caller intent wins.** If `opts.parent` is defined (including
|
|
211
|
+
* an empty string), use it verbatim. The caller may legitimately want to
|
|
212
|
+
* change or clear the parent on re-spawn.
|
|
213
|
+
* - **Caller silence preserves linkage.** If `opts.parent` is undefined and
|
|
214
|
+
* a prior session row exists with a non-null `parentAgent`, fall back to
|
|
215
|
+
* the prior value. Otherwise return null.
|
|
216
|
+
*
|
|
217
|
+
* Pure function so the regression test in `sling.test.ts` can assert behavior
|
|
218
|
+
* without spinning up the full sling command pipeline.
|
|
219
|
+
*/
|
|
220
|
+
export function resolveParentAgent(
|
|
221
|
+
optsParent: string | undefined,
|
|
222
|
+
existingSession: { parentAgent: string | null } | null,
|
|
223
|
+
): string | null {
|
|
224
|
+
if (optsParent !== undefined) {
|
|
225
|
+
return optsParent;
|
|
226
|
+
}
|
|
227
|
+
return existingSession?.parentAgent ?? null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface AutoDispatchOptions {
|
|
231
|
+
agentName: string;
|
|
232
|
+
taskId: string;
|
|
233
|
+
capability: string;
|
|
234
|
+
specPath: string | null;
|
|
235
|
+
parentAgent: string | null;
|
|
236
|
+
/**
|
|
237
|
+
* The agent who invoked `ap sling` (from `AGENTPLATE_AGENT_NAME` env var);
|
|
238
|
+
* takes precedence over `parentAgent` for the mail `from` field, since
|
|
239
|
+
* `--parent` describes the new agent's hierarchical parent, not the slinger.
|
|
240
|
+
*/
|
|
241
|
+
slingerName: string | null;
|
|
242
|
+
instructionPath: string;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Build a structured auto-dispatch mail message for a newly slung agent.
|
|
247
|
+
*
|
|
248
|
+
* Sending this mail before creating the tmux session ensures it exists
|
|
249
|
+
* in the DB when SessionStart fires, eliminating the race where dispatch
|
|
250
|
+
* mail arrives after the agent boots and sits idle forever.
|
|
251
|
+
*/
|
|
252
|
+
export function buildAutoDispatch(opts: AutoDispatchOptions): {
|
|
253
|
+
from: string;
|
|
254
|
+
to: string;
|
|
255
|
+
subject: string;
|
|
256
|
+
body: string;
|
|
257
|
+
} {
|
|
258
|
+
const from = opts.slingerName ?? opts.parentAgent ?? "orchestrator";
|
|
259
|
+
const specLine = opts.specPath
|
|
260
|
+
? `Spec file: ${opts.specPath}`
|
|
261
|
+
: "No spec file provided. Check your overlay for task details.";
|
|
262
|
+
const body = [
|
|
263
|
+
`You have been assigned task ${opts.taskId} as a ${opts.capability} agent.`,
|
|
264
|
+
specLine,
|
|
265
|
+
`Read your overlay at ${opts.instructionPath} and begin immediately.`,
|
|
266
|
+
].join(" ");
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
from,
|
|
270
|
+
to: opts.agentName,
|
|
271
|
+
subject: `Dispatch: ${opts.taskId}`,
|
|
272
|
+
body,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Options for building the structured startup beacon.
|
|
278
|
+
*/
|
|
279
|
+
export interface BeaconOptions {
|
|
280
|
+
agentName: string;
|
|
281
|
+
capability: string;
|
|
282
|
+
taskId: string;
|
|
283
|
+
parentAgent: string | null;
|
|
284
|
+
depth: number;
|
|
285
|
+
instructionPath: string;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Build a structured startup beacon for an agent.
|
|
290
|
+
*
|
|
291
|
+
* The beacon is the first user message sent to a Claude Code agent via
|
|
292
|
+
* tmux send-keys. It provides identity context and a numbered startup
|
|
293
|
+
* protocol so the agent knows exactly what to do on boot.
|
|
294
|
+
*
|
|
295
|
+
* Format:
|
|
296
|
+
* [AGENTPLATE] <agent-name> (<capability>) <ISO timestamp> task:<task-id>
|
|
297
|
+
* Depth: <n> | Parent: <parent-name|none>
|
|
298
|
+
* Startup protocol:
|
|
299
|
+
* 1. Read your assignment in .claude/CLAUDE.md
|
|
300
|
+
* 2. Load expertise: loam prime
|
|
301
|
+
* 3. Check mail: ap mail check --agent <name>
|
|
302
|
+
* 4. Begin working on task <task-id>
|
|
303
|
+
*/
|
|
304
|
+
export function buildBeacon(opts: BeaconOptions): string {
|
|
305
|
+
const timestamp = new Date().toISOString();
|
|
306
|
+
const parent = opts.parentAgent ?? "none";
|
|
307
|
+
const parts = [
|
|
308
|
+
`[AGENTPLATE] ${opts.agentName} (${opts.capability}) ${timestamp} task:${opts.taskId}`,
|
|
309
|
+
`Depth: ${opts.depth} | Parent: ${parent}`,
|
|
310
|
+
`Startup: read ${opts.instructionPath}, run loam prime, check mail (ap mail check --agent ${opts.agentName}), then begin task ${opts.taskId}`,
|
|
311
|
+
];
|
|
312
|
+
return parts.join(" — ");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Check if a parent agent has spawned any scouts.
|
|
317
|
+
* Returns true if the parent has at least one scout child in the session history.
|
|
318
|
+
*/
|
|
319
|
+
export function parentHasScouts(
|
|
320
|
+
sessions: ReadonlyArray<{ parentAgent: string | null; capability: string }>,
|
|
321
|
+
parentAgent: string,
|
|
322
|
+
): boolean {
|
|
323
|
+
return sessions.some((s) => s.parentAgent === parentAgent && s.capability === "scout");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Determine whether to emit the scout-before-build warning.
|
|
328
|
+
*
|
|
329
|
+
* Returns true when all of the following hold:
|
|
330
|
+
* - The incoming capability is "builder" (only builders trigger the check)
|
|
331
|
+
* - A parent agent is set (orphaned builders don't trigger it)
|
|
332
|
+
* - The parent has not yet spawned any scouts
|
|
333
|
+
* - noScoutCheck is false (caller has not suppressed the warning)
|
|
334
|
+
* - skipScout is false (the lead is not intentionally running without scouts)
|
|
335
|
+
*
|
|
336
|
+
* Extracted from slingCommand for testability (agentplate-6eyw).
|
|
337
|
+
*
|
|
338
|
+
* @param capability - The requested agent capability
|
|
339
|
+
* @param parentAgent - The --parent flag value (null = coordinator/human)
|
|
340
|
+
* @param sessions - All sessions (not just active) for parentHasScouts query
|
|
341
|
+
* @param noScoutCheck - True when --no-scout-check flag is set
|
|
342
|
+
* @param skipScout - True when --skip-scout flag is set (lead opted out of scouting)
|
|
343
|
+
*/
|
|
344
|
+
export function shouldShowScoutWarning(
|
|
345
|
+
capability: string,
|
|
346
|
+
parentAgent: string | null,
|
|
347
|
+
sessions: ReadonlyArray<{ parentAgent: string | null; capability: string }>,
|
|
348
|
+
noScoutCheck: boolean,
|
|
349
|
+
skipScout: boolean,
|
|
350
|
+
): boolean {
|
|
351
|
+
if (capability !== "builder") return false;
|
|
352
|
+
if (parentAgent === null) return false;
|
|
353
|
+
if (noScoutCheck) return false;
|
|
354
|
+
if (skipScout) return false;
|
|
355
|
+
return !parentHasScouts(sessions, parentAgent);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Resolve which canonical repo directories should be writable to an
|
|
360
|
+
* interactive agent runtime in addition to its worktree sandbox.
|
|
361
|
+
*
|
|
362
|
+
* All interactive agents need `.agentplate` so they can access shared mail,
|
|
363
|
+
* metrics, and session state. Only `lead` agents need canonical `.git`
|
|
364
|
+
* because they can spawn child worktrees from inside the runtime.
|
|
365
|
+
*
|
|
366
|
+
* @param projectRoot - Absolute path to the canonical repository root
|
|
367
|
+
* @param capability - Capability being launched
|
|
368
|
+
*/
|
|
369
|
+
export function getSharedWritableDirs(projectRoot: string, capability: string): string[] {
|
|
370
|
+
const sharedWritableDirs = [join(projectRoot, ".agentplate")];
|
|
371
|
+
|
|
372
|
+
if (capability === "lead") {
|
|
373
|
+
sharedWritableDirs.push(join(projectRoot, ".git"));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return sharedWritableDirs;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Check if any active agent is already working on the given task ID.
|
|
381
|
+
* Returns the agent name if locked, or null if the task is free.
|
|
382
|
+
*
|
|
383
|
+
* @param activeSessions - Currently active (non-zombie) sessions
|
|
384
|
+
* @param taskId - The task ID to check for concurrent work
|
|
385
|
+
*/
|
|
386
|
+
export function checkTaskLock(
|
|
387
|
+
activeSessions: ReadonlyArray<{ agentName: string; taskId: string }>,
|
|
388
|
+
taskId: string,
|
|
389
|
+
): string | null {
|
|
390
|
+
const existing = activeSessions.find((s) => s.taskId === taskId);
|
|
391
|
+
return existing?.agentName ?? null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Check if an active lead agent is already assigned to the given task ID.
|
|
396
|
+
* Returns the lead agent name if found, or null if no active lead exists.
|
|
397
|
+
*
|
|
398
|
+
* This prevents the duplicate-lead anti-pattern where two leads run
|
|
399
|
+
* simultaneously on the same bead, causing duplicate work streams and
|
|
400
|
+
* wasted tokens (agentplate-gktc postmortem).
|
|
401
|
+
*
|
|
402
|
+
* Only checks sessions with capability "lead". Builder/scout children
|
|
403
|
+
* working the same bead (via parent delegation) do not trigger this check.
|
|
404
|
+
*
|
|
405
|
+
* @param activeSessions - Currently active (non-zombie, non-completed) sessions
|
|
406
|
+
* @param taskId - The task ID to check for an existing lead
|
|
407
|
+
*/
|
|
408
|
+
export function checkDuplicateLead(
|
|
409
|
+
activeSessions: ReadonlyArray<{ agentName: string; taskId: string; capability: string }>,
|
|
410
|
+
taskId: string,
|
|
411
|
+
): string | null {
|
|
412
|
+
const existing = activeSessions.find((s) => s.taskId === taskId && s.capability === "lead");
|
|
413
|
+
return existing?.agentName ?? null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Check if spawning another agent would exceed the per-run session limit.
|
|
418
|
+
* Returns true if the limit is reached. A limit of 0 means unlimited.
|
|
419
|
+
*
|
|
420
|
+
* @param maxSessionsPerRun - Config limit (0 = unlimited)
|
|
421
|
+
* @param currentRunAgentCount - Number of agents already spawned in this run
|
|
422
|
+
*/
|
|
423
|
+
export function checkRunSessionLimit(
|
|
424
|
+
maxSessionsPerRun: number,
|
|
425
|
+
currentRunAgentCount: number,
|
|
426
|
+
): boolean {
|
|
427
|
+
if (maxSessionsPerRun <= 0) return false;
|
|
428
|
+
return currentRunAgentCount >= maxSessionsPerRun;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Check if a parent agent has reached its per-lead child ceiling.
|
|
433
|
+
* Returns true if the limit is reached. A limit of 0 means unlimited.
|
|
434
|
+
*
|
|
435
|
+
* @param activeSessions - Currently active (non-zombie) sessions
|
|
436
|
+
* @param parentAgent - The parent agent name to count children for
|
|
437
|
+
* @param maxAgentsPerLead - Config or CLI limit (0 = unlimited)
|
|
438
|
+
*/
|
|
439
|
+
export function checkParentAgentLimit(
|
|
440
|
+
activeSessions: ReadonlyArray<{ parentAgent: string | null }>,
|
|
441
|
+
parentAgent: string,
|
|
442
|
+
maxAgentsPerLead: number,
|
|
443
|
+
): boolean {
|
|
444
|
+
if (maxAgentsPerLead <= 0) return false;
|
|
445
|
+
const count = activeSessions.filter((s) => s.parentAgent === parentAgent).length;
|
|
446
|
+
return count >= maxAgentsPerLead;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Validate hierarchy constraints for direct coordinator/human spawns.
|
|
451
|
+
*
|
|
452
|
+
* When parentAgent is null, the caller is the coordinator or a human.
|
|
453
|
+
* Direct spawns are allowed for "lead", "scout", and "builder".
|
|
454
|
+
* Other capabilities (reviewer, merger, etc.) must be spawned by a lead
|
|
455
|
+
* that passes --parent.
|
|
456
|
+
*
|
|
457
|
+
* @param parentAgent - The --parent flag value (null = coordinator/human)
|
|
458
|
+
* @param capability - The requested agent capability
|
|
459
|
+
* @param name - The agent name (for error context)
|
|
460
|
+
* @param depth - The requested hierarchy depth
|
|
461
|
+
* @param forceHierarchy - If true, bypass the check (for debugging)
|
|
462
|
+
* @throws HierarchyError if the constraint is violated
|
|
463
|
+
*/
|
|
464
|
+
export function validateHierarchy(
|
|
465
|
+
parentAgent: string | null,
|
|
466
|
+
capability: string,
|
|
467
|
+
name: string,
|
|
468
|
+
_depth: number,
|
|
469
|
+
forceHierarchy: boolean,
|
|
470
|
+
): void {
|
|
471
|
+
if (forceHierarchy) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const directSpawnCapabilities = ["lead", "scout", "builder"];
|
|
476
|
+
if (parentAgent === null && !directSpawnCapabilities.includes(capability)) {
|
|
477
|
+
throw new HierarchyError(
|
|
478
|
+
`Coordinator cannot spawn "${capability}" directly. Only lead, scout, and builder are allowed without --parent. Use a lead as intermediary, or pass --force-hierarchy to bypass.`,
|
|
479
|
+
{ agentName: name, requestedCapability: capability },
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Extract loam record IDs and their domains from loam prime output text.
|
|
486
|
+
* Parses the markdown structure produced by lm prime: domain headings
|
|
487
|
+
* (## <name>) followed by record lines containing (mx-XXXXXX) identifiers.
|
|
488
|
+
* @param primeText - The output text from lm prime
|
|
489
|
+
* @returns Array of {id, domain} pairs. Deduplicated.
|
|
490
|
+
*/
|
|
491
|
+
export function extractLoamRecordIds(primeText: string): Array<{ id: string; domain: string }> {
|
|
492
|
+
const results: Array<{ id: string; domain: string }> = [];
|
|
493
|
+
const seen = new Set<string>();
|
|
494
|
+
let currentDomain = "";
|
|
495
|
+
|
|
496
|
+
for (const line of primeText.split("\n")) {
|
|
497
|
+
const domainMatch = line.match(/^## ([\w-]+)/);
|
|
498
|
+
if (domainMatch) {
|
|
499
|
+
currentDomain = domainMatch[1] ?? "";
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (currentDomain) {
|
|
503
|
+
const idRegex = /\(mx-([a-f0-9]+)\)/g;
|
|
504
|
+
let match = idRegex.exec(line);
|
|
505
|
+
while (match !== null) {
|
|
506
|
+
const shortId = match[1] ?? "";
|
|
507
|
+
if (shortId) {
|
|
508
|
+
const key = `${currentDomain}:mx-${shortId}`;
|
|
509
|
+
if (!seen.has(key)) {
|
|
510
|
+
seen.add(key);
|
|
511
|
+
results.push({ id: `mx-${shortId}`, domain: currentDomain });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
match = idRegex.exec(line);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return results;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Get the current git branch name for the repo at the given path.
|
|
523
|
+
*
|
|
524
|
+
* Returns null if in detached HEAD state, the directory is not a git repo,
|
|
525
|
+
* or git exits non-zero.
|
|
526
|
+
*
|
|
527
|
+
* @param repoRoot - Absolute path to the git repository root
|
|
528
|
+
*/
|
|
529
|
+
export async function getCurrentBranch(repoRoot: string): Promise<string | null> {
|
|
530
|
+
const proc = Bun.spawn(["git", "rev-parse", "--abbrev-ref", "HEAD"], {
|
|
531
|
+
cwd: repoRoot,
|
|
532
|
+
stdout: "pipe",
|
|
533
|
+
stderr: "pipe",
|
|
534
|
+
});
|
|
535
|
+
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
536
|
+
if (exitCode !== 0) return null;
|
|
537
|
+
const branch = stdout.trim();
|
|
538
|
+
// "HEAD" is returned when in detached HEAD state
|
|
539
|
+
if (branch === "HEAD" || branch === "") return null;
|
|
540
|
+
return branch;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Resolve whether to use the headless spawn path for a given runtime + flags + config.
|
|
545
|
+
*
|
|
546
|
+
* Precedence (highest first):
|
|
547
|
+
* 1. runtime.headless === true (statically headless runtimes always use headless)
|
|
548
|
+
* 2. Explicit --headless / --no-headless flag (boolean | undefined from commander)
|
|
549
|
+
* 3. config.runtime.claudeHeadlessByDefault (only applies when runtime.id === "claude")
|
|
550
|
+
* 4. Default: false (tmux)
|
|
551
|
+
*
|
|
552
|
+
* Throws ValidationError when --headless is explicitly true but the runtime has no
|
|
553
|
+
* buildDirectSpawn implementation.
|
|
554
|
+
*/
|
|
555
|
+
export function resolveUseHeadless(
|
|
556
|
+
runtime: { id: string; headless?: boolean; buildDirectSpawn?: unknown },
|
|
557
|
+
flag: boolean | undefined,
|
|
558
|
+
config: AgentplateConfig,
|
|
559
|
+
): boolean {
|
|
560
|
+
if (runtime.headless === true) return true;
|
|
561
|
+
|
|
562
|
+
if (flag === true) {
|
|
563
|
+
if (typeof runtime.buildDirectSpawn !== "function") {
|
|
564
|
+
throw new ValidationError(
|
|
565
|
+
`--headless requires a runtime with headless support. Runtime "${runtime.id}" does not implement buildDirectSpawn.`,
|
|
566
|
+
{ field: "headless", value: true },
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
if (flag === false) return false;
|
|
572
|
+
|
|
573
|
+
if (runtime.id === "claude" && config.runtime?.claudeHeadlessByDefault === true) {
|
|
574
|
+
if (typeof runtime.buildDirectSpawn !== "function") return false;
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Entry point for `ap sling <task-id> [flags]`.
|
|
583
|
+
*
|
|
584
|
+
* @param taskId - The task ID to assign to the agent
|
|
585
|
+
* @param opts - Command options
|
|
586
|
+
*/
|
|
587
|
+
export async function slingCommand(taskId: string, opts: SlingOptions): Promise<void> {
|
|
588
|
+
if (!taskId) {
|
|
589
|
+
throw new ValidationError("Task ID is required: ap sling <task-id>", {
|
|
590
|
+
field: "taskId",
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const capability = opts.capability ?? "builder";
|
|
595
|
+
const rawName = opts.name?.trim() ?? "";
|
|
596
|
+
const nameWasAutoGenerated = rawName.length === 0;
|
|
597
|
+
let name = nameWasAutoGenerated ? `${capability}-${taskId}` : rawName;
|
|
598
|
+
const specPath = opts.spec ?? null;
|
|
599
|
+
const filesRaw = opts.files;
|
|
600
|
+
// Reassigned later when re-spawning an existing agent to preserve the prior
|
|
601
|
+
// row's parentAgent — see agentplate-de3c at the existingSession lookup below.
|
|
602
|
+
let parentAgent = opts.parent ?? null;
|
|
603
|
+
const depthStr = opts.depth;
|
|
604
|
+
const depth = depthStr !== undefined ? Number.parseInt(depthStr, 10) : 0;
|
|
605
|
+
const forceHierarchy = opts.forceHierarchy ?? false;
|
|
606
|
+
const skipScout = opts.skipScout ?? false;
|
|
607
|
+
const skipTaskCheck = opts.skipTaskCheck ?? false;
|
|
608
|
+
const recover = opts.recover ?? false;
|
|
609
|
+
|
|
610
|
+
if (Number.isNaN(depth) || depth < 0) {
|
|
611
|
+
throw new ValidationError("--depth must be a non-negative integer", {
|
|
612
|
+
field: "depth",
|
|
613
|
+
value: depthStr,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (isRunningAsRoot()) {
|
|
618
|
+
throw new AgentError(
|
|
619
|
+
"Cannot spawn agents as root (UID 0). The claude CLI rejects --permission-mode bypassPermissions when run as root, causing the tmux session to die immediately. Run agentplate as a non-root user.",
|
|
620
|
+
{ agentName: name },
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (opts.maxAgents !== undefined) {
|
|
625
|
+
const parsed = Number.parseInt(opts.maxAgents, 10);
|
|
626
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
627
|
+
throw new ValidationError("--max-agents must be a non-negative integer", {
|
|
628
|
+
field: "maxAgents",
|
|
629
|
+
value: opts.maxAgents,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (opts.dispatchMaxAgents !== undefined) {
|
|
635
|
+
const parsed = Number.parseInt(opts.dispatchMaxAgents, 10);
|
|
636
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
637
|
+
throw new ValidationError("--dispatch-max-agents must be a non-negative integer", {
|
|
638
|
+
field: "dispatchMaxAgents",
|
|
639
|
+
value: opts.dispatchMaxAgents,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Warn if --skip-scout is used for a non-lead capability (harmless but confusing)
|
|
645
|
+
if (skipScout && capability !== "lead") {
|
|
646
|
+
process.stderr.write(
|
|
647
|
+
`Warning: --skip-scout is only meaningful for leads. Ignoring for "${capability}" agent "${name}".\n`,
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (skipTaskCheck && !parentAgent) {
|
|
652
|
+
process.stderr.write(
|
|
653
|
+
`Warning: --skip-task-check without --parent is unusual. This flag is designed for leads spawning builders with worktree-created issues.\n`,
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Validate that spec file exists if provided, and resolve to absolute path
|
|
658
|
+
// so agents in worktrees can access it (worktrees don't have .agentplate/)
|
|
659
|
+
let absoluteSpecPath: string | null = null;
|
|
660
|
+
if (specPath !== null) {
|
|
661
|
+
absoluteSpecPath = resolve(specPath);
|
|
662
|
+
const specFile = Bun.file(absoluteSpecPath);
|
|
663
|
+
const specExists = await specFile.exists();
|
|
664
|
+
if (!specExists) {
|
|
665
|
+
throw new ValidationError(`Spec file not found: ${specPath}`, {
|
|
666
|
+
field: "spec",
|
|
667
|
+
value: specPath,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const fileScope = filesRaw
|
|
673
|
+
? filesRaw
|
|
674
|
+
.split(",")
|
|
675
|
+
.map((f) => f.trim())
|
|
676
|
+
.filter((f) => f.length > 0)
|
|
677
|
+
: [];
|
|
678
|
+
|
|
679
|
+
const siblings = parseSiblings(opts.siblings);
|
|
680
|
+
|
|
681
|
+
// 1. Load config
|
|
682
|
+
const cwd = process.cwd();
|
|
683
|
+
const config = await loadConfig(cwd);
|
|
684
|
+
const resolvedBackend = await resolveBackend(config.taskTracker.backend, config.project.root);
|
|
685
|
+
|
|
686
|
+
// 2. Validate depth limit
|
|
687
|
+
// Hierarchy: orchestrator(0) -> lead(1) -> specialist(2)
|
|
688
|
+
// With maxDepth=2, depth=2 is the deepest allowed leaf, so reject only depth > maxDepth
|
|
689
|
+
if (depth > config.agents.maxDepth) {
|
|
690
|
+
throw new AgentError(
|
|
691
|
+
`Depth limit exceeded: depth ${depth} > maxDepth ${config.agents.maxDepth}`,
|
|
692
|
+
{ agentName: name },
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// 2b. Validate hierarchy: coordinator (no --parent) can only spawn leads
|
|
697
|
+
validateHierarchy(parentAgent, capability, name, depth, forceHierarchy);
|
|
698
|
+
|
|
699
|
+
// 3. Load manifest and validate capability
|
|
700
|
+
const manifestLoader = createManifestLoader(
|
|
701
|
+
join(config.project.root, config.agents.manifestPath),
|
|
702
|
+
join(config.project.root, config.agents.baseDir),
|
|
703
|
+
);
|
|
704
|
+
const manifest = await manifestLoader.load();
|
|
705
|
+
|
|
706
|
+
const agentDef = manifest.agents[capability];
|
|
707
|
+
if (!agentDef) {
|
|
708
|
+
throw new AgentError(
|
|
709
|
+
`Unknown capability "${capability}". Available: ${Object.keys(manifest.agents).join(", ")}`,
|
|
710
|
+
{ agentName: name, capability },
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// 4. Resolve or create run_id for this spawn
|
|
715
|
+
const agentplateDir = join(config.project.root, ".agentplate");
|
|
716
|
+
const currentRunPath = join(agentplateDir, "current-run.txt");
|
|
717
|
+
|
|
718
|
+
// 5. Check name uniqueness and concurrency limit against active sessions
|
|
719
|
+
// (Session store opened here so we can also use it for parent run ID inheritance in step 4.)
|
|
720
|
+
const { store } = openSessionStore(agentplateDir);
|
|
721
|
+
try {
|
|
722
|
+
// 4a. Resolve run ID: inherit from parent → current-run.txt fallback → create new.
|
|
723
|
+
// Parent inheritance ensures child agents belong to the same run as their coordinator.
|
|
724
|
+
const runId = await (async (): Promise<string> => {
|
|
725
|
+
if (parentAgent) {
|
|
726
|
+
const parentSession = store.getByName(parentAgent);
|
|
727
|
+
if (parentSession?.runId) {
|
|
728
|
+
return parentSession.runId;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Fallback: read current-run.txt (backward compat with single-coordinator setups).
|
|
733
|
+
const currentRunFile = Bun.file(currentRunPath);
|
|
734
|
+
if (await currentRunFile.exists()) {
|
|
735
|
+
const text = (await currentRunFile.text()).trim();
|
|
736
|
+
if (text) return text;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Create a new run if none exists.
|
|
740
|
+
const newRunId = `run-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
741
|
+
const runStore = createRunStore(join(agentplateDir, "sessions.db"));
|
|
742
|
+
try {
|
|
743
|
+
runStore.createRun({
|
|
744
|
+
id: newRunId,
|
|
745
|
+
startedAt: new Date().toISOString(),
|
|
746
|
+
coordinatorSessionId: null,
|
|
747
|
+
coordinatorName: null,
|
|
748
|
+
status: "active",
|
|
749
|
+
});
|
|
750
|
+
} finally {
|
|
751
|
+
runStore.close();
|
|
752
|
+
}
|
|
753
|
+
await Bun.write(currentRunPath, newRunId);
|
|
754
|
+
return newRunId;
|
|
755
|
+
})();
|
|
756
|
+
|
|
757
|
+
// 4b. Check per-run session limit
|
|
758
|
+
if (config.agents.maxSessionsPerRun > 0) {
|
|
759
|
+
const runCheckStore = createRunStore(join(agentplateDir, "sessions.db"));
|
|
760
|
+
try {
|
|
761
|
+
const run = runCheckStore.getRun(runId);
|
|
762
|
+
if (run && checkRunSessionLimit(config.agents.maxSessionsPerRun, run.agentCount)) {
|
|
763
|
+
throw new AgentError(
|
|
764
|
+
`Run session limit reached: ${run.agentCount}/${config.agents.maxSessionsPerRun} agents spawned in run "${runId}". ` +
|
|
765
|
+
`Increase agents.maxSessionsPerRun in config.yaml or start a new run.`,
|
|
766
|
+
{ agentName: name },
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
} finally {
|
|
770
|
+
runCheckStore.close();
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const activeSessions = store.getActive();
|
|
775
|
+
if (activeSessions.length >= config.agents.maxConcurrent) {
|
|
776
|
+
throw new AgentError(
|
|
777
|
+
`Max concurrent agent limit reached: ${activeSessions.length}/${config.agents.maxConcurrent} active agents`,
|
|
778
|
+
{ agentName: name },
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Track the prior session row when re-spawning against an existing agent
|
|
783
|
+
// name so downstream code can preserve linkage (parentAgent, claudeSessionId)
|
|
784
|
+
// that the upsert would otherwise erase. Auto-generated names are unique
|
|
785
|
+
// so there is never a prior row to preserve.
|
|
786
|
+
let existingSession: AgentSession | null = null;
|
|
787
|
+
if (nameWasAutoGenerated) {
|
|
788
|
+
const takenNames = activeSessions.map((s) => s.agentName);
|
|
789
|
+
name = generateAgentName(capability, taskId, takenNames);
|
|
790
|
+
} else {
|
|
791
|
+
existingSession = store.getByName(name);
|
|
792
|
+
if (
|
|
793
|
+
existingSession &&
|
|
794
|
+
existingSession.state !== "zombie" &&
|
|
795
|
+
existingSession.state !== "completed"
|
|
796
|
+
) {
|
|
797
|
+
throw new AgentError(
|
|
798
|
+
`Agent name "${name}" is already in use (state: ${existingSession.state})`,
|
|
799
|
+
{
|
|
800
|
+
agentName: name,
|
|
801
|
+
},
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Preserve the prior session's parentAgent on re-spawn when --parent was
|
|
807
|
+
// not explicitly passed (agentplate-de3c). See `resolveParentAgent` for the
|
|
808
|
+
// full rationale and resolution rules.
|
|
809
|
+
parentAgent = resolveParentAgent(opts.parent, existingSession);
|
|
810
|
+
|
|
811
|
+
// 5d. Task-level locking: prevent concurrent agents on the same task ID.
|
|
812
|
+
// Exception: the parent agent may delegate its own task to a child.
|
|
813
|
+
const lockHolder = checkTaskLock(activeSessions, taskId);
|
|
814
|
+
if (lockHolder !== null && lockHolder !== parentAgent) {
|
|
815
|
+
throw new AgentError(
|
|
816
|
+
`Task "${taskId}" is already being worked by agent "${lockHolder}". ` +
|
|
817
|
+
`Concurrent work on the same task causes duplicate issues and wasted tokens.`,
|
|
818
|
+
{ agentName: name },
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// 5b. Enforce stagger delay between agent spawns
|
|
823
|
+
const staggerMs = calculateStaggerDelay(config.agents.staggerDelayMs, activeSessions);
|
|
824
|
+
if (staggerMs > 0) {
|
|
825
|
+
await Bun.sleep(staggerMs);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// 5e. Enforce per-lead agent ceiling when spawning under a parent
|
|
829
|
+
if (parentAgent !== null) {
|
|
830
|
+
const maxPerLead =
|
|
831
|
+
opts.maxAgents !== undefined
|
|
832
|
+
? Number.parseInt(opts.maxAgents, 10)
|
|
833
|
+
: config.agents.maxAgentsPerLead;
|
|
834
|
+
if (checkParentAgentLimit(activeSessions, parentAgent, maxPerLead)) {
|
|
835
|
+
const currentCount = activeSessions.filter((s) => s.parentAgent === parentAgent).length;
|
|
836
|
+
throw new AgentError(
|
|
837
|
+
`Per-lead agent limit reached: "${parentAgent}" has ${currentCount}/${maxPerLead} active children. ` +
|
|
838
|
+
`Increase agents.maxAgentsPerLead in config.yaml or pass --max-agents <n>.`,
|
|
839
|
+
{ agentName: name },
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// 5c. Structural enforcement: warn when a lead spawns a builder without prior scouts.
|
|
845
|
+
// This is a non-blocking warning — it does not prevent the spawn, but surfaces
|
|
846
|
+
// the scout-skip pattern so agents and operators can see it happening.
|
|
847
|
+
// Use --no-scout-check to suppress this warning when intentionally skipping scouts.
|
|
848
|
+
if (
|
|
849
|
+
shouldShowScoutWarning(
|
|
850
|
+
capability,
|
|
851
|
+
parentAgent,
|
|
852
|
+
store.getAll(),
|
|
853
|
+
opts.noScoutCheck ?? false,
|
|
854
|
+
skipScout,
|
|
855
|
+
)
|
|
856
|
+
) {
|
|
857
|
+
process.stderr.write(
|
|
858
|
+
`Warning: "${parentAgent}" is spawning builder "${name}" without having spawned any scouts.\n`,
|
|
859
|
+
);
|
|
860
|
+
process.stderr.write(
|
|
861
|
+
" Leads should spawn scouts in Phase 1 before building. See agents/lead.md.\n",
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// 6. Validate task exists and is in a workable state (if tracker enabled)
|
|
866
|
+
const tracker = createTrackerClient(resolvedBackend, config.project.root);
|
|
867
|
+
if (config.taskTracker.enabled && !skipTaskCheck) {
|
|
868
|
+
let issue: TrackerIssue;
|
|
869
|
+
try {
|
|
870
|
+
issue = await tracker.show(taskId);
|
|
871
|
+
} catch (err) {
|
|
872
|
+
throw new AgentError(`Task "${taskId}" not found or inaccessible`, {
|
|
873
|
+
agentName: name,
|
|
874
|
+
cause: err instanceof Error ? err : undefined,
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (!isTaskWorkable(issue.status, recover)) {
|
|
879
|
+
throw new ValidationError(
|
|
880
|
+
`Task "${taskId}" is not workable (status: ${issue.status}). Only open or in_progress issues can be assigned. Pass --recover to re-dispatch against a closed task.`,
|
|
881
|
+
{ field: "taskId", value: taskId },
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
if (recover && !(WORKABLE_STATUSES as readonly string[]).includes(issue.status)) {
|
|
885
|
+
process.stderr.write(
|
|
886
|
+
`Warning: --recover dispatching against task "${taskId}" with status "${issue.status}". Previous owner may have exited unexpectedly.\n`,
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// 7. Create worktree
|
|
892
|
+
const worktreeBaseDir = join(config.project.root, config.worktrees.baseDir);
|
|
893
|
+
await mkdir(worktreeBaseDir, { recursive: true });
|
|
894
|
+
|
|
895
|
+
// Resolve base branch: --base-branch flag > current HEAD > config.project.canonicalBranch
|
|
896
|
+
const baseBranch =
|
|
897
|
+
opts.baseBranch ??
|
|
898
|
+
(await getCurrentBranch(config.project.root)) ??
|
|
899
|
+
config.project.canonicalBranch;
|
|
900
|
+
|
|
901
|
+
const { path: worktreePath, branch: branchName } = await createWorktree({
|
|
902
|
+
repoRoot: config.project.root,
|
|
903
|
+
baseDir: worktreeBaseDir,
|
|
904
|
+
agentName: name,
|
|
905
|
+
baseBranch,
|
|
906
|
+
taskId: taskId,
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
try {
|
|
910
|
+
// 8. Generate + write overlay CLAUDE.md
|
|
911
|
+
const agentDefPath = join(config.project.root, config.agents.baseDir, agentDef.file);
|
|
912
|
+
const baseDefinition = await Bun.file(agentDefPath).text();
|
|
913
|
+
|
|
914
|
+
// 8a. Fetch file-scoped loam expertise if loam is enabled and files are provided
|
|
915
|
+
let loamExpertise: string | undefined;
|
|
916
|
+
if (config.loam.enabled && fileScope.length > 0) {
|
|
917
|
+
try {
|
|
918
|
+
const loam = createLoamClient(config.project.root);
|
|
919
|
+
loamExpertise = await loam.prime(undefined, undefined, {
|
|
920
|
+
files: fileScope,
|
|
921
|
+
sortByScore: true,
|
|
922
|
+
});
|
|
923
|
+
} catch {
|
|
924
|
+
// Non-fatal: loam expertise is supplementary context
|
|
925
|
+
loamExpertise = undefined;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// 8b. Resolve trellis profile if specified
|
|
930
|
+
const profileName =
|
|
931
|
+
opts.profile ?? process.env.AGENTPLATE_PROFILE ?? config.project.defaultProfile;
|
|
932
|
+
let profileContent: string | undefined;
|
|
933
|
+
if (profileName) {
|
|
934
|
+
try {
|
|
935
|
+
const trellis = createTrellisClient(config.project.root);
|
|
936
|
+
const rendered = await trellis.render(profileName);
|
|
937
|
+
if (rendered.success && rendered.sections.length > 0) {
|
|
938
|
+
profileContent = rendered.sections.map((s) => s.body).join("\n\n");
|
|
939
|
+
}
|
|
940
|
+
} catch {
|
|
941
|
+
// Non-fatal: trellis may not be installed or profile may not exist
|
|
942
|
+
profileContent = undefined;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Resolve runtime before overlayConfig so we can pass runtime.instructionPath
|
|
947
|
+
const runtime = getRuntime(opts.runtime, config, capability);
|
|
948
|
+
|
|
949
|
+
// Runtime-specific worktree preparation (e.g., Copilot folder trust)
|
|
950
|
+
if (runtime.prepareWorktree) {
|
|
951
|
+
await runtime.prepareWorktree(worktreePath);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const overlayConfig: OverlayConfig = {
|
|
955
|
+
agentName: name,
|
|
956
|
+
taskId: taskId,
|
|
957
|
+
specPath: absoluteSpecPath,
|
|
958
|
+
branchName,
|
|
959
|
+
worktreePath,
|
|
960
|
+
fileScope,
|
|
961
|
+
loamDomains: config.loam.enabled
|
|
962
|
+
? inferDomainsFromFiles(fileScope, config.loam.domains)
|
|
963
|
+
: [],
|
|
964
|
+
parentAgent: parentAgent,
|
|
965
|
+
depth,
|
|
966
|
+
canSpawn: agentDef.canSpawn,
|
|
967
|
+
capability,
|
|
968
|
+
baseDefinition,
|
|
969
|
+
profileContent,
|
|
970
|
+
loamExpertise,
|
|
971
|
+
skipScout: skipScout && capability === "lead",
|
|
972
|
+
skipReview: opts.skipReview === true && capability === "lead",
|
|
973
|
+
maxAgentsOverride:
|
|
974
|
+
opts.dispatchMaxAgents !== undefined
|
|
975
|
+
? Number.parseInt(opts.dispatchMaxAgents, 10)
|
|
976
|
+
: undefined,
|
|
977
|
+
qualityGates: config.project.qualityGates,
|
|
978
|
+
trackerCli: trackerCliName(resolvedBackend),
|
|
979
|
+
trackerName: resolvedBackend,
|
|
980
|
+
instructionPath: runtime.instructionPath,
|
|
981
|
+
siblings,
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
await writeOverlay(worktreePath, overlayConfig, config.project.root, runtime.instructionPath);
|
|
985
|
+
|
|
986
|
+
// 9. Resolve runtime + model (needed for deployConfig, spawn, and beacon)
|
|
987
|
+
const resolvedModel = resolveModel(config, manifest, capability, agentDef.model);
|
|
988
|
+
|
|
989
|
+
// 9a. Resolve headless mode before deployConfig so hooks can be skipped for headless agents.
|
|
990
|
+
// resolveUseHeadless is also used at 11c for spawn routing — hoisted here to share the value.
|
|
991
|
+
const useHeadless = resolveUseHeadless(runtime, opts.headless, config);
|
|
992
|
+
|
|
993
|
+
// 9b. Deploy hooks config (capability-specific guards). In headless mode we deploy
|
|
994
|
+
// a PreToolUse-only subset (security guards) — agentplate-e24b. Headless Claude Code
|
|
995
|
+
// dispatches settings.local.json hooks, so dropping them would leave destructive
|
|
996
|
+
// commands unblocked.
|
|
997
|
+
await runtime.deployConfig(worktreePath, undefined, {
|
|
998
|
+
agentName: name,
|
|
999
|
+
capability,
|
|
1000
|
+
worktreePath,
|
|
1001
|
+
qualityGates: config.project.qualityGates,
|
|
1002
|
+
isHeadless: useHeadless,
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
// 9b. Send auto-dispatch mail so it exists when SessionStart hook fires.
|
|
1006
|
+
// This eliminates the race where coordinator sends dispatch AFTER agent boots.
|
|
1007
|
+
const slingerName = process.env.AGENTPLATE_AGENT_NAME?.trim() || null;
|
|
1008
|
+
const dispatch = buildAutoDispatch({
|
|
1009
|
+
agentName: name,
|
|
1010
|
+
taskId,
|
|
1011
|
+
capability,
|
|
1012
|
+
specPath: absoluteSpecPath,
|
|
1013
|
+
parentAgent,
|
|
1014
|
+
slingerName,
|
|
1015
|
+
instructionPath: runtime.instructionPath,
|
|
1016
|
+
});
|
|
1017
|
+
const mailStore = createMailStore(join(agentplateDir, "mail.db"));
|
|
1018
|
+
try {
|
|
1019
|
+
const mailClient = createMailClient(mailStore);
|
|
1020
|
+
mailClient.send({
|
|
1021
|
+
from: dispatch.from,
|
|
1022
|
+
to: dispatch.to,
|
|
1023
|
+
subject: dispatch.subject,
|
|
1024
|
+
body: dispatch.body,
|
|
1025
|
+
type: "dispatch",
|
|
1026
|
+
priority: "normal",
|
|
1027
|
+
});
|
|
1028
|
+
} finally {
|
|
1029
|
+
mailStore.close();
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// 10. Claim tracker issue
|
|
1033
|
+
if (config.taskTracker.enabled && !skipTaskCheck) {
|
|
1034
|
+
try {
|
|
1035
|
+
await tracker.claim(taskId);
|
|
1036
|
+
} catch {
|
|
1037
|
+
// Non-fatal: issue may already be claimed
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// 11. Create agent identity (if new)
|
|
1042
|
+
const identityBaseDir = join(config.project.root, ".agentplate", "agents");
|
|
1043
|
+
const existingIdentity = await loadIdentity(identityBaseDir, name);
|
|
1044
|
+
if (!existingIdentity) {
|
|
1045
|
+
await createIdentity(identityBaseDir, {
|
|
1046
|
+
name,
|
|
1047
|
+
capability,
|
|
1048
|
+
created: new Date().toISOString(),
|
|
1049
|
+
sessionsCompleted: 0,
|
|
1050
|
+
expertiseDomains: config.loam.enabled ? config.loam.domains : [],
|
|
1051
|
+
recentTasks: [],
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// 11b. Save applied loam record IDs for session-end outcome tracking.
|
|
1056
|
+
// Written to .agentplate/agents/{name}/applied-records.json so log.ts
|
|
1057
|
+
// can append outcomes when the session completes.
|
|
1058
|
+
if (loamExpertise) {
|
|
1059
|
+
const appliedRecords = extractLoamRecordIds(loamExpertise);
|
|
1060
|
+
if (appliedRecords.length > 0) {
|
|
1061
|
+
const appliedRecordsPath = join(identityBaseDir, name, "applied-records.json");
|
|
1062
|
+
const appliedData = { taskId, agentName: name, capability, records: appliedRecords };
|
|
1063
|
+
try {
|
|
1064
|
+
await Bun.write(appliedRecordsPath, `${JSON.stringify(appliedData, null, "\t")}\n`);
|
|
1065
|
+
} catch {
|
|
1066
|
+
// Non-fatal: outcome tracking is supplementary context
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// 11c. Spawn: headless runtimes bypass tmux entirely; tmux path is unchanged.
|
|
1072
|
+
// useHeadless was resolved at step 9a (hoisted so deployConfig can skip hooks for headless).
|
|
1073
|
+
if (useHeadless && runtime.buildDirectSpawn) {
|
|
1074
|
+
// Phase 3 spawn-per-turn: headless agents have NO long-lived process.
|
|
1075
|
+
// sling builds the initial prompt, upserts the session row in
|
|
1076
|
+
// "booting", then drives the first user turn synchronously through
|
|
1077
|
+
// `runTurn`. The runner spawns claude with `--resume` (when a prior
|
|
1078
|
+
// session id exists), writes the prompt to a real stdin pipe, drains
|
|
1079
|
+
// stream-json, captures session id, transitions state to "working"
|
|
1080
|
+
// (or "completed" if terminal mail observed), and exits. No persistent
|
|
1081
|
+
// process remains after this returns; subsequent turns are driven by
|
|
1082
|
+
// `ap serve` (mail) or `ap nudge`.
|
|
1083
|
+
// `existingSession` was captured during the name-collision check (above).
|
|
1084
|
+
// Re-using it here keeps re-spawn linkage (parentAgent + claudeSessionId)
|
|
1085
|
+
// resolved from the same row.
|
|
1086
|
+
const priorClaudeSessionId = existingSession?.claudeSessionId ?? null;
|
|
1087
|
+
|
|
1088
|
+
// Build the initial prompt (loam expertise + pending mail + beacon)
|
|
1089
|
+
// as the first user turn.
|
|
1090
|
+
const pendingMailStore = createMailStore(join(agentplateDir, "mail.db"));
|
|
1091
|
+
let initialPrompt: string;
|
|
1092
|
+
try {
|
|
1093
|
+
const pendingMailClient = createMailClient(pendingMailStore);
|
|
1094
|
+
const pendingMessages = pendingMailClient.check(name);
|
|
1095
|
+
const mailSection = formatMailSection(pendingMessages);
|
|
1096
|
+
const beacon = buildBeacon({
|
|
1097
|
+
agentName: name,
|
|
1098
|
+
capability,
|
|
1099
|
+
taskId,
|
|
1100
|
+
parentAgent,
|
|
1101
|
+
depth,
|
|
1102
|
+
instructionPath: runtime.instructionPath,
|
|
1103
|
+
});
|
|
1104
|
+
initialPrompt = buildInitialHeadlessPrompt(
|
|
1105
|
+
loamExpertise,
|
|
1106
|
+
mailSection || undefined,
|
|
1107
|
+
beacon,
|
|
1108
|
+
);
|
|
1109
|
+
} finally {
|
|
1110
|
+
pendingMailStore.close();
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// 13. Record session BEFORE runTurn so the runner reads it under its
|
|
1114
|
+
// lock. pid is null — there is no persistent process; the runner
|
|
1115
|
+
// publishes a per-turn PID via .agentplate/agents/<name>/turn.pid for
|
|
1116
|
+
// the duration of each turn. Carry priorClaudeSessionId (mx-5c5ae6).
|
|
1117
|
+
const session: AgentSession = {
|
|
1118
|
+
id: `session-${Date.now()}-${name}`,
|
|
1119
|
+
agentName: name,
|
|
1120
|
+
capability,
|
|
1121
|
+
worktreePath,
|
|
1122
|
+
branchName,
|
|
1123
|
+
taskId: taskId,
|
|
1124
|
+
tmuxSession: "",
|
|
1125
|
+
state: "booting",
|
|
1126
|
+
pid: null,
|
|
1127
|
+
parentAgent: parentAgent,
|
|
1128
|
+
depth,
|
|
1129
|
+
runId,
|
|
1130
|
+
startedAt: new Date().toISOString(),
|
|
1131
|
+
lastActivity: new Date().toISOString(),
|
|
1132
|
+
escalationLevel: 0,
|
|
1133
|
+
stalledSince: null,
|
|
1134
|
+
transcriptPath: null,
|
|
1135
|
+
...(priorClaudeSessionId !== null ? { claudeSessionId: priorClaudeSessionId } : {}),
|
|
1136
|
+
};
|
|
1137
|
+
store.upsert(session);
|
|
1138
|
+
|
|
1139
|
+
// Drive the first user turn synchronously. runTurn manages spawn,
|
|
1140
|
+
// stdin write+EOF, event drain, session_id capture, terminal-mail
|
|
1141
|
+
// detection, and state transition.
|
|
1142
|
+
const turnResult = await runTurn({
|
|
1143
|
+
agentName: name,
|
|
1144
|
+
capability,
|
|
1145
|
+
agentplateDir,
|
|
1146
|
+
worktreePath,
|
|
1147
|
+
projectRoot: config.project.root,
|
|
1148
|
+
taskId,
|
|
1149
|
+
userTurnNdjson: initialPrompt,
|
|
1150
|
+
runtime,
|
|
1151
|
+
resolvedModel,
|
|
1152
|
+
runId,
|
|
1153
|
+
mailDbPath: join(agentplateDir, "mail.db"),
|
|
1154
|
+
eventsDbPath: join(agentplateDir, "events.db"),
|
|
1155
|
+
sessionsDbPath: join(agentplateDir, "sessions.db"),
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
// 14. Output result (headless)
|
|
1159
|
+
if (opts.json ?? false) {
|
|
1160
|
+
jsonOutput("sling", {
|
|
1161
|
+
agentName: name,
|
|
1162
|
+
capability,
|
|
1163
|
+
taskId,
|
|
1164
|
+
branch: branchName,
|
|
1165
|
+
worktree: worktreePath,
|
|
1166
|
+
tmuxSession: "",
|
|
1167
|
+
pid: null,
|
|
1168
|
+
initialTurnFinalState: turnResult.finalState,
|
|
1169
|
+
claudeSessionId: turnResult.newSessionId,
|
|
1170
|
+
});
|
|
1171
|
+
} else {
|
|
1172
|
+
printSuccess("Agent launched (headless, spawn-per-turn)", name);
|
|
1173
|
+
process.stdout.write(` Task: ${taskId}\n`);
|
|
1174
|
+
process.stdout.write(` Branch: ${branchName}\n`);
|
|
1175
|
+
process.stdout.write(` Worktree: ${worktreePath}\n`);
|
|
1176
|
+
process.stdout.write(` First-turn state: ${turnResult.finalState}\n`);
|
|
1177
|
+
if (turnResult.newSessionId) {
|
|
1178
|
+
process.stdout.write(` Claude session id: ${turnResult.newSessionId}\n`);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
} else {
|
|
1182
|
+
// 11c. Preflight: verify tmux is available before attempting session creation
|
|
1183
|
+
await ensureTmuxAvailable();
|
|
1184
|
+
|
|
1185
|
+
// 12. Create tmux session running claude in interactive mode
|
|
1186
|
+
const tmuxSessionName = `agentplate-${sanitizeTmuxName(config.project.name)}-${name}`;
|
|
1187
|
+
const spawnCmd = runtime.buildSpawnCommand({
|
|
1188
|
+
model: resolvedModel.model,
|
|
1189
|
+
permissionMode: "bypass",
|
|
1190
|
+
cwd: worktreePath,
|
|
1191
|
+
sharedWritableDirs: getSharedWritableDirs(config.project.root, capability),
|
|
1192
|
+
env: {
|
|
1193
|
+
...runtime.buildEnv(resolvedModel),
|
|
1194
|
+
AGENTPLATE_AGENT_NAME: name,
|
|
1195
|
+
AGENTPLATE_WORKTREE_PATH: worktreePath,
|
|
1196
|
+
AGENTPLATE_TASK_ID: taskId,
|
|
1197
|
+
AGENTPLATE_PROJECT_ROOT: config.project.root,
|
|
1198
|
+
},
|
|
1199
|
+
});
|
|
1200
|
+
const pid = await createSession(tmuxSessionName, worktreePath, spawnCmd, {
|
|
1201
|
+
...runtime.buildEnv(resolvedModel),
|
|
1202
|
+
AGENTPLATE_AGENT_NAME: name,
|
|
1203
|
+
AGENTPLATE_WORKTREE_PATH: worktreePath,
|
|
1204
|
+
AGENTPLATE_TASK_ID: taskId,
|
|
1205
|
+
AGENTPLATE_PROJECT_ROOT: config.project.root,
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
// 13. Record session BEFORE sending the beacon so that hook-triggered
|
|
1209
|
+
// updateLastActivity() can find the entry and transition booting->working.
|
|
1210
|
+
// Without this, a race exists: hooks fire before the session is persisted,
|
|
1211
|
+
// leaving the agent stuck in "booting" (agentplate-036f).
|
|
1212
|
+
const session: AgentSession = {
|
|
1213
|
+
id: `session-${Date.now()}-${name}`,
|
|
1214
|
+
agentName: name,
|
|
1215
|
+
capability,
|
|
1216
|
+
worktreePath,
|
|
1217
|
+
branchName,
|
|
1218
|
+
taskId: taskId,
|
|
1219
|
+
tmuxSession: tmuxSessionName,
|
|
1220
|
+
state: "booting",
|
|
1221
|
+
pid,
|
|
1222
|
+
parentAgent: parentAgent,
|
|
1223
|
+
depth,
|
|
1224
|
+
runId,
|
|
1225
|
+
startedAt: new Date().toISOString(),
|
|
1226
|
+
lastActivity: new Date().toISOString(),
|
|
1227
|
+
escalationLevel: 0,
|
|
1228
|
+
stalledSince: null,
|
|
1229
|
+
transcriptPath: null,
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
store.upsert(session);
|
|
1233
|
+
|
|
1234
|
+
// 13b. Give slow shells time to finish initializing before polling for TUI readiness.
|
|
1235
|
+
const shellDelay = config.runtime?.shellInitDelayMs ?? 0;
|
|
1236
|
+
if (shellDelay > 0) {
|
|
1237
|
+
await Bun.sleep(shellDelay);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// Wait for Claude Code TUI to render before sending input.
|
|
1241
|
+
// Polling capture-pane is more reliable than a fixed sleep because
|
|
1242
|
+
// TUI init time varies by machine load and model state.
|
|
1243
|
+
const tuiReady = await waitForTuiReady(tmuxSessionName, (content) =>
|
|
1244
|
+
runtime.detectReady(content),
|
|
1245
|
+
);
|
|
1246
|
+
if (!tuiReady) {
|
|
1247
|
+
const alive = await isSessionAlive(tmuxSessionName);
|
|
1248
|
+
// Mark as zombie (not completed) so the watchdog detects this failed
|
|
1249
|
+
// startup. 'completed' is a terminal success state that the watchdog
|
|
1250
|
+
// skips entirely (agentplate-c40e).
|
|
1251
|
+
store.updateState(name, "zombie");
|
|
1252
|
+
|
|
1253
|
+
if (alive) {
|
|
1254
|
+
await killSession(tmuxSessionName);
|
|
1255
|
+
throw new AgentError(
|
|
1256
|
+
`Agent tmux session "${tmuxSessionName}" did not become ready during startup. The runtime may still be waiting on an interactive dialog or initializing too slowly.`,
|
|
1257
|
+
{ agentName: name },
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const sessionState = await checkSessionState(tmuxSessionName);
|
|
1262
|
+
const detail =
|
|
1263
|
+
sessionState === "no_server"
|
|
1264
|
+
? "The tmux server is no longer running. It may have crashed or been killed externally."
|
|
1265
|
+
: "The agent process may have crashed or exited immediately before the TUI became ready.";
|
|
1266
|
+
throw new AgentError(
|
|
1267
|
+
`Agent tmux session "${tmuxSessionName}" died during startup. ${detail}`,
|
|
1268
|
+
{ agentName: name },
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
// Buffer for the input handler to attach after initial render
|
|
1272
|
+
await Bun.sleep(1_000);
|
|
1273
|
+
|
|
1274
|
+
const beacon = buildBeacon({
|
|
1275
|
+
agentName: name,
|
|
1276
|
+
capability,
|
|
1277
|
+
taskId,
|
|
1278
|
+
parentAgent,
|
|
1279
|
+
depth,
|
|
1280
|
+
instructionPath: runtime.instructionPath,
|
|
1281
|
+
});
|
|
1282
|
+
await sendKeys(tmuxSessionName, beacon);
|
|
1283
|
+
|
|
1284
|
+
// 13c. Follow-up Enters with increasing delays to ensure submission.
|
|
1285
|
+
// Claude Code's TUI may consume early Enters during late initialization
|
|
1286
|
+
// (agentplate-yhv6). An Enter on an empty input line is harmless.
|
|
1287
|
+
for (const delay of [1_000, 2_000, 3_000, 5_000]) {
|
|
1288
|
+
await Bun.sleep(delay);
|
|
1289
|
+
await sendKeys(tmuxSessionName, "");
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// 13d. Verify beacon was received — if pane still shows the welcome
|
|
1293
|
+
// screen (detectReady returns "ready"), resend the beacon. Claude Code's TUI
|
|
1294
|
+
// sometimes consumes the Enter keystroke during late initialization, swallowing
|
|
1295
|
+
// the beacon text entirely (agentplate-3271).
|
|
1296
|
+
//
|
|
1297
|
+
// Skipped for runtimes that return false from requiresBeaconVerification().
|
|
1298
|
+
// Pi's TUI idle and processing states are indistinguishable via detectReady
|
|
1299
|
+
// (both show "pi v..." header and the token-usage status bar), so the loop
|
|
1300
|
+
// would incorrectly conclude the beacon was not received and spam duplicate
|
|
1301
|
+
// startup messages.
|
|
1302
|
+
const needsVerification =
|
|
1303
|
+
!runtime.requiresBeaconVerification || runtime.requiresBeaconVerification();
|
|
1304
|
+
if (needsVerification) {
|
|
1305
|
+
const verifyAttempts = 5;
|
|
1306
|
+
for (let v = 0; v < verifyAttempts; v++) {
|
|
1307
|
+
await Bun.sleep(2_000);
|
|
1308
|
+
const paneContent = await capturePaneContent(tmuxSessionName);
|
|
1309
|
+
if (paneContent) {
|
|
1310
|
+
const readyState = runtime.detectReady(paneContent);
|
|
1311
|
+
if (readyState.phase !== "ready") {
|
|
1312
|
+
break; // Agent is processing — beacon was received
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
// Still at welcome/idle screen — resend beacon
|
|
1316
|
+
await sendKeys(tmuxSessionName, beacon);
|
|
1317
|
+
await Bun.sleep(1_000);
|
|
1318
|
+
await sendKeys(tmuxSessionName, ""); // Follow-up Enter
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// 14. Output result
|
|
1323
|
+
const output = {
|
|
1324
|
+
agentName: name,
|
|
1325
|
+
capability,
|
|
1326
|
+
taskId,
|
|
1327
|
+
branch: branchName,
|
|
1328
|
+
worktree: worktreePath,
|
|
1329
|
+
tmuxSession: tmuxSessionName,
|
|
1330
|
+
pid,
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
if (opts.json ?? false) {
|
|
1334
|
+
jsonOutput("sling", output);
|
|
1335
|
+
} else {
|
|
1336
|
+
printSuccess("Agent launched", name);
|
|
1337
|
+
process.stdout.write(` Task: ${taskId}\n`);
|
|
1338
|
+
process.stdout.write(` Branch: ${branchName}\n`);
|
|
1339
|
+
process.stdout.write(` Worktree: ${worktreePath}\n`);
|
|
1340
|
+
process.stdout.write(` Tmux: ${tmuxSessionName}\n`);
|
|
1341
|
+
process.stdout.write(` PID: ${pid}\n`);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
} catch (err) {
|
|
1345
|
+
await rollbackWorktree(config.project.root, worktreePath, branchName);
|
|
1346
|
+
throw err;
|
|
1347
|
+
}
|
|
1348
|
+
} finally {
|
|
1349
|
+
store.close();
|
|
1350
|
+
}
|
|
1351
|
+
}
|