@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,273 @@
|
|
|
1
|
+
// Codex runtime adapter for agentplate's AgentRuntime interface.
|
|
2
|
+
// Implements the AgentRuntime contract for the OpenAI `codex` CLI.
|
|
3
|
+
//
|
|
4
|
+
// Key differences from Claude/Pi adapters:
|
|
5
|
+
// - Interactive: `codex` (without `exec`) stays alive in tmux for orchestration
|
|
6
|
+
// - Instruction file: AGENTS.md (not .claude/CLAUDE.md)
|
|
7
|
+
// - No hooks: Codex uses OS-level sandbox (Seatbelt/Landlock)
|
|
8
|
+
// - One-shot calls still use `codex exec` (buildPrintCommand)
|
|
9
|
+
|
|
10
|
+
import { mkdir } from "node:fs/promises";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import type { ResolvedModel } from "../types.ts";
|
|
13
|
+
import type {
|
|
14
|
+
AgentRuntime,
|
|
15
|
+
HooksDef,
|
|
16
|
+
OverlayContent,
|
|
17
|
+
ReadyState,
|
|
18
|
+
SpawnOpts,
|
|
19
|
+
TranscriptSummary,
|
|
20
|
+
} from "./types.ts";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Codex runtime adapter.
|
|
24
|
+
*
|
|
25
|
+
* Implements AgentRuntime for the OpenAI `codex` CLI. Tmux-spawned Codex
|
|
26
|
+
* agents run in interactive mode (`codex`) so sessions stay alive and can be
|
|
27
|
+
* nudged via tmux.
|
|
28
|
+
*
|
|
29
|
+
* Security is enforced via Codex's OS-level sandbox (Seatbelt on macOS,
|
|
30
|
+
* Landlock on Linux) rather than hook-based guards. The `--full-auto` flag
|
|
31
|
+
* enables `workspace-write` sandbox + automatic approvals.
|
|
32
|
+
*
|
|
33
|
+
* Instructions are delivered via `AGENTS.md` (Codex's native convention),
|
|
34
|
+
* not `.claude/CLAUDE.md`.
|
|
35
|
+
*/
|
|
36
|
+
export class CodexRuntime implements AgentRuntime {
|
|
37
|
+
/** Unique identifier for this runtime. */
|
|
38
|
+
readonly id = "codex";
|
|
39
|
+
|
|
40
|
+
/** Stability level. Codex adapter is experimental — not fully validated. */
|
|
41
|
+
readonly stability = "experimental" as const;
|
|
42
|
+
|
|
43
|
+
/** Relative path to the instruction file within a worktree. */
|
|
44
|
+
readonly instructionPath = "AGENTS.md";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Anthropic aliases used by agentplate manifests that Codex CLI does not
|
|
48
|
+
* accept as --model values.
|
|
49
|
+
*/
|
|
50
|
+
private static readonly MANIFEST_ALIASES = new Set(["sonnet", "opus", "haiku"]);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Strip a provider prefix from a model ID.
|
|
54
|
+
*
|
|
55
|
+
* Codex CLI expects bare model names. The orchestrator may resolve a model to
|
|
56
|
+
* a provider-qualified form (e.g. `"openai/gpt-5.4"`) — strip the `"openai/"`
|
|
57
|
+
* prefix before passing to the CLI.
|
|
58
|
+
*
|
|
59
|
+
* @param model - Possibly provider-qualified model ID
|
|
60
|
+
* @returns Bare model name (everything after the first `/`, or unchanged if no `/`)
|
|
61
|
+
*/
|
|
62
|
+
private static stripProviderPrefix(model: string): string {
|
|
63
|
+
const slashIdx = model.indexOf("/");
|
|
64
|
+
return slashIdx !== -1 ? model.slice(slashIdx + 1) : model;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Escape a directory path for use in a single-quoted shell argument.
|
|
69
|
+
*
|
|
70
|
+
* @param path - Absolute directory path
|
|
71
|
+
* @returns POSIX shell-safe path string
|
|
72
|
+
*/
|
|
73
|
+
private static shellEscape(path: string): string {
|
|
74
|
+
return path.replace(/'/g, "'\\''");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build the shell command string to spawn a Codex agent in a tmux pane.
|
|
79
|
+
*
|
|
80
|
+
* Uses interactive `codex` with `--full-auto` for workspace-write sandbox +
|
|
81
|
+
* automatic approvals.
|
|
82
|
+
*
|
|
83
|
+
* The prompt directs the agent to read AGENTS.md for its full instructions.
|
|
84
|
+
* If `appendSystemPrompt` or `appendSystemPromptFile` is provided, the
|
|
85
|
+
* content is prepended to the prompt (Codex has no --append-system-prompt
|
|
86
|
+
* flag — all context goes through the exec prompt or AGENTS.md).
|
|
87
|
+
*
|
|
88
|
+
* @param opts - Spawn options (model, appendSystemPrompt; permissionMode is accepted but
|
|
89
|
+
* not mapped — Codex enforces security via OS sandbox, not permission flags)
|
|
90
|
+
* @returns Shell command string suitable for tmux new-session -c
|
|
91
|
+
*/
|
|
92
|
+
buildSpawnCommand(opts: SpawnOpts): string {
|
|
93
|
+
// Strip provider prefix before alias check and model flag injection.
|
|
94
|
+
// Codex CLI expects bare model names (e.g. "gpt-5.4", not "openai/gpt-5.4").
|
|
95
|
+
const bareModel = CodexRuntime.stripProviderPrefix(opts.model);
|
|
96
|
+
// When model comes from default manifest aliases (sonnet/opus/haiku),
|
|
97
|
+
// omit --model so Codex uses the user's configured default model.
|
|
98
|
+
let cmd = "codex --full-auto";
|
|
99
|
+
if (!CodexRuntime.MANIFEST_ALIASES.has(bareModel)) {
|
|
100
|
+
cmd += ` --model ${bareModel}`;
|
|
101
|
+
}
|
|
102
|
+
for (const dir of opts.sharedWritableDirs ?? []) {
|
|
103
|
+
cmd += ` --add-dir '${CodexRuntime.shellEscape(dir)}'`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (opts.appendSystemPromptFile) {
|
|
107
|
+
// Read role definition from file at shell expansion time — avoids tmux
|
|
108
|
+
// IPC message size limits. Append the "read AGENTS.md" instruction.
|
|
109
|
+
const escaped = CodexRuntime.shellEscape(opts.appendSystemPromptFile);
|
|
110
|
+
cmd += ` "$(cat '${escaped}')"' Read AGENTS.md for your task assignment and begin immediately.'`;
|
|
111
|
+
} else if (opts.appendSystemPrompt) {
|
|
112
|
+
// Inline role definition + instruction to read AGENTS.md.
|
|
113
|
+
const prompt = `${opts.appendSystemPrompt}\n\nRead AGENTS.md for your task assignment and begin immediately.`;
|
|
114
|
+
const escaped = CodexRuntime.shellEscape(prompt);
|
|
115
|
+
cmd += ` '${escaped}'`;
|
|
116
|
+
} else {
|
|
117
|
+
cmd += ` 'Read AGENTS.md for your task assignment and begin immediately.'`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return cmd;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build the argv array for a headless one-shot Codex invocation.
|
|
125
|
+
*
|
|
126
|
+
* Returns an argv array suitable for `Bun.spawn()`. Uses `codex exec`
|
|
127
|
+
* with `--full-auto` and `--ephemeral` (no session persistence).
|
|
128
|
+
* Without `--json`, stdout contains the plain text final message.
|
|
129
|
+
*
|
|
130
|
+
* Used by merge/resolver.ts (AI-assisted conflict resolution) and
|
|
131
|
+
* watchdog/triage.ts (AI-assisted failure classification).
|
|
132
|
+
*
|
|
133
|
+
* @param prompt - The prompt to pass as the exec argument
|
|
134
|
+
* @param model - Optional model override
|
|
135
|
+
* @returns Argv array for Bun.spawn
|
|
136
|
+
*/
|
|
137
|
+
buildPrintCommand(prompt: string, model?: string): string[] {
|
|
138
|
+
const cmd = ["codex", "exec", "--full-auto", "--ephemeral"];
|
|
139
|
+
if (model !== undefined) {
|
|
140
|
+
// Strip provider prefix — Codex CLI expects bare model names.
|
|
141
|
+
cmd.push("--model", CodexRuntime.stripProviderPrefix(model));
|
|
142
|
+
}
|
|
143
|
+
cmd.push(prompt);
|
|
144
|
+
return cmd;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Deploy per-agent instructions to a worktree.
|
|
149
|
+
*
|
|
150
|
+
* Writes the overlay to `AGENTS.md` in the worktree root (Codex's native
|
|
151
|
+
* instruction file convention). Unlike Claude/Pi adapters, no hooks or
|
|
152
|
+
* guard extensions are deployed — Codex enforces security boundaries via
|
|
153
|
+
* its OS-level sandbox (Seatbelt on macOS, Landlock on Linux).
|
|
154
|
+
*
|
|
155
|
+
* When overlay is undefined (hooks-only deployment for coordinator/supervisor/monitor),
|
|
156
|
+
* this is a no-op since Codex has no hook system to deploy.
|
|
157
|
+
*
|
|
158
|
+
* @param worktreePath - Absolute path to the agent's git worktree
|
|
159
|
+
* @param overlay - Overlay content to write as AGENTS.md, or undefined for no-op
|
|
160
|
+
* @param _hooks - Hook definition (unused — Codex uses OS sandbox, not hooks)
|
|
161
|
+
*/
|
|
162
|
+
async deployConfig(
|
|
163
|
+
worktreePath: string,
|
|
164
|
+
overlay: OverlayContent | undefined,
|
|
165
|
+
_hooks: HooksDef,
|
|
166
|
+
): Promise<void> {
|
|
167
|
+
if (!overlay) return;
|
|
168
|
+
|
|
169
|
+
const agentsPath = join(worktreePath, this.instructionPath);
|
|
170
|
+
// Ensure parent directory exists (AGENTS.md is in the worktree root,
|
|
171
|
+
// but the worktree dir itself might not exist yet).
|
|
172
|
+
await mkdir(dirname(agentsPath), { recursive: true });
|
|
173
|
+
await Bun.write(agentsPath, overlay.content);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Codex interactive startup is treated as ready once a pane exists.
|
|
178
|
+
*
|
|
179
|
+
* @param _paneContent - Captured tmux pane content (unused)
|
|
180
|
+
* @returns Always `{ phase: "ready" }`
|
|
181
|
+
*/
|
|
182
|
+
detectReady(_paneContent: string): ReadyState {
|
|
183
|
+
return { phase: "ready" };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Codex does not require beacon verification/resend.
|
|
188
|
+
*
|
|
189
|
+
* Codex accepts startup input reliably once spawned.
|
|
190
|
+
*/
|
|
191
|
+
requiresBeaconVerification(): boolean {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Parse a Codex NDJSON transcript file into normalized token usage.
|
|
197
|
+
*
|
|
198
|
+
* Codex NDJSON format (from `--json` flag) differs from Claude/Pi:
|
|
199
|
+
* - Token counts are in `turn.completed` events with
|
|
200
|
+
* `usage.input_tokens` and `usage.output_tokens`
|
|
201
|
+
* - Model identity may appear in `thread.started` events or item metadata
|
|
202
|
+
*
|
|
203
|
+
* Returns null if the file does not exist or cannot be parsed.
|
|
204
|
+
*
|
|
205
|
+
* @param path - Absolute path to the Codex NDJSON transcript file
|
|
206
|
+
* @returns Aggregated token usage, or null if unavailable
|
|
207
|
+
*/
|
|
208
|
+
async parseTranscript(path: string): Promise<TranscriptSummary | null> {
|
|
209
|
+
const file = Bun.file(path);
|
|
210
|
+
if (!(await file.exists())) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const text = await file.text();
|
|
216
|
+
const lines = text.split("\n").filter((l) => l.trim().length > 0);
|
|
217
|
+
|
|
218
|
+
let inputTokens = 0;
|
|
219
|
+
let outputTokens = 0;
|
|
220
|
+
let model = "";
|
|
221
|
+
|
|
222
|
+
for (const line of lines) {
|
|
223
|
+
let event: Record<string, unknown>;
|
|
224
|
+
try {
|
|
225
|
+
event = JSON.parse(line) as Record<string, unknown>;
|
|
226
|
+
} catch {
|
|
227
|
+
// Skip malformed lines — partial writes during capture.
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (event.type === "turn.completed") {
|
|
232
|
+
const usage = event.usage as Record<string, number | undefined> | undefined;
|
|
233
|
+
if (usage) {
|
|
234
|
+
if (typeof usage.input_tokens === "number") {
|
|
235
|
+
inputTokens += usage.input_tokens;
|
|
236
|
+
}
|
|
237
|
+
if (typeof usage.output_tokens === "number") {
|
|
238
|
+
outputTokens += usage.output_tokens;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Capture model from any event that carries it.
|
|
244
|
+
if (typeof event.model === "string") {
|
|
245
|
+
model = event.model;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { inputTokens, outputTokens, model };
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Build runtime-specific environment variables for model/provider routing.
|
|
257
|
+
*
|
|
258
|
+
* Returns the provider environment variables from the resolved model.
|
|
259
|
+
* For OpenAI native: may include OPENAI_API_KEY, OPENAI_BASE_URL.
|
|
260
|
+
* For gateway providers: may include gateway-specific auth and routing vars.
|
|
261
|
+
*
|
|
262
|
+
* @param model - Resolved model with optional provider env vars
|
|
263
|
+
* @returns Environment variable map (may be empty)
|
|
264
|
+
*/
|
|
265
|
+
buildEnv(model: ResolvedModel): Record<string, string> {
|
|
266
|
+
return model.env ?? {};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Codex does not produce transcript files. */
|
|
270
|
+
getTranscriptDir(_projectRoot: string): string | null {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
addHeadlessConnectionListener,
|
|
4
|
+
getConnection,
|
|
5
|
+
registerHeadlessConnection,
|
|
6
|
+
removeConnection,
|
|
7
|
+
setConnection,
|
|
8
|
+
} from "./connections.ts";
|
|
9
|
+
import type { ConnectionState, RuntimeConnection } from "./types.ts";
|
|
10
|
+
|
|
11
|
+
/** Minimal RuntimeConnection stub for testing the registry. */
|
|
12
|
+
function makeConn(onClose?: () => void): RuntimeConnection {
|
|
13
|
+
return {
|
|
14
|
+
sendPrompt: async (_text: string) => {},
|
|
15
|
+
followUp: async (_text: string) => {},
|
|
16
|
+
abort: async () => {},
|
|
17
|
+
getState: async (): Promise<ConnectionState> => ({ status: "idle" }),
|
|
18
|
+
close: () => {
|
|
19
|
+
if (onClose) onClose();
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("connection registry", () => {
|
|
25
|
+
// Reset registry between tests by removing any entries set during each test.
|
|
26
|
+
// We track names used so we can clean up without affecting other entries.
|
|
27
|
+
const usedNames: string[] = [];
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
for (const name of usedNames.splice(0)) {
|
|
31
|
+
const conn = getConnection(name);
|
|
32
|
+
if (conn) {
|
|
33
|
+
removeConnection(name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("set and get returns the registered connection", () => {
|
|
39
|
+
const conn = makeConn();
|
|
40
|
+
usedNames.push("agent-alpha");
|
|
41
|
+
setConnection("agent-alpha", conn);
|
|
42
|
+
expect(getConnection("agent-alpha")).toBe(conn);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("get unknown returns undefined", () => {
|
|
46
|
+
expect(getConnection("does-not-exist-xyz")).toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("removeConnection calls close() on the connection", () => {
|
|
50
|
+
let closed = false;
|
|
51
|
+
const conn = makeConn(() => {
|
|
52
|
+
closed = true;
|
|
53
|
+
});
|
|
54
|
+
usedNames.push("agent-beta");
|
|
55
|
+
setConnection("agent-beta", conn);
|
|
56
|
+
removeConnection("agent-beta");
|
|
57
|
+
expect(closed).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("removeConnection deletes the entry (get returns undefined after)", () => {
|
|
61
|
+
const conn = makeConn();
|
|
62
|
+
usedNames.push("agent-gamma");
|
|
63
|
+
setConnection("agent-gamma", conn);
|
|
64
|
+
removeConnection("agent-gamma");
|
|
65
|
+
expect(getConnection("agent-gamma")).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("removeConnection on unknown name is a no-op (does not throw)", () => {
|
|
69
|
+
expect(() => removeConnection("never-registered-xyz")).not.toThrow();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("setConnection overwrites an existing entry", () => {
|
|
73
|
+
const conn1 = makeConn();
|
|
74
|
+
const conn2 = makeConn();
|
|
75
|
+
usedNames.push("agent-delta");
|
|
76
|
+
setConnection("agent-delta", conn1);
|
|
77
|
+
setConnection("agent-delta", conn2);
|
|
78
|
+
expect(getConnection("agent-delta")).toBe(conn2);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("headless connection listener API", () => {
|
|
83
|
+
const usedNames: string[] = [];
|
|
84
|
+
const unsubscribers: Array<() => void> = [];
|
|
85
|
+
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
for (const unsub of unsubscribers.splice(0)) unsub();
|
|
88
|
+
for (const name of usedNames.splice(0)) {
|
|
89
|
+
if (getConnection(name)) {
|
|
90
|
+
removeConnection(name);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
function makeStdin(): { write(): Promise<number> } {
|
|
96
|
+
return { write: () => Promise.resolve(0) };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
test("onRegister fires when registerHeadlessConnection runs", () => {
|
|
100
|
+
const seen: Array<{ name: string }> = [];
|
|
101
|
+
unsubscribers.push(
|
|
102
|
+
addHeadlessConnectionListener({
|
|
103
|
+
onRegister(agentName) {
|
|
104
|
+
seen.push({ name: agentName });
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
usedNames.push("listener-agent-1");
|
|
110
|
+
registerHeadlessConnection("listener-agent-1", { pid: 99999, stdin: makeStdin() });
|
|
111
|
+
|
|
112
|
+
expect(seen).toEqual([{ name: "listener-agent-1" }]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("onRemove fires when a headless connection is removed", () => {
|
|
116
|
+
const removed: string[] = [];
|
|
117
|
+
unsubscribers.push(
|
|
118
|
+
addHeadlessConnectionListener({
|
|
119
|
+
onRegister: () => {},
|
|
120
|
+
onRemove(agentName) {
|
|
121
|
+
removed.push(agentName);
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
usedNames.push("listener-agent-2");
|
|
127
|
+
registerHeadlessConnection("listener-agent-2", { pid: 99999, stdin: makeStdin() });
|
|
128
|
+
removeConnection("listener-agent-2");
|
|
129
|
+
|
|
130
|
+
expect(removed).toEqual(["listener-agent-2"]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("onRemove does NOT fire for non-headless connections", () => {
|
|
134
|
+
const removed: string[] = [];
|
|
135
|
+
unsubscribers.push(
|
|
136
|
+
addHeadlessConnectionListener({
|
|
137
|
+
onRegister: () => {},
|
|
138
|
+
onRemove(agentName) {
|
|
139
|
+
removed.push(agentName);
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const conn: RuntimeConnection = {
|
|
145
|
+
sendPrompt: async () => {},
|
|
146
|
+
followUp: async () => {},
|
|
147
|
+
abort: async () => {},
|
|
148
|
+
getState: async (): Promise<ConnectionState> => ({ status: "idle" }),
|
|
149
|
+
close: () => {},
|
|
150
|
+
};
|
|
151
|
+
usedNames.push("non-headless-agent");
|
|
152
|
+
setConnection("non-headless-agent", conn);
|
|
153
|
+
removeConnection("non-headless-agent");
|
|
154
|
+
|
|
155
|
+
expect(removed).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("listener added after registration sees existing agents via catch-up", () => {
|
|
159
|
+
usedNames.push("preexisting-agent");
|
|
160
|
+
registerHeadlessConnection("preexisting-agent", { pid: 99999, stdin: makeStdin() });
|
|
161
|
+
|
|
162
|
+
const seen: string[] = [];
|
|
163
|
+
unsubscribers.push(
|
|
164
|
+
addHeadlessConnectionListener({
|
|
165
|
+
onRegister(agentName) {
|
|
166
|
+
seen.push(agentName);
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(seen).toContain("preexisting-agent");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("unsubscribe stops further notifications", () => {
|
|
175
|
+
const seen: string[] = [];
|
|
176
|
+
const unsub = addHeadlessConnectionListener({
|
|
177
|
+
onRegister(agentName) {
|
|
178
|
+
seen.push(agentName);
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
unsub();
|
|
182
|
+
|
|
183
|
+
usedNames.push("after-unsub-agent");
|
|
184
|
+
registerHeadlessConnection("after-unsub-agent", { pid: 99999, stdin: makeStdin() });
|
|
185
|
+
|
|
186
|
+
expect(seen).toEqual([]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("onRegister receives the stdin handle from the spawned process", async () => {
|
|
190
|
+
const writes: string[] = [];
|
|
191
|
+
const stdin = {
|
|
192
|
+
write(data: string | Uint8Array) {
|
|
193
|
+
writes.push(typeof data === "string" ? data : new TextDecoder().decode(data));
|
|
194
|
+
return Promise.resolve(0);
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const observed: Array<{ write(data: string | Uint8Array): number | Promise<number> }> = [];
|
|
199
|
+
unsubscribers.push(
|
|
200
|
+
addHeadlessConnectionListener({
|
|
201
|
+
onRegister(_agentName, s) {
|
|
202
|
+
observed.push(s);
|
|
203
|
+
},
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
usedNames.push("stdin-pass-agent");
|
|
208
|
+
registerHeadlessConnection("stdin-pass-agent", { pid: 99999, stdin });
|
|
209
|
+
expect(observed.length).toBe(1);
|
|
210
|
+
expect(observed[0]).toBe(stdin);
|
|
211
|
+
await observed[0]?.write("hello");
|
|
212
|
+
expect(writes).toEqual(["hello"]);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-level connection registry for active RuntimeConnection instances.
|
|
3
|
+
*
|
|
4
|
+
* Tracks RPC connections to headless agent processes (e.g., Sapling, headless Claude).
|
|
5
|
+
* Keyed by agent name — same namespace as AgentSession.agentName.
|
|
6
|
+
*
|
|
7
|
+
* Thread safety: single-threaded Bun runtime; no locking needed.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { HeadlessClaudeConnection } from "./headless-connection.ts";
|
|
11
|
+
import type { RuntimeConnection } from "./types.ts";
|
|
12
|
+
|
|
13
|
+
/** Writable handle exposed to headless connection listeners. */
|
|
14
|
+
export type HeadlessStdin = { write(data: string | Uint8Array): number | Promise<number> };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Listener fired on headless Claude connection lifecycle events.
|
|
18
|
+
* Fires only for connections created via registerHeadlessConnection() — Sapling
|
|
19
|
+
* and other RuntimeConnection registrants via setConnection() are not surfaced
|
|
20
|
+
* because their wire format differs from headless Claude's stream-json stdin.
|
|
21
|
+
*/
|
|
22
|
+
export interface HeadlessConnectionListener {
|
|
23
|
+
onRegister(agentName: string, stdin: HeadlessStdin): void;
|
|
24
|
+
onRemove?(agentName: string): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const connections = new Map<string, RuntimeConnection>();
|
|
28
|
+
const headlessAgents = new Map<string, HeadlessStdin>();
|
|
29
|
+
const listeners = new Set<HeadlessConnectionListener>();
|
|
30
|
+
|
|
31
|
+
/** Retrieve the active connection for a given agent, or undefined if none. */
|
|
32
|
+
export function getConnection(agentName: string): RuntimeConnection | undefined {
|
|
33
|
+
return connections.get(agentName);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Register a connection for a given agent. Overwrites any existing entry. */
|
|
37
|
+
export function setConnection(agentName: string, conn: RuntimeConnection): void {
|
|
38
|
+
connections.set(agentName, conn);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Remove the connection for a given agent, calling close() first.
|
|
43
|
+
* Safe to call if no connection exists (no-op).
|
|
44
|
+
*/
|
|
45
|
+
export function removeConnection(agentName: string): void {
|
|
46
|
+
const conn = connections.get(agentName);
|
|
47
|
+
if (!conn) return;
|
|
48
|
+
if (headlessAgents.delete(agentName)) {
|
|
49
|
+
for (const listener of listeners) {
|
|
50
|
+
listener.onRemove?.(agentName);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
conn.close();
|
|
54
|
+
connections.delete(agentName);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create a HeadlessClaudeConnection from a spawned process handle and register it.
|
|
59
|
+
*
|
|
60
|
+
* Called by spawnHeadlessAgent() (process.ts) when an agentName is provided.
|
|
61
|
+
* The registered connection is retrievable via getConnection(agentName) for
|
|
62
|
+
* follow-up delivery, state polling, and abort — all without tmux.
|
|
63
|
+
*
|
|
64
|
+
* This is the sibling registration path to Sapling's connect() flow. It does NOT
|
|
65
|
+
* generalize RpcProcessHandle and does NOT touch other runtime adapters.
|
|
66
|
+
*
|
|
67
|
+
* @param agentName - Unique agent identifier (same namespace as AgentSession.agentName)
|
|
68
|
+
* @param proc - Spawned headless process with pid and stdin
|
|
69
|
+
* @returns The newly created and registered RuntimeConnection
|
|
70
|
+
*/
|
|
71
|
+
export function registerHeadlessConnection(
|
|
72
|
+
agentName: string,
|
|
73
|
+
proc: { pid: number; stdin: HeadlessStdin },
|
|
74
|
+
): RuntimeConnection {
|
|
75
|
+
const conn = new HeadlessClaudeConnection(proc.pid, proc.stdin);
|
|
76
|
+
setConnection(agentName, conn);
|
|
77
|
+
headlessAgents.set(agentName, proc.stdin);
|
|
78
|
+
for (const listener of listeners) {
|
|
79
|
+
listener.onRegister(agentName, proc.stdin);
|
|
80
|
+
}
|
|
81
|
+
return conn;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Subscribe to headless Claude connection lifecycle events.
|
|
86
|
+
*
|
|
87
|
+
* onRegister fires synchronously after registerHeadlessConnection() inserts the
|
|
88
|
+
* connection. onRemove fires synchronously before removeConnection() closes the
|
|
89
|
+
* connection. Listeners observing already-registered agents at subscribe time
|
|
90
|
+
* receive an immediate onRegister for each — this lets late subscribers (e.g.,
|
|
91
|
+
* runServe started after agents already exist) catch up without rescanning.
|
|
92
|
+
*
|
|
93
|
+
* @returns Unsubscribe function that removes this listener.
|
|
94
|
+
*/
|
|
95
|
+
export function addHeadlessConnectionListener(listener: HeadlessConnectionListener): () => void {
|
|
96
|
+
listeners.add(listener);
|
|
97
|
+
for (const [agentName, stdin] of headlessAgents) {
|
|
98
|
+
listener.onRegister(agentName, stdin);
|
|
99
|
+
}
|
|
100
|
+
return () => {
|
|
101
|
+
listeners.delete(listener);
|
|
102
|
+
};
|
|
103
|
+
}
|