@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,830 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { AgentSession } from "../types.ts";
|
|
3
|
+
import { evaluateHealth, isProcessRunning, transitionState } from "./health.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tests for the ZFC-based health evaluation and state machine.
|
|
7
|
+
*
|
|
8
|
+
* evaluateHealth is a pure function that takes session state + tmux liveness +
|
|
9
|
+
* thresholds and returns a HealthCheck. No mocks needed for the core logic.
|
|
10
|
+
*
|
|
11
|
+
* isProcessRunning uses process.kill(pid, 0) which is safe to test with real PIDs:
|
|
12
|
+
* the current process PID (alive) and a known-dead PID (not alive).
|
|
13
|
+
*
|
|
14
|
+
* Note: evaluateHealth calls isProcessRunning internally. For tests that need
|
|
15
|
+
* to control pid liveness independently of the actual OS process table, we set
|
|
16
|
+
* session.pid to known-alive (current process) or known-dead PIDs.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const THRESHOLDS = { staleMs: 30_000, zombieMs: 120_000 };
|
|
20
|
+
|
|
21
|
+
/** PID that is guaranteed to be alive during tests: our own process. */
|
|
22
|
+
const ALIVE_PID = process.pid;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* PID that is very likely dead. PID 2147483647 (max 32-bit signed int) is
|
|
26
|
+
* almost never in use. If by some miracle it is, the test still works because
|
|
27
|
+
* we use it only for the "pid dead" path and the test validates behavior, not
|
|
28
|
+
* the exact PID value.
|
|
29
|
+
*/
|
|
30
|
+
const DEAD_PID = 2147483647;
|
|
31
|
+
|
|
32
|
+
function makeSession(overrides: Partial<AgentSession> = {}): AgentSession {
|
|
33
|
+
return {
|
|
34
|
+
id: "session-test",
|
|
35
|
+
agentName: "test-agent",
|
|
36
|
+
capability: "builder",
|
|
37
|
+
worktreePath: "/tmp/test",
|
|
38
|
+
branchName: "agentplate/test-agent/test-task",
|
|
39
|
+
taskId: "test-task",
|
|
40
|
+
tmuxSession: "agentplate-test-agent",
|
|
41
|
+
state: "booting",
|
|
42
|
+
pid: ALIVE_PID,
|
|
43
|
+
parentAgent: null,
|
|
44
|
+
depth: 0,
|
|
45
|
+
runId: null,
|
|
46
|
+
startedAt: new Date().toISOString(),
|
|
47
|
+
lastActivity: new Date().toISOString(),
|
|
48
|
+
escalationLevel: 0,
|
|
49
|
+
stalledSince: null,
|
|
50
|
+
transcriptPath: null,
|
|
51
|
+
...overrides,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// === isProcessRunning ===
|
|
56
|
+
|
|
57
|
+
describe("isProcessRunning", () => {
|
|
58
|
+
test("returns true for the current process PID", () => {
|
|
59
|
+
expect(isProcessRunning(process.pid)).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("returns false for a PID that does not exist", () => {
|
|
63
|
+
// PID 2147483647 is max 32-bit signed — extremely unlikely to be alive
|
|
64
|
+
expect(isProcessRunning(DEAD_PID)).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// === evaluateHealth ===
|
|
69
|
+
|
|
70
|
+
describe("evaluateHealth", () => {
|
|
71
|
+
// --- ZFC Rule 1: tmux dead → zombie (observable state wins) ---
|
|
72
|
+
|
|
73
|
+
test("ZFC: tmux dead + sessions.json says working → zombie with reconciliation note", () => {
|
|
74
|
+
const session = makeSession({ state: "working" });
|
|
75
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
76
|
+
|
|
77
|
+
expect(check.state).toBe("zombie");
|
|
78
|
+
expect(check.action).toBe("terminate");
|
|
79
|
+
expect(check.tmuxAlive).toBe(false);
|
|
80
|
+
expect(check.processAlive).toBe(false);
|
|
81
|
+
expect(check.reconciliationNote).toContain("ZFC");
|
|
82
|
+
expect(check.reconciliationNote).toContain("tmux dead");
|
|
83
|
+
expect(check.reconciliationNote).toContain('"working"');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("ZFC: tmux dead + sessions.json says booting → zombie with reconciliation note", () => {
|
|
87
|
+
const session = makeSession({ state: "booting" });
|
|
88
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
89
|
+
|
|
90
|
+
expect(check.state).toBe("zombie");
|
|
91
|
+
expect(check.action).toBe("terminate");
|
|
92
|
+
expect(check.reconciliationNote).toContain("ZFC");
|
|
93
|
+
expect(check.reconciliationNote).toContain('"booting"');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("ZFC: tmux dead + sessions.json says stalled → zombie (no reconciliation note for already-degraded)", () => {
|
|
97
|
+
const session = makeSession({ state: "stalled" });
|
|
98
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
99
|
+
|
|
100
|
+
expect(check.state).toBe("zombie");
|
|
101
|
+
expect(check.action).toBe("terminate");
|
|
102
|
+
// No reconciliation note for stalled → zombie (expected progression)
|
|
103
|
+
expect(check.reconciliationNote).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// --- ZFC Rule 1 fallback: tmux dead + stale lastActivity → completed ---
|
|
107
|
+
|
|
108
|
+
test("ZFC fallback: tmux dead + stale lastActivity (working) → complete (missed signal)", () => {
|
|
109
|
+
const staleActivity = new Date(Date.now() - 60_000).toISOString();
|
|
110
|
+
const session = makeSession({ state: "working", lastActivity: staleActivity });
|
|
111
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
112
|
+
|
|
113
|
+
expect(check.state).toBe("completed");
|
|
114
|
+
expect(check.action).toBe("complete");
|
|
115
|
+
expect(check.tmuxAlive).toBe(false);
|
|
116
|
+
expect(check.processAlive).toBe(false);
|
|
117
|
+
expect(check.reconciliationNote).toContain("missed session-end signal");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("ZFC fallback: tmux dead + stale lastActivity (stalled) → complete (missed signal)", () => {
|
|
121
|
+
const staleActivity = new Date(Date.now() - 90_000).toISOString();
|
|
122
|
+
const session = makeSession({ state: "stalled", lastActivity: staleActivity });
|
|
123
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
124
|
+
|
|
125
|
+
expect(check.state).toBe("completed");
|
|
126
|
+
expect(check.action).toBe("complete");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("ZFC: tmux dead + recent lastActivity → still zombie (true crash)", () => {
|
|
130
|
+
const recentActivity = new Date(Date.now() - 1_000).toISOString();
|
|
131
|
+
const session = makeSession({ state: "working", lastActivity: recentActivity });
|
|
132
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
133
|
+
|
|
134
|
+
expect(check.state).toBe("zombie");
|
|
135
|
+
expect(check.action).toBe("terminate");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("ZFC fallback (headless): pid dead + stale lastActivity → complete", () => {
|
|
139
|
+
const staleActivity = new Date(Date.now() - 60_000).toISOString();
|
|
140
|
+
const session = makeSession({
|
|
141
|
+
state: "working",
|
|
142
|
+
tmuxSession: "",
|
|
143
|
+
pid: DEAD_PID,
|
|
144
|
+
lastActivity: staleActivity,
|
|
145
|
+
});
|
|
146
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
147
|
+
|
|
148
|
+
expect(check.state).toBe("completed");
|
|
149
|
+
expect(check.action).toBe("complete");
|
|
150
|
+
expect(check.reconciliationNote).toContain("missed session-end signal");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("ZFC (headless): pid dead + recent lastActivity → still zombie", () => {
|
|
154
|
+
const recentActivity = new Date(Date.now() - 1_000).toISOString();
|
|
155
|
+
const session = makeSession({
|
|
156
|
+
state: "working",
|
|
157
|
+
tmuxSession: "",
|
|
158
|
+
pid: DEAD_PID,
|
|
159
|
+
lastActivity: recentActivity,
|
|
160
|
+
});
|
|
161
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
162
|
+
|
|
163
|
+
expect(check.state).toBe("zombie");
|
|
164
|
+
expect(check.action).toBe("terminate");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// --- ZFC Rule 2: tmux alive + sessions.json says zombie → investigate ---
|
|
168
|
+
|
|
169
|
+
test("ZFC: tmux alive + sessions.json says zombie → investigate (don't auto-kill)", () => {
|
|
170
|
+
const session = makeSession({ state: "zombie", pid: ALIVE_PID });
|
|
171
|
+
const check = evaluateHealth(session, true, THRESHOLDS);
|
|
172
|
+
|
|
173
|
+
expect(check.state).toBe("zombie");
|
|
174
|
+
expect(check.action).toBe("investigate");
|
|
175
|
+
expect(check.processAlive).toBe(true);
|
|
176
|
+
expect(check.reconciliationNote).toContain("ZFC");
|
|
177
|
+
expect(check.reconciliationNote).toContain("investigation needed");
|
|
178
|
+
expect(check.reconciliationNote).toContain("don't auto-kill");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// --- ZFC Rule 3: pid dead + tmux alive → zombie ---
|
|
182
|
+
|
|
183
|
+
test("ZFC: pid dead + tmux alive → zombie (agent process exited, shell survived)", () => {
|
|
184
|
+
const session = makeSession({ state: "working", pid: DEAD_PID });
|
|
185
|
+
const check = evaluateHealth(session, true, THRESHOLDS);
|
|
186
|
+
|
|
187
|
+
expect(check.state).toBe("zombie");
|
|
188
|
+
expect(check.action).toBe("terminate");
|
|
189
|
+
expect(check.processAlive).toBe(false);
|
|
190
|
+
expect(check.pidAlive).toBe(false);
|
|
191
|
+
expect(check.tmuxAlive).toBe(true);
|
|
192
|
+
expect(check.reconciliationNote).toContain("ZFC");
|
|
193
|
+
expect(check.reconciliationNote).toContain("pid");
|
|
194
|
+
expect(check.reconciliationNote).toContain("shell survived");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// --- pid null (unavailable) ---
|
|
198
|
+
|
|
199
|
+
test("pid null does not trigger pid-based zombie detection", () => {
|
|
200
|
+
const session = makeSession({ state: "working", pid: null });
|
|
201
|
+
const check = evaluateHealth(session, true, THRESHOLDS);
|
|
202
|
+
|
|
203
|
+
expect(check.state).toBe("working");
|
|
204
|
+
expect(check.action).toBe("none");
|
|
205
|
+
expect(check.pidAlive).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// --- Time-based checks (both tmux and pid alive) ---
|
|
209
|
+
|
|
210
|
+
test("activity older than zombieMs → zombie", () => {
|
|
211
|
+
const oldActivity = new Date(Date.now() - 200_000).toISOString();
|
|
212
|
+
const session = makeSession({ state: "working", lastActivity: oldActivity });
|
|
213
|
+
const check = evaluateHealth(session, true, THRESHOLDS);
|
|
214
|
+
|
|
215
|
+
expect(check.state).toBe("zombie");
|
|
216
|
+
expect(check.action).toBe("terminate");
|
|
217
|
+
expect(check.reconciliationNote).toBeNull();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("activity older than staleMs → stalled", () => {
|
|
221
|
+
const staleActivity = new Date(Date.now() - 60_000).toISOString();
|
|
222
|
+
const session = makeSession({ state: "working", lastActivity: staleActivity });
|
|
223
|
+
const check = evaluateHealth(session, true, THRESHOLDS);
|
|
224
|
+
|
|
225
|
+
expect(check.state).toBe("stalled");
|
|
226
|
+
expect(check.action).toBe("escalate");
|
|
227
|
+
expect(check.reconciliationNote).toBeNull();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// --- Normal state transitions ---
|
|
231
|
+
|
|
232
|
+
test("booting with recent activity → transitions to working", () => {
|
|
233
|
+
const recentActivity = new Date(Date.now() - 5_000).toISOString();
|
|
234
|
+
const session = makeSession({ state: "booting", lastActivity: recentActivity });
|
|
235
|
+
const check = evaluateHealth(session, true, THRESHOLDS);
|
|
236
|
+
|
|
237
|
+
expect(check.state).toBe("working");
|
|
238
|
+
expect(check.action).toBe("none");
|
|
239
|
+
expect(check.reconciliationNote).toBeNull();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("working with recent activity → stays working", () => {
|
|
243
|
+
const recentActivity = new Date(Date.now() - 5_000).toISOString();
|
|
244
|
+
const session = makeSession({ state: "working", lastActivity: recentActivity });
|
|
245
|
+
const check = evaluateHealth(session, true, THRESHOLDS);
|
|
246
|
+
|
|
247
|
+
expect(check.state).toBe("working");
|
|
248
|
+
expect(check.action).toBe("none");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("booting with stale activity → stalled", () => {
|
|
252
|
+
const staleActivity = new Date(Date.now() - 60_000).toISOString();
|
|
253
|
+
const session = makeSession({ state: "booting", lastActivity: staleActivity });
|
|
254
|
+
const check = evaluateHealth(session, true, THRESHOLDS);
|
|
255
|
+
|
|
256
|
+
expect(check.state).toBe("stalled");
|
|
257
|
+
expect(check.action).toBe("escalate");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// --- Persistent capabilities (coordinator, orchestrator, monitor) ---
|
|
261
|
+
|
|
262
|
+
test("persistent capability: coordinator with stale activity → still working, no escalation", () => {
|
|
263
|
+
const staleActivity = new Date(Date.now() - 60_000).toISOString();
|
|
264
|
+
const session = makeSession({
|
|
265
|
+
capability: "coordinator",
|
|
266
|
+
state: "working",
|
|
267
|
+
lastActivity: staleActivity,
|
|
268
|
+
});
|
|
269
|
+
const check = evaluateHealth(session, true, THRESHOLDS);
|
|
270
|
+
|
|
271
|
+
expect(check.state).toBe("working");
|
|
272
|
+
expect(check.action).toBe("none");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("persistent capability: coordinator with zombie-level staleness → still working", () => {
|
|
276
|
+
const oldActivity = new Date(Date.now() - 200_000).toISOString();
|
|
277
|
+
const session = makeSession({
|
|
278
|
+
capability: "coordinator",
|
|
279
|
+
state: "working",
|
|
280
|
+
lastActivity: oldActivity,
|
|
281
|
+
});
|
|
282
|
+
const check = evaluateHealth(session, true, THRESHOLDS);
|
|
283
|
+
|
|
284
|
+
expect(check.state).toBe("working");
|
|
285
|
+
expect(check.action).toBe("none");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("persistent capability: monitor with stale activity → still working", () => {
|
|
289
|
+
const staleActivity = new Date(Date.now() - 60_000).toISOString();
|
|
290
|
+
const session = makeSession({
|
|
291
|
+
capability: "monitor",
|
|
292
|
+
state: "working",
|
|
293
|
+
lastActivity: staleActivity,
|
|
294
|
+
});
|
|
295
|
+
const check = evaluateHealth(session, true, THRESHOLDS);
|
|
296
|
+
|
|
297
|
+
expect(check.state).toBe("working");
|
|
298
|
+
expect(check.action).toBe("none");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("persistent capability: orchestrator with stale activity → still working", () => {
|
|
302
|
+
const staleActivity = new Date(Date.now() - 60_000).toISOString();
|
|
303
|
+
const session = makeSession({
|
|
304
|
+
agentName: "orchestrator",
|
|
305
|
+
capability: "orchestrator",
|
|
306
|
+
state: "working",
|
|
307
|
+
lastActivity: staleActivity,
|
|
308
|
+
});
|
|
309
|
+
const check = evaluateHealth(session, true, THRESHOLDS);
|
|
310
|
+
|
|
311
|
+
expect(check.state).toBe("working");
|
|
312
|
+
expect(check.action).toBe("none");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("persistent capability: coordinator booting → transitions to working", () => {
|
|
316
|
+
const session = makeSession({
|
|
317
|
+
capability: "coordinator",
|
|
318
|
+
state: "booting",
|
|
319
|
+
});
|
|
320
|
+
const check = evaluateHealth(session, true, THRESHOLDS);
|
|
321
|
+
|
|
322
|
+
expect(check.state).toBe("working");
|
|
323
|
+
expect(check.action).toBe("none");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("persistent capability: coordinator previously stalled → resets to working", () => {
|
|
327
|
+
const staleActivity = new Date(Date.now() - 60_000).toISOString();
|
|
328
|
+
const session = makeSession({
|
|
329
|
+
capability: "coordinator",
|
|
330
|
+
state: "stalled",
|
|
331
|
+
lastActivity: staleActivity,
|
|
332
|
+
});
|
|
333
|
+
const check = evaluateHealth(session, true, THRESHOLDS);
|
|
334
|
+
|
|
335
|
+
expect(check.state).toBe("working");
|
|
336
|
+
expect(check.action).toBe("none");
|
|
337
|
+
expect(check.reconciliationNote).toContain("Persistent capability");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("persistent capability: coordinator with tmux dead → still zombie (ZFC Rule 1 applies)", () => {
|
|
341
|
+
const session = makeSession({
|
|
342
|
+
capability: "coordinator",
|
|
343
|
+
state: "working",
|
|
344
|
+
});
|
|
345
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
346
|
+
|
|
347
|
+
expect(check.state).toBe("zombie");
|
|
348
|
+
expect(check.action).toBe("terminate");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("persistent capability: coordinator with pid dead → still zombie (ZFC Rule 3 applies)", () => {
|
|
352
|
+
const session = makeSession({
|
|
353
|
+
capability: "coordinator",
|
|
354
|
+
state: "working",
|
|
355
|
+
pid: DEAD_PID,
|
|
356
|
+
});
|
|
357
|
+
const check = evaluateHealth(session, true, THRESHOLDS);
|
|
358
|
+
|
|
359
|
+
expect(check.state).toBe("zombie");
|
|
360
|
+
expect(check.action).toBe("terminate");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// --- Completed agents ---
|
|
364
|
+
|
|
365
|
+
test("completed agents skip monitoring", () => {
|
|
366
|
+
const session = makeSession({ state: "completed" });
|
|
367
|
+
const check = evaluateHealth(session, true, THRESHOLDS);
|
|
368
|
+
|
|
369
|
+
expect(check.state).toBe("completed");
|
|
370
|
+
expect(check.action).toBe("none");
|
|
371
|
+
expect(check.reconciliationNote).toBeNull();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// --- pidAlive field is populated ---
|
|
375
|
+
|
|
376
|
+
test("pidAlive reflects actual process state for alive PID", () => {
|
|
377
|
+
const session = makeSession({ pid: ALIVE_PID, state: "working" });
|
|
378
|
+
const check = evaluateHealth(session, true, THRESHOLDS);
|
|
379
|
+
|
|
380
|
+
expect(check.pidAlive).toBe(true);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("pidAlive reflects actual process state for dead PID", () => {
|
|
384
|
+
// Use dead pid but also tmux dead to avoid pid-zombie path intercepting
|
|
385
|
+
const session = makeSession({ pid: DEAD_PID, state: "working" });
|
|
386
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
387
|
+
|
|
388
|
+
// tmux dead takes priority, so state is zombie via ZFC Rule 1
|
|
389
|
+
expect(check.state).toBe("zombie");
|
|
390
|
+
expect(check.pidAlive).toBe(false);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// === Headless agents (tmuxSession === '', PID-based lifecycle) ===
|
|
395
|
+
|
|
396
|
+
describe("headless agents (tmuxSession empty, PID-based lifecycle)", () => {
|
|
397
|
+
// Headless agents always have tmuxAlive=false passed by the caller (no tmux).
|
|
398
|
+
// PID is the primary liveness signal.
|
|
399
|
+
|
|
400
|
+
test("headless agent with alive PID → working (NOT zombie)", () => {
|
|
401
|
+
const session = makeSession({ tmuxSession: "", pid: ALIVE_PID, state: "working" });
|
|
402
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
403
|
+
|
|
404
|
+
expect(check.state).toBe("working");
|
|
405
|
+
expect(check.action).toBe("none");
|
|
406
|
+
expect(check.processAlive).toBe(true);
|
|
407
|
+
expect(check.pidAlive).toBe(true);
|
|
408
|
+
// tmuxAlive is always false for headless
|
|
409
|
+
expect(check.tmuxAlive).toBe(false);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("headless agent with dead PID → zombie, terminate", () => {
|
|
413
|
+
const session = makeSession({ tmuxSession: "", pid: DEAD_PID, state: "working" });
|
|
414
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
415
|
+
|
|
416
|
+
expect(check.state).toBe("zombie");
|
|
417
|
+
expect(check.action).toBe("terminate");
|
|
418
|
+
expect(check.processAlive).toBe(false);
|
|
419
|
+
expect(check.pidAlive).toBe(false);
|
|
420
|
+
expect(check.reconciliationNote).toContain("ZFC");
|
|
421
|
+
expect(check.reconciliationNote).toContain("headless");
|
|
422
|
+
expect(check.reconciliationNote).toContain("dead");
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("headless agent with alive PID + state=zombie → investigate (don't auto-kill)", () => {
|
|
426
|
+
const session = makeSession({ tmuxSession: "", pid: ALIVE_PID, state: "zombie" });
|
|
427
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
428
|
+
|
|
429
|
+
expect(check.state).toBe("zombie");
|
|
430
|
+
expect(check.action).toBe("investigate");
|
|
431
|
+
expect(check.processAlive).toBe(true);
|
|
432
|
+
expect(check.reconciliationNote).toContain("ZFC");
|
|
433
|
+
expect(check.reconciliationNote).toContain("don't auto-kill");
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("headless booting agent with alive PID → transitions to working", () => {
|
|
437
|
+
const session = makeSession({ tmuxSession: "", pid: ALIVE_PID, state: "booting" });
|
|
438
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
439
|
+
|
|
440
|
+
expect(check.state).toBe("working");
|
|
441
|
+
expect(check.action).toBe("none");
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("headless agent with stale activity → stalled", () => {
|
|
445
|
+
const staleActivity = new Date(Date.now() - 60_000).toISOString();
|
|
446
|
+
const session = makeSession({
|
|
447
|
+
tmuxSession: "",
|
|
448
|
+
pid: ALIVE_PID,
|
|
449
|
+
state: "working",
|
|
450
|
+
lastActivity: staleActivity,
|
|
451
|
+
});
|
|
452
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
453
|
+
|
|
454
|
+
expect(check.state).toBe("stalled");
|
|
455
|
+
expect(check.action).toBe("escalate");
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("headless agent with zombie-level staleness → zombie", () => {
|
|
459
|
+
const oldActivity = new Date(Date.now() - 200_000).toISOString();
|
|
460
|
+
const session = makeSession({
|
|
461
|
+
tmuxSession: "",
|
|
462
|
+
pid: ALIVE_PID,
|
|
463
|
+
state: "working",
|
|
464
|
+
lastActivity: oldActivity,
|
|
465
|
+
});
|
|
466
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
467
|
+
|
|
468
|
+
expect(check.state).toBe("zombie");
|
|
469
|
+
expect(check.action).toBe("terminate");
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("headless persistent capability (coordinator) with stale activity → still working", () => {
|
|
473
|
+
const staleActivity = new Date(Date.now() - 60_000).toISOString();
|
|
474
|
+
const session = makeSession({
|
|
475
|
+
tmuxSession: "",
|
|
476
|
+
pid: ALIVE_PID,
|
|
477
|
+
capability: "coordinator",
|
|
478
|
+
state: "working",
|
|
479
|
+
lastActivity: staleActivity,
|
|
480
|
+
});
|
|
481
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
482
|
+
|
|
483
|
+
expect(check.state).toBe("working");
|
|
484
|
+
expect(check.action).toBe("none");
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("headless completed agent → skips monitoring", () => {
|
|
488
|
+
const session = makeSession({ tmuxSession: "", pid: ALIVE_PID, state: "completed" });
|
|
489
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
490
|
+
|
|
491
|
+
expect(check.state).toBe("completed");
|
|
492
|
+
expect(check.action).toBe("none");
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// === Spawn-per-turn workers (tmuxSession === '' && pid === null) ===
|
|
497
|
+
|
|
498
|
+
describe("spawn-per-turn workers (agentplate-7a34)", () => {
|
|
499
|
+
// Spawn-per-turn workers (builder/scout/reviewer/lead/merger under the
|
|
500
|
+
// headless default) have no persistent process between turns. The previous
|
|
501
|
+
// "headless" branch only matched pid !== null, so these sessions fell into
|
|
502
|
+
// the TUI/tmux path where tmuxAlive=false → ZFC Rule 1 → zombie within
|
|
503
|
+
// seconds of sling, despite being actively executing tools (agentplate-7a34).
|
|
504
|
+
|
|
505
|
+
test("freshly slung spawn-per-turn lead (booting, no pid, no tmux) → between_turns (agentplate-3087)", () => {
|
|
506
|
+
// Spec change: spawn-per-turn workers report `between_turns` instead
|
|
507
|
+
// of `working` for the healthy classification, including the booting
|
|
508
|
+
// → healthy transition. The turn-runner authoritatively writes
|
|
509
|
+
// `in_turn` once the first parser event of a turn arrives.
|
|
510
|
+
const session = makeSession({
|
|
511
|
+
tmuxSession: "",
|
|
512
|
+
pid: null,
|
|
513
|
+
capability: "lead",
|
|
514
|
+
state: "booting",
|
|
515
|
+
lastActivity: new Date().toISOString(),
|
|
516
|
+
});
|
|
517
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
518
|
+
|
|
519
|
+
expect(check.state).toBe("between_turns");
|
|
520
|
+
expect(check.action).toBe("none");
|
|
521
|
+
expect(check.reconciliationNote).toBeNull();
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("legacy spawn-per-turn worker still at 'working' is reported as between_turns (agentplate-3087)", () => {
|
|
525
|
+
// A row that predates the substate split (state=working) gets
|
|
526
|
+
// reclassified to `between_turns` by the watchdog's healthy-state
|
|
527
|
+
// reporter. transitionState then promotes the row forward (working
|
|
528
|
+
// and between_turns share rank 1 in STATE_ORDER, so the actual
|
|
529
|
+
// promotion happens via tryTransitionState elsewhere — here we just
|
|
530
|
+
// verify the check itself reports the new substate).
|
|
531
|
+
const session = makeSession({
|
|
532
|
+
tmuxSession: "",
|
|
533
|
+
pid: null,
|
|
534
|
+
capability: "builder",
|
|
535
|
+
state: "working",
|
|
536
|
+
lastActivity: new Date(Date.now() - 5_000).toISOString(),
|
|
537
|
+
});
|
|
538
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
539
|
+
|
|
540
|
+
expect(check.state).toBe("between_turns");
|
|
541
|
+
expect(check.action).toBe("none");
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("spawn-per-turn worker between turns (recent activity) → between_turns, NOT zombie (agentplate-3087)", () => {
|
|
545
|
+
// Repro of agentplate-7a34: ap sling --capability lead any-task; within
|
|
546
|
+
// ~30s ap dashboard previously showed state='zombie' while ap feed
|
|
547
|
+
// showed live tool calls. The healthy classification now lands
|
|
548
|
+
// between_turns; the test still verifies that recent activity does
|
|
549
|
+
// not trigger zombie classification.
|
|
550
|
+
const session = makeSession({
|
|
551
|
+
tmuxSession: "",
|
|
552
|
+
pid: null,
|
|
553
|
+
capability: "lead",
|
|
554
|
+
state: "working",
|
|
555
|
+
lastActivity: new Date().toISOString(),
|
|
556
|
+
});
|
|
557
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
558
|
+
|
|
559
|
+
expect(check.state).toBe("between_turns");
|
|
560
|
+
expect(check.action).toBe("none");
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test("spawn-per-turn worker with stale activity → stalled", () => {
|
|
564
|
+
const session = makeSession({
|
|
565
|
+
tmuxSession: "",
|
|
566
|
+
pid: null,
|
|
567
|
+
capability: "builder",
|
|
568
|
+
state: "working",
|
|
569
|
+
lastActivity: new Date(Date.now() - 60_000).toISOString(),
|
|
570
|
+
});
|
|
571
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
572
|
+
|
|
573
|
+
expect(check.state).toBe("stalled");
|
|
574
|
+
expect(check.action).toBe("escalate");
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test("spawn-per-turn worker with zombie-level staleness → zombie, terminate", () => {
|
|
578
|
+
const session = makeSession({
|
|
579
|
+
tmuxSession: "",
|
|
580
|
+
pid: null,
|
|
581
|
+
capability: "builder",
|
|
582
|
+
state: "working",
|
|
583
|
+
lastActivity: new Date(Date.now() - 200_000).toISOString(),
|
|
584
|
+
});
|
|
585
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
586
|
+
|
|
587
|
+
expect(check.state).toBe("zombie");
|
|
588
|
+
expect(check.action).toBe("terminate");
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test("spawn-per-turn worker that already completed → skips monitoring", () => {
|
|
592
|
+
const session = makeSession({
|
|
593
|
+
tmuxSession: "",
|
|
594
|
+
pid: null,
|
|
595
|
+
capability: "builder",
|
|
596
|
+
state: "completed",
|
|
597
|
+
});
|
|
598
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
599
|
+
|
|
600
|
+
expect(check.state).toBe("completed");
|
|
601
|
+
expect(check.action).toBe("none");
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test("preserves in_turn for healthy spawn-per-turn worker (agentplate-3087)", () => {
|
|
605
|
+
// A spawn-per-turn worker the turn-runner has marked in_turn must
|
|
606
|
+
// have its state preserved by the health evaluation when activity is
|
|
607
|
+
// recent — otherwise the watchdog would stomp the substate back to
|
|
608
|
+
// `working` and the UI would lose the distinction between mid-turn
|
|
609
|
+
// and idling.
|
|
610
|
+
const session = makeSession({
|
|
611
|
+
tmuxSession: "",
|
|
612
|
+
pid: null,
|
|
613
|
+
capability: "builder",
|
|
614
|
+
state: "in_turn",
|
|
615
|
+
lastActivity: new Date().toISOString(),
|
|
616
|
+
});
|
|
617
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
618
|
+
|
|
619
|
+
expect(check.state).toBe("in_turn");
|
|
620
|
+
expect(check.action).toBe("none");
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test("preserves between_turns for healthy spawn-per-turn worker (agentplate-3087)", () => {
|
|
624
|
+
const session = makeSession({
|
|
625
|
+
tmuxSession: "",
|
|
626
|
+
pid: null,
|
|
627
|
+
capability: "builder",
|
|
628
|
+
state: "between_turns",
|
|
629
|
+
lastActivity: new Date().toISOString(),
|
|
630
|
+
});
|
|
631
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
632
|
+
|
|
633
|
+
expect(check.state).toBe("between_turns");
|
|
634
|
+
expect(check.action).toBe("none");
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test("escalates an in_turn worker with stale activity to stalled (agentplate-3087)", () => {
|
|
638
|
+
const session = makeSession({
|
|
639
|
+
tmuxSession: "",
|
|
640
|
+
pid: null,
|
|
641
|
+
capability: "builder",
|
|
642
|
+
state: "in_turn",
|
|
643
|
+
lastActivity: new Date(Date.now() - 60_000).toISOString(),
|
|
644
|
+
});
|
|
645
|
+
const check = evaluateHealth(session, false, THRESHOLDS);
|
|
646
|
+
|
|
647
|
+
expect(check.state).toBe("stalled");
|
|
648
|
+
expect(check.action).toBe("escalate");
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// === transitionState ===
|
|
653
|
+
|
|
654
|
+
describe("transitionState", () => {
|
|
655
|
+
test("advances from booting to working", () => {
|
|
656
|
+
const check = {
|
|
657
|
+
state: "working" as const,
|
|
658
|
+
agentName: "a",
|
|
659
|
+
timestamp: "",
|
|
660
|
+
tmuxAlive: true,
|
|
661
|
+
pidAlive: true as boolean | null,
|
|
662
|
+
lastActivity: "",
|
|
663
|
+
processAlive: true,
|
|
664
|
+
action: "none" as const,
|
|
665
|
+
reconciliationNote: null,
|
|
666
|
+
};
|
|
667
|
+
expect(transitionState("booting", check)).toBe("working");
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
test("advances from working to stalled", () => {
|
|
671
|
+
const check = {
|
|
672
|
+
state: "stalled" as const,
|
|
673
|
+
agentName: "a",
|
|
674
|
+
timestamp: "",
|
|
675
|
+
tmuxAlive: true,
|
|
676
|
+
pidAlive: true as boolean | null,
|
|
677
|
+
lastActivity: "",
|
|
678
|
+
processAlive: true,
|
|
679
|
+
action: "escalate" as const,
|
|
680
|
+
reconciliationNote: null,
|
|
681
|
+
};
|
|
682
|
+
expect(transitionState("working", check)).toBe("stalled");
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test("never regresses from stalled to working", () => {
|
|
686
|
+
const check = {
|
|
687
|
+
state: "working" as const,
|
|
688
|
+
agentName: "a",
|
|
689
|
+
timestamp: "",
|
|
690
|
+
tmuxAlive: true,
|
|
691
|
+
pidAlive: true as boolean | null,
|
|
692
|
+
lastActivity: "",
|
|
693
|
+
processAlive: true,
|
|
694
|
+
action: "none" as const,
|
|
695
|
+
reconciliationNote: null,
|
|
696
|
+
};
|
|
697
|
+
expect(transitionState("stalled", check)).toBe("stalled");
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
test("never regresses from zombie to booting", () => {
|
|
701
|
+
const check = {
|
|
702
|
+
state: "booting" as const,
|
|
703
|
+
agentName: "a",
|
|
704
|
+
timestamp: "",
|
|
705
|
+
tmuxAlive: true,
|
|
706
|
+
pidAlive: true as boolean | null,
|
|
707
|
+
lastActivity: "",
|
|
708
|
+
processAlive: true,
|
|
709
|
+
action: "none" as const,
|
|
710
|
+
reconciliationNote: null,
|
|
711
|
+
};
|
|
712
|
+
expect(transitionState("zombie", check)).toBe("zombie");
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
test("same state stays the same", () => {
|
|
716
|
+
const check = {
|
|
717
|
+
state: "working" as const,
|
|
718
|
+
agentName: "a",
|
|
719
|
+
timestamp: "",
|
|
720
|
+
tmuxAlive: true,
|
|
721
|
+
pidAlive: true as boolean | null,
|
|
722
|
+
lastActivity: "",
|
|
723
|
+
processAlive: true,
|
|
724
|
+
action: "none" as const,
|
|
725
|
+
reconciliationNote: null,
|
|
726
|
+
};
|
|
727
|
+
expect(transitionState("working", check)).toBe("working");
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// --- ZFC: investigate holds state ---
|
|
731
|
+
|
|
732
|
+
test("ZFC: investigate action holds current state (does not advance)", () => {
|
|
733
|
+
const check = {
|
|
734
|
+
state: "zombie" as const,
|
|
735
|
+
agentName: "a",
|
|
736
|
+
timestamp: "",
|
|
737
|
+
tmuxAlive: true,
|
|
738
|
+
pidAlive: true as boolean | null,
|
|
739
|
+
lastActivity: "",
|
|
740
|
+
processAlive: true,
|
|
741
|
+
action: "investigate" as const,
|
|
742
|
+
reconciliationNote: "ZFC: tmux alive but sessions.json says zombie",
|
|
743
|
+
};
|
|
744
|
+
// Even though check.state is zombie (order 4) and current is zombie (order 4),
|
|
745
|
+
// investigate should hold — not advance
|
|
746
|
+
expect(transitionState("zombie", check)).toBe("zombie");
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
test("ZFC: investigate prevents forward transition", () => {
|
|
750
|
+
const check = {
|
|
751
|
+
state: "zombie" as const,
|
|
752
|
+
agentName: "a",
|
|
753
|
+
timestamp: "",
|
|
754
|
+
tmuxAlive: true,
|
|
755
|
+
pidAlive: true as boolean | null,
|
|
756
|
+
lastActivity: "",
|
|
757
|
+
processAlive: true,
|
|
758
|
+
action: "investigate" as const,
|
|
759
|
+
reconciliationNote: "ZFC conflict",
|
|
760
|
+
};
|
|
761
|
+
// If something were at "working" and check says zombie with investigate,
|
|
762
|
+
// the state should NOT advance
|
|
763
|
+
expect(transitionState("working", check)).toBe("working");
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
// --- in_turn / between_turns coexist with working at the active rank (agentplate-3087) ---
|
|
767
|
+
|
|
768
|
+
test("preserves in_turn when watchdog reports a healthy 'working' check", () => {
|
|
769
|
+
// The watchdog's healthy-classification check returns state=working;
|
|
770
|
+
// since in_turn shares rank 1 with working, transitionState must not
|
|
771
|
+
// advance and the spawn-per-turn substate the turn-runner wrote stays.
|
|
772
|
+
const check = {
|
|
773
|
+
state: "working" as const,
|
|
774
|
+
agentName: "a",
|
|
775
|
+
timestamp: "",
|
|
776
|
+
tmuxAlive: true,
|
|
777
|
+
pidAlive: true as boolean | null,
|
|
778
|
+
lastActivity: "",
|
|
779
|
+
processAlive: true,
|
|
780
|
+
action: "none" as const,
|
|
781
|
+
reconciliationNote: null,
|
|
782
|
+
};
|
|
783
|
+
expect(transitionState("in_turn", check)).toBe("in_turn");
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
test("preserves between_turns when watchdog reports a healthy 'working' check", () => {
|
|
787
|
+
const check = {
|
|
788
|
+
state: "working" as const,
|
|
789
|
+
agentName: "a",
|
|
790
|
+
timestamp: "",
|
|
791
|
+
tmuxAlive: true,
|
|
792
|
+
pidAlive: true as boolean | null,
|
|
793
|
+
lastActivity: "",
|
|
794
|
+
processAlive: true,
|
|
795
|
+
action: "none" as const,
|
|
796
|
+
reconciliationNote: null,
|
|
797
|
+
};
|
|
798
|
+
expect(transitionState("between_turns", check)).toBe("between_turns");
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
test("advances in_turn → stalled when the watchdog escalates", () => {
|
|
802
|
+
const check = {
|
|
803
|
+
state: "stalled" as const,
|
|
804
|
+
agentName: "a",
|
|
805
|
+
timestamp: "",
|
|
806
|
+
tmuxAlive: true,
|
|
807
|
+
pidAlive: true as boolean | null,
|
|
808
|
+
lastActivity: "",
|
|
809
|
+
processAlive: true,
|
|
810
|
+
action: "escalate" as const,
|
|
811
|
+
reconciliationNote: null,
|
|
812
|
+
};
|
|
813
|
+
expect(transitionState("in_turn", check)).toBe("stalled");
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
test("advances between_turns → zombie when the watchdog terminates", () => {
|
|
817
|
+
const check = {
|
|
818
|
+
state: "zombie" as const,
|
|
819
|
+
agentName: "a",
|
|
820
|
+
timestamp: "",
|
|
821
|
+
tmuxAlive: false,
|
|
822
|
+
pidAlive: false as boolean | null,
|
|
823
|
+
lastActivity: "",
|
|
824
|
+
processAlive: false,
|
|
825
|
+
action: "terminate" as const,
|
|
826
|
+
reconciliationNote: null,
|
|
827
|
+
};
|
|
828
|
+
expect(transitionState("between_turns", check)).toBe("zombie");
|
|
829
|
+
});
|
|
830
|
+
});
|