@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,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health check state machine and evaluation logic for agent monitoring.
|
|
3
|
+
*
|
|
4
|
+
* ZFC Principle (Zero Failure Crash)
|
|
5
|
+
* ==================================
|
|
6
|
+
* Observable state is the source of truth, not recorded state.
|
|
7
|
+
*
|
|
8
|
+
* Signal priority (highest to lowest):
|
|
9
|
+
* 1. tmux session liveness — Is the tmux session actually running?
|
|
10
|
+
* 2. Process liveness (pid) — Is the Claude Code process still alive?
|
|
11
|
+
* 3. Recorded state — What does sessions.json claim?
|
|
12
|
+
*
|
|
13
|
+
* When signals conflict, always trust what you can observe:
|
|
14
|
+
* - tmux dead + sessions.json says "working" → mark zombie immediately.
|
|
15
|
+
* The recorded state is stale; the process is gone.
|
|
16
|
+
* - tmux alive + sessions.json says "zombie" → investigate, don't auto-kill.
|
|
17
|
+
* Something marked it zombie but the process recovered or was misclassified.
|
|
18
|
+
* - pid dead + tmux alive → the pane's shell survived but the agent process
|
|
19
|
+
* exited. Treat as zombie (the agent is not doing work).
|
|
20
|
+
* - pid alive + tmux dead → should not happen (tmux owns the pid), but if it
|
|
21
|
+
* does, trust tmux (the session is gone).
|
|
22
|
+
*
|
|
23
|
+
* Headless agents (tmuxSession === ''):
|
|
24
|
+
* Headless agents have no tmux session. For these, PID is the PRIMARY liveness
|
|
25
|
+
* signal. The tmuxAlive parameter is meaningless and ignored. ZFC rules are
|
|
26
|
+
* applied using PID liveness instead of tmux liveness.
|
|
27
|
+
*
|
|
28
|
+
* The rationale: sessions.json is updated asynchronously by hooks and can become
|
|
29
|
+
* stale if the agent crashes between hook invocations. tmux and the OS process
|
|
30
|
+
* table are always up-to-date because they reflect real kernel state.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { isPersistentCapability } from "../agents/capabilities.ts";
|
|
34
|
+
import type { AgentSession, AgentState, HealthCheck } from "../types.ts";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Numeric ordering for forward-only state transitions.
|
|
38
|
+
*
|
|
39
|
+
* `in_turn` and `between_turns` share the `working` rank (1) because, from
|
|
40
|
+
* the watchdog's perspective, all three are "agent is alive and active" —
|
|
41
|
+
* they only differ in whether the spawn-per-turn worker is currently
|
|
42
|
+
* mid-execution or idling between mail batches (agentplate-3087). Same rank
|
|
43
|
+
* means a healthy-classification check (`check.state === "working"`) will
|
|
44
|
+
* not stomp on the more specific in_turn/between_turns states the
|
|
45
|
+
* turn-runner has already written.
|
|
46
|
+
*/
|
|
47
|
+
const STATE_ORDER: Record<AgentState, number> = {
|
|
48
|
+
booting: 0,
|
|
49
|
+
working: 1,
|
|
50
|
+
in_turn: 1,
|
|
51
|
+
between_turns: 1,
|
|
52
|
+
completed: 2,
|
|
53
|
+
stalled: 3,
|
|
54
|
+
zombie: 4,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check whether a process with the given PID is still running.
|
|
59
|
+
*
|
|
60
|
+
* Uses signal 0 which does not kill the process — it only checks
|
|
61
|
+
* whether it exists and we have permission to signal it.
|
|
62
|
+
*
|
|
63
|
+
* @param pid - The process ID to check
|
|
64
|
+
* @returns true if the process exists, false otherwise
|
|
65
|
+
*/
|
|
66
|
+
export function isProcessRunning(pid: number): boolean {
|
|
67
|
+
try {
|
|
68
|
+
// Signal 0 doesn't kill the process — just checks if it exists
|
|
69
|
+
process.kill(pid, 0);
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Detect whether a session is a long-lived headless agent.
|
|
78
|
+
*
|
|
79
|
+
* Long-lived headless agents (coordinator, orchestrator, monitor, sapling, etc.)
|
|
80
|
+
* have no tmux session (tmuxSession === '') but do have a persistent process —
|
|
81
|
+
* so `session.pid` is non-null and PID is the primary liveness signal.
|
|
82
|
+
*/
|
|
83
|
+
function isHeadlessSession(session: AgentSession): boolean {
|
|
84
|
+
return session.tmuxSession === "" && session.pid !== null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Detect whether a session is a spawn-per-turn worker between turns.
|
|
89
|
+
*
|
|
90
|
+
* Spawn-per-turn workers (task-scoped capabilities under the new headless
|
|
91
|
+
* default — builder/scout/reviewer/lead/merger) have no tmux session AND no
|
|
92
|
+
* persistent process: `tmuxSession === ''` and `session.pid === null` from
|
|
93
|
+
* sling onward. The per-turn claude PID lives in
|
|
94
|
+
* `.agentplate/agents/<name>/turn.pid` only while a turn is in flight.
|
|
95
|
+
*
|
|
96
|
+
* "No process" is the normal state between turns, so neither tmux liveness nor
|
|
97
|
+
* pid liveness can be used as a death signal — only `lastActivity` recency
|
|
98
|
+
* (refreshed by the turn-runner on every event and by the watchdog from
|
|
99
|
+
* events.db) can. (agentplate-7a34)
|
|
100
|
+
*/
|
|
101
|
+
export function isSpawnPerTurnSession(session: AgentSession): boolean {
|
|
102
|
+
return session.tmuxSession === "" && session.pid === null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Evaluate time-based health (persistent capability exemptions, stale, zombie thresholds,
|
|
107
|
+
* booting→working transition). Called after liveness is confirmed for both TUI and headless paths.
|
|
108
|
+
*
|
|
109
|
+
* Assumes that by the time this is called:
|
|
110
|
+
* - The agent is not completed
|
|
111
|
+
* - The agent is not in a liveness-based zombie state
|
|
112
|
+
* - The agent is not in a zombie state that needs investigation
|
|
113
|
+
*/
|
|
114
|
+
function evaluateTimeBased(
|
|
115
|
+
session: AgentSession,
|
|
116
|
+
base: Pick<HealthCheck, "agentName" | "timestamp" | "tmuxAlive" | "pidAlive" | "lastActivity">,
|
|
117
|
+
elapsedMs: number,
|
|
118
|
+
thresholds: { staleMs: number; zombieMs: number },
|
|
119
|
+
): HealthCheck {
|
|
120
|
+
// Persistent capabilities (coordinator, monitor) are expected to have long idle
|
|
121
|
+
// periods waiting for mail/events. Skip time-based stale/zombie detection for
|
|
122
|
+
// them — only tmux/pid liveness matters (checked above).
|
|
123
|
+
if (isPersistentCapability(session.capability)) {
|
|
124
|
+
// Transition booting → working if we reach here (process alive)
|
|
125
|
+
const state = session.state === "booting" ? "working" : session.state;
|
|
126
|
+
return {
|
|
127
|
+
...base,
|
|
128
|
+
processAlive: true,
|
|
129
|
+
state: state === "stalled" ? "working" : state,
|
|
130
|
+
action: "none",
|
|
131
|
+
reconciliationNote:
|
|
132
|
+
session.state === "stalled"
|
|
133
|
+
? `Persistent capability "${session.capability}" exempted from stale detection — resetting to working`
|
|
134
|
+
: null,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// lastActivity older than zombieMs → zombie
|
|
139
|
+
if (elapsedMs > thresholds.zombieMs) {
|
|
140
|
+
return {
|
|
141
|
+
...base,
|
|
142
|
+
processAlive: true,
|
|
143
|
+
state: "zombie",
|
|
144
|
+
action: "terminate",
|
|
145
|
+
reconciliationNote: null,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// lastActivity older than staleMs → stalled
|
|
150
|
+
if (elapsedMs > thresholds.staleMs) {
|
|
151
|
+
return {
|
|
152
|
+
...base,
|
|
153
|
+
processAlive: true,
|
|
154
|
+
state: "stalled",
|
|
155
|
+
action: "escalate",
|
|
156
|
+
reconciliationNote: null,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Spawn-per-turn workers (agentplate-3087): healthy classification reports
|
|
161
|
+
// `between_turns` instead of `working`, including the booting → healthy
|
|
162
|
+
// transition. The turn-runner authoritatively writes `in_turn` /
|
|
163
|
+
// `between_turns` while a turn is alive; in_turn is preserved here when
|
|
164
|
+
// already set so a watchdog tick mid-turn does not overwrite it.
|
|
165
|
+
const isSpawnPerTurn = isSpawnPerTurnSession(session);
|
|
166
|
+
|
|
167
|
+
// booting → transition to the healthy state once there's recent activity.
|
|
168
|
+
if (session.state === "booting") {
|
|
169
|
+
return {
|
|
170
|
+
...base,
|
|
171
|
+
processAlive: true,
|
|
172
|
+
state: isSpawnPerTurn ? "between_turns" : "working",
|
|
173
|
+
action: "none",
|
|
174
|
+
reconciliationNote: null,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Default: healthy active state. For spawn-per-turn workers report the
|
|
179
|
+
// existing in_turn/between_turns substate; for tmux/long-lived agents
|
|
180
|
+
// report `working`. The turn-runner is authoritative for in_turn ↔
|
|
181
|
+
// between_turns transitions, so the watchdog must not stomp the more
|
|
182
|
+
// specific state — same rank in STATE_ORDER ensures `transitionState`
|
|
183
|
+
// also leaves the row alone.
|
|
184
|
+
let healthyState: AgentState;
|
|
185
|
+
if (session.state === "in_turn" || session.state === "between_turns") {
|
|
186
|
+
healthyState = session.state;
|
|
187
|
+
} else if (isSpawnPerTurn) {
|
|
188
|
+
healthyState = "between_turns";
|
|
189
|
+
} else {
|
|
190
|
+
healthyState = "working";
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
...base,
|
|
194
|
+
processAlive: true,
|
|
195
|
+
state: healthyState,
|
|
196
|
+
action: "none",
|
|
197
|
+
reconciliationNote: null,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Evaluate the health of an agent session.
|
|
203
|
+
*
|
|
204
|
+
* Implements the ZFC principle: observable state (tmux liveness, pid liveness)
|
|
205
|
+
* takes priority over recorded state (sessions.json fields).
|
|
206
|
+
*
|
|
207
|
+
* Decision logic (in priority order):
|
|
208
|
+
*
|
|
209
|
+
* 1. Completed agents skip monitoring entirely.
|
|
210
|
+
* 2. Spawn-per-turn workers (tmuxSession === '' && pid === null): no
|
|
211
|
+
* persistent process between turns — fall straight through to time-based
|
|
212
|
+
* checks driven by lastActivity. PID/tmux liveness are meaningless here.
|
|
213
|
+
* 3. Headless agents with persistent process (tmuxSession === '' && pid !== null):
|
|
214
|
+
* PID is primary liveness signal.
|
|
215
|
+
* - pid dead → zombie, terminate.
|
|
216
|
+
* - pid alive + state zombie → investigate.
|
|
217
|
+
* - pid alive → fall through to time-based checks.
|
|
218
|
+
* 4. tmux dead → zombie, terminate (regardless of what sessions.json says).
|
|
219
|
+
* 5. tmux alive + sessions.json says zombie → investigate (don't auto-kill).
|
|
220
|
+
* Something external marked this zombie, but the process is still running.
|
|
221
|
+
* 6. pid dead + tmux alive → zombie, terminate. The agent process exited but
|
|
222
|
+
* the tmux pane shell survived. The agent is not doing work.
|
|
223
|
+
* 7. lastActivity older than zombieMs → zombie, terminate.
|
|
224
|
+
* 8. lastActivity older than staleMs → stalled, escalate.
|
|
225
|
+
* 9. booting with recent activity → working.
|
|
226
|
+
* 10. Otherwise → working, healthy.
|
|
227
|
+
*
|
|
228
|
+
* @param session - The agent session to evaluate
|
|
229
|
+
* @param tmuxAlive - Whether the agent's tmux session is still running
|
|
230
|
+
* (ignored for headless agents where tmuxSession === '')
|
|
231
|
+
* @param thresholds - Staleness and zombie time thresholds in milliseconds
|
|
232
|
+
* @returns A HealthCheck describing the agent's current state and recommended action
|
|
233
|
+
*/
|
|
234
|
+
export function evaluateHealth(
|
|
235
|
+
session: AgentSession,
|
|
236
|
+
tmuxAlive: boolean,
|
|
237
|
+
thresholds: { staleMs: number; zombieMs: number },
|
|
238
|
+
): HealthCheck {
|
|
239
|
+
const now = new Date();
|
|
240
|
+
const lastActivityTime = new Date(session.lastActivity).getTime();
|
|
241
|
+
const elapsedMs = now.getTime() - lastActivityTime;
|
|
242
|
+
|
|
243
|
+
// Check pid liveness as secondary signal (null if pid unavailable)
|
|
244
|
+
const pidAlive = session.pid !== null ? isProcessRunning(session.pid) : null;
|
|
245
|
+
|
|
246
|
+
// Headless agents have no tmux session; tmuxAlive is always false for them.
|
|
247
|
+
const effectiveTmuxAlive = isHeadlessSession(session) ? false : tmuxAlive;
|
|
248
|
+
|
|
249
|
+
const base: Pick<
|
|
250
|
+
HealthCheck,
|
|
251
|
+
"agentName" | "timestamp" | "tmuxAlive" | "pidAlive" | "lastActivity"
|
|
252
|
+
> = {
|
|
253
|
+
agentName: session.agentName,
|
|
254
|
+
timestamp: now.toISOString(),
|
|
255
|
+
tmuxAlive: effectiveTmuxAlive,
|
|
256
|
+
pidAlive,
|
|
257
|
+
lastActivity: session.lastActivity,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Completed agents don't need health monitoring
|
|
261
|
+
if (session.state === "completed") {
|
|
262
|
+
return {
|
|
263
|
+
...base,
|
|
264
|
+
processAlive: effectiveTmuxAlive,
|
|
265
|
+
state: "completed",
|
|
266
|
+
action: "none",
|
|
267
|
+
reconciliationNote: null,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// === Spawn-per-turn path: no persistent process between turns ===
|
|
272
|
+
// For these workers (agentplate-7a34) `session.pid` is null by design and
|
|
273
|
+
// there is no tmux session. Liveness signals reduce to lastActivity
|
|
274
|
+
// recency: the turn-runner updates it on every parser event during a
|
|
275
|
+
// turn, and the watchdog refreshes it from events.db between turns. PID
|
|
276
|
+
// and tmux checks would always say "dead" and false-positive every fresh
|
|
277
|
+
// agent as zombie within seconds of sling.
|
|
278
|
+
if (isSpawnPerTurnSession(session)) {
|
|
279
|
+
return evaluateTimeBased(session, base, elapsedMs, thresholds);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// === Headless path: PID is the primary liveness signal ===
|
|
283
|
+
if (isHeadlessSession(session)) {
|
|
284
|
+
// pid dead: zombie OR completed-with-missed-signal.
|
|
285
|
+
// Distinguish by lastActivity age — recent activity means the agent
|
|
286
|
+
// crashed mid-work (true zombie); stale activity means it likely
|
|
287
|
+
// finished naturally and only the session-end hook didn't deliver
|
|
288
|
+
// (treat as completed). (agentplate-e74b)
|
|
289
|
+
if (pidAlive === false) {
|
|
290
|
+
if (
|
|
291
|
+
elapsedMs > thresholds.staleMs &&
|
|
292
|
+
(session.state === "working" || session.state === "booting" || session.state === "stalled")
|
|
293
|
+
) {
|
|
294
|
+
return {
|
|
295
|
+
...base,
|
|
296
|
+
processAlive: false,
|
|
297
|
+
state: "completed",
|
|
298
|
+
action: "complete",
|
|
299
|
+
reconciliationNote: `ZFC: headless pid ${session.pid} dead + stale lastActivity (${Math.round(elapsedMs / 1000)}s ago) — assumed completed (missed session-end signal)`,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
...base,
|
|
304
|
+
processAlive: false,
|
|
305
|
+
state: "zombie",
|
|
306
|
+
action: "terminate",
|
|
307
|
+
reconciliationNote: `ZFC: headless agent pid ${session.pid} dead — marking zombie`,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// pid alive + state zombie → investigate (equivalent to ZFC Rule 2 for headless)
|
|
312
|
+
if (session.state === "zombie") {
|
|
313
|
+
return {
|
|
314
|
+
...base,
|
|
315
|
+
processAlive: true,
|
|
316
|
+
state: "zombie",
|
|
317
|
+
action: "investigate",
|
|
318
|
+
reconciliationNote:
|
|
319
|
+
"ZFC: headless pid alive but sessions.json says zombie — investigation needed (don't auto-kill)",
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// pid alive → fall through to time-based checks
|
|
324
|
+
return evaluateTimeBased(session, base, elapsedMs, thresholds);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// === TUI/tmux path ===
|
|
328
|
+
|
|
329
|
+
// ZFC Rule 1: tmux dead → zombie OR completed-with-missed-signal.
|
|
330
|
+
// Distinguish by lastActivity age — recent activity means the agent
|
|
331
|
+
// crashed mid-work (true zombie); stale activity means it likely
|
|
332
|
+
// finished naturally and only the session-end hook didn't deliver
|
|
333
|
+
// (treat as completed). (agentplate-e74b)
|
|
334
|
+
if (!tmuxAlive) {
|
|
335
|
+
if (
|
|
336
|
+
elapsedMs > thresholds.staleMs &&
|
|
337
|
+
(session.state === "working" || session.state === "booting" || session.state === "stalled")
|
|
338
|
+
) {
|
|
339
|
+
return {
|
|
340
|
+
...base,
|
|
341
|
+
processAlive: false,
|
|
342
|
+
state: "completed",
|
|
343
|
+
action: "complete",
|
|
344
|
+
reconciliationNote: `ZFC: tmux dead + stale lastActivity (${Math.round(elapsedMs / 1000)}s ago) — assumed completed (missed session-end signal)`,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const note =
|
|
349
|
+
session.state === "working" || session.state === "booting"
|
|
350
|
+
? `ZFC: tmux dead but sessions.json says "${session.state}" — marking zombie (observable state wins)`
|
|
351
|
+
: null;
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
...base,
|
|
355
|
+
processAlive: false,
|
|
356
|
+
state: "zombie",
|
|
357
|
+
action: "terminate",
|
|
358
|
+
reconciliationNote: note,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ZFC Rule 2: tmux alive but sessions.json says zombie → investigate.
|
|
363
|
+
// Something marked it zombie but the process is still running. Don't auto-kill;
|
|
364
|
+
// a human or higher-tier agent should decide.
|
|
365
|
+
if (session.state === "zombie") {
|
|
366
|
+
return {
|
|
367
|
+
...base,
|
|
368
|
+
processAlive: true,
|
|
369
|
+
state: "zombie",
|
|
370
|
+
action: "investigate",
|
|
371
|
+
reconciliationNote:
|
|
372
|
+
"ZFC: tmux alive but sessions.json says zombie — investigation needed (don't auto-kill)",
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ZFC Rule 3: pid dead but tmux alive → the agent process exited but the
|
|
377
|
+
// tmux pane shell survived. The agent is not doing work.
|
|
378
|
+
if (pidAlive === false) {
|
|
379
|
+
return {
|
|
380
|
+
...base,
|
|
381
|
+
processAlive: false,
|
|
382
|
+
state: "zombie",
|
|
383
|
+
action: "terminate",
|
|
384
|
+
reconciliationNote: `ZFC: pid ${session.pid} dead but tmux alive — agent process exited, shell survived`,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Time-based checks (both tmux and pid confirmed alive, or pid unavailable)
|
|
389
|
+
return evaluateTimeBased(session, base, elapsedMs, thresholds);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Compute the next agent state based on a health check.
|
|
394
|
+
*
|
|
395
|
+
* State transitions are strictly forward-only using the ordering:
|
|
396
|
+
* booting(0) → working(1) → stalled(2) → zombie(3)
|
|
397
|
+
*
|
|
398
|
+
* A state can only advance forward, never move backwards.
|
|
399
|
+
* For example, a zombie can never become working again.
|
|
400
|
+
*
|
|
401
|
+
* Exception (ZFC): When the health check action is "investigate", the state
|
|
402
|
+
* is NOT advanced. This allows a human or higher-tier agent to review the
|
|
403
|
+
* conflicting signals before making a state change.
|
|
404
|
+
*
|
|
405
|
+
* @param currentState - The agent's current state
|
|
406
|
+
* @param check - The latest health check result
|
|
407
|
+
* @returns The new state (always >= currentState in ordering)
|
|
408
|
+
*/
|
|
409
|
+
export function transitionState(currentState: AgentState, check: HealthCheck): AgentState {
|
|
410
|
+
// ZFC: investigate means signals conflict — hold state until reviewed
|
|
411
|
+
if (check.action === "investigate") {
|
|
412
|
+
return currentState;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// `complete` is a terminal classification triggered when observable state
|
|
416
|
+
// proves the agent finished naturally (missed session-end signal —
|
|
417
|
+
// agentplate-e74b). It bypasses the forward-only STATE_ORDER guard because
|
|
418
|
+
// `completed` (order 2) sits before `stalled` (order 3) and would
|
|
419
|
+
// otherwise be blocked from advancing the recorded state. The matrix in
|
|
420
|
+
// SessionStore.tryTransitionState still gates the actual write.
|
|
421
|
+
if (check.action === "complete") {
|
|
422
|
+
return check.state;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const currentOrder = STATE_ORDER[currentState];
|
|
426
|
+
const checkOrder = STATE_ORDER[check.state];
|
|
427
|
+
|
|
428
|
+
// Only move forward — never regress
|
|
429
|
+
if (checkOrder > currentOrder) {
|
|
430
|
+
return check.state;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return currentState;
|
|
434
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Tier 1 AI-assisted triage.
|
|
3
|
+
* classifyResponse and buildTriagePrompt are pure functions — tested directly.
|
|
4
|
+
* triageAgent uses real filesystem (temp dirs). Claude spawn is expected to
|
|
5
|
+
* fail in test environments, exercising the fallback-to-extend path.
|
|
6
|
+
* spawnClaude is NOT mocked — we rely on it failing naturally in tests.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
10
|
+
import { mkdir, mkdtemp } from "node:fs/promises";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { cleanupTempDir } from "../test-helpers.ts";
|
|
14
|
+
import { buildTriagePrompt, classifyResponse, triageAgent } from "./triage.ts";
|
|
15
|
+
|
|
16
|
+
describe("classifyResponse", () => {
|
|
17
|
+
test("returns 'retry' when response contains 'retry'", () => {
|
|
18
|
+
const result = classifyResponse("The operation should retry.");
|
|
19
|
+
expect(result).toBe("retry");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("returns 'retry' when response contains 'recoverable'", () => {
|
|
23
|
+
const result = classifyResponse("This error is recoverable.");
|
|
24
|
+
expect(result).toBe("retry");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("returns 'terminate' when response contains 'terminate'", () => {
|
|
28
|
+
const result = classifyResponse("You should terminate the agent.");
|
|
29
|
+
expect(result).toBe("terminate");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("returns 'terminate' when response contains 'fatal'", () => {
|
|
33
|
+
const result = classifyResponse("This is a fatal error.");
|
|
34
|
+
expect(result).toBe("terminate");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("returns 'terminate' when response contains 'failed'", () => {
|
|
38
|
+
const result = classifyResponse("The operation has failed.");
|
|
39
|
+
expect(result).toBe("terminate");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("handles mixed case (e.g., 'RETRY', 'Fatal')", () => {
|
|
43
|
+
expect(classifyResponse("RETRY this operation")).toBe("retry");
|
|
44
|
+
expect(classifyResponse("Fatal error occurred")).toBe("terminate");
|
|
45
|
+
expect(classifyResponse("RecOverAble issue")).toBe("retry");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("returns 'extend' when response contains none of the keywords", () => {
|
|
49
|
+
const result = classifyResponse("The agent is processing data.");
|
|
50
|
+
expect(result).toBe("extend");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("returns 'extend' for empty string", () => {
|
|
54
|
+
const result = classifyResponse("");
|
|
55
|
+
expect(result).toBe("extend");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("first match wins when response has multiple keywords", () => {
|
|
59
|
+
// 'retry' is checked before 'terminate'
|
|
60
|
+
const result = classifyResponse("retry this but it may terminate later");
|
|
61
|
+
expect(result).toBe("retry");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("buildTriagePrompt", () => {
|
|
66
|
+
test("contains agent name in output", () => {
|
|
67
|
+
const prompt = buildTriagePrompt("test-agent", "2026-02-13T10:00:00Z", "log content");
|
|
68
|
+
expect(prompt).toContain("test-agent");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("contains lastActivity timestamp in output", () => {
|
|
72
|
+
const timestamp = "2026-02-13T10:00:00Z";
|
|
73
|
+
const prompt = buildTriagePrompt("test-agent", timestamp, "log content");
|
|
74
|
+
expect(prompt).toContain(timestamp);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("contains log content wrapped in code fences", () => {
|
|
78
|
+
const logContent = "Error: something went wrong\nat line 42";
|
|
79
|
+
const prompt = buildTriagePrompt("test-agent", "2026-02-13T10:00:00Z", logContent);
|
|
80
|
+
expect(prompt).toContain("```");
|
|
81
|
+
expect(prompt).toContain(logContent);
|
|
82
|
+
expect(prompt.split("```").length).toBeGreaterThanOrEqual(3); // Opening and closing fences
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("contains classification instructions (retry/terminate/extend)", () => {
|
|
86
|
+
const prompt = buildTriagePrompt("test-agent", "2026-02-13T10:00:00Z", "log content");
|
|
87
|
+
expect(prompt).toContain("retry");
|
|
88
|
+
expect(prompt).toContain("terminate");
|
|
89
|
+
expect(prompt).toContain("extend");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("triageAgent", () => {
|
|
94
|
+
let tempRoot: string;
|
|
95
|
+
|
|
96
|
+
beforeEach(async () => {
|
|
97
|
+
tempRoot = await mkdtemp(join(tmpdir(), "triage-test-"));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
afterEach(async () => {
|
|
101
|
+
await cleanupTempDir(tempRoot);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("returns fallback TriageResult when no logs directory exists", async () => {
|
|
105
|
+
const result = await triageAgent({
|
|
106
|
+
agentName: "test-agent",
|
|
107
|
+
root: tempRoot,
|
|
108
|
+
lastActivity: "2026-02-13T10:00:00Z",
|
|
109
|
+
});
|
|
110
|
+
expect(result.verdict).toBe("extend");
|
|
111
|
+
expect(result.fallback).toBe(true);
|
|
112
|
+
expect(result.reason).toBe("No logs available");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("returns fallback TriageResult when logs directory exists but is empty", async () => {
|
|
116
|
+
const logsDir = join(tempRoot, ".agentplate", "logs", "test-agent");
|
|
117
|
+
await mkdir(logsDir, { recursive: true });
|
|
118
|
+
|
|
119
|
+
const result = await triageAgent({
|
|
120
|
+
agentName: "test-agent",
|
|
121
|
+
root: tempRoot,
|
|
122
|
+
lastActivity: "2026-02-13T10:00:00Z",
|
|
123
|
+
});
|
|
124
|
+
expect(result.verdict).toBe("extend");
|
|
125
|
+
expect(result.fallback).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("returns fallback TriageResult when logs directory has session dir but no session.log", async () => {
|
|
129
|
+
const logsDir = join(tempRoot, ".agentplate", "logs", "test-agent", "2026-02-13T10-00-00");
|
|
130
|
+
await Bun.write(join(logsDir, ".gitkeep"), "");
|
|
131
|
+
|
|
132
|
+
const result = await triageAgent({
|
|
133
|
+
agentName: "test-agent",
|
|
134
|
+
root: tempRoot,
|
|
135
|
+
lastActivity: "2026-02-13T10:00:00Z",
|
|
136
|
+
});
|
|
137
|
+
expect(result.verdict).toBe("extend");
|
|
138
|
+
expect(result.fallback).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("returns fallback TriageResult when session.log exists but claude binary fails", async () => {
|
|
142
|
+
const timestamp = "2026-02-13T10-00-00";
|
|
143
|
+
const sessionLogPath = join(
|
|
144
|
+
tempRoot,
|
|
145
|
+
".agentplate",
|
|
146
|
+
"logs",
|
|
147
|
+
"test-agent",
|
|
148
|
+
timestamp,
|
|
149
|
+
"session.log",
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Create session.log with some content
|
|
153
|
+
await Bun.write(
|
|
154
|
+
sessionLogPath,
|
|
155
|
+
"Agent started\nProcessing data\nError: something went wrong\n",
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// triageAgent will try to spawn claude which should fail or be killed by timeout.
|
|
159
|
+
// Short timeout ensures the test doesn't hang even if the claude binary
|
|
160
|
+
// exists on the system (e.g., inside a Claude Code session).
|
|
161
|
+
const result = await triageAgent({
|
|
162
|
+
agentName: "test-agent",
|
|
163
|
+
root: tempRoot,
|
|
164
|
+
lastActivity: "2026-02-13T10:00:00Z",
|
|
165
|
+
timeoutMs: 500,
|
|
166
|
+
});
|
|
167
|
+
expect(result.verdict).toBe("extend");
|
|
168
|
+
expect(result.fallback).toBe(true);
|
|
169
|
+
expect(result.reason).toBe("Claude unavailable");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("writes stderr warning when claude is unavailable (fallback path)", async () => {
|
|
173
|
+
const timestamp = "2026-02-13T10-00-00";
|
|
174
|
+
const sessionLogPath = join(
|
|
175
|
+
tempRoot,
|
|
176
|
+
".agentplate",
|
|
177
|
+
"logs",
|
|
178
|
+
"test-agent",
|
|
179
|
+
timestamp,
|
|
180
|
+
"session.log",
|
|
181
|
+
);
|
|
182
|
+
await Bun.write(sessionLogPath, "some log content\n");
|
|
183
|
+
|
|
184
|
+
const written: string[] = [];
|
|
185
|
+
const spy = spyOn(process.stderr, "write").mockImplementation((chunk: unknown) => {
|
|
186
|
+
written.push(String(chunk));
|
|
187
|
+
return true;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await triageAgent({
|
|
192
|
+
agentName: "test-agent",
|
|
193
|
+
root: tempRoot,
|
|
194
|
+
lastActivity: "2026-02-13T10:00:00Z",
|
|
195
|
+
timeoutMs: 500,
|
|
196
|
+
});
|
|
197
|
+
} finally {
|
|
198
|
+
spy.mockRestore();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
expect(written.some((s) => s.includes("triage fallback") && s.includes("test-agent"))).toBe(
|
|
202
|
+
true,
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
});
|