@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,85 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
isPersistentCapability,
|
|
4
|
+
isStopHookPersistentCapability,
|
|
5
|
+
isTaskScopedCapability,
|
|
6
|
+
PERSISTENT_CAPABILITIES,
|
|
7
|
+
STOP_HOOK_PERSISTENT_CAPABILITIES,
|
|
8
|
+
TASK_SCOPED_CAPABILITIES,
|
|
9
|
+
terminalMailTypesFor,
|
|
10
|
+
} from "./capabilities.ts";
|
|
11
|
+
|
|
12
|
+
describe("TASK_SCOPED_CAPABILITIES", () => {
|
|
13
|
+
test("contains the five worker capabilities", () => {
|
|
14
|
+
expect([...TASK_SCOPED_CAPABILITIES].sort()).toEqual([
|
|
15
|
+
"builder",
|
|
16
|
+
"lead",
|
|
17
|
+
"merger",
|
|
18
|
+
"reviewer",
|
|
19
|
+
"scout",
|
|
20
|
+
]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("excludes persistent capabilities", () => {
|
|
24
|
+
for (const c of ["coordinator", "orchestrator", "monitor", "supervisor"]) {
|
|
25
|
+
expect(isTaskScopedCapability(c)).toBe(false);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("terminalMailTypesFor", () => {
|
|
31
|
+
test("merger uses merged + merge_failed", () => {
|
|
32
|
+
expect(terminalMailTypesFor("merger")).toEqual(["merged", "merge_failed"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("builder/scout/reviewer/lead accept worker_done and result", () => {
|
|
36
|
+
// `result` is a legacy/drift fallback (agentplate-1a4c): models often pick
|
|
37
|
+
// `--type result` for their terminal summary because the prompts also use
|
|
38
|
+
// `result` for non-terminal updates elsewhere. Accepting both keeps the
|
|
39
|
+
// lifecycle unstuck without losing the canonical `worker_done` contract.
|
|
40
|
+
for (const c of ["builder", "scout", "reviewer", "lead"]) {
|
|
41
|
+
expect(terminalMailTypesFor(c)).toEqual(["worker_done", "result"]);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("unknown capability falls back to worker_done set", () => {
|
|
46
|
+
expect(terminalMailTypesFor("unknown")).toEqual(["worker_done", "result"]);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("PERSISTENT_CAPABILITIES", () => {
|
|
51
|
+
test("contains coordinator, orchestrator, monitor only", () => {
|
|
52
|
+
expect([...PERSISTENT_CAPABILITIES].sort()).toEqual(["coordinator", "monitor", "orchestrator"]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("excludes lead and other workers", () => {
|
|
56
|
+
for (const c of ["lead", "builder", "scout", "reviewer", "merger", "supervisor"]) {
|
|
57
|
+
expect(isPersistentCapability(c)).toBe(false);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("STOP_HOOK_PERSISTENT_CAPABILITIES", () => {
|
|
63
|
+
test("equals PERSISTENT_CAPABILITIES plus lead", () => {
|
|
64
|
+
// Tmux-mode leads span many model turns within a single dispatch
|
|
65
|
+
// (agentplate-49a7), so the per-turn Stop hook is NOT a "done" signal.
|
|
66
|
+
expect([...STOP_HOOK_PERSISTENT_CAPABILITIES].sort()).toEqual([
|
|
67
|
+
"coordinator",
|
|
68
|
+
"lead",
|
|
69
|
+
"monitor",
|
|
70
|
+
"orchestrator",
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("isStopHookPersistentCapability is true for lead and the persistent set", () => {
|
|
75
|
+
for (const c of ["coordinator", "orchestrator", "monitor", "lead"]) {
|
|
76
|
+
expect(isStopHookPersistentCapability(c)).toBe(true);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("excludes non-lead workers", () => {
|
|
81
|
+
for (const c of ["builder", "scout", "reviewer", "merger", "supervisor"]) {
|
|
82
|
+
expect(isStopHookPersistentCapability(c)).toBe(false);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared capability classification for the runtime layer.
|
|
3
|
+
*
|
|
4
|
+
* Three orthogonal classifications:
|
|
5
|
+
*
|
|
6
|
+
* - `TASK_SCOPED_CAPABILITIES` — worker capabilities that operate under the
|
|
7
|
+
* spawn-per-turn model. Each user turn (mail, nudge, initial dispatch)
|
|
8
|
+
* spawns a fresh claude with `--resume <session-id>`, processes the turn,
|
|
9
|
+
* and exits on stdin EOF. There is no persistent process between turns.
|
|
10
|
+
*
|
|
11
|
+
* - `PERSISTENT_CAPABILITIES` — capabilities that run as long-lived sessions
|
|
12
|
+
* for the lifetime of a run/operator interaction (coordinator, orchestrator,
|
|
13
|
+
* monitor). Used by the watchdog to exempt them from time-based stale/zombie
|
|
14
|
+
* detection and exclude them from run-completion accounting.
|
|
15
|
+
*
|
|
16
|
+
* - `STOP_HOOK_PERSISTENT_CAPABILITIES` — capabilities whose Claude Code Stop
|
|
17
|
+
* hook fires per model turn rather than once at process exit. The same as
|
|
18
|
+
* `PERSISTENT_CAPABILITIES` plus `lead`, because tmux-mode leads still span
|
|
19
|
+
* many model turns within a single dispatch (agentplate-49a7).
|
|
20
|
+
*
|
|
21
|
+
* Phase 4 (agentplate-2724) consolidated these definitions here as the single
|
|
22
|
+
* source of truth — previously they were duplicated across log.ts, health.ts,
|
|
23
|
+
* and daemon.ts with a load-bearing `lead` mismatch.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { Capability } from "../types.ts";
|
|
27
|
+
|
|
28
|
+
/** Worker capabilities driven by the spawn-per-turn engine. */
|
|
29
|
+
export const TASK_SCOPED_CAPABILITIES = new Set<Capability>([
|
|
30
|
+
"builder",
|
|
31
|
+
"scout",
|
|
32
|
+
"reviewer",
|
|
33
|
+
"merger",
|
|
34
|
+
"lead",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/** True iff `capability` is a task-scoped worker (drives spawn-per-turn). */
|
|
38
|
+
export function isTaskScopedCapability(capability: string): boolean {
|
|
39
|
+
return TASK_SCOPED_CAPABILITIES.has(capability as Capability);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Capabilities that run as long-lived sessions for the duration of a run /
|
|
44
|
+
* operator interaction.
|
|
45
|
+
*
|
|
46
|
+
* `coordinator` and `orchestrator` are operator-driven persistent agents (one
|
|
47
|
+
* tmux session per run / multi-run); `monitor` is a continuous fleet patrol
|
|
48
|
+
* with its own long-lived session. They are expected to have long idle
|
|
49
|
+
* periods (waiting for worker mail, operator input) and must NOT be flagged
|
|
50
|
+
* stale/zombie based on `lastActivity` — only tmux/pid liveness applies.
|
|
51
|
+
*
|
|
52
|
+
* Consumers:
|
|
53
|
+
* - `src/watchdog/health.ts` — exempt from time-based stale/zombie detection
|
|
54
|
+
* - `src/watchdog/daemon.ts` — excluded from run-completion accounting
|
|
55
|
+
*
|
|
56
|
+
* Note: `lead` is NOT in this set. Under spawn-per-turn (the default), each
|
|
57
|
+
* lead turn is its own short-lived process; under tmux mode the lead still
|
|
58
|
+
* runs to a terminal `worker_done` and exits, so it counts toward
|
|
59
|
+
* run-completion accounting. The per-model-turn nuance for tmux-mode leads
|
|
60
|
+
* is captured separately by `STOP_HOOK_PERSISTENT_CAPABILITIES`.
|
|
61
|
+
*/
|
|
62
|
+
export const PERSISTENT_CAPABILITIES = new Set<Capability>([
|
|
63
|
+
"coordinator",
|
|
64
|
+
"orchestrator",
|
|
65
|
+
"monitor",
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/** True iff `capability` is a long-lived persistent session. */
|
|
69
|
+
export function isPersistentCapability(capability: string): boolean {
|
|
70
|
+
return PERSISTENT_CAPABILITIES.has(capability as Capability);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Capabilities whose Claude Code Stop hook fires per **model turn** rather
|
|
75
|
+
* than once at process exit. Under tmux mode these agents have many model
|
|
76
|
+
* turns within a single dispatch, so a `session-end` event is NOT a reliable
|
|
77
|
+
* "agent done" signal — the hook fires every time the model finishes
|
|
78
|
+
* generating a response and waits for the next user turn.
|
|
79
|
+
*
|
|
80
|
+
* Equals `PERSISTENT_CAPABILITIES` plus `lead`. Tmux-mode leads delegate to
|
|
81
|
+
* sub-workers and process mail injection over many turns (agentplate-49a7);
|
|
82
|
+
* marking the lead `completed` on the first Stop hook fire makes it vanish
|
|
83
|
+
* from `getActive()` while its tmux process is still working.
|
|
84
|
+
*
|
|
85
|
+
* The tmux-mode-only nuance does not apply to spawn-per-turn (headless) leads:
|
|
86
|
+
* (a) they run one model turn per process; (b) `hooks-deployer.ts` drops the
|
|
87
|
+
* Stop hook entirely under `headlessOnly=true`, so this gate is a no-op for
|
|
88
|
+
* them. Including `lead` here is therefore safe in both modes.
|
|
89
|
+
*
|
|
90
|
+
* Consumer:
|
|
91
|
+
* - `src/commands/log.ts` — guards `transitionToCompleted`,
|
|
92
|
+
* `autoRecordExpertise`, and `appendOutcomeToAppliedRecords` so they do
|
|
93
|
+
* not fire on every turn boundary.
|
|
94
|
+
*/
|
|
95
|
+
export const STOP_HOOK_PERSISTENT_CAPABILITIES = new Set<Capability>([
|
|
96
|
+
...PERSISTENT_CAPABILITIES,
|
|
97
|
+
"lead" as Capability,
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
/** True iff `capability`'s Stop hook is per-model-turn (not per-session-exit). */
|
|
101
|
+
export function isStopHookPersistentCapability(capability: string): boolean {
|
|
102
|
+
return STOP_HOOK_PERSISTENT_CAPABILITIES.has(capability as Capability);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Mail types that signal an agent's terminal action for its capability.
|
|
107
|
+
*
|
|
108
|
+
* The turn-runner watches for any of these from the agent during a turn; when
|
|
109
|
+
* observed alongside a clean `result` event, the agent is transitioned to
|
|
110
|
+
* `completed`. Capabilities not listed here use the worker_done set by default.
|
|
111
|
+
*
|
|
112
|
+
* - builder | scout | reviewer | lead → `worker_done` (canonical) or `result`
|
|
113
|
+
* (legacy/drift fallback). Agent prompts treat `worker_done` as the
|
|
114
|
+
* completion signal, but the model frequently picks `result` because it is
|
|
115
|
+
* also a valid mail type used for non-terminal summaries elsewhere in the
|
|
116
|
+
* protocol. Accepting `result` keeps the lifecycle from getting stuck on
|
|
117
|
+
* prompt drift (agentplate-1a4c). Combined with `cleanResult` (claude exited
|
|
118
|
+
* cleanly at end-of-turn), this is safe: an interim `result` mid-turn does
|
|
119
|
+
* not transition the agent because the turn has not ended yet.
|
|
120
|
+
* - merger → `merged` (success) or `merge_failed` (failure)
|
|
121
|
+
*/
|
|
122
|
+
export function terminalMailTypesFor(capability: string): readonly string[] {
|
|
123
|
+
if (capability === "merger") return ["merged", "merge_failed"];
|
|
124
|
+
return ["worker_done", "result"];
|
|
125
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { cleanupTempDir } from "../test-helpers.ts";
|
|
6
|
+
import type { SessionCheckpoint } from "../types.ts";
|
|
7
|
+
import { clearCheckpoint, loadCheckpoint, saveCheckpoint } from "./checkpoint.ts";
|
|
8
|
+
|
|
9
|
+
function makeCheckpoint(overrides?: Partial<SessionCheckpoint>): SessionCheckpoint {
|
|
10
|
+
return {
|
|
11
|
+
agentName: "test-agent",
|
|
12
|
+
taskId: "agentplate-abc1",
|
|
13
|
+
sessionId: "session-001",
|
|
14
|
+
timestamp: "2025-01-01T00:00:00.000Z",
|
|
15
|
+
progressSummary: "Implemented checkpoint module",
|
|
16
|
+
filesModified: ["src/agents/checkpoint.ts"],
|
|
17
|
+
currentBranch: "agentplate/test-agent/agentplate-abc1",
|
|
18
|
+
pendingWork: "Write tests",
|
|
19
|
+
loamDomains: ["agents"],
|
|
20
|
+
...overrides,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("checkpoint", () => {
|
|
25
|
+
let agentsDir: string;
|
|
26
|
+
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
agentsDir = await mkdtemp(join(tmpdir(), "agentplate-checkpoint-test-"));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
await cleanupTempDir(agentsDir);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("save and load a checkpoint", async () => {
|
|
36
|
+
const checkpoint = makeCheckpoint();
|
|
37
|
+
|
|
38
|
+
await saveCheckpoint(agentsDir, checkpoint);
|
|
39
|
+
const loaded = await loadCheckpoint(agentsDir, "test-agent");
|
|
40
|
+
|
|
41
|
+
expect(loaded).not.toBeNull();
|
|
42
|
+
expect(loaded?.agentName).toBe("test-agent");
|
|
43
|
+
expect(loaded?.taskId).toBe("agentplate-abc1");
|
|
44
|
+
expect(loaded?.sessionId).toBe("session-001");
|
|
45
|
+
expect(loaded?.progressSummary).toBe("Implemented checkpoint module");
|
|
46
|
+
expect(loaded?.filesModified).toEqual(["src/agents/checkpoint.ts"]);
|
|
47
|
+
expect(loaded?.currentBranch).toBe("agentplate/test-agent/agentplate-abc1");
|
|
48
|
+
expect(loaded?.pendingWork).toBe("Write tests");
|
|
49
|
+
expect(loaded?.loamDomains).toEqual(["agents"]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("load returns null when no checkpoint exists", async () => {
|
|
53
|
+
const result = await loadCheckpoint(agentsDir, "nonexistent-agent");
|
|
54
|
+
expect(result).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("clear removes the checkpoint file", async () => {
|
|
58
|
+
const checkpoint = makeCheckpoint();
|
|
59
|
+
|
|
60
|
+
await saveCheckpoint(agentsDir, checkpoint);
|
|
61
|
+
const before = await loadCheckpoint(agentsDir, "test-agent");
|
|
62
|
+
expect(before).not.toBeNull();
|
|
63
|
+
|
|
64
|
+
await clearCheckpoint(agentsDir, "test-agent");
|
|
65
|
+
const after = await loadCheckpoint(agentsDir, "test-agent");
|
|
66
|
+
expect(after).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("clear does not error when file does not exist", async () => {
|
|
70
|
+
// Should not throw
|
|
71
|
+
await clearCheckpoint(agentsDir, "nonexistent-agent");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("overwrite existing checkpoint", async () => {
|
|
75
|
+
const first = makeCheckpoint({ progressSummary: "First pass" });
|
|
76
|
+
await saveCheckpoint(agentsDir, first);
|
|
77
|
+
|
|
78
|
+
const second = makeCheckpoint({
|
|
79
|
+
progressSummary: "Second pass",
|
|
80
|
+
filesModified: ["src/agents/checkpoint.ts", "src/agents/lifecycle.ts"],
|
|
81
|
+
});
|
|
82
|
+
await saveCheckpoint(agentsDir, second);
|
|
83
|
+
|
|
84
|
+
const loaded = await loadCheckpoint(agentsDir, "test-agent");
|
|
85
|
+
expect(loaded?.progressSummary).toBe("Second pass");
|
|
86
|
+
expect(loaded?.filesModified).toEqual(["src/agents/checkpoint.ts", "src/agents/lifecycle.ts"]);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { mkdir, unlink } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { LifecycleError } from "../errors.ts";
|
|
4
|
+
import type { SessionCheckpoint } from "../types.ts";
|
|
5
|
+
|
|
6
|
+
const CHECKPOINT_FILENAME = "checkpoint.json";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Save a session checkpoint to disk.
|
|
10
|
+
*
|
|
11
|
+
* Writes to `{agentsDir}/{checkpoint.agentName}/checkpoint.json`.
|
|
12
|
+
* Creates the directory if it doesn't exist.
|
|
13
|
+
*/
|
|
14
|
+
export async function saveCheckpoint(
|
|
15
|
+
agentsDir: string,
|
|
16
|
+
checkpoint: SessionCheckpoint,
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
const filePath = join(agentsDir, checkpoint.agentName, CHECKPOINT_FILENAME);
|
|
19
|
+
const dir = dirname(filePath);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await mkdir(dir, { recursive: true });
|
|
23
|
+
} catch (err) {
|
|
24
|
+
throw new LifecycleError(`Failed to create checkpoint directory: ${dir}`, {
|
|
25
|
+
agentName: checkpoint.agentName,
|
|
26
|
+
sessionId: checkpoint.sessionId,
|
|
27
|
+
cause: err instanceof Error ? err : undefined,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await Bun.write(filePath, `${JSON.stringify(checkpoint, null, "\t")}\n`);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
throw new LifecycleError(`Failed to write checkpoint: ${filePath}`, {
|
|
35
|
+
agentName: checkpoint.agentName,
|
|
36
|
+
sessionId: checkpoint.sessionId,
|
|
37
|
+
cause: err instanceof Error ? err : undefined,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Load a session checkpoint from disk.
|
|
44
|
+
*
|
|
45
|
+
* Reads from `{agentsDir}/{agentName}/checkpoint.json`.
|
|
46
|
+
* Returns null if the file doesn't exist.
|
|
47
|
+
*/
|
|
48
|
+
export async function loadCheckpoint(
|
|
49
|
+
agentsDir: string,
|
|
50
|
+
agentName: string,
|
|
51
|
+
): Promise<SessionCheckpoint | null> {
|
|
52
|
+
const filePath = join(agentsDir, agentName, CHECKPOINT_FILENAME);
|
|
53
|
+
const file = Bun.file(filePath);
|
|
54
|
+
const exists = await file.exists();
|
|
55
|
+
|
|
56
|
+
if (!exists) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let text: string;
|
|
61
|
+
try {
|
|
62
|
+
text = await file.text();
|
|
63
|
+
} catch (err) {
|
|
64
|
+
throw new LifecycleError(`Failed to read checkpoint: ${filePath}`, {
|
|
65
|
+
agentName,
|
|
66
|
+
cause: err instanceof Error ? err : undefined,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(text) as SessionCheckpoint;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
throw new LifecycleError(`Failed to parse checkpoint JSON: ${filePath}`, {
|
|
74
|
+
agentName,
|
|
75
|
+
cause: err instanceof Error ? err : undefined,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Clear (delete) a session checkpoint from disk.
|
|
82
|
+
*
|
|
83
|
+
* Removes `{agentsDir}/{agentName}/checkpoint.json`.
|
|
84
|
+
* No error if the file doesn't exist.
|
|
85
|
+
*/
|
|
86
|
+
export async function clearCheckpoint(agentsDir: string, agentName: string): Promise<void> {
|
|
87
|
+
const filePath = join(agentsDir, agentName, CHECKPOINT_FILENAME);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await unlink(filePath);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// ENOENT means file doesn't exist — that's fine
|
|
93
|
+
if (err instanceof Error && "code" in err && (err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
throw new LifecycleError(`Failed to clear checkpoint: ${filePath}`, {
|
|
97
|
+
agentName,
|
|
98
|
+
cause: err instanceof Error ? err : undefined,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { cleanupTempDir } from "../test-helpers.ts";
|
|
6
|
+
import { deployCopilotHooks } from "./copilot-hooks-deployer.ts";
|
|
7
|
+
import { PATH_PREFIX } from "./hooks-deployer.ts";
|
|
8
|
+
|
|
9
|
+
describe("deployCopilotHooks", () => {
|
|
10
|
+
let tempDir: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
tempDir = await mkdtemp(join(tmpdir(), "agentplate-copilot-hooks-test-"));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
await cleanupTempDir(tempDir);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("writes hooks.json to .github/hooks/ directory", async () => {
|
|
21
|
+
const worktreePath = join(tempDir, "worktree");
|
|
22
|
+
await deployCopilotHooks(worktreePath, "my-builder");
|
|
23
|
+
|
|
24
|
+
const hooksPath = join(worktreePath, ".github", "hooks", "hooks.json");
|
|
25
|
+
const exists = await Bun.file(hooksPath).exists();
|
|
26
|
+
expect(exists).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("creates .github/hooks/ directory if it does not exist", async () => {
|
|
30
|
+
const worktreePath = join(tempDir, "new-worktree");
|
|
31
|
+
// Directory does not exist before the call
|
|
32
|
+
await deployCopilotHooks(worktreePath, "builder-1");
|
|
33
|
+
|
|
34
|
+
const hooksPath = join(worktreePath, ".github", "hooks", "hooks.json");
|
|
35
|
+
expect(await Bun.file(hooksPath).exists()).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("output file is valid JSON", async () => {
|
|
39
|
+
const worktreePath = join(tempDir, "worktree");
|
|
40
|
+
await deployCopilotHooks(worktreePath, "test-agent");
|
|
41
|
+
|
|
42
|
+
const hooksPath = join(worktreePath, ".github", "hooks", "hooks.json");
|
|
43
|
+
const raw = await Bun.file(hooksPath).text();
|
|
44
|
+
expect(() => JSON.parse(raw)).not.toThrow();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("output has Copilot schema structure (top-level hooks with onSessionStart)", async () => {
|
|
48
|
+
const worktreePath = join(tempDir, "worktree");
|
|
49
|
+
await deployCopilotHooks(worktreePath, "test-agent");
|
|
50
|
+
|
|
51
|
+
const hooksPath = join(worktreePath, ".github", "hooks", "hooks.json");
|
|
52
|
+
const config = JSON.parse(await Bun.file(hooksPath).text()) as Record<string, unknown>;
|
|
53
|
+
|
|
54
|
+
expect(config).toHaveProperty("hooks");
|
|
55
|
+
const hooks = config.hooks as Record<string, unknown>;
|
|
56
|
+
expect(hooks).toHaveProperty("onSessionStart");
|
|
57
|
+
expect(Array.isArray(hooks.onSessionStart)).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("replaces {{AGENT_NAME}} with agentName in all commands", async () => {
|
|
61
|
+
const worktreePath = join(tempDir, "worktree");
|
|
62
|
+
await deployCopilotHooks(worktreePath, "scout-agent-42");
|
|
63
|
+
|
|
64
|
+
const hooksPath = join(worktreePath, ".github", "hooks", "hooks.json");
|
|
65
|
+
const raw = await Bun.file(hooksPath).text();
|
|
66
|
+
|
|
67
|
+
expect(raw).toContain("scout-agent-42");
|
|
68
|
+
expect(raw).not.toContain("{{AGENT_NAME}}");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("prepends PATH_PREFIX to all hook commands", async () => {
|
|
72
|
+
const worktreePath = join(tempDir, "worktree");
|
|
73
|
+
await deployCopilotHooks(worktreePath, "builder-1");
|
|
74
|
+
|
|
75
|
+
const hooksPath = join(worktreePath, ".github", "hooks", "hooks.json");
|
|
76
|
+
const config = JSON.parse(await Bun.file(hooksPath).text()) as {
|
|
77
|
+
hooks: Record<string, Array<{ command: string }>>;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const allCommands = Object.values(config.hooks)
|
|
81
|
+
.flat()
|
|
82
|
+
.map((e) => e.command);
|
|
83
|
+
expect(allCommands.length).toBeGreaterThan(0);
|
|
84
|
+
for (const cmd of allCommands) {
|
|
85
|
+
expect(cmd).toStartWith(PATH_PREFIX);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("onSessionStart entries are objects with command field only (no matcher, no type)", async () => {
|
|
90
|
+
const worktreePath = join(tempDir, "worktree");
|
|
91
|
+
await deployCopilotHooks(worktreePath, "builder-1");
|
|
92
|
+
|
|
93
|
+
const hooksPath = join(worktreePath, ".github", "hooks", "hooks.json");
|
|
94
|
+
const config = JSON.parse(await Bun.file(hooksPath).text()) as {
|
|
95
|
+
hooks: { onSessionStart: Array<Record<string, unknown>> };
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
for (const entry of config.hooks.onSessionStart) {
|
|
99
|
+
expect(typeof entry.command).toBe("string");
|
|
100
|
+
// Copilot schema has no matcher or type fields
|
|
101
|
+
expect(entry).not.toHaveProperty("matcher");
|
|
102
|
+
expect(entry).not.toHaveProperty("type");
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("onSessionStart includes ap prime command", async () => {
|
|
107
|
+
const worktreePath = join(tempDir, "worktree");
|
|
108
|
+
await deployCopilotHooks(worktreePath, "prime-test-agent");
|
|
109
|
+
|
|
110
|
+
const hooksPath = join(worktreePath, ".github", "hooks", "hooks.json");
|
|
111
|
+
const config = JSON.parse(await Bun.file(hooksPath).text()) as {
|
|
112
|
+
hooks: { onSessionStart: Array<{ command: string }> };
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const commands = config.hooks.onSessionStart.map((e) => e.command);
|
|
116
|
+
expect(commands.some((c) => c.includes("ap prime") && c.includes("prime-test-agent"))).toBe(
|
|
117
|
+
true,
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("onSessionStart includes ap mail check --inject command", async () => {
|
|
122
|
+
const worktreePath = join(tempDir, "worktree");
|
|
123
|
+
await deployCopilotHooks(worktreePath, "mail-test-agent");
|
|
124
|
+
|
|
125
|
+
const hooksPath = join(worktreePath, ".github", "hooks", "hooks.json");
|
|
126
|
+
const config = JSON.parse(await Bun.file(hooksPath).text()) as {
|
|
127
|
+
hooks: { onSessionStart: Array<{ command: string }> };
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const commands = config.hooks.onSessionStart.map((e) => e.command);
|
|
131
|
+
expect(
|
|
132
|
+
commands.some((c) => c.includes("ap mail check --inject") && c.includes("mail-test-agent")),
|
|
133
|
+
).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("all hook commands include ENV_GUARD pattern", async () => {
|
|
137
|
+
const worktreePath = join(tempDir, "worktree");
|
|
138
|
+
await deployCopilotHooks(worktreePath, "guard-test-agent");
|
|
139
|
+
|
|
140
|
+
const hooksPath = join(worktreePath, ".github", "hooks", "hooks.json");
|
|
141
|
+
const config = JSON.parse(await Bun.file(hooksPath).text()) as {
|
|
142
|
+
hooks: Record<string, Array<{ command: string }>>;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const allCommands = Object.values(config.hooks)
|
|
146
|
+
.flat()
|
|
147
|
+
.map((e) => e.command);
|
|
148
|
+
for (const cmd of allCommands) {
|
|
149
|
+
expect(cmd).toContain("AGENTPLATE_AGENT_NAME");
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("template file exists and is valid JSON after substitution", async () => {
|
|
154
|
+
// Verify template file is present and parseable (basic template health check).
|
|
155
|
+
const templatePath = join(import.meta.dir, "..", "..", "templates", "copilot-hooks.json.tmpl");
|
|
156
|
+
const exists = await Bun.file(templatePath).exists();
|
|
157
|
+
expect(exists).toBe(true);
|
|
158
|
+
|
|
159
|
+
const raw = (await Bun.file(templatePath).text()).replace(/\{\{AGENT_NAME\}\}/g, "test");
|
|
160
|
+
expect(() => JSON.parse(raw)).not.toThrow();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { AgentError } from "../errors.ts";
|
|
4
|
+
import { PATH_PREFIX } from "./hooks-deployer.ts";
|
|
5
|
+
|
|
6
|
+
/** Copilot hook entry shape — simpler than Claude Code (no matcher, no type field). */
|
|
7
|
+
interface CopilotHookEntry {
|
|
8
|
+
command: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the path to the Copilot hooks template file.
|
|
13
|
+
* The template lives at `templates/copilot-hooks.json.tmpl` relative to the repo root.
|
|
14
|
+
*/
|
|
15
|
+
function getTemplatePath(): string {
|
|
16
|
+
// src/agents/copilot-hooks-deployer.ts -> repo root is ../../
|
|
17
|
+
return join(dirname(import.meta.dir), "..", "templates", "copilot-hooks.json.tmpl");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Deploy Copilot lifecycle hooks to an agent's worktree.
|
|
22
|
+
*
|
|
23
|
+
* Reads `templates/copilot-hooks.json.tmpl`, replaces all `{{AGENT_NAME}}` tokens,
|
|
24
|
+
* prepends PATH_PREFIX to every hook command so CLIs (ap, lm, sr) resolve correctly
|
|
25
|
+
* under Copilot's minimal PATH, then writes the result to
|
|
26
|
+
* `<worktreePath>/.github/hooks/hooks.json`.
|
|
27
|
+
*
|
|
28
|
+
* Phase 1: lifecycle hooks only (onSessionStart). No security guards.
|
|
29
|
+
*
|
|
30
|
+
* @param worktreePath - Absolute path to the agent's git worktree
|
|
31
|
+
* @param agentName - The unique name of the agent (replaces {{AGENT_NAME}} in template)
|
|
32
|
+
* @throws {AgentError} If the template is missing or the write fails
|
|
33
|
+
*/
|
|
34
|
+
export async function deployCopilotHooks(worktreePath: string, agentName: string): Promise<void> {
|
|
35
|
+
const templatePath = getTemplatePath();
|
|
36
|
+
const file = Bun.file(templatePath);
|
|
37
|
+
const exists = await file.exists();
|
|
38
|
+
|
|
39
|
+
if (!exists) {
|
|
40
|
+
throw new AgentError(`Copilot hooks template not found: ${templatePath}`, {
|
|
41
|
+
agentName,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let template: string;
|
|
46
|
+
try {
|
|
47
|
+
template = await file.text();
|
|
48
|
+
} catch (err) {
|
|
49
|
+
throw new AgentError(`Failed to read Copilot hooks template: ${templatePath}`, {
|
|
50
|
+
agentName,
|
|
51
|
+
cause: err instanceof Error ? err : undefined,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Replace all occurrences of {{AGENT_NAME}}
|
|
56
|
+
let content = template;
|
|
57
|
+
while (content.includes("{{AGENT_NAME}}")) {
|
|
58
|
+
content = content.replace("{{AGENT_NAME}}", agentName);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Parse the base config from the template
|
|
62
|
+
const config = JSON.parse(content) as { hooks: Record<string, CopilotHookEntry[]> };
|
|
63
|
+
|
|
64
|
+
// Extend PATH in all hook commands.
|
|
65
|
+
// Copilot CLI executes hooks with a minimal PATH — ~/.bun/bin (where ap, lm, sr live)
|
|
66
|
+
// is not included. Prepend PATH_PREFIX so CLIs resolve correctly.
|
|
67
|
+
for (const entries of Object.values(config.hooks)) {
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
entry.command = `${PATH_PREFIX} ${entry.command}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const hooksDir = join(worktreePath, ".github", "hooks");
|
|
74
|
+
const outputPath = join(hooksDir, "hooks.json");
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
await mkdir(hooksDir, { recursive: true });
|
|
78
|
+
} catch (err) {
|
|
79
|
+
throw new AgentError(`Failed to create .github/hooks/ directory at: ${hooksDir}`, {
|
|
80
|
+
agentName,
|
|
81
|
+
cause: err instanceof Error ? err : undefined,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await Bun.write(outputPath, `${JSON.stringify(config, null, "\t")}\n`);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
throw new AgentError(`Failed to write Copilot hooks config to: ${outputPath}`, {
|
|
89
|
+
agentName,
|
|
90
|
+
cause: err instanceof Error ? err : undefined,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|