@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,721 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tmux session management for agentplate agent workers.
|
|
3
|
+
*
|
|
4
|
+
* All operations use Bun.spawn to call the tmux CLI directly.
|
|
5
|
+
* Session naming convention: `agentplate-{projectName}-{agentName}`.
|
|
6
|
+
* The project name prefix prevents cross-project tmux session collisions
|
|
7
|
+
* and enables project-scoped cleanup (agentplate-pcef).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { dirname, resolve } from "node:path";
|
|
11
|
+
import { AgentError } from "../errors.ts";
|
|
12
|
+
import type { ReadyState } from "../runtimes/types.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Dedicated tmux server socket name for agent session isolation.
|
|
16
|
+
*
|
|
17
|
+
* All agentplate agent sessions use `tmux -L agentplate` so they run on a
|
|
18
|
+
* separate server from the user's personal tmux. This prevents user tmux
|
|
19
|
+
* config (themes, plugins, keybindings) from interfering with agent spawn.
|
|
20
|
+
* See GitHub #93.
|
|
21
|
+
*/
|
|
22
|
+
export const TMUX_SOCKET = "agentplate";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Sanitize a name component for use in tmux session names.
|
|
26
|
+
*
|
|
27
|
+
* Tmux interprets dots (.) as session.window.pane separators and colons (:)
|
|
28
|
+
* as session:window separators in target strings (`-t`). If a project name
|
|
29
|
+
* contains these characters (e.g., "consulting.hgoudat.com"), the session
|
|
30
|
+
* is created fine but subsequent lookups via `-t` parse the dots as delimiters
|
|
31
|
+
* and fail to find the session. Replace both with underscores.
|
|
32
|
+
*/
|
|
33
|
+
export function sanitizeTmuxName(name: string): string {
|
|
34
|
+
return name.replace(/[.:]/g, "_");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build a tmux command array with the dedicated server socket.
|
|
39
|
+
* All agent session operations should use this to ensure isolation.
|
|
40
|
+
*/
|
|
41
|
+
function tmuxCmd(...args: string[]): string[] {
|
|
42
|
+
return ["tmux", "-L", TMUX_SOCKET, ...args];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Detect the directory containing the agentplate binary.
|
|
47
|
+
*
|
|
48
|
+
* Tries `which ap` first (the short alias), then falls back to
|
|
49
|
+
* `which agentplate` (the original name). Both are registered in
|
|
50
|
+
* package.json bin, but depending on how the tool was installed
|
|
51
|
+
* (bun link, npm link, global install), only one may be on PATH.
|
|
52
|
+
*
|
|
53
|
+
* Returns null if detection fails.
|
|
54
|
+
*/
|
|
55
|
+
async function detectAgentplateBinDir(): Promise<string | null> {
|
|
56
|
+
// Try both command names — the alias migration may leave only one resolvable
|
|
57
|
+
for (const cmdName of ["ap", "agentplate"]) {
|
|
58
|
+
try {
|
|
59
|
+
const proc = Bun.spawn(["which", cmdName], {
|
|
60
|
+
stdout: "pipe",
|
|
61
|
+
stderr: "pipe",
|
|
62
|
+
});
|
|
63
|
+
const exitCode = await proc.exited;
|
|
64
|
+
if (exitCode === 0) {
|
|
65
|
+
const binPath = (await new Response(proc.stdout).text()).trim();
|
|
66
|
+
if (binPath.length > 0) {
|
|
67
|
+
return dirname(resolve(binPath));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// which not available or command not on PATH — try next
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Fallback: if process.argv[1] points to agentplate's own entry point (src/index.ts),
|
|
76
|
+
// derive the bin dir from the bun binary that's running it
|
|
77
|
+
const scriptPath = process.argv[1];
|
|
78
|
+
if (scriptPath?.includes("agentplate")) {
|
|
79
|
+
const bunPath = process.argv[0];
|
|
80
|
+
if (bunPath) {
|
|
81
|
+
return dirname(resolve(bunPath));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Run a shell command and capture its output.
|
|
90
|
+
*/
|
|
91
|
+
async function runCommand(
|
|
92
|
+
cmd: string[],
|
|
93
|
+
cwd?: string,
|
|
94
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
95
|
+
const proc = Bun.spawn(cmd, {
|
|
96
|
+
cwd,
|
|
97
|
+
stdout: "pipe",
|
|
98
|
+
stderr: "pipe",
|
|
99
|
+
});
|
|
100
|
+
const stdout = await new Response(proc.stdout).text();
|
|
101
|
+
const stderr = await new Response(proc.stderr).text();
|
|
102
|
+
const exitCode = await proc.exited;
|
|
103
|
+
return { stdout, stderr, exitCode };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create a new detached tmux session running the given command.
|
|
108
|
+
*
|
|
109
|
+
* @param name - Session name (e.g., "agentplate-myproject-auth-login")
|
|
110
|
+
* @param cwd - Working directory for the session
|
|
111
|
+
* @param command - Command to execute inside the session
|
|
112
|
+
* @param env - Optional environment variables to export in the session
|
|
113
|
+
* @returns The PID of the tmux server process for this session
|
|
114
|
+
* @throws AgentError if tmux is not installed or session creation fails
|
|
115
|
+
*/
|
|
116
|
+
export async function createSession(
|
|
117
|
+
name: string,
|
|
118
|
+
cwd: string,
|
|
119
|
+
command: string,
|
|
120
|
+
env?: Record<string, string>,
|
|
121
|
+
maxRetries = 3,
|
|
122
|
+
): Promise<number> {
|
|
123
|
+
// Build environment exports for the tmux session
|
|
124
|
+
const exports: string[] = [];
|
|
125
|
+
|
|
126
|
+
// Ensure PATH includes the agentplate binary directory
|
|
127
|
+
// so that hooks calling `agentplate` inside the session can find it
|
|
128
|
+
const agentplateBinDir = await detectAgentplateBinDir();
|
|
129
|
+
if (agentplateBinDir) {
|
|
130
|
+
exports.push(`export PATH="${agentplateBinDir}:$PATH"`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Clear Claude Code nesting guard so child agents can start.
|
|
134
|
+
// Claude Code >=2.1.66 sets CLAUDECODE=1 and refuses to launch when it's present.
|
|
135
|
+
// Agentplate's agent spawning is intentional, not accidental nesting.
|
|
136
|
+
exports.push("unset CLAUDECODE CLAUDE_CODE_SSE_PORT CLAUDE_CODE_ENTRYPOINT");
|
|
137
|
+
|
|
138
|
+
// Add any additional environment variables
|
|
139
|
+
if (env) {
|
|
140
|
+
for (const [key, value] of Object.entries(env)) {
|
|
141
|
+
exports.push(`export ${key}="${value}"`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Build the startup script using bash syntax (export/unset).
|
|
146
|
+
// Then wrap it in `/bin/bash -c '...'` so it always runs in bash,
|
|
147
|
+
// regardless of the user's $SHELL. Without this, tmux uses the user's
|
|
148
|
+
// default shell (e.g. fish), which rejects bash export/unset syntax and
|
|
149
|
+
// causes the session to die instantly. Single-quote wrapping with escaped
|
|
150
|
+
// single quotes prevents any intermediate shell from expanding variables
|
|
151
|
+
// before bash receives them. (GitHub #86)
|
|
152
|
+
//
|
|
153
|
+
// The `exec` prefix replaces the bash wrapper with the spawned command
|
|
154
|
+
// so there is no separate wrapper PID to orphan if the tmux server dies
|
|
155
|
+
// externally. Without exec, bash receives SIGHUP on tmux teardown but its
|
|
156
|
+
// claude child gets reparented to init and continues running. With exec,
|
|
157
|
+
// the wrapper IS the command — SIGHUP is delivered directly to claude.
|
|
158
|
+
// (agentplate-505d)
|
|
159
|
+
const startupScript =
|
|
160
|
+
exports.length > 0 ? `${exports.join(" && ")} && exec ${command}` : `exec ${command}`;
|
|
161
|
+
const wrappedCommand = `/bin/bash -c '${startupScript.replace(/'/g, "'\\''")}'`;
|
|
162
|
+
|
|
163
|
+
const { exitCode, stderr } = await runCommand(
|
|
164
|
+
tmuxCmd("new-session", "-d", "-s", name, "-c", cwd, wrappedCommand),
|
|
165
|
+
cwd,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
if (exitCode !== 0) {
|
|
169
|
+
throw new AgentError(`Failed to create tmux session "${name}": ${stderr.trim()}`, {
|
|
170
|
+
agentName: name,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Retrieve the actual PID of the process running inside the tmux pane.
|
|
175
|
+
// Retry up to maxRetries times with backoff for WSL2 race conditions where
|
|
176
|
+
// the session exists but the pane hasn't been registered yet (#73).
|
|
177
|
+
let pidResult: { stdout: string; stderr: string; exitCode: number } | undefined;
|
|
178
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
179
|
+
pidResult = await runCommand(tmuxCmd("list-panes", "-t", name, "-F", "#{pane_pid}"));
|
|
180
|
+
if (pidResult.exitCode === 0) break;
|
|
181
|
+
await Bun.sleep(250 * (attempt + 1));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!pidResult || pidResult.exitCode !== 0) {
|
|
185
|
+
throw new AgentError(
|
|
186
|
+
`Created tmux session "${name}" but failed to retrieve PID: ${pidResult?.stderr.trim() ?? "unknown error"}`,
|
|
187
|
+
{ agentName: name },
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const pidStr = pidResult.stdout.trim().split("\n")[0];
|
|
192
|
+
if (pidStr) {
|
|
193
|
+
const pid = Number.parseInt(pidStr, 10);
|
|
194
|
+
if (!Number.isNaN(pid)) {
|
|
195
|
+
return pid;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
throw new AgentError(`Created tmux session "${name}" but could not find its pane PID`, {
|
|
200
|
+
agentName: name,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* List all active tmux sessions.
|
|
206
|
+
*
|
|
207
|
+
* @returns Array of session name/pid pairs
|
|
208
|
+
* @throws AgentError if tmux is not installed
|
|
209
|
+
*/
|
|
210
|
+
export async function listSessions(): Promise<Array<{ name: string; pid: number }>> {
|
|
211
|
+
const { exitCode, stdout, stderr } = await runCommand(
|
|
212
|
+
tmuxCmd("list-sessions", "-F", "#{session_name}:#{pid}"),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// Exit code 1 with "no server running" means no sessions exist — not an error
|
|
216
|
+
if (exitCode !== 0) {
|
|
217
|
+
if (stderr.includes("no server running") || stderr.includes("no sessions")) {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
throw new AgentError(`Failed to list tmux sessions: ${stderr.trim()}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const sessions: Array<{ name: string; pid: number }> = [];
|
|
224
|
+
const lines = stdout.trim().split("\n");
|
|
225
|
+
|
|
226
|
+
for (const line of lines) {
|
|
227
|
+
if (line.trim() === "") continue;
|
|
228
|
+
const sepIndex = line.indexOf(":");
|
|
229
|
+
if (sepIndex === -1) continue;
|
|
230
|
+
|
|
231
|
+
const name = line.slice(0, sepIndex);
|
|
232
|
+
const pidStr = line.slice(sepIndex + 1);
|
|
233
|
+
if (name && pidStr) {
|
|
234
|
+
const pid = Number.parseInt(pidStr, 10);
|
|
235
|
+
if (!Number.isNaN(pid)) {
|
|
236
|
+
sessions.push({ name, pid });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return sessions;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Grace period (ms) between SIGTERM and SIGKILL during process cleanup.
|
|
246
|
+
*/
|
|
247
|
+
const KILL_GRACE_PERIOD_MS = 2000;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get the pane PID for a tmux session.
|
|
251
|
+
*
|
|
252
|
+
* @param name - Tmux session name
|
|
253
|
+
* @returns The PID of the process running in the session's pane, or null if
|
|
254
|
+
* the session doesn't exist or the PID can't be determined
|
|
255
|
+
*/
|
|
256
|
+
export async function getPanePid(name: string): Promise<number | null> {
|
|
257
|
+
const { exitCode, stdout } = await runCommand(
|
|
258
|
+
tmuxCmd("display-message", "-p", "-t", name, "#{pane_pid}"),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
if (exitCode !== 0) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const pidStr = stdout.trim();
|
|
266
|
+
if (pidStr.length === 0) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const pid = Number.parseInt(pidStr, 10);
|
|
271
|
+
return Number.isNaN(pid) ? null : pid;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Recursively collect all descendant PIDs of a given process.
|
|
276
|
+
*
|
|
277
|
+
* Uses `pgrep -P <pid>` to find direct children, then recurses into each child.
|
|
278
|
+
* Returns PIDs in depth-first order (deepest descendants first), which is the
|
|
279
|
+
* correct order for sending signals — kill children before parents so processes
|
|
280
|
+
* don't get reparented to init (PID 1).
|
|
281
|
+
*
|
|
282
|
+
* @param pid - The root process PID to walk from
|
|
283
|
+
* @returns Array of descendant PIDs, deepest-first
|
|
284
|
+
*/
|
|
285
|
+
export async function getDescendantPids(pid: number): Promise<number[]> {
|
|
286
|
+
const { exitCode, stdout } = await runCommand(["pgrep", "-P", String(pid)]);
|
|
287
|
+
|
|
288
|
+
// pgrep exits 1 when no children found — not an error
|
|
289
|
+
if (exitCode !== 0 || stdout.trim().length === 0) {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const childPids: number[] = [];
|
|
294
|
+
for (const line of stdout.trim().split("\n")) {
|
|
295
|
+
const childPid = Number.parseInt(line.trim(), 10);
|
|
296
|
+
if (!Number.isNaN(childPid)) {
|
|
297
|
+
childPids.push(childPid);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Recurse into each child to get their descendants first (depth-first)
|
|
302
|
+
const allDescendants: number[] = [];
|
|
303
|
+
for (const childPid of childPids) {
|
|
304
|
+
const grandchildren = await getDescendantPids(childPid);
|
|
305
|
+
allDescendants.push(...grandchildren);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Append the direct children after their descendants (deepest-first order)
|
|
309
|
+
allDescendants.push(...childPids);
|
|
310
|
+
|
|
311
|
+
return allDescendants;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Check if a process is still alive.
|
|
316
|
+
*
|
|
317
|
+
* @param pid - Process ID to check
|
|
318
|
+
* @returns true if the process exists, false otherwise
|
|
319
|
+
*/
|
|
320
|
+
export function isProcessAlive(pid: number): boolean {
|
|
321
|
+
try {
|
|
322
|
+
// signal 0 doesn't send a signal but checks if the process exists
|
|
323
|
+
process.kill(pid, 0);
|
|
324
|
+
return true;
|
|
325
|
+
} catch {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Kill a process tree: SIGTERM deepest-first, wait grace period, SIGKILL survivors.
|
|
332
|
+
*
|
|
333
|
+
* Follows gastown's KillSessionWithProcesses pattern:
|
|
334
|
+
* 1. Walk descendant tree from the root PID
|
|
335
|
+
* 2. Send SIGTERM to all descendants (deepest-first so children die before parents)
|
|
336
|
+
* 3. Wait a grace period for processes to clean up
|
|
337
|
+
* 4. Send SIGKILL to any survivors
|
|
338
|
+
*
|
|
339
|
+
* Handles edge cases:
|
|
340
|
+
* - Already-dead processes (ESRCH) — silently ignored
|
|
341
|
+
* - Reparented processes (PPID=1) — caught in the initial tree walk
|
|
342
|
+
* - Permission errors — silently ignored (process belongs to another user)
|
|
343
|
+
*
|
|
344
|
+
* @param rootPid - The root PID whose descendants should be killed
|
|
345
|
+
* @param gracePeriodMs - Time to wait between SIGTERM and SIGKILL (default 2000ms)
|
|
346
|
+
*/
|
|
347
|
+
export async function killProcessTree(
|
|
348
|
+
rootPid: number,
|
|
349
|
+
gracePeriodMs: number = KILL_GRACE_PERIOD_MS,
|
|
350
|
+
): Promise<void> {
|
|
351
|
+
const descendants = await getDescendantPids(rootPid);
|
|
352
|
+
|
|
353
|
+
if (descendants.length === 0) {
|
|
354
|
+
// No descendants — just try to kill the root process
|
|
355
|
+
sendSignal(rootPid, "SIGTERM");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Phase 1: SIGTERM all descendants (deepest-first, then root)
|
|
360
|
+
for (const pid of descendants) {
|
|
361
|
+
sendSignal(pid, "SIGTERM");
|
|
362
|
+
}
|
|
363
|
+
sendSignal(rootPid, "SIGTERM");
|
|
364
|
+
|
|
365
|
+
// Phase 2: Wait grace period for processes to clean up
|
|
366
|
+
await Bun.sleep(gracePeriodMs);
|
|
367
|
+
|
|
368
|
+
// Phase 3: SIGKILL any survivors (same order: deepest-first, then root)
|
|
369
|
+
for (const pid of descendants) {
|
|
370
|
+
if (isProcessAlive(pid)) {
|
|
371
|
+
sendSignal(pid, "SIGKILL");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (isProcessAlive(rootPid)) {
|
|
375
|
+
sendSignal(rootPid, "SIGKILL");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Send a signal to a process, ignoring errors for already-dead or inaccessible processes.
|
|
381
|
+
*
|
|
382
|
+
* @param pid - Process ID to signal
|
|
383
|
+
* @param signal - Signal name (e.g., "SIGTERM", "SIGKILL")
|
|
384
|
+
*/
|
|
385
|
+
function sendSignal(pid: number, signal: "SIGTERM" | "SIGKILL"): void {
|
|
386
|
+
try {
|
|
387
|
+
process.kill(pid, signal);
|
|
388
|
+
} catch {
|
|
389
|
+
// Process already dead (ESRCH), permission denied (EPERM), or invalid PID — all OK
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Kill a tmux session by name, with proper process tree cleanup.
|
|
395
|
+
*
|
|
396
|
+
* Before killing the tmux session, walks the descendant process tree from the
|
|
397
|
+
* pane PID, sends SIGTERM to all descendants (deepest-first), waits a grace
|
|
398
|
+
* period, then sends SIGKILL to survivors. This ensures child processes
|
|
399
|
+
* (git, bun test, biome, etc.) are properly cleaned up rather than being
|
|
400
|
+
* orphaned or reparented to init.
|
|
401
|
+
*
|
|
402
|
+
* @param name - Session name to kill
|
|
403
|
+
* @throws AgentError if the tmux session cannot be killed (process cleanup
|
|
404
|
+
* failures are silently handled since the goal is best-effort cleanup)
|
|
405
|
+
*/
|
|
406
|
+
export async function killSession(name: string): Promise<void> {
|
|
407
|
+
// Defense in depth: an empty session name passed to `tmux -t` is prefix-matched
|
|
408
|
+
// against every session in the server, wildcard-killing the entire agentplate
|
|
409
|
+
// swarm (agentplate-74ce). Reject empty names at the boundary so a regression in
|
|
410
|
+
// any caller surfaces loudly instead of silently nuking the tmux server.
|
|
411
|
+
if (name === "") {
|
|
412
|
+
throw new AgentError(
|
|
413
|
+
"killSession called with empty session name (would wildcard-kill all tmux sessions due to prefix matching)",
|
|
414
|
+
{ agentName: name },
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Step 1: Get the pane PID before killing the tmux session
|
|
419
|
+
const panePid = await getPanePid(name);
|
|
420
|
+
|
|
421
|
+
// Step 2: If we have a pane PID, walk and kill the process tree
|
|
422
|
+
if (panePid !== null) {
|
|
423
|
+
await killProcessTree(panePid);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Step 3: Kill the tmux session itself
|
|
427
|
+
const { exitCode, stderr } = await runCommand(tmuxCmd("kill-session", "-t", name));
|
|
428
|
+
|
|
429
|
+
if (exitCode !== 0) {
|
|
430
|
+
// If the session is already gone (e.g., died during process cleanup), that's fine
|
|
431
|
+
if (stderr.includes("session not found") || stderr.includes("can't find session")) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
throw new AgentError(`Failed to kill tmux session "${name}": ${stderr.trim()}`, {
|
|
435
|
+
agentName: name,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Detect the current tmux session name.
|
|
442
|
+
*
|
|
443
|
+
* Returns the session name if running inside tmux, null otherwise.
|
|
444
|
+
* Used by `agentplate prime` to register the orchestrator's tmux session
|
|
445
|
+
* so agents can nudge the orchestrator when they have results.
|
|
446
|
+
*/
|
|
447
|
+
export async function getCurrentSessionName(): Promise<string | null> {
|
|
448
|
+
if (!process.env.TMUX) {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
const { exitCode, stdout } = await runCommand([
|
|
452
|
+
"tmux",
|
|
453
|
+
"display-message",
|
|
454
|
+
"-p",
|
|
455
|
+
"#{session_name}",
|
|
456
|
+
]);
|
|
457
|
+
if (exitCode !== 0) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
const name = stdout.trim();
|
|
461
|
+
return name.length > 0 ? name : null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Check whether a tmux session is still alive.
|
|
466
|
+
*
|
|
467
|
+
* @param name - Session name to check
|
|
468
|
+
* @returns true if the session exists, false otherwise
|
|
469
|
+
*/
|
|
470
|
+
export async function isSessionAlive(name: string): Promise<boolean> {
|
|
471
|
+
// Defense in depth: an empty `-t` argument is prefix-matched against every
|
|
472
|
+
// session, so `has-session` would return true whenever any agentplate session
|
|
473
|
+
// exists. Treat empty as "not alive" without contacting tmux (agentplate-74ce).
|
|
474
|
+
if (name === "") {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
const { exitCode } = await runCommand(tmuxCmd("has-session", "-t", name));
|
|
478
|
+
return exitCode === 0;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Detailed session state for distinguishing failure modes.
|
|
483
|
+
*
|
|
484
|
+
* - `"alive"` -- tmux session exists and is reachable.
|
|
485
|
+
* - `"dead"` -- tmux server is running but the session does not exist.
|
|
486
|
+
* - `"no_server"` -- tmux server is not running at all.
|
|
487
|
+
*/
|
|
488
|
+
export type SessionState = "alive" | "dead" | "no_server";
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Check tmux session state with detailed failure mode reporting.
|
|
492
|
+
*
|
|
493
|
+
* Unlike `isSessionAlive()` which returns a simple boolean, this function
|
|
494
|
+
* distinguishes between three states:
|
|
495
|
+
* - `"alive"`: session exists -- the agent may still be running.
|
|
496
|
+
* - `"dead"`: tmux server is running but session is gone -- agent exited or was killed.
|
|
497
|
+
* - `"no_server"`: tmux server itself is not running -- all sessions are gone.
|
|
498
|
+
*
|
|
499
|
+
* Callers can use this to provide targeted error messages and decide whether
|
|
500
|
+
* stale session records should be cleaned up vs flagged as errors.
|
|
501
|
+
*
|
|
502
|
+
* @param name - Session name to check
|
|
503
|
+
* @returns The session state
|
|
504
|
+
*/
|
|
505
|
+
export async function checkSessionState(name: string): Promise<SessionState> {
|
|
506
|
+
const { exitCode, stderr } = await runCommand(tmuxCmd("has-session", "-t", name));
|
|
507
|
+
if (exitCode === 0) return "alive";
|
|
508
|
+
if (stderr.includes("no server running") || stderr.includes("no sessions")) {
|
|
509
|
+
return "no_server";
|
|
510
|
+
}
|
|
511
|
+
return "dead";
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Capture the visible content of a tmux session's pane.
|
|
516
|
+
*
|
|
517
|
+
* @param name - Session name to capture from
|
|
518
|
+
* @param lines - Number of history lines to capture (default 50)
|
|
519
|
+
* @returns The trimmed pane content, or null if capture fails
|
|
520
|
+
*/
|
|
521
|
+
export async function capturePaneContent(name: string, lines = 50): Promise<string | null> {
|
|
522
|
+
const { exitCode, stdout } = await runCommand(
|
|
523
|
+
tmuxCmd("capture-pane", "-t", name, "-p", "-S", `-${lines}`),
|
|
524
|
+
);
|
|
525
|
+
if (exitCode !== 0) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
const content = stdout.trim();
|
|
529
|
+
return content.length > 0 ? content : null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Wait for a tmux session's TUI to become ready for input.
|
|
534
|
+
*
|
|
535
|
+
* Delegates all readiness detection to the provided `detectReady` callback,
|
|
536
|
+
* making this function runtime-agnostic. The callback inspects pane content
|
|
537
|
+
* and returns a ReadyState phase: "loading" (keep waiting), "dialog" (send
|
|
538
|
+
* the requested action, then continue), or "ready" (return true).
|
|
539
|
+
*
|
|
540
|
+
* Dialog actions that type raw text (for example Claude Code's `type:2`
|
|
541
|
+
* bypass confirmation) are retried if the same dialog is still visible on
|
|
542
|
+
* later polls. This avoids one-shot startup flakes when tmux or the TUI drops
|
|
543
|
+
* the first keypress during initialization.
|
|
544
|
+
*
|
|
545
|
+
* @param name - Tmux session name to poll
|
|
546
|
+
* @param detectReady - Callback that inspects pane content and returns ReadyState
|
|
547
|
+
* @param timeoutMs - Maximum time to wait before giving up (default 30s)
|
|
548
|
+
* @param pollIntervalMs - Time between polls (default 500ms)
|
|
549
|
+
* @returns true once detectReady returns { phase: "ready" }, false on timeout or dead session
|
|
550
|
+
*/
|
|
551
|
+
export async function waitForTuiReady(
|
|
552
|
+
name: string,
|
|
553
|
+
detectReady: (paneContent: string) => ReadyState,
|
|
554
|
+
timeoutMs = 30_000,
|
|
555
|
+
pollIntervalMs = 500,
|
|
556
|
+
): Promise<boolean> {
|
|
557
|
+
const maxAttempts = Math.ceil(timeoutMs / pollIntervalMs);
|
|
558
|
+
const handledDialogs = new Map<string, number>();
|
|
559
|
+
const typedDialogRetryPolls = Math.max(2, Math.ceil(1_000 / pollIntervalMs));
|
|
560
|
+
|
|
561
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
562
|
+
const content = await capturePaneContent(name);
|
|
563
|
+
if (content !== null) {
|
|
564
|
+
const state = detectReady(content);
|
|
565
|
+
|
|
566
|
+
if (state.phase === "dialog") {
|
|
567
|
+
const lastHandledAttempt = handledDialogs.get(state.action);
|
|
568
|
+
const shouldRetryTypedDialog =
|
|
569
|
+
state.action.startsWith("type:") &&
|
|
570
|
+
lastHandledAttempt !== undefined &&
|
|
571
|
+
i - lastHandledAttempt >= typedDialogRetryPolls;
|
|
572
|
+
const shouldHandleDialog = lastHandledAttempt === undefined || shouldRetryTypedDialog;
|
|
573
|
+
|
|
574
|
+
if (shouldHandleDialog) {
|
|
575
|
+
await handleDialogAction(name, state.action, pollIntervalMs);
|
|
576
|
+
handledDialogs.set(state.action, i);
|
|
577
|
+
await Bun.sleep(pollIntervalMs);
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (state.phase === "ready") {
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const alive = await isSessionAlive(name);
|
|
588
|
+
if (!alive) {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
await Bun.sleep(pollIntervalMs);
|
|
592
|
+
}
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Verify that tmux is installed and executable.
|
|
598
|
+
* Throws AgentError with a clear message if tmux is not available.
|
|
599
|
+
*/
|
|
600
|
+
export async function ensureTmuxAvailable(): Promise<void> {
|
|
601
|
+
const { exitCode } = await runCommand(["tmux", "-V"]);
|
|
602
|
+
if (exitCode !== 0) {
|
|
603
|
+
throw new AgentError(
|
|
604
|
+
"tmux is not installed or not on PATH. Install tmux to use agentplate agent orchestration.",
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Send keys to a tmux session, with retry for WSL2 pane registration race.
|
|
611
|
+
*
|
|
612
|
+
* On WSL2, tmux occasionally reports "can't find pane" immediately after session
|
|
613
|
+
* creation even though the session exists. This is a timing issue where the pane
|
|
614
|
+
* hasn't been fully registered yet. We retry with backoff to handle this.
|
|
615
|
+
*
|
|
616
|
+
* @param name - Session name to send keys to
|
|
617
|
+
* @param keys - The keys/text to send
|
|
618
|
+
* @param maxRetries - Maximum retry attempts for transient pane errors (default 3)
|
|
619
|
+
* @throws AgentError if the session does not exist or send fails after retries
|
|
620
|
+
*/
|
|
621
|
+
export async function sendKeys(name: string, keys: string, maxRetries = 3): Promise<void> {
|
|
622
|
+
// Flatten newlines to spaces — multiline text via tmux send-keys causes
|
|
623
|
+
// Claude Code's TUI to receive embedded Enter keystrokes which prevent
|
|
624
|
+
// the final "Enter" from triggering message submission (agentplate-y2ob).
|
|
625
|
+
const flatKeys = keys.replace(/\n/g, " ");
|
|
626
|
+
|
|
627
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
628
|
+
const { exitCode, stderr } = await runCommand(
|
|
629
|
+
tmuxCmd("send-keys", "-t", name, flatKeys, "Enter"),
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
if (exitCode === 0) {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const trimmedStderr = stderr.trim();
|
|
637
|
+
|
|
638
|
+
if (trimmedStderr.includes("no server running")) {
|
|
639
|
+
throw new AgentError(
|
|
640
|
+
`Tmux server is not running (cannot reach session "${name}"). This often happens when running as root (UID 0) or when tmux crashed. Original error: ${trimmedStderr}`,
|
|
641
|
+
{ agentName: name },
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// "can't find pane" is a transient race condition on WSL2 — the session
|
|
646
|
+
// exists but the pane hasn't been fully registered yet. Retry with backoff.
|
|
647
|
+
if (trimmedStderr.includes("can't find pane") || trimmedStderr.includes("cant find pane")) {
|
|
648
|
+
if (attempt < maxRetries) {
|
|
649
|
+
const delayMs = 250 * (attempt + 1);
|
|
650
|
+
await Bun.sleep(delayMs);
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
// Exhausted retries — report as pane-specific error
|
|
654
|
+
throw new AgentError(
|
|
655
|
+
`Tmux pane for session "${name}" not found after ${maxRetries + 1} attempts. On WSL2, this can indicate a tmux startup race condition. Try increasing the retry count or adding a delay after session creation.`,
|
|
656
|
+
{ agentName: name },
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (
|
|
661
|
+
trimmedStderr.includes("session not found") ||
|
|
662
|
+
trimmedStderr.includes("can't find session") ||
|
|
663
|
+
trimmedStderr.includes("cant find session")
|
|
664
|
+
) {
|
|
665
|
+
throw new AgentError(
|
|
666
|
+
`Tmux session "${name}" does not exist. The agent may have crashed or been killed before receiving input.`,
|
|
667
|
+
{ agentName: name },
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
throw new AgentError(`Failed to send keys to tmux session "${name}": ${trimmedStderr}`, {
|
|
672
|
+
agentName: name,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async function sendRawKeys(name: string, keys: string): Promise<void> {
|
|
678
|
+
const flatKeys = keys.replace(/\n/g, " ");
|
|
679
|
+
const { exitCode, stderr } = await runCommand(tmuxCmd("send-keys", "-t", name, flatKeys));
|
|
680
|
+
|
|
681
|
+
if (exitCode !== 0) {
|
|
682
|
+
const trimmedStderr = stderr.trim();
|
|
683
|
+
|
|
684
|
+
if (trimmedStderr.includes("no server running")) {
|
|
685
|
+
throw new AgentError(
|
|
686
|
+
`Tmux server is not running (cannot reach session "${name}"). This often happens when running as root (UID 0) or when tmux crashed. Original error: ${trimmedStderr}`,
|
|
687
|
+
{ agentName: name },
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (
|
|
692
|
+
trimmedStderr.includes("session not found") ||
|
|
693
|
+
trimmedStderr.includes("can't find session") ||
|
|
694
|
+
trimmedStderr.includes("cant find session")
|
|
695
|
+
) {
|
|
696
|
+
throw new AgentError(
|
|
697
|
+
`Tmux session "${name}" does not exist. The agent may have crashed or been killed before receiving input.`,
|
|
698
|
+
{ agentName: name },
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
throw new AgentError(`Failed to send keys to tmux session "${name}": ${trimmedStderr}`, {
|
|
703
|
+
agentName: name,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async function handleDialogAction(
|
|
709
|
+
name: string,
|
|
710
|
+
action: string,
|
|
711
|
+
pollIntervalMs: number,
|
|
712
|
+
): Promise<void> {
|
|
713
|
+
if (action.startsWith("type:")) {
|
|
714
|
+
await sendRawKeys(name, action.slice("type:".length));
|
|
715
|
+
await Bun.sleep(Math.min(pollIntervalMs, 250));
|
|
716
|
+
await sendKeys(name, "");
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
await sendKeys(name, action === "Enter" ? "" : action);
|
|
721
|
+
}
|