@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,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side mail dispatcher for spawn-per-turn headless agents.
|
|
3
|
+
*
|
|
4
|
+
* In tmux mode, the UserPromptSubmit hook fires `ap mail check --inject` before
|
|
5
|
+
* each prompt, delivering new mail to the agent. In headless spawn-per-turn
|
|
6
|
+
* mode there is no persistent process — `ap serve` polls the mail store and,
|
|
7
|
+
* when unread mail appears for an agent, drives a fresh `runTurn` that spawns
|
|
8
|
+
* claude with `--resume <session-id>`, writes the batched user turn to a real
|
|
9
|
+
* stdin pipe, and exits when claude does.
|
|
10
|
+
*
|
|
11
|
+
* This module exports `startTurnRunnerMailLoop` (the dispatcher loop) and
|
|
12
|
+
* `_runTurnRunnerTick` (a single-tick variant for deterministic tests).
|
|
13
|
+
*
|
|
14
|
+
* State authority (agentplate-3087): this module does NOT write session state.
|
|
15
|
+
* The turn-runner (`src/agents/turn-runner.ts`) is the sole authority for
|
|
16
|
+
* `in_turn` ↔ `between_turns` transitions — it writes `in_turn` on the first
|
|
17
|
+
* parser event of a turn and settles to `between_turns` at end-of-turn when
|
|
18
|
+
* the agent did not deliver a terminal mail. Adding a duplicate writer here
|
|
19
|
+
* would race with the turn-runner under the per-agent turn lock and make
|
|
20
|
+
* the substate non-deterministic.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { createMailStore } from "../mail/store.ts";
|
|
24
|
+
import type { MailMessage } from "../types.ts";
|
|
25
|
+
import { encodeUserTurn } from "./headless-prompt.ts";
|
|
26
|
+
import type { RunTurnOpts, TurnResult } from "./turn-runner.ts";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Escape characters that would otherwise corrupt the `[MAIL] From: ... | Subject: ... |
|
|
30
|
+
* Priority: ...\n\n<body>` framing. `|` is the field delimiter and `\n\n` separates
|
|
31
|
+
* metadata from body, so an unescaped pipe or newline in a metadata value would let a
|
|
32
|
+
* crafted subject inject a fake field or smuggle a fake body. Backslash is escaped
|
|
33
|
+
* first so the escape sequence itself is unambiguous (agentplate-2231).
|
|
34
|
+
*/
|
|
35
|
+
function escapeMailMetadata(value: string): string {
|
|
36
|
+
return value
|
|
37
|
+
.replace(/\\/g, "\\\\")
|
|
38
|
+
.replace(/\|/g, "\\|")
|
|
39
|
+
.replace(/\r/g, "\\r")
|
|
40
|
+
.replace(/\n/g, "\\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Format a batch of unread messages into the user-turn text the agent receives.
|
|
45
|
+
* Metadata values are escaped so a hostile or human-authored subject can't break
|
|
46
|
+
* the line framing.
|
|
47
|
+
*/
|
|
48
|
+
export function formatMailBatch(messages: readonly MailMessage[]): string {
|
|
49
|
+
return messages
|
|
50
|
+
.map(
|
|
51
|
+
(m) =>
|
|
52
|
+
`[MAIL] From: ${escapeMailMetadata(m.from)} | Subject: ${escapeMailMetadata(
|
|
53
|
+
m.subject,
|
|
54
|
+
)} | Priority: ${escapeMailMetadata(m.priority)}\n\n${m.body}`,
|
|
55
|
+
)
|
|
56
|
+
.join("\n\n---\n\n");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build the runTurn opts for delivering a user turn (Phase 2 builder dispatcher).
|
|
61
|
+
*
|
|
62
|
+
* The injector polls mail for a single agent and only knows the agent name,
|
|
63
|
+
* the user-turn payload, and the mail database path. The remaining fields
|
|
64
|
+
* (worktree path, runtime, model, run id, etc.) are provided by the caller
|
|
65
|
+
* (typically `ap serve`) once at install time. This factory produces a
|
|
66
|
+
* `RunTurnOpts` for each batch by combining the static caller-provided
|
|
67
|
+
* fields with the per-batch payload.
|
|
68
|
+
*/
|
|
69
|
+
export type TurnRunnerOptsFactory = (userTurnNdjson: string) => RunTurnOpts;
|
|
70
|
+
|
|
71
|
+
/** Function that drives a single agent turn end-to-end. Production passes `runTurn`. */
|
|
72
|
+
export type TurnRunnerFn = (opts: RunTurnOpts) => Promise<TurnResult>;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Outcome of a single dispatcher tick. Returned for testability so callers
|
|
76
|
+
* can assert delivery behavior without inspecting the runner internals.
|
|
77
|
+
*/
|
|
78
|
+
export type TurnRunnerTickResult =
|
|
79
|
+
| { kind: "idle" }
|
|
80
|
+
| { kind: "in-flight" }
|
|
81
|
+
| { kind: "delivered"; result: TurnResult; messageIds: string[] }
|
|
82
|
+
| { kind: "error"; error: unknown; messageIds: string[] };
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Start a server-side mail dispatcher that drives the spawn-per-turn engine.
|
|
86
|
+
*
|
|
87
|
+
* Phase 2 builder path. Polls the mail store every intervalMs milliseconds,
|
|
88
|
+
* batches unread messages into a single stream-json user turn, and invokes
|
|
89
|
+
* `runTurn(...)` to spawn one claude turn that consumes them. While a turn
|
|
90
|
+
* is in flight, subsequent ticks short-circuit — they never spawn a second
|
|
91
|
+
* claude process for the same agent. Per-agent serialization is also enforced
|
|
92
|
+
* cross-process by the turn-lock inside `runTurn`.
|
|
93
|
+
*
|
|
94
|
+
* Mark-as-read happens AFTER the runTurn returns successfully (`exitCode === 0`
|
|
95
|
+
* and no thrown error). On any failure, messages remain unread and will be
|
|
96
|
+
* retried on the next tick.
|
|
97
|
+
*
|
|
98
|
+
* @param agentName - Agentplate agent name (mail inbox address)
|
|
99
|
+
* @param optsFactory - Builds the RunTurnOpts from the per-batch user turn payload
|
|
100
|
+
* @param runTurnFn - Function that drives one turn (typically `runTurn` from turn-runner.ts)
|
|
101
|
+
* @param mailStorePath - Absolute path to the project's mail.db
|
|
102
|
+
* @param intervalMs - Poll interval in milliseconds (default: 2000)
|
|
103
|
+
* @param isAgentLive - Optional per-tick predicate. When provided and it returns
|
|
104
|
+
* false, the loop short-circuits (no mail dispatch) and self-terminates.
|
|
105
|
+
* This closes the gap between `ap stop` writing state=completed and the
|
|
106
|
+
* serve.ts rescan timer reaping this loop, which would otherwise keep
|
|
107
|
+
* ticking and dispatch a new turn against a stopped agent (agentplate-eb7c).
|
|
108
|
+
* @returns Cleanup function that stops the dispatcher
|
|
109
|
+
*/
|
|
110
|
+
export function startTurnRunnerMailLoop(
|
|
111
|
+
agentName: string,
|
|
112
|
+
optsFactory: TurnRunnerOptsFactory,
|
|
113
|
+
runTurnFn: TurnRunnerFn,
|
|
114
|
+
mailStorePath: string,
|
|
115
|
+
intervalMs = 2000,
|
|
116
|
+
isAgentLive?: () => boolean,
|
|
117
|
+
): () => void {
|
|
118
|
+
let stopped = false;
|
|
119
|
+
let inFlight = false;
|
|
120
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
121
|
+
|
|
122
|
+
const stop = (): void => {
|
|
123
|
+
stopped = true;
|
|
124
|
+
if (timer !== null) {
|
|
125
|
+
clearInterval(timer);
|
|
126
|
+
timer = null;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const tick = async (): Promise<TurnRunnerTickResult> => {
|
|
131
|
+
if (stopped) return { kind: "idle" };
|
|
132
|
+
if (inFlight) return { kind: "in-flight" };
|
|
133
|
+
// Per-tick state guard. `ap stop` flips state=completed and kills the
|
|
134
|
+
// in-flight claude, but until the rescan reaps this loop the next tick
|
|
135
|
+
// would otherwise dispatch a fresh turn against the stopped agent.
|
|
136
|
+
if (isAgentLive && !isAgentLive()) {
|
|
137
|
+
stop();
|
|
138
|
+
return { kind: "idle" };
|
|
139
|
+
}
|
|
140
|
+
const store = createMailStore(mailStorePath);
|
|
141
|
+
let messages: ReturnType<typeof store.getUnread>;
|
|
142
|
+
try {
|
|
143
|
+
messages = store.getUnread(agentName);
|
|
144
|
+
} finally {
|
|
145
|
+
store.close();
|
|
146
|
+
}
|
|
147
|
+
if (messages.length === 0) return { kind: "idle" };
|
|
148
|
+
|
|
149
|
+
const userTurnNdjson = encodeUserTurn(formatMailBatch(messages));
|
|
150
|
+
const ids = messages.map((m) => m.id);
|
|
151
|
+
|
|
152
|
+
inFlight = true;
|
|
153
|
+
try {
|
|
154
|
+
const result = await runTurnFn(optsFactory(userTurnNdjson));
|
|
155
|
+
// Mark read only on a clean turn — exit code 0 (or null on abort with
|
|
156
|
+
// no error) AND no thrown error. Failed turns leave messages unread
|
|
157
|
+
// so the next tick retries cleanly.
|
|
158
|
+
if (result.exitCode === 0) {
|
|
159
|
+
const markStore = createMailStore(mailStorePath);
|
|
160
|
+
try {
|
|
161
|
+
for (const id of ids) markStore.markRead(id);
|
|
162
|
+
} finally {
|
|
163
|
+
markStore.close();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { kind: "delivered", result, messageIds: ids };
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return { kind: "error", error, messageIds: ids };
|
|
169
|
+
} finally {
|
|
170
|
+
inFlight = false;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
timer = setInterval(() => {
|
|
175
|
+
// Errors and rejections are absorbed inside tick; this layer just
|
|
176
|
+
// prevents an unhandled-rejection if tick itself throws synchronously.
|
|
177
|
+
tick().catch(() => {});
|
|
178
|
+
}, intervalMs);
|
|
179
|
+
|
|
180
|
+
return stop;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Mail dispatcher for persistent headless agents (coordinator/orchestrator/monitor).
|
|
185
|
+
*
|
|
186
|
+
* Persistent capabilities run as a single long-lived claude process — unlike
|
|
187
|
+
* task-scoped workers, there is no spawn-per-turn engine to reach them. Mail
|
|
188
|
+
* delivery instead writes directly to the live process's stdin via the
|
|
189
|
+
* registered HeadlessClaudeConnection. Without this loop, mail sent to a
|
|
190
|
+
* headless coordinator (e.g. operator messages from `ap serve`'s UI, worker
|
|
191
|
+
* `worker_done`/`merge_ready` mails) lands in `mail.db` but is never seen by
|
|
192
|
+
* the agent — `installMailInjectors`'s spawn-per-turn loop filters out
|
|
193
|
+
* persistent capabilities (agentplate-b03a).
|
|
194
|
+
*
|
|
195
|
+
* The loop polls every `intervalMs` ms. Each tick:
|
|
196
|
+
* 1. Resolve the live `RuntimeConnection` via `getConn(agentName)`. If the
|
|
197
|
+
* connection has been removed (agent stopped), self-terminate.
|
|
198
|
+
* 2. If `isAgentLive()` reports terminal state, self-terminate.
|
|
199
|
+
* 3. Read unread mail for the agent. If empty, return idle.
|
|
200
|
+
* 4. Format the batch as a stream-json user turn, write to the connection
|
|
201
|
+
* via `followUp()`, then mark messages read.
|
|
202
|
+
*
|
|
203
|
+
* The write is best-effort: Claude Code may not pick up stdin until the
|
|
204
|
+
* current model turn finishes (see `HeadlessClaudeConnection.nudge` notes),
|
|
205
|
+
* but the data sits in the pipe buffer and gets consumed at the next turn
|
|
206
|
+
* boundary. Marking-read happens unconditionally after a successful write —
|
|
207
|
+
* the agent processes batches in arrival order and we do not want to keep
|
|
208
|
+
* re-injecting the same messages on every tick.
|
|
209
|
+
*/
|
|
210
|
+
export interface PersistentMailLoopDeps {
|
|
211
|
+
/** Resolve the live connection (returns undefined when none registered). */
|
|
212
|
+
getConn: (agentName: string) => { followUp(text: string): Promise<void> } | undefined;
|
|
213
|
+
/** Optional liveness predicate; when false, the loop stops itself. */
|
|
214
|
+
isAgentLive?: () => boolean;
|
|
215
|
+
/** Poll interval in ms. Default: 2000. */
|
|
216
|
+
intervalMs?: number;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export type PersistentMailTickResult =
|
|
220
|
+
| { kind: "idle" }
|
|
221
|
+
| { kind: "no-connection" }
|
|
222
|
+
| { kind: "agent-stopped" }
|
|
223
|
+
| { kind: "delivered"; messageIds: string[] }
|
|
224
|
+
| { kind: "error"; error: unknown; messageIds: string[] };
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Start the persistent-mail dispatcher for one agent. Returns the stop function.
|
|
228
|
+
* The caller owns the connection lifecycle — when the agent terminates, the
|
|
229
|
+
* caller is responsible for invoking the returned stop function (or letting
|
|
230
|
+
* the `isAgentLive` predicate self-terminate the loop).
|
|
231
|
+
*/
|
|
232
|
+
export function startPersistentMailLoop(
|
|
233
|
+
agentName: string,
|
|
234
|
+
mailStorePath: string,
|
|
235
|
+
deps: PersistentMailLoopDeps,
|
|
236
|
+
): () => void {
|
|
237
|
+
let stopped = false;
|
|
238
|
+
let inFlight = false;
|
|
239
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
240
|
+
const intervalMs = deps.intervalMs ?? 2000;
|
|
241
|
+
|
|
242
|
+
const stop = (): void => {
|
|
243
|
+
stopped = true;
|
|
244
|
+
if (timer !== null) {
|
|
245
|
+
clearInterval(timer);
|
|
246
|
+
timer = null;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const tick = async (): Promise<PersistentMailTickResult> => {
|
|
251
|
+
if (stopped) return { kind: "idle" };
|
|
252
|
+
if (inFlight) return { kind: "idle" };
|
|
253
|
+
if (deps.isAgentLive && !deps.isAgentLive()) {
|
|
254
|
+
stop();
|
|
255
|
+
return { kind: "agent-stopped" };
|
|
256
|
+
}
|
|
257
|
+
const conn = deps.getConn(agentName);
|
|
258
|
+
if (conn === undefined) {
|
|
259
|
+
// Connection dropped — caller will reap us via session rescan, but
|
|
260
|
+
// short-circuit until then so we don't spin reading mail we can't
|
|
261
|
+
// deliver.
|
|
262
|
+
return { kind: "no-connection" };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const store = createMailStore(mailStorePath);
|
|
266
|
+
let messages: ReturnType<typeof store.getUnread>;
|
|
267
|
+
try {
|
|
268
|
+
messages = store.getUnread(agentName);
|
|
269
|
+
} finally {
|
|
270
|
+
store.close();
|
|
271
|
+
}
|
|
272
|
+
if (messages.length === 0) return { kind: "idle" };
|
|
273
|
+
|
|
274
|
+
const userTurn = encodeUserTurn(formatMailBatch(messages));
|
|
275
|
+
const ids = messages.map((m) => m.id);
|
|
276
|
+
|
|
277
|
+
inFlight = true;
|
|
278
|
+
try {
|
|
279
|
+
await conn.followUp(userTurn);
|
|
280
|
+
const markStore = createMailStore(mailStorePath);
|
|
281
|
+
try {
|
|
282
|
+
for (const id of ids) markStore.markRead(id);
|
|
283
|
+
} finally {
|
|
284
|
+
markStore.close();
|
|
285
|
+
}
|
|
286
|
+
return { kind: "delivered", messageIds: ids };
|
|
287
|
+
} catch (error) {
|
|
288
|
+
return { kind: "error", error, messageIds: ids };
|
|
289
|
+
} finally {
|
|
290
|
+
inFlight = false;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
timer = setInterval(() => {
|
|
295
|
+
tick().catch(() => {});
|
|
296
|
+
}, intervalMs);
|
|
297
|
+
|
|
298
|
+
return stop;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Internal: single-tick variant of `startPersistentMailLoop` for deterministic tests.
|
|
303
|
+
*/
|
|
304
|
+
export async function _runPersistentMailTick(
|
|
305
|
+
agentName: string,
|
|
306
|
+
mailStorePath: string,
|
|
307
|
+
deps: PersistentMailLoopDeps,
|
|
308
|
+
): Promise<PersistentMailTickResult> {
|
|
309
|
+
if (deps.isAgentLive && !deps.isAgentLive()) {
|
|
310
|
+
return { kind: "agent-stopped" };
|
|
311
|
+
}
|
|
312
|
+
const conn = deps.getConn(agentName);
|
|
313
|
+
if (conn === undefined) return { kind: "no-connection" };
|
|
314
|
+
|
|
315
|
+
const store = createMailStore(mailStorePath);
|
|
316
|
+
let messages: ReturnType<typeof store.getUnread>;
|
|
317
|
+
try {
|
|
318
|
+
messages = store.getUnread(agentName);
|
|
319
|
+
} finally {
|
|
320
|
+
store.close();
|
|
321
|
+
}
|
|
322
|
+
if (messages.length === 0) return { kind: "idle" };
|
|
323
|
+
|
|
324
|
+
const userTurn = encodeUserTurn(formatMailBatch(messages));
|
|
325
|
+
const ids = messages.map((m) => m.id);
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
await conn.followUp(userTurn);
|
|
329
|
+
const markStore = createMailStore(mailStorePath);
|
|
330
|
+
try {
|
|
331
|
+
for (const id of ids) markStore.markRead(id);
|
|
332
|
+
} finally {
|
|
333
|
+
markStore.close();
|
|
334
|
+
}
|
|
335
|
+
return { kind: "delivered", messageIds: ids };
|
|
336
|
+
} catch (error) {
|
|
337
|
+
return { kind: "error", error, messageIds: ids };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Internal: run a single dispatcher tick. Exported for tests so they can
|
|
343
|
+
* drive the loop deterministically without setInterval timing.
|
|
344
|
+
*/
|
|
345
|
+
export async function _runTurnRunnerTick(
|
|
346
|
+
agentName: string,
|
|
347
|
+
optsFactory: TurnRunnerOptsFactory,
|
|
348
|
+
runTurnFn: TurnRunnerFn,
|
|
349
|
+
mailStorePath: string,
|
|
350
|
+
): Promise<TurnRunnerTickResult> {
|
|
351
|
+
const store = createMailStore(mailStorePath);
|
|
352
|
+
let messages: ReturnType<typeof store.getUnread>;
|
|
353
|
+
try {
|
|
354
|
+
messages = store.getUnread(agentName);
|
|
355
|
+
} finally {
|
|
356
|
+
store.close();
|
|
357
|
+
}
|
|
358
|
+
if (messages.length === 0) return { kind: "idle" };
|
|
359
|
+
|
|
360
|
+
const userTurnNdjson = encodeUserTurn(formatMailBatch(messages));
|
|
361
|
+
const ids = messages.map((m) => m.id);
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const result = await runTurnFn(optsFactory(userTurnNdjson));
|
|
365
|
+
if (result.exitCode === 0) {
|
|
366
|
+
const markStore = createMailStore(mailStorePath);
|
|
367
|
+
try {
|
|
368
|
+
for (const id of ids) markStore.markRead(id);
|
|
369
|
+
} finally {
|
|
370
|
+
markStore.close();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return { kind: "delivered", result, messageIds: ids };
|
|
374
|
+
} catch (error) {
|
|
375
|
+
return { kind: "error", error, messageIds: ids };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
buildInitialHeadlessPrompt,
|
|
4
|
+
encodeUserTurn,
|
|
5
|
+
formatMailSection,
|
|
6
|
+
} from "./headless-prompt.ts";
|
|
7
|
+
|
|
8
|
+
describe("encodeUserTurn", () => {
|
|
9
|
+
test("produces a valid NDJSON line", () => {
|
|
10
|
+
const line = encodeUserTurn("hello world");
|
|
11
|
+
expect(line).toEndWith("\n");
|
|
12
|
+
const parsed = JSON.parse(line.trim());
|
|
13
|
+
expect(parsed.type).toBe("user");
|
|
14
|
+
expect(parsed.message.role).toBe("user");
|
|
15
|
+
expect(parsed.message.content).toHaveLength(1);
|
|
16
|
+
expect(parsed.message.content[0].type).toBe("text");
|
|
17
|
+
expect(parsed.message.content[0].text).toBe("hello world");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("handles multi-line text", () => {
|
|
21
|
+
const text = "line one\nline two\nline three";
|
|
22
|
+
const line = encodeUserTurn(text);
|
|
23
|
+
const parsed = JSON.parse(line.trim());
|
|
24
|
+
expect(parsed.message.content[0].text).toBe(text);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("formatMailSection", () => {
|
|
29
|
+
test("returns empty string for no messages", () => {
|
|
30
|
+
expect(formatMailSection([])).toBe("");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("formats a single message", () => {
|
|
34
|
+
const result = formatMailSection([
|
|
35
|
+
{ from: "coordinator", subject: "dispatch", priority: "normal", body: "Start working." },
|
|
36
|
+
]);
|
|
37
|
+
expect(result).toContain("[MAIL] From: coordinator");
|
|
38
|
+
expect(result).toContain("Subject: dispatch");
|
|
39
|
+
expect(result).toContain("Start working.");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("separates multiple messages with dividers", () => {
|
|
43
|
+
const result = formatMailSection([
|
|
44
|
+
{ from: "lead", subject: "task-1", priority: "high", body: "First task." },
|
|
45
|
+
{ from: "orchestrator", subject: "context", priority: "low", body: "Extra context." },
|
|
46
|
+
]);
|
|
47
|
+
expect(result).toContain("---");
|
|
48
|
+
expect(result).toContain("First task.");
|
|
49
|
+
expect(result).toContain("Extra context.");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("buildInitialHeadlessPrompt", () => {
|
|
54
|
+
test("combines all three sections", () => {
|
|
55
|
+
const result = buildInitialHeadlessPrompt(
|
|
56
|
+
"## Prime Context\nExpertise here.",
|
|
57
|
+
"[MAIL] From: orchestrator | Subject: dispatch\n\nDo the thing.",
|
|
58
|
+
"Read your overlay and begin immediately.",
|
|
59
|
+
);
|
|
60
|
+
const parsed = JSON.parse(result.trim());
|
|
61
|
+
const text: string = parsed.message.content[0].text;
|
|
62
|
+
expect(text).toContain("Prime Context");
|
|
63
|
+
expect(text).toContain("[MAIL]");
|
|
64
|
+
expect(text).toContain("Read your overlay and begin immediately.");
|
|
65
|
+
expect(text).toContain("---");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("omits primeContext when undefined", () => {
|
|
69
|
+
const result = buildInitialHeadlessPrompt(
|
|
70
|
+
undefined,
|
|
71
|
+
"[MAIL] From: lead | Subject: dispatch\n\nTask body.",
|
|
72
|
+
"Begin.",
|
|
73
|
+
);
|
|
74
|
+
const parsed = JSON.parse(result.trim());
|
|
75
|
+
const text: string = parsed.message.content[0].text;
|
|
76
|
+
expect(text).not.toContain("Prime Context");
|
|
77
|
+
expect(text).toContain("[MAIL]");
|
|
78
|
+
expect(text).toContain("Begin.");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("omits dispatchMail when undefined", () => {
|
|
82
|
+
const result = buildInitialHeadlessPrompt("## Prime Context", undefined, "Begin.");
|
|
83
|
+
const parsed = JSON.parse(result.trim());
|
|
84
|
+
const text: string = parsed.message.content[0].text;
|
|
85
|
+
expect(text).toContain("Prime Context");
|
|
86
|
+
expect(text).not.toContain("[MAIL]");
|
|
87
|
+
expect(text).toContain("Begin.");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("always includes beacon even when other sections are empty", () => {
|
|
91
|
+
const result = buildInitialHeadlessPrompt(undefined, undefined, "Start now.");
|
|
92
|
+
const parsed = JSON.parse(result.trim());
|
|
93
|
+
const text: string = parsed.message.content[0].text;
|
|
94
|
+
expect(text).toBe("Start now.");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("output is valid NDJSON ending with newline", () => {
|
|
98
|
+
const result = buildInitialHeadlessPrompt("ctx", "mail", "go");
|
|
99
|
+
expect(result).toEndWith("\n");
|
|
100
|
+
expect(() => JSON.parse(result.trim())).not.toThrow();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the initial stdin prompt for a headless Claude Code agent.
|
|
3
|
+
*
|
|
4
|
+
* In headless mode (--input-format stream-json), the orchestrator owns stdin.
|
|
5
|
+
* Rather than relying on SessionStart hooks (which don't fire in headless mode),
|
|
6
|
+
* the orchestrator writes the prime context, pending dispatch mail, and activation
|
|
7
|
+
* beacon as the agent's first user turn immediately after spawn.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Encode text as a stream-json user turn for Claude Code's --input-format stream-json.
|
|
12
|
+
*
|
|
13
|
+
* Format matches the Claude Code headless stdin protocol:
|
|
14
|
+
* {"type":"user","message":{"role":"user","content":[{"type":"text","text":"..."}]}}
|
|
15
|
+
*/
|
|
16
|
+
export function encodeUserTurn(text: string): string {
|
|
17
|
+
const message = {
|
|
18
|
+
type: "user",
|
|
19
|
+
message: { role: "user", content: [{ type: "text", text }] },
|
|
20
|
+
};
|
|
21
|
+
return `${JSON.stringify(message)}\n`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build the initial stdin prompt for a headless Claude agent.
|
|
26
|
+
*
|
|
27
|
+
* Combines prime context (loam expertise, session state), pending dispatch mail,
|
|
28
|
+
* and the activation beacon into a single user turn. Replaces the SessionStart
|
|
29
|
+
* hook equivalents (ap prime + ap mail check --inject) for headless agents.
|
|
30
|
+
*
|
|
31
|
+
* Sections are separated by "---" dividers. Empty sections are omitted.
|
|
32
|
+
*
|
|
33
|
+
* @param primeContext - Output of `ap prime --agent <name>` (may be empty/undefined)
|
|
34
|
+
* @param dispatchMail - Pre-formatted dispatch mail body (may be empty/undefined)
|
|
35
|
+
* @param beacon - Activation phrase sent via tmux send-keys in interactive mode
|
|
36
|
+
* @returns NDJSON line ready to write to the agent's stdin
|
|
37
|
+
*/
|
|
38
|
+
export function buildInitialHeadlessPrompt(
|
|
39
|
+
primeContext: string | undefined,
|
|
40
|
+
dispatchMail: string | undefined,
|
|
41
|
+
beacon: string,
|
|
42
|
+
): string {
|
|
43
|
+
const parts: string[] = [];
|
|
44
|
+
if (primeContext) parts.push(primeContext);
|
|
45
|
+
if (dispatchMail) parts.push(dispatchMail);
|
|
46
|
+
parts.push(beacon);
|
|
47
|
+
|
|
48
|
+
const text = parts.join("\n\n---\n\n");
|
|
49
|
+
return encodeUserTurn(text);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Format a list of pending mail messages as a dispatch mail section.
|
|
54
|
+
*
|
|
55
|
+
* Used to inline pending inbox messages into the initial stdin prompt so
|
|
56
|
+
* the agent starts with all pre-dispatch mail already in context.
|
|
57
|
+
*/
|
|
58
|
+
export function formatMailSection(
|
|
59
|
+
messages: ReadonlyArray<{ from: string; subject: string; priority: string; body: string }>,
|
|
60
|
+
): string {
|
|
61
|
+
if (messages.length === 0) return "";
|
|
62
|
+
return messages
|
|
63
|
+
.map(
|
|
64
|
+
(m) =>
|
|
65
|
+
`[MAIL] From: ${m.from} | Subject: ${m.subject} | Priority: ${m.priority}\n\n${m.body}`,
|
|
66
|
+
)
|
|
67
|
+
.join("\n\n---\n\n");
|
|
68
|
+
}
|