@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,1297 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, realpathSync } from "node:fs";
|
|
3
|
+
import { mkdir, mkdtemp } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { ValidationError } from "../errors.ts";
|
|
7
|
+
import { createSessionStore } from "../sessions/store.ts";
|
|
8
|
+
import { cleanupTempDir, commitFile, createTempGitRepo, runGitInDir } from "../test-helpers.ts";
|
|
9
|
+
import type { AgentSession } from "../types.ts";
|
|
10
|
+
import { createWorktree } from "../worktree/manager.ts";
|
|
11
|
+
import { checkLiveChildren, worktreeCommand } from "./worktree.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Tests for `agentplate worktree` command.
|
|
15
|
+
*
|
|
16
|
+
* Uses real git worktrees in temp repos to test list and clean subcommands.
|
|
17
|
+
* Captures process.stdout.write to verify output formatting.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
describe("worktreeCommand", () => {
|
|
21
|
+
let chunks: string[];
|
|
22
|
+
let originalWrite: typeof process.stdout.write;
|
|
23
|
+
let tempDir: string;
|
|
24
|
+
let originalCwd: string;
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
// Spy on stdout
|
|
28
|
+
chunks = [];
|
|
29
|
+
originalWrite = process.stdout.write;
|
|
30
|
+
process.stdout.write = ((chunk: string) => {
|
|
31
|
+
chunks.push(chunk);
|
|
32
|
+
return true;
|
|
33
|
+
}) as typeof process.stdout.write;
|
|
34
|
+
|
|
35
|
+
// Create temp git repo with .agentplate/config.yaml structure
|
|
36
|
+
tempDir = await createTempGitRepo();
|
|
37
|
+
// Normalize tempDir to resolve macOS /var -> /private/var symlink
|
|
38
|
+
tempDir = realpathSync(tempDir);
|
|
39
|
+
const agentplateDir = join(tempDir, ".agentplate");
|
|
40
|
+
await mkdir(agentplateDir, { recursive: true });
|
|
41
|
+
await Bun.write(
|
|
42
|
+
join(agentplateDir, "config.yaml"),
|
|
43
|
+
`project:\n name: test\n root: ${tempDir}\n canonicalBranch: main\n`,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Change to temp dir so loadConfig() works
|
|
47
|
+
originalCwd = process.cwd();
|
|
48
|
+
process.chdir(tempDir);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(async () => {
|
|
52
|
+
process.stdout.write = originalWrite;
|
|
53
|
+
process.chdir(originalCwd);
|
|
54
|
+
await cleanupTempDir(tempDir);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function output(): string {
|
|
58
|
+
return chunks.join("");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Helper to create an AgentSession with sensible defaults.
|
|
63
|
+
* Uses FAKE tmux session names to avoid real tmux calls during tests.
|
|
64
|
+
*/
|
|
65
|
+
function makeSession(overrides: Partial<AgentSession> = {}): AgentSession {
|
|
66
|
+
return {
|
|
67
|
+
id: "session-test",
|
|
68
|
+
agentName: "test-agent",
|
|
69
|
+
capability: "builder",
|
|
70
|
+
worktreePath: join(tempDir, ".agentplate", "worktrees", "test-agent"),
|
|
71
|
+
branchName: "agentplate/test-agent/task-1",
|
|
72
|
+
taskId: "task-1",
|
|
73
|
+
tmuxSession: "agentplate-test-agent-fake", // FAKE tmux session name
|
|
74
|
+
state: "working",
|
|
75
|
+
pid: 12345,
|
|
76
|
+
parentAgent: null,
|
|
77
|
+
depth: 0,
|
|
78
|
+
runId: null,
|
|
79
|
+
startedAt: new Date().toISOString(),
|
|
80
|
+
lastActivity: new Date().toISOString(),
|
|
81
|
+
escalationLevel: 0,
|
|
82
|
+
stalledSince: null,
|
|
83
|
+
transcriptPath: null,
|
|
84
|
+
...overrides,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Helper to write sessions to SessionStore (sessions.db) in the temp repo.
|
|
90
|
+
*/
|
|
91
|
+
function writeSessionsToStore(sessions: AgentSession[]): void {
|
|
92
|
+
const dbPath = join(tempDir, ".agentplate", "sessions.db");
|
|
93
|
+
const store = createSessionStore(dbPath);
|
|
94
|
+
for (const session of sessions) {
|
|
95
|
+
store.upsert(session);
|
|
96
|
+
}
|
|
97
|
+
store.close();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
describe("help flags", () => {
|
|
101
|
+
test("--help shows help text", async () => {
|
|
102
|
+
await worktreeCommand(["--help"]);
|
|
103
|
+
const out = output();
|
|
104
|
+
|
|
105
|
+
expect(out).toContain("worktree");
|
|
106
|
+
expect(out).toContain("list");
|
|
107
|
+
expect(out).toContain("clean");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("-h shows help text", async () => {
|
|
111
|
+
await worktreeCommand(["-h"]);
|
|
112
|
+
const out = output();
|
|
113
|
+
|
|
114
|
+
expect(out).toContain("worktree");
|
|
115
|
+
expect(out).toContain("list");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("validation", () => {
|
|
120
|
+
test("unknown subcommand throws ValidationError", async () => {
|
|
121
|
+
await expect(worktreeCommand(["unknown"])).rejects.toThrow(ValidationError);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("empty args shows help text", async () => {
|
|
125
|
+
await worktreeCommand([]);
|
|
126
|
+
const out = output();
|
|
127
|
+
expect(out).toContain("worktree");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("worktree list", () => {
|
|
132
|
+
test("no agentplate worktrees returns empty message", async () => {
|
|
133
|
+
await worktreeCommand(["list"]);
|
|
134
|
+
const out = output();
|
|
135
|
+
|
|
136
|
+
expect(out).toContain("No agent worktrees found");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("with agentplate worktrees lists them with agent info", async () => {
|
|
140
|
+
// Create a real git worktree with agentplate/ prefix branch
|
|
141
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
142
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
143
|
+
|
|
144
|
+
const worktreePath = join(worktreesDir, "test-agent");
|
|
145
|
+
await runGitInDir(tempDir, [
|
|
146
|
+
"worktree",
|
|
147
|
+
"add",
|
|
148
|
+
worktreePath,
|
|
149
|
+
"-b",
|
|
150
|
+
"agentplate/test-agent/task-1",
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
// Write sessions.db to associate worktree with agent
|
|
154
|
+
writeSessionsToStore([
|
|
155
|
+
{
|
|
156
|
+
id: "session-1",
|
|
157
|
+
agentName: "test-agent",
|
|
158
|
+
capability: "builder",
|
|
159
|
+
worktreePath,
|
|
160
|
+
branchName: "agentplate/test-agent/task-1",
|
|
161
|
+
taskId: "task-1",
|
|
162
|
+
tmuxSession: "agentplate-test-agent",
|
|
163
|
+
state: "working",
|
|
164
|
+
pid: 12345,
|
|
165
|
+
parentAgent: null,
|
|
166
|
+
depth: 0,
|
|
167
|
+
runId: null,
|
|
168
|
+
startedAt: new Date().toISOString(),
|
|
169
|
+
lastActivity: new Date().toISOString(),
|
|
170
|
+
escalationLevel: 0,
|
|
171
|
+
stalledSince: null,
|
|
172
|
+
transcriptPath: null,
|
|
173
|
+
},
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
await worktreeCommand(["list"]);
|
|
177
|
+
const out = output();
|
|
178
|
+
|
|
179
|
+
expect(out).toContain("Agent worktrees: 1");
|
|
180
|
+
expect(out).toContain("agentplate/test-agent/task-1");
|
|
181
|
+
expect(out).toContain("Agent: test-agent");
|
|
182
|
+
expect(out).toContain("State: working");
|
|
183
|
+
expect(out).toContain("Task: task-1");
|
|
184
|
+
expect(out).toContain(`Path: ${worktreePath}`);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("--json flag outputs valid JSON array", async () => {
|
|
188
|
+
// Create a real git worktree
|
|
189
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
190
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
191
|
+
|
|
192
|
+
const worktreePath = join(worktreesDir, "test-agent");
|
|
193
|
+
await runGitInDir(tempDir, [
|
|
194
|
+
"worktree",
|
|
195
|
+
"add",
|
|
196
|
+
worktreePath,
|
|
197
|
+
"-b",
|
|
198
|
+
"agentplate/test-agent/task-1",
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
// Write sessions.db
|
|
202
|
+
writeSessionsToStore([
|
|
203
|
+
{
|
|
204
|
+
id: "session-1",
|
|
205
|
+
agentName: "test-agent",
|
|
206
|
+
capability: "builder",
|
|
207
|
+
worktreePath,
|
|
208
|
+
branchName: "agentplate/test-agent/task-1",
|
|
209
|
+
taskId: "task-1",
|
|
210
|
+
tmuxSession: "agentplate-test-agent",
|
|
211
|
+
state: "working",
|
|
212
|
+
pid: 12345,
|
|
213
|
+
parentAgent: null,
|
|
214
|
+
depth: 0,
|
|
215
|
+
runId: null,
|
|
216
|
+
startedAt: new Date().toISOString(),
|
|
217
|
+
lastActivity: new Date().toISOString(),
|
|
218
|
+
escalationLevel: 0,
|
|
219
|
+
stalledSince: null,
|
|
220
|
+
transcriptPath: null,
|
|
221
|
+
},
|
|
222
|
+
]);
|
|
223
|
+
|
|
224
|
+
await worktreeCommand(["list", "--json"]);
|
|
225
|
+
const out = output();
|
|
226
|
+
|
|
227
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
228
|
+
success: boolean;
|
|
229
|
+
command: string;
|
|
230
|
+
worktrees: Array<{
|
|
231
|
+
path: string;
|
|
232
|
+
branch: string;
|
|
233
|
+
head: string;
|
|
234
|
+
agentName: string | null;
|
|
235
|
+
state: string | null;
|
|
236
|
+
taskId: string | null;
|
|
237
|
+
}>;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
expect(parsed.success).toBe(true);
|
|
241
|
+
expect(parsed.command).toBe("worktree list");
|
|
242
|
+
expect(parsed.worktrees).toHaveLength(1);
|
|
243
|
+
expect(parsed.worktrees[0]?.path).toBe(worktreePath);
|
|
244
|
+
expect(parsed.worktrees[0]?.branch).toBe("agentplate/test-agent/task-1");
|
|
245
|
+
expect(parsed.worktrees[0]?.agentName).toBe("test-agent");
|
|
246
|
+
expect(parsed.worktrees[0]?.state).toBe("working");
|
|
247
|
+
expect(parsed.worktrees[0]?.taskId).toBe("task-1");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("worktrees without sessions show unknown state", async () => {
|
|
251
|
+
// Create a worktree but no sessions.db entry
|
|
252
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
253
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
254
|
+
|
|
255
|
+
const worktreePath = join(worktreesDir, "orphan-agent");
|
|
256
|
+
await runGitInDir(tempDir, [
|
|
257
|
+
"worktree",
|
|
258
|
+
"add",
|
|
259
|
+
worktreePath,
|
|
260
|
+
"-b",
|
|
261
|
+
"agentplate/orphan-agent/task-2",
|
|
262
|
+
]);
|
|
263
|
+
|
|
264
|
+
await worktreeCommand(["list"]);
|
|
265
|
+
const out = output();
|
|
266
|
+
|
|
267
|
+
expect(out).toContain("agentplate/orphan-agent/task-2");
|
|
268
|
+
expect(out).toContain("Agent: ?");
|
|
269
|
+
expect(out).toContain("State: unknown");
|
|
270
|
+
expect(out).toContain("Task: ?");
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe("worktree clean", () => {
|
|
275
|
+
test("no agentplate worktrees returns empty message", async () => {
|
|
276
|
+
await worktreeCommand(["clean"]);
|
|
277
|
+
const out = output();
|
|
278
|
+
|
|
279
|
+
expect(out).toContain("No worktrees to clean");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("with completed agent worktree removes it and reports count", async () => {
|
|
283
|
+
// Create a real git worktree
|
|
284
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
285
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
286
|
+
|
|
287
|
+
const worktreePath = join(worktreesDir, "completed-agent");
|
|
288
|
+
await runGitInDir(tempDir, [
|
|
289
|
+
"worktree",
|
|
290
|
+
"add",
|
|
291
|
+
worktreePath,
|
|
292
|
+
"-b",
|
|
293
|
+
"agentplate/completed-agent/task-done",
|
|
294
|
+
]);
|
|
295
|
+
|
|
296
|
+
// Write sessions.db with completed state
|
|
297
|
+
writeSessionsToStore([
|
|
298
|
+
{
|
|
299
|
+
id: "session-1",
|
|
300
|
+
agentName: "completed-agent",
|
|
301
|
+
capability: "builder",
|
|
302
|
+
worktreePath,
|
|
303
|
+
branchName: "agentplate/completed-agent/task-done",
|
|
304
|
+
taskId: "task-done",
|
|
305
|
+
tmuxSession: "agentplate-completed-agent",
|
|
306
|
+
state: "completed",
|
|
307
|
+
pid: 12345,
|
|
308
|
+
parentAgent: null,
|
|
309
|
+
depth: 0,
|
|
310
|
+
runId: null,
|
|
311
|
+
startedAt: new Date().toISOString(),
|
|
312
|
+
lastActivity: new Date().toISOString(),
|
|
313
|
+
escalationLevel: 0,
|
|
314
|
+
stalledSince: null,
|
|
315
|
+
transcriptPath: null,
|
|
316
|
+
},
|
|
317
|
+
]);
|
|
318
|
+
|
|
319
|
+
await worktreeCommand(["clean"]);
|
|
320
|
+
const out = output();
|
|
321
|
+
|
|
322
|
+
expect(out).toContain("Removed");
|
|
323
|
+
expect(out).toContain("agentplate/completed-agent/task-done");
|
|
324
|
+
expect(out).toContain("Cleaned 1 worktree");
|
|
325
|
+
|
|
326
|
+
// Verify the worktree directory is gone
|
|
327
|
+
const worktreeExists = await Bun.file(worktreePath).exists();
|
|
328
|
+
expect(worktreeExists).toBe(false);
|
|
329
|
+
|
|
330
|
+
// Verify the branch is deleted
|
|
331
|
+
const branchListProc = Bun.spawn(
|
|
332
|
+
["git", "branch", "--list", "agentplate/completed-agent/*"],
|
|
333
|
+
{
|
|
334
|
+
cwd: tempDir,
|
|
335
|
+
stdout: "pipe",
|
|
336
|
+
},
|
|
337
|
+
);
|
|
338
|
+
const branchList = await new Response(branchListProc.stdout).text();
|
|
339
|
+
expect(branchList.trim()).toBe("");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("--json flag returns JSON with cleaned/failed/pruned arrays", async () => {
|
|
343
|
+
// Create a completed worktree
|
|
344
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
345
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
346
|
+
|
|
347
|
+
const worktreePath = join(worktreesDir, "done-agent");
|
|
348
|
+
await runGitInDir(tempDir, [
|
|
349
|
+
"worktree",
|
|
350
|
+
"add",
|
|
351
|
+
worktreePath,
|
|
352
|
+
"-b",
|
|
353
|
+
"agentplate/done-agent/task-x",
|
|
354
|
+
]);
|
|
355
|
+
|
|
356
|
+
writeSessionsToStore([
|
|
357
|
+
{
|
|
358
|
+
id: "session-1",
|
|
359
|
+
agentName: "done-agent",
|
|
360
|
+
capability: "builder",
|
|
361
|
+
worktreePath,
|
|
362
|
+
branchName: "agentplate/done-agent/task-x",
|
|
363
|
+
taskId: "task-x",
|
|
364
|
+
tmuxSession: "agentplate-done-agent",
|
|
365
|
+
state: "completed",
|
|
366
|
+
pid: 12345,
|
|
367
|
+
parentAgent: null,
|
|
368
|
+
depth: 0,
|
|
369
|
+
runId: null,
|
|
370
|
+
startedAt: new Date().toISOString(),
|
|
371
|
+
lastActivity: new Date().toISOString(),
|
|
372
|
+
escalationLevel: 0,
|
|
373
|
+
stalledSince: null,
|
|
374
|
+
transcriptPath: null,
|
|
375
|
+
},
|
|
376
|
+
]);
|
|
377
|
+
|
|
378
|
+
await worktreeCommand(["clean", "--json"]);
|
|
379
|
+
const out = output();
|
|
380
|
+
|
|
381
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
382
|
+
cleaned: string[];
|
|
383
|
+
failed: string[];
|
|
384
|
+
pruned: number;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
expect(parsed.cleaned).toEqual(["agentplate/done-agent/task-x"]);
|
|
388
|
+
expect(parsed.failed).toEqual([]);
|
|
389
|
+
expect(parsed.pruned).toBe(1); // The zombie session was pruned
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("zombie sessions whose worktree paths no longer exist get pruned from sessions.db", async () => {
|
|
393
|
+
// Create sessions.db with a zombie entry whose worktree doesn't exist
|
|
394
|
+
const nonExistentPath = join(tempDir, ".agentplate", "worktrees", "ghost-agent");
|
|
395
|
+
writeSessionsToStore([
|
|
396
|
+
{
|
|
397
|
+
id: "session-ghost",
|
|
398
|
+
agentName: "ghost-agent",
|
|
399
|
+
capability: "builder",
|
|
400
|
+
worktreePath: nonExistentPath,
|
|
401
|
+
branchName: "agentplate/ghost-agent/task-ghost",
|
|
402
|
+
taskId: "task-ghost",
|
|
403
|
+
tmuxSession: "agentplate-ghost-agent",
|
|
404
|
+
state: "zombie",
|
|
405
|
+
pid: null,
|
|
406
|
+
parentAgent: null,
|
|
407
|
+
depth: 0,
|
|
408
|
+
runId: null,
|
|
409
|
+
startedAt: new Date().toISOString(),
|
|
410
|
+
lastActivity: new Date().toISOString(),
|
|
411
|
+
escalationLevel: 0,
|
|
412
|
+
stalledSince: null,
|
|
413
|
+
transcriptPath: null,
|
|
414
|
+
},
|
|
415
|
+
]);
|
|
416
|
+
|
|
417
|
+
await worktreeCommand(["clean", "--json"]);
|
|
418
|
+
const out = output();
|
|
419
|
+
|
|
420
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
421
|
+
cleaned: string[];
|
|
422
|
+
failed: string[];
|
|
423
|
+
pruned: number;
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
expect(parsed.pruned).toBe(1);
|
|
427
|
+
|
|
428
|
+
// Verify sessions.db no longer contains the zombie
|
|
429
|
+
const dbPath = join(tempDir, ".agentplate", "sessions.db");
|
|
430
|
+
const store = createSessionStore(dbPath);
|
|
431
|
+
const updatedSessions = store.getAll();
|
|
432
|
+
store.close();
|
|
433
|
+
expect(updatedSessions).toHaveLength(0);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("stalled agents are cleaned like working agents (not by default)", async () => {
|
|
437
|
+
// Create a worktree with stalled state
|
|
438
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
439
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
440
|
+
|
|
441
|
+
const worktreePath = join(worktreesDir, "stalled-agent");
|
|
442
|
+
await runGitInDir(tempDir, [
|
|
443
|
+
"worktree",
|
|
444
|
+
"add",
|
|
445
|
+
worktreePath,
|
|
446
|
+
"-b",
|
|
447
|
+
"agentplate/stalled-agent/task-stuck",
|
|
448
|
+
]);
|
|
449
|
+
|
|
450
|
+
writeSessionsToStore([
|
|
451
|
+
{
|
|
452
|
+
id: "session-1",
|
|
453
|
+
agentName: "stalled-agent",
|
|
454
|
+
capability: "builder",
|
|
455
|
+
worktreePath,
|
|
456
|
+
branchName: "agentplate/stalled-agent/task-stuck",
|
|
457
|
+
taskId: "task-stuck",
|
|
458
|
+
tmuxSession: "agentplate-stalled-agent",
|
|
459
|
+
state: "stalled",
|
|
460
|
+
pid: 12345,
|
|
461
|
+
parentAgent: null,
|
|
462
|
+
depth: 0,
|
|
463
|
+
runId: null,
|
|
464
|
+
startedAt: new Date().toISOString(),
|
|
465
|
+
lastActivity: new Date().toISOString(),
|
|
466
|
+
escalationLevel: 0,
|
|
467
|
+
stalledSince: new Date().toISOString(),
|
|
468
|
+
transcriptPath: null,
|
|
469
|
+
},
|
|
470
|
+
]);
|
|
471
|
+
|
|
472
|
+
await worktreeCommand(["clean"]);
|
|
473
|
+
const out = output();
|
|
474
|
+
|
|
475
|
+
// Stalled agents should not be cleaned by default (only completed/zombie are cleaned)
|
|
476
|
+
expect(out).toContain("No worktrees to clean");
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("--completed flag only cleans completed agents", async () => {
|
|
480
|
+
// Create two worktrees using createWorktree
|
|
481
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
482
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
483
|
+
|
|
484
|
+
const { path: completedPath } = await createWorktree({
|
|
485
|
+
repoRoot: tempDir,
|
|
486
|
+
baseDir: worktreesDir,
|
|
487
|
+
agentName: "completed-agent",
|
|
488
|
+
baseBranch: "main",
|
|
489
|
+
taskId: "task-done",
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const { path: workingPath } = await createWorktree({
|
|
493
|
+
repoRoot: tempDir,
|
|
494
|
+
baseDir: worktreesDir,
|
|
495
|
+
agentName: "working-agent",
|
|
496
|
+
baseBranch: "main",
|
|
497
|
+
taskId: "task-wip",
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// Write sessions.db with both agents
|
|
501
|
+
writeSessionsToStore([
|
|
502
|
+
makeSession({
|
|
503
|
+
id: "session-1",
|
|
504
|
+
agentName: "completed-agent",
|
|
505
|
+
worktreePath: completedPath,
|
|
506
|
+
branchName: "agentplate/completed-agent/task-done",
|
|
507
|
+
taskId: "task-done",
|
|
508
|
+
tmuxSession: "agentplate-completed-agent-fake",
|
|
509
|
+
state: "completed",
|
|
510
|
+
}),
|
|
511
|
+
makeSession({
|
|
512
|
+
id: "session-2",
|
|
513
|
+
agentName: "working-agent",
|
|
514
|
+
worktreePath: workingPath,
|
|
515
|
+
branchName: "agentplate/working-agent/task-wip",
|
|
516
|
+
taskId: "task-wip",
|
|
517
|
+
tmuxSession: "agentplate-working-agent-fake",
|
|
518
|
+
state: "working",
|
|
519
|
+
pid: 12346,
|
|
520
|
+
}),
|
|
521
|
+
]);
|
|
522
|
+
|
|
523
|
+
await worktreeCommand(["clean", "--completed"]);
|
|
524
|
+
const out = output();
|
|
525
|
+
|
|
526
|
+
expect(out).toContain("Cleaned 1 worktree");
|
|
527
|
+
|
|
528
|
+
// Verify only the completed worktree is removed
|
|
529
|
+
expect(existsSync(completedPath)).toBe(false);
|
|
530
|
+
expect(existsSync(workingPath)).toBe(true);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("--all flag cleans all worktrees regardless of state", async () => {
|
|
534
|
+
// Create three worktrees with different states
|
|
535
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
536
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
537
|
+
|
|
538
|
+
const { path: completedPath } = await createWorktree({
|
|
539
|
+
repoRoot: tempDir,
|
|
540
|
+
baseDir: worktreesDir,
|
|
541
|
+
agentName: "completed-agent",
|
|
542
|
+
baseBranch: "main",
|
|
543
|
+
taskId: "task-done",
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const { path: workingPath } = await createWorktree({
|
|
547
|
+
repoRoot: tempDir,
|
|
548
|
+
baseDir: worktreesDir,
|
|
549
|
+
agentName: "working-agent",
|
|
550
|
+
baseBranch: "main",
|
|
551
|
+
taskId: "task-wip",
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
const { path: stalledPath } = await createWorktree({
|
|
555
|
+
repoRoot: tempDir,
|
|
556
|
+
baseDir: worktreesDir,
|
|
557
|
+
agentName: "stalled-agent",
|
|
558
|
+
baseBranch: "main",
|
|
559
|
+
taskId: "task-stuck",
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// Write sessions with different states
|
|
563
|
+
writeSessionsToStore([
|
|
564
|
+
makeSession({
|
|
565
|
+
id: "session-1",
|
|
566
|
+
agentName: "completed-agent",
|
|
567
|
+
worktreePath: completedPath,
|
|
568
|
+
branchName: "agentplate/completed-agent/task-done",
|
|
569
|
+
taskId: "task-done",
|
|
570
|
+
state: "completed",
|
|
571
|
+
}),
|
|
572
|
+
makeSession({
|
|
573
|
+
id: "session-2",
|
|
574
|
+
agentName: "working-agent",
|
|
575
|
+
worktreePath: workingPath,
|
|
576
|
+
branchName: "agentplate/working-agent/task-wip",
|
|
577
|
+
taskId: "task-wip",
|
|
578
|
+
state: "working",
|
|
579
|
+
}),
|
|
580
|
+
makeSession({
|
|
581
|
+
id: "session-3",
|
|
582
|
+
agentName: "stalled-agent",
|
|
583
|
+
worktreePath: stalledPath,
|
|
584
|
+
branchName: "agentplate/stalled-agent/task-stuck",
|
|
585
|
+
taskId: "task-stuck",
|
|
586
|
+
state: "stalled",
|
|
587
|
+
}),
|
|
588
|
+
]);
|
|
589
|
+
|
|
590
|
+
await worktreeCommand(["clean", "--all"]);
|
|
591
|
+
const out = output();
|
|
592
|
+
|
|
593
|
+
expect(out).toContain("Cleaned 3 worktrees");
|
|
594
|
+
|
|
595
|
+
// Verify all worktrees are removed
|
|
596
|
+
expect(existsSync(completedPath)).toBe(false);
|
|
597
|
+
expect(existsSync(workingPath)).toBe(false);
|
|
598
|
+
expect(existsSync(stalledPath)).toBe(false);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test("multiple completed worktrees reports correct count", async () => {
|
|
602
|
+
// Create two completed worktrees
|
|
603
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
604
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
605
|
+
|
|
606
|
+
const path1 = join(worktreesDir, "agent-1");
|
|
607
|
+
await runGitInDir(tempDir, ["worktree", "add", path1, "-b", "agentplate/agent-1/task-1"]);
|
|
608
|
+
|
|
609
|
+
const path2 = join(worktreesDir, "agent-2");
|
|
610
|
+
await runGitInDir(tempDir, ["worktree", "add", path2, "-b", "agentplate/agent-2/task-2"]);
|
|
611
|
+
|
|
612
|
+
writeSessionsToStore([
|
|
613
|
+
{
|
|
614
|
+
id: "session-1",
|
|
615
|
+
agentName: "agent-1",
|
|
616
|
+
capability: "builder",
|
|
617
|
+
worktreePath: path1,
|
|
618
|
+
branchName: "agentplate/agent-1/task-1",
|
|
619
|
+
taskId: "task-1",
|
|
620
|
+
tmuxSession: "agentplate-agent-1",
|
|
621
|
+
state: "completed",
|
|
622
|
+
pid: 12345,
|
|
623
|
+
parentAgent: null,
|
|
624
|
+
depth: 0,
|
|
625
|
+
runId: null,
|
|
626
|
+
startedAt: new Date().toISOString(),
|
|
627
|
+
lastActivity: new Date().toISOString(),
|
|
628
|
+
escalationLevel: 0,
|
|
629
|
+
stalledSince: null,
|
|
630
|
+
transcriptPath: null,
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
id: "session-2",
|
|
634
|
+
agentName: "agent-2",
|
|
635
|
+
capability: "builder",
|
|
636
|
+
worktreePath: path2,
|
|
637
|
+
branchName: "agentplate/agent-2/task-2",
|
|
638
|
+
taskId: "task-2",
|
|
639
|
+
tmuxSession: "agentplate-agent-2",
|
|
640
|
+
state: "completed",
|
|
641
|
+
pid: 12346,
|
|
642
|
+
parentAgent: null,
|
|
643
|
+
depth: 0,
|
|
644
|
+
runId: null,
|
|
645
|
+
startedAt: new Date().toISOString(),
|
|
646
|
+
lastActivity: new Date().toISOString(),
|
|
647
|
+
escalationLevel: 0,
|
|
648
|
+
stalledSince: null,
|
|
649
|
+
transcriptPath: null,
|
|
650
|
+
},
|
|
651
|
+
]);
|
|
652
|
+
|
|
653
|
+
await worktreeCommand(["clean"]);
|
|
654
|
+
const out = output();
|
|
655
|
+
|
|
656
|
+
expect(out).toContain("Cleaned 2 worktrees");
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test("without --force, skips worktrees with unmerged branches and prints warning", async () => {
|
|
660
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
661
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
662
|
+
|
|
663
|
+
const { path: wtPath } = await createWorktree({
|
|
664
|
+
repoRoot: tempDir,
|
|
665
|
+
baseDir: worktreesDir,
|
|
666
|
+
agentName: "unmerged-agent",
|
|
667
|
+
baseBranch: "main",
|
|
668
|
+
taskId: "task-unmerged",
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// Add an unmerged commit
|
|
672
|
+
await commitFile(wtPath, "work.ts", "export const y = 2;", "unmerged work");
|
|
673
|
+
|
|
674
|
+
writeSessionsToStore([
|
|
675
|
+
makeSession({
|
|
676
|
+
id: "session-u",
|
|
677
|
+
agentName: "unmerged-agent",
|
|
678
|
+
worktreePath: wtPath,
|
|
679
|
+
branchName: "agentplate/unmerged-agent/task-unmerged",
|
|
680
|
+
taskId: "task-unmerged",
|
|
681
|
+
state: "completed",
|
|
682
|
+
}),
|
|
683
|
+
]);
|
|
684
|
+
|
|
685
|
+
await worktreeCommand(["clean"]);
|
|
686
|
+
const out = output();
|
|
687
|
+
|
|
688
|
+
// Worktree should NOT have been removed
|
|
689
|
+
expect(existsSync(wtPath)).toBe(true);
|
|
690
|
+
// Warning should be printed
|
|
691
|
+
expect(out).toContain("Skipped 1 worktree");
|
|
692
|
+
expect(out).toContain("agentplate/unmerged-agent/task-unmerged");
|
|
693
|
+
expect(out).toContain("--force");
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test("with --force, deletes worktrees with unmerged branches", async () => {
|
|
697
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
698
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
699
|
+
|
|
700
|
+
const { path: wtPath } = await createWorktree({
|
|
701
|
+
repoRoot: tempDir,
|
|
702
|
+
baseDir: worktreesDir,
|
|
703
|
+
agentName: "unmerged-agent",
|
|
704
|
+
baseBranch: "main",
|
|
705
|
+
taskId: "task-force",
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// Add an unmerged commit
|
|
709
|
+
await commitFile(wtPath, "work.ts", "export const y = 2;", "unmerged work");
|
|
710
|
+
|
|
711
|
+
writeSessionsToStore([
|
|
712
|
+
makeSession({
|
|
713
|
+
id: "session-f",
|
|
714
|
+
agentName: "unmerged-agent",
|
|
715
|
+
worktreePath: wtPath,
|
|
716
|
+
branchName: "agentplate/unmerged-agent/task-force",
|
|
717
|
+
taskId: "task-force",
|
|
718
|
+
state: "completed",
|
|
719
|
+
}),
|
|
720
|
+
]);
|
|
721
|
+
|
|
722
|
+
await worktreeCommand(["clean", "--force"]);
|
|
723
|
+
const out = output();
|
|
724
|
+
|
|
725
|
+
// Worktree should be removed
|
|
726
|
+
expect(existsSync(wtPath)).toBe(false);
|
|
727
|
+
expect(out).toContain("agentplate/unmerged-agent/task-force");
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
test("without --force, removes worktrees whose branches ARE merged", async () => {
|
|
731
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
732
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
733
|
+
|
|
734
|
+
const { path: wtPath, branch } = await createWorktree({
|
|
735
|
+
repoRoot: tempDir,
|
|
736
|
+
baseDir: worktreesDir,
|
|
737
|
+
agentName: "merged-agent",
|
|
738
|
+
baseBranch: "main",
|
|
739
|
+
taskId: "task-merged",
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
// Add a commit and merge it into main
|
|
743
|
+
await commitFile(wtPath, "work.ts", "export const z = 3;", "work to merge");
|
|
744
|
+
await runGitInDir(tempDir, ["merge", "--no-ff", branch, "-m", "merge feature"]);
|
|
745
|
+
|
|
746
|
+
writeSessionsToStore([
|
|
747
|
+
makeSession({
|
|
748
|
+
id: "session-m",
|
|
749
|
+
agentName: "merged-agent",
|
|
750
|
+
worktreePath: wtPath,
|
|
751
|
+
branchName: branch,
|
|
752
|
+
taskId: "task-merged",
|
|
753
|
+
state: "completed",
|
|
754
|
+
}),
|
|
755
|
+
]);
|
|
756
|
+
|
|
757
|
+
await worktreeCommand(["clean"]);
|
|
758
|
+
const out = output();
|
|
759
|
+
|
|
760
|
+
// Merged worktree should be cleaned
|
|
761
|
+
expect(existsSync(wtPath)).toBe(false);
|
|
762
|
+
expect(out).toContain("Cleaned 1 worktree");
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
test("--json output includes skipped array for unmerged branches", async () => {
|
|
766
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
767
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
768
|
+
|
|
769
|
+
const { path: wtPath } = await createWorktree({
|
|
770
|
+
repoRoot: tempDir,
|
|
771
|
+
baseDir: worktreesDir,
|
|
772
|
+
agentName: "unmerged-json-agent",
|
|
773
|
+
baseBranch: "main",
|
|
774
|
+
taskId: "task-json",
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
// Add an unmerged commit
|
|
778
|
+
await commitFile(wtPath, "work.ts", "export const w = 4;", "unmerged work");
|
|
779
|
+
|
|
780
|
+
writeSessionsToStore([
|
|
781
|
+
makeSession({
|
|
782
|
+
id: "session-j",
|
|
783
|
+
agentName: "unmerged-json-agent",
|
|
784
|
+
worktreePath: wtPath,
|
|
785
|
+
branchName: "agentplate/unmerged-json-agent/task-json",
|
|
786
|
+
taskId: "task-json",
|
|
787
|
+
state: "completed",
|
|
788
|
+
}),
|
|
789
|
+
]);
|
|
790
|
+
|
|
791
|
+
await worktreeCommand(["clean", "--json"]);
|
|
792
|
+
const out = output();
|
|
793
|
+
|
|
794
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
795
|
+
cleaned: string[];
|
|
796
|
+
failed: string[];
|
|
797
|
+
skipped: string[];
|
|
798
|
+
pruned: number;
|
|
799
|
+
mailPurged: number;
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
expect(parsed.cleaned).toEqual([]);
|
|
803
|
+
expect(parsed.skipped).toEqual(["agentplate/unmerged-json-agent/task-json"]);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
test("lead worktree with .sprout/ changes preserves them to canonical before cleanup", async () => {
|
|
807
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
808
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
809
|
+
|
|
810
|
+
const { path: wtPath, branch } = await createWorktree({
|
|
811
|
+
repoRoot: tempDir,
|
|
812
|
+
baseDir: worktreesDir,
|
|
813
|
+
agentName: "lead-with-sprout",
|
|
814
|
+
baseBranch: "main",
|
|
815
|
+
taskId: "task-lead-sprout",
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
// Commit a .sprout/ file in the lead worktree
|
|
819
|
+
await commitFile(
|
|
820
|
+
wtPath,
|
|
821
|
+
".sprout/issues/test-issue.yaml",
|
|
822
|
+
"id: test-issue\ntitle: Test Issue\nstatus: open\n",
|
|
823
|
+
"sprout: add test issue",
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
writeSessionsToStore([
|
|
827
|
+
makeSession({
|
|
828
|
+
id: "session-lead-sprout",
|
|
829
|
+
agentName: "lead-with-sprout",
|
|
830
|
+
capability: "lead",
|
|
831
|
+
worktreePath: wtPath,
|
|
832
|
+
branchName: branch,
|
|
833
|
+
taskId: "task-lead-sprout",
|
|
834
|
+
state: "completed",
|
|
835
|
+
}),
|
|
836
|
+
]);
|
|
837
|
+
|
|
838
|
+
await worktreeCommand(["clean"]);
|
|
839
|
+
const out = output();
|
|
840
|
+
|
|
841
|
+
// The worktree should be removed
|
|
842
|
+
expect(existsSync(wtPath)).toBe(false);
|
|
843
|
+
|
|
844
|
+
// The .sprout/ changes should have been preserved to main
|
|
845
|
+
const showProc = Bun.spawn(["git", "show", "main:.sprout/issues/test-issue.yaml"], {
|
|
846
|
+
cwd: tempDir,
|
|
847
|
+
stdout: "pipe",
|
|
848
|
+
stderr: "pipe",
|
|
849
|
+
});
|
|
850
|
+
const showOut = await new Response(showProc.stdout).text();
|
|
851
|
+
const showExit = await showProc.exited;
|
|
852
|
+
expect(showExit).toBe(0);
|
|
853
|
+
expect(showOut).toContain("test-issue");
|
|
854
|
+
|
|
855
|
+
// Output should mention preservation
|
|
856
|
+
expect(out).toContain("Preserved .sprout/");
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
test("lead worktree without .sprout/ changes cleans normally", async () => {
|
|
860
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
861
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
862
|
+
|
|
863
|
+
const { path: wtPath, branch } = await createWorktree({
|
|
864
|
+
repoRoot: tempDir,
|
|
865
|
+
baseDir: worktreesDir,
|
|
866
|
+
agentName: "lead-no-sprout",
|
|
867
|
+
baseBranch: "main",
|
|
868
|
+
taskId: "task-lead-no-sprout",
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
// Commit a non-.sprout/ file
|
|
872
|
+
await commitFile(wtPath, "src/work.ts", "export const x = 1;", "non-sprout work");
|
|
873
|
+
|
|
874
|
+
writeSessionsToStore([
|
|
875
|
+
makeSession({
|
|
876
|
+
id: "session-lead-no-sprout",
|
|
877
|
+
agentName: "lead-no-sprout",
|
|
878
|
+
capability: "lead",
|
|
879
|
+
worktreePath: wtPath,
|
|
880
|
+
branchName: branch,
|
|
881
|
+
taskId: "task-lead-no-sprout",
|
|
882
|
+
state: "completed",
|
|
883
|
+
}),
|
|
884
|
+
]);
|
|
885
|
+
|
|
886
|
+
await worktreeCommand(["clean"]);
|
|
887
|
+
const out = output();
|
|
888
|
+
|
|
889
|
+
// Worktree should be removed
|
|
890
|
+
expect(existsSync(wtPath)).toBe(false);
|
|
891
|
+
// Output should NOT mention .sprout/ preservation
|
|
892
|
+
expect(out).not.toContain("Preserved .sprout/");
|
|
893
|
+
// Should still report as cleaned
|
|
894
|
+
expect(out).toContain("Cleaned 1 worktree");
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
test("lead worktrees are cleaned without --force even with unmerged non-sprout changes", async () => {
|
|
898
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
899
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
900
|
+
|
|
901
|
+
const { path: wtPath, branch } = await createWorktree({
|
|
902
|
+
repoRoot: tempDir,
|
|
903
|
+
baseDir: worktreesDir,
|
|
904
|
+
agentName: "lead-unmerged",
|
|
905
|
+
baseBranch: "main",
|
|
906
|
+
taskId: "task-lead-unmerged",
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
// Add unmerged non-.sprout/ commit
|
|
910
|
+
await commitFile(wtPath, "src/lead-work.ts", "export const y = 2;", "unmerged lead work");
|
|
911
|
+
|
|
912
|
+
writeSessionsToStore([
|
|
913
|
+
makeSession({
|
|
914
|
+
id: "session-lead-unmerged",
|
|
915
|
+
agentName: "lead-unmerged",
|
|
916
|
+
capability: "lead",
|
|
917
|
+
worktreePath: wtPath,
|
|
918
|
+
branchName: branch,
|
|
919
|
+
taskId: "task-lead-unmerged",
|
|
920
|
+
state: "completed",
|
|
921
|
+
}),
|
|
922
|
+
]);
|
|
923
|
+
|
|
924
|
+
// Run clean WITHOUT --force — leads bypass merge check
|
|
925
|
+
await worktreeCommand(["clean"]);
|
|
926
|
+
const out = output();
|
|
927
|
+
|
|
928
|
+
// Lead worktree SHOULD be removed (not skipped)
|
|
929
|
+
expect(existsSync(wtPath)).toBe(false);
|
|
930
|
+
expect(out).toContain("Cleaned 1 worktree");
|
|
931
|
+
expect(out).not.toContain("Skipped");
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
test("--json output includes sproutPreserved array", async () => {
|
|
935
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
936
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
937
|
+
|
|
938
|
+
const { path: wtPath, branch } = await createWorktree({
|
|
939
|
+
repoRoot: tempDir,
|
|
940
|
+
baseDir: worktreesDir,
|
|
941
|
+
agentName: "lead-sprout-json",
|
|
942
|
+
baseBranch: "main",
|
|
943
|
+
taskId: "task-sprout-json",
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
// Commit a .sprout/ file in the lead worktree
|
|
947
|
+
await commitFile(
|
|
948
|
+
wtPath,
|
|
949
|
+
".sprout/issues/json-issue.yaml",
|
|
950
|
+
"id: json-issue\ntitle: JSON Issue\nstatus: open\n",
|
|
951
|
+
"sprout: add json issue",
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
writeSessionsToStore([
|
|
955
|
+
makeSession({
|
|
956
|
+
id: "session-lead-json",
|
|
957
|
+
agentName: "lead-sprout-json",
|
|
958
|
+
capability: "lead",
|
|
959
|
+
worktreePath: wtPath,
|
|
960
|
+
branchName: branch,
|
|
961
|
+
taskId: "task-sprout-json",
|
|
962
|
+
state: "completed",
|
|
963
|
+
}),
|
|
964
|
+
]);
|
|
965
|
+
|
|
966
|
+
await worktreeCommand(["clean", "--json"]);
|
|
967
|
+
const out = output();
|
|
968
|
+
|
|
969
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
970
|
+
cleaned: string[];
|
|
971
|
+
failed: string[];
|
|
972
|
+
skipped: string[];
|
|
973
|
+
pruned: number;
|
|
974
|
+
mailPurged: number;
|
|
975
|
+
sproutPreserved: string[];
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
expect(parsed.cleaned).toContain(branch);
|
|
979
|
+
expect(parsed.sproutPreserved).toContain(branch);
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
describe("live-children guard", () => {
|
|
983
|
+
/**
|
|
984
|
+
* Write sessions into a nested .agentplate/sessions.db inside a worktree.
|
|
985
|
+
* Simulates a lead worktree that has spawned builder children.
|
|
986
|
+
*/
|
|
987
|
+
function writeNestedSessions(worktreePath: string, sessions: AgentSession[]): void {
|
|
988
|
+
const nestedAgentplate = join(worktreePath, ".agentplate");
|
|
989
|
+
mkdirSync(nestedAgentplate, { recursive: true });
|
|
990
|
+
const store = createSessionStore(join(nestedAgentplate, "sessions.db"));
|
|
991
|
+
for (const s of sessions) {
|
|
992
|
+
store.upsert(s);
|
|
993
|
+
}
|
|
994
|
+
store.close();
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
test("clean skipped when live children present (no --force)", async () => {
|
|
998
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
999
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
1000
|
+
|
|
1001
|
+
const { path: wtPath } = await createWorktree({
|
|
1002
|
+
repoRoot: tempDir,
|
|
1003
|
+
baseDir: worktreesDir,
|
|
1004
|
+
agentName: "lead-with-children",
|
|
1005
|
+
baseBranch: "main",
|
|
1006
|
+
taskId: "task-lead",
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
// Parent session is completed
|
|
1010
|
+
writeSessionsToStore([
|
|
1011
|
+
makeSession({
|
|
1012
|
+
id: "session-lead",
|
|
1013
|
+
agentName: "lead-with-children",
|
|
1014
|
+
capability: "lead",
|
|
1015
|
+
worktreePath: wtPath,
|
|
1016
|
+
branchName: "agentplate/lead-with-children/task-lead",
|
|
1017
|
+
taskId: "task-lead",
|
|
1018
|
+
state: "completed",
|
|
1019
|
+
}),
|
|
1020
|
+
]);
|
|
1021
|
+
|
|
1022
|
+
// Nested session with process.pid (guaranteed alive)
|
|
1023
|
+
writeNestedSessions(wtPath, [
|
|
1024
|
+
{
|
|
1025
|
+
id: "nested-builder",
|
|
1026
|
+
agentName: "nested-builder",
|
|
1027
|
+
capability: "builder",
|
|
1028
|
+
worktreePath: join(wtPath, ".agentplate", "worktrees", "nested-builder"),
|
|
1029
|
+
branchName: "agentplate/nested-builder/task-child",
|
|
1030
|
+
taskId: "task-child",
|
|
1031
|
+
tmuxSession: "agentplate-nested-builder-fake",
|
|
1032
|
+
state: "working",
|
|
1033
|
+
pid: process.pid, // current process — guaranteed alive
|
|
1034
|
+
parentAgent: "lead-with-children",
|
|
1035
|
+
depth: 2,
|
|
1036
|
+
runId: null,
|
|
1037
|
+
startedAt: new Date().toISOString(),
|
|
1038
|
+
lastActivity: new Date().toISOString(),
|
|
1039
|
+
escalationLevel: 0,
|
|
1040
|
+
stalledSince: null,
|
|
1041
|
+
transcriptPath: null,
|
|
1042
|
+
},
|
|
1043
|
+
]);
|
|
1044
|
+
|
|
1045
|
+
await worktreeCommand(["clean", "--completed", "--json"]);
|
|
1046
|
+
const out = output();
|
|
1047
|
+
|
|
1048
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
1049
|
+
cleaned: string[];
|
|
1050
|
+
blockedByChildren: string[];
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
expect(parsed.cleaned).toEqual([]);
|
|
1054
|
+
expect(parsed.blockedByChildren).toContain("agentplate/lead-with-children/task-lead");
|
|
1055
|
+
// Worktree still exists
|
|
1056
|
+
expect(existsSync(wtPath)).toBe(true);
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
test("clean proceeds when nested sessions are dead (pid unreachable)", async () => {
|
|
1060
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
1061
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
1062
|
+
|
|
1063
|
+
const { path: wtPath } = await createWorktree({
|
|
1064
|
+
repoRoot: tempDir,
|
|
1065
|
+
baseDir: worktreesDir,
|
|
1066
|
+
agentName: "lead-dead-children",
|
|
1067
|
+
baseBranch: "main",
|
|
1068
|
+
taskId: "task-dead",
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
writeSessionsToStore([
|
|
1072
|
+
makeSession({
|
|
1073
|
+
id: "session-lead-dead",
|
|
1074
|
+
agentName: "lead-dead-children",
|
|
1075
|
+
capability: "lead",
|
|
1076
|
+
worktreePath: wtPath,
|
|
1077
|
+
branchName: "agentplate/lead-dead-children/task-dead",
|
|
1078
|
+
taskId: "task-dead",
|
|
1079
|
+
state: "completed",
|
|
1080
|
+
}),
|
|
1081
|
+
]);
|
|
1082
|
+
|
|
1083
|
+
// Nested session with a dead pid (extremely high, will not exist)
|
|
1084
|
+
writeNestedSessions(wtPath, [
|
|
1085
|
+
{
|
|
1086
|
+
id: "nested-dead",
|
|
1087
|
+
agentName: "nested-dead",
|
|
1088
|
+
capability: "builder",
|
|
1089
|
+
worktreePath: join(wtPath, ".agentplate", "worktrees", "nested-dead"),
|
|
1090
|
+
branchName: "agentplate/nested-dead/task-dead-child",
|
|
1091
|
+
taskId: "task-dead-child",
|
|
1092
|
+
tmuxSession: "agentplate-nested-dead-fake",
|
|
1093
|
+
state: "working",
|
|
1094
|
+
pid: 999999999, // dead pid
|
|
1095
|
+
parentAgent: "lead-dead-children",
|
|
1096
|
+
depth: 2,
|
|
1097
|
+
runId: null,
|
|
1098
|
+
startedAt: new Date().toISOString(),
|
|
1099
|
+
lastActivity: new Date().toISOString(),
|
|
1100
|
+
escalationLevel: 0,
|
|
1101
|
+
stalledSince: null,
|
|
1102
|
+
transcriptPath: null,
|
|
1103
|
+
},
|
|
1104
|
+
]);
|
|
1105
|
+
|
|
1106
|
+
await worktreeCommand(["clean", "--completed", "--json"]);
|
|
1107
|
+
const out = output();
|
|
1108
|
+
|
|
1109
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
1110
|
+
cleaned: string[];
|
|
1111
|
+
blockedByChildren: string[];
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
expect(parsed.cleaned).toContain("agentplate/lead-dead-children/task-dead");
|
|
1115
|
+
expect(parsed.blockedByChildren).toEqual([]);
|
|
1116
|
+
expect(existsSync(wtPath)).toBe(false);
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
test("--force removes worktree even with live children", async () => {
|
|
1120
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
1121
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
1122
|
+
|
|
1123
|
+
const { path: wtPath } = await createWorktree({
|
|
1124
|
+
repoRoot: tempDir,
|
|
1125
|
+
baseDir: worktreesDir,
|
|
1126
|
+
agentName: "lead-force",
|
|
1127
|
+
baseBranch: "main",
|
|
1128
|
+
taskId: "task-force-children",
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
writeSessionsToStore([
|
|
1132
|
+
makeSession({
|
|
1133
|
+
id: "session-lead-force",
|
|
1134
|
+
agentName: "lead-force",
|
|
1135
|
+
capability: "lead",
|
|
1136
|
+
worktreePath: wtPath,
|
|
1137
|
+
branchName: "agentplate/lead-force/task-force-children",
|
|
1138
|
+
taskId: "task-force-children",
|
|
1139
|
+
state: "completed",
|
|
1140
|
+
}),
|
|
1141
|
+
]);
|
|
1142
|
+
|
|
1143
|
+
// Use a dead pid — avoids actually killing any live process,
|
|
1144
|
+
// but still exercises the --force code path.
|
|
1145
|
+
writeNestedSessions(wtPath, [
|
|
1146
|
+
{
|
|
1147
|
+
id: "nested-force",
|
|
1148
|
+
agentName: "nested-force",
|
|
1149
|
+
capability: "builder",
|
|
1150
|
+
worktreePath: join(wtPath, ".agentplate", "worktrees", "nested-force"),
|
|
1151
|
+
branchName: "agentplate/nested-force/task-force-child",
|
|
1152
|
+
taskId: "task-force-child",
|
|
1153
|
+
tmuxSession: "agentplate-nested-force-fake",
|
|
1154
|
+
state: "working",
|
|
1155
|
+
pid: 999999999, // dead pid, safe to kill
|
|
1156
|
+
parentAgent: "lead-force",
|
|
1157
|
+
depth: 2,
|
|
1158
|
+
runId: null,
|
|
1159
|
+
startedAt: new Date().toISOString(),
|
|
1160
|
+
lastActivity: new Date().toISOString(),
|
|
1161
|
+
escalationLevel: 0,
|
|
1162
|
+
stalledSince: null,
|
|
1163
|
+
transcriptPath: null,
|
|
1164
|
+
},
|
|
1165
|
+
]);
|
|
1166
|
+
|
|
1167
|
+
await worktreeCommand(["clean", "--force", "--json"]);
|
|
1168
|
+
const out = output();
|
|
1169
|
+
|
|
1170
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
1171
|
+
cleaned: string[];
|
|
1172
|
+
blockedByChildren: string[];
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
// Should be cleaned (not blocked) even though nested sessions existed
|
|
1176
|
+
expect(parsed.cleaned).toContain("agentplate/lead-force/task-force-children");
|
|
1177
|
+
expect(existsSync(wtPath)).toBe(false);
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
test("no nested .agentplate — treated as no live children, clean proceeds", async () => {
|
|
1181
|
+
const worktreesDir = join(tempDir, ".agentplate", "worktrees");
|
|
1182
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
1183
|
+
|
|
1184
|
+
const { path: wtPath } = await createWorktree({
|
|
1185
|
+
repoRoot: tempDir,
|
|
1186
|
+
baseDir: worktreesDir,
|
|
1187
|
+
agentName: "lead-no-nested",
|
|
1188
|
+
baseBranch: "main",
|
|
1189
|
+
taskId: "task-no-nested",
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
writeSessionsToStore([
|
|
1193
|
+
makeSession({
|
|
1194
|
+
id: "session-lead-no-nested",
|
|
1195
|
+
agentName: "lead-no-nested",
|
|
1196
|
+
capability: "lead",
|
|
1197
|
+
worktreePath: wtPath,
|
|
1198
|
+
branchName: "agentplate/lead-no-nested/task-no-nested",
|
|
1199
|
+
taskId: "task-no-nested",
|
|
1200
|
+
state: "completed",
|
|
1201
|
+
}),
|
|
1202
|
+
]);
|
|
1203
|
+
|
|
1204
|
+
// No nested .agentplate/ directory written
|
|
1205
|
+
|
|
1206
|
+
await worktreeCommand(["clean", "--completed", "--json"]);
|
|
1207
|
+
const out = output();
|
|
1208
|
+
|
|
1209
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
1210
|
+
cleaned: string[];
|
|
1211
|
+
blockedByChildren: string[];
|
|
1212
|
+
};
|
|
1213
|
+
|
|
1214
|
+
expect(parsed.cleaned).toContain("agentplate/lead-no-nested/task-no-nested");
|
|
1215
|
+
expect(parsed.blockedByChildren).toEqual([]);
|
|
1216
|
+
expect(existsSync(wtPath)).toBe(false);
|
|
1217
|
+
});
|
|
1218
|
+
});
|
|
1219
|
+
});
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
describe("checkLiveChildren", () => {
|
|
1223
|
+
let tempDir: string;
|
|
1224
|
+
|
|
1225
|
+
beforeEach(async () => {
|
|
1226
|
+
tempDir = await mkdtemp(join(tmpdir(), "agentplate-checkchildren-"));
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
afterEach(async () => {
|
|
1230
|
+
await cleanupTempDir(tempDir);
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
test("returns empty array when no nested .agentplate/sessions.db", async () => {
|
|
1234
|
+
const result = await checkLiveChildren(tempDir);
|
|
1235
|
+
expect(result).toEqual([]);
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
test("returns empty array when all sessions are completed", async () => {
|
|
1239
|
+
const nestedAgentplate = join(tempDir, ".agentplate");
|
|
1240
|
+
mkdirSync(nestedAgentplate, { recursive: true });
|
|
1241
|
+
const store = createSessionStore(join(nestedAgentplate, "sessions.db"));
|
|
1242
|
+
store.upsert({
|
|
1243
|
+
id: "s1",
|
|
1244
|
+
agentName: "done-agent",
|
|
1245
|
+
capability: "builder",
|
|
1246
|
+
worktreePath: "/fake/wt",
|
|
1247
|
+
branchName: "agentplate/done/task",
|
|
1248
|
+
taskId: "task",
|
|
1249
|
+
tmuxSession: "",
|
|
1250
|
+
state: "completed",
|
|
1251
|
+
pid: process.pid,
|
|
1252
|
+
parentAgent: null,
|
|
1253
|
+
depth: 2,
|
|
1254
|
+
runId: null,
|
|
1255
|
+
startedAt: new Date().toISOString(),
|
|
1256
|
+
lastActivity: new Date().toISOString(),
|
|
1257
|
+
escalationLevel: 0,
|
|
1258
|
+
stalledSince: null,
|
|
1259
|
+
transcriptPath: null,
|
|
1260
|
+
});
|
|
1261
|
+
store.close();
|
|
1262
|
+
|
|
1263
|
+
const result = await checkLiveChildren(tempDir);
|
|
1264
|
+
expect(result).toEqual([]);
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
test("returns live children when working session with alive pid exists", async () => {
|
|
1268
|
+
const nestedAgentplate = join(tempDir, ".agentplate");
|
|
1269
|
+
mkdirSync(nestedAgentplate, { recursive: true });
|
|
1270
|
+
const store = createSessionStore(join(nestedAgentplate, "sessions.db"));
|
|
1271
|
+
store.upsert({
|
|
1272
|
+
id: "s1",
|
|
1273
|
+
agentName: "live-agent",
|
|
1274
|
+
capability: "builder",
|
|
1275
|
+
worktreePath: "/fake/wt",
|
|
1276
|
+
branchName: "agentplate/live/task",
|
|
1277
|
+
taskId: "task",
|
|
1278
|
+
tmuxSession: "",
|
|
1279
|
+
state: "working",
|
|
1280
|
+
pid: process.pid, // current process — alive
|
|
1281
|
+
parentAgent: null,
|
|
1282
|
+
depth: 2,
|
|
1283
|
+
runId: null,
|
|
1284
|
+
startedAt: new Date().toISOString(),
|
|
1285
|
+
lastActivity: new Date().toISOString(),
|
|
1286
|
+
escalationLevel: 0,
|
|
1287
|
+
stalledSince: null,
|
|
1288
|
+
transcriptPath: null,
|
|
1289
|
+
});
|
|
1290
|
+
store.close();
|
|
1291
|
+
|
|
1292
|
+
const result = await checkLiveChildren(tempDir);
|
|
1293
|
+
expect(result).toHaveLength(1);
|
|
1294
|
+
expect(result[0]?.agentName).toBe("live-agent");
|
|
1295
|
+
expect(result[0]?.pid).toBe(process.pid);
|
|
1296
|
+
});
|
|
1297
|
+
});
|