@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,1913 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, readdir, stat } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { ValidationError } from "../errors.ts";
|
|
6
|
+
import { createEventStore } from "../events/store.ts";
|
|
7
|
+
import type { LoamClient } from "../loam/client.ts";
|
|
8
|
+
import { createMailClient } from "../mail/client.ts";
|
|
9
|
+
import { createMailStore } from "../mail/store.ts";
|
|
10
|
+
import { createMetricsStore } from "../metrics/store.ts";
|
|
11
|
+
import { createRunStore, createSessionStore } from "../sessions/store.ts";
|
|
12
|
+
import { cleanupTempDir } from "../test-helpers.ts";
|
|
13
|
+
import type { AgentSession, LoamLearnResult, StoredEvent } from "../types.ts";
|
|
14
|
+
import { appendOutcomeToAppliedRecords, autoRecordExpertise, logCommand } from "./log.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Tests for `agentplate log` command.
|
|
18
|
+
*
|
|
19
|
+
* Uses real filesystem (temp dirs) and real bun:sqlite to test logging behavior.
|
|
20
|
+
* Captures process.stdout.write to verify help text output.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
describe("logCommand", () => {
|
|
24
|
+
let chunks: string[];
|
|
25
|
+
let originalWrite: typeof process.stdout.write;
|
|
26
|
+
let tempDir: string;
|
|
27
|
+
let originalCwd: string;
|
|
28
|
+
|
|
29
|
+
beforeEach(async () => {
|
|
30
|
+
// Spy on stdout
|
|
31
|
+
chunks = [];
|
|
32
|
+
originalWrite = process.stdout.write;
|
|
33
|
+
process.stdout.write = ((chunk: string) => {
|
|
34
|
+
chunks.push(chunk);
|
|
35
|
+
return true;
|
|
36
|
+
}) as typeof process.stdout.write;
|
|
37
|
+
|
|
38
|
+
// Create temp dir with .agentplate/config.yaml structure
|
|
39
|
+
tempDir = await mkdtemp(join(tmpdir(), "log-test-"));
|
|
40
|
+
const agentplateDir = join(tempDir, ".agentplate");
|
|
41
|
+
await Bun.write(
|
|
42
|
+
join(agentplateDir, "config.yaml"),
|
|
43
|
+
`project:\n name: test\n root: ${tempDir}\n canonicalBranch: main\n`,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Change to temp dir so loadConfig() works
|
|
47
|
+
originalCwd = process.cwd();
|
|
48
|
+
process.chdir(tempDir);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(async () => {
|
|
52
|
+
process.stdout.write = originalWrite;
|
|
53
|
+
process.chdir(originalCwd);
|
|
54
|
+
await cleanupTempDir(tempDir);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function output(): string {
|
|
58
|
+
return chunks.join("");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fake LoamClient for testing autoRecordExpertise and appendOutcomeToAppliedRecords.
|
|
63
|
+
* Only learn(), record(), and appendOutcome() are implemented — other methods are stubs.
|
|
64
|
+
* Justified: we are testing orchestration logic, not the loam CLI itself.
|
|
65
|
+
*/
|
|
66
|
+
function createFakeLoamClient(
|
|
67
|
+
learnResult: LoamLearnResult,
|
|
68
|
+
opts?: { recordShouldFail?: boolean; appendOutcomeShouldFail?: boolean },
|
|
69
|
+
): {
|
|
70
|
+
client: LoamClient;
|
|
71
|
+
recordCalls: Array<{ domain: string; options: Record<string, unknown> }>;
|
|
72
|
+
appendOutcomeCalls: Array<{
|
|
73
|
+
domain: string;
|
|
74
|
+
id: string;
|
|
75
|
+
outcome: Record<string, unknown>;
|
|
76
|
+
}>;
|
|
77
|
+
} {
|
|
78
|
+
const recordCalls: Array<{ domain: string; options: Record<string, unknown> }> = [];
|
|
79
|
+
const appendOutcomeCalls: Array<{
|
|
80
|
+
domain: string;
|
|
81
|
+
id: string;
|
|
82
|
+
outcome: Record<string, unknown>;
|
|
83
|
+
}> = [];
|
|
84
|
+
const client = {
|
|
85
|
+
async learn() {
|
|
86
|
+
return learnResult;
|
|
87
|
+
},
|
|
88
|
+
async record(domain: string, options: Record<string, unknown>) {
|
|
89
|
+
if (opts?.recordShouldFail) {
|
|
90
|
+
throw new Error("loam record failed");
|
|
91
|
+
}
|
|
92
|
+
recordCalls.push({ domain, options });
|
|
93
|
+
},
|
|
94
|
+
async appendOutcome(domain: string, id: string, outcome: Record<string, unknown>) {
|
|
95
|
+
if (opts?.appendOutcomeShouldFail) {
|
|
96
|
+
throw new Error("loam appendOutcome failed");
|
|
97
|
+
}
|
|
98
|
+
appendOutcomeCalls.push({ domain, id, outcome });
|
|
99
|
+
},
|
|
100
|
+
} as unknown as LoamClient;
|
|
101
|
+
return { client, recordCalls, appendOutcomeCalls };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
test("--help flag shows help text", async () => {
|
|
105
|
+
await logCommand(["--help"]);
|
|
106
|
+
const out = output();
|
|
107
|
+
|
|
108
|
+
expect(out).toContain("log");
|
|
109
|
+
expect(out).toContain("tool-start");
|
|
110
|
+
expect(out).toContain("tool-end");
|
|
111
|
+
expect(out).toContain("session-end");
|
|
112
|
+
expect(out).toContain("--agent");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("-h flag shows help text", async () => {
|
|
116
|
+
await logCommand(["-h"]);
|
|
117
|
+
const out = output();
|
|
118
|
+
|
|
119
|
+
expect(out).toContain("log");
|
|
120
|
+
expect(out).toContain("tool-start");
|
|
121
|
+
expect(out).toContain("tool-end");
|
|
122
|
+
expect(out).toContain("session-end");
|
|
123
|
+
expect(out).toContain("--agent");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("missing event argument throws when required argument missing", async () => {
|
|
127
|
+
// Commander throws when a required positional argument is missing
|
|
128
|
+
await expect(async () => {
|
|
129
|
+
await logCommand([]);
|
|
130
|
+
}).toThrow();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("invalid event name throws ValidationError", async () => {
|
|
134
|
+
expect(async () => {
|
|
135
|
+
await logCommand(["invalid-event", "--agent", "test-agent"]);
|
|
136
|
+
}).toThrow(ValidationError);
|
|
137
|
+
|
|
138
|
+
expect(async () => {
|
|
139
|
+
await logCommand(["invalid-event", "--agent", "test-agent"]);
|
|
140
|
+
}).toThrow("Invalid event");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("missing --agent flag throws ValidationError", async () => {
|
|
144
|
+
expect(async () => {
|
|
145
|
+
await logCommand(["tool-start"]);
|
|
146
|
+
}).toThrow(ValidationError);
|
|
147
|
+
|
|
148
|
+
expect(async () => {
|
|
149
|
+
await logCommand(["tool-start"]);
|
|
150
|
+
}).toThrow("--agent is required");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("tool-start creates log directory structure", async () => {
|
|
154
|
+
await logCommand(["tool-start", "--agent", "test-builder", "--tool-name", "Read"]);
|
|
155
|
+
|
|
156
|
+
const logsDir = join(tempDir, ".agentplate", "logs", "test-builder");
|
|
157
|
+
const contents = await readdir(logsDir);
|
|
158
|
+
|
|
159
|
+
// Should have at least .current-session marker and a session directory
|
|
160
|
+
expect(contents).toContain(".current-session");
|
|
161
|
+
expect(contents.length).toBeGreaterThanOrEqual(2);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("tool-start creates session directory and .current-session marker", async () => {
|
|
165
|
+
await logCommand(["tool-start", "--agent", "test-scout", "--tool-name", "Grep"]);
|
|
166
|
+
|
|
167
|
+
const logsDir = join(tempDir, ".agentplate", "logs", "test-scout");
|
|
168
|
+
const markerPath = join(logsDir, ".current-session");
|
|
169
|
+
const markerFile = Bun.file(markerPath);
|
|
170
|
+
|
|
171
|
+
expect(await markerFile.exists()).toBe(true);
|
|
172
|
+
|
|
173
|
+
const sessionDir = (await markerFile.text()).trim();
|
|
174
|
+
expect(sessionDir).toBeTruthy();
|
|
175
|
+
expect(sessionDir).toContain(logsDir);
|
|
176
|
+
|
|
177
|
+
// Session directory should exist
|
|
178
|
+
const dirStat = await stat(sessionDir);
|
|
179
|
+
expect(dirStat.isDirectory()).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("tool-start creates log files in session directory", async () => {
|
|
183
|
+
await logCommand(["tool-start", "--agent", "test-builder", "--tool-name", "Write"]);
|
|
184
|
+
|
|
185
|
+
// Wait for async file writes to complete
|
|
186
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
187
|
+
|
|
188
|
+
const logsDir = join(tempDir, ".agentplate", "logs", "test-builder");
|
|
189
|
+
const markerPath = join(logsDir, ".current-session");
|
|
190
|
+
const sessionDir = (await Bun.file(markerPath).text()).trim();
|
|
191
|
+
|
|
192
|
+
// Check for events.ndjson file
|
|
193
|
+
const eventsFile = Bun.file(join(sessionDir, "events.ndjson"));
|
|
194
|
+
expect(await eventsFile.exists()).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("tool-end uses the same session directory as tool-start", async () => {
|
|
198
|
+
await logCommand(["tool-start", "--agent", "test-agent", "--tool-name", "Edit"]);
|
|
199
|
+
|
|
200
|
+
const logsDir = join(tempDir, ".agentplate", "logs", "test-agent");
|
|
201
|
+
const markerPath = join(logsDir, ".current-session");
|
|
202
|
+
const sessionDirAfterStart = (await Bun.file(markerPath).text()).trim();
|
|
203
|
+
|
|
204
|
+
await logCommand(["tool-end", "--agent", "test-agent", "--tool-name", "Edit"]);
|
|
205
|
+
|
|
206
|
+
const sessionDirAfterEnd = (await Bun.file(markerPath).text()).trim();
|
|
207
|
+
expect(sessionDirAfterEnd).toBe(sessionDirAfterStart);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("tool-end writes to the same session directory", async () => {
|
|
211
|
+
await logCommand(["tool-start", "--agent", "test-worker", "--tool-name", "Bash"]);
|
|
212
|
+
await logCommand(["tool-end", "--agent", "test-worker", "--tool-name", "Bash"]);
|
|
213
|
+
|
|
214
|
+
// Wait for async file writes to complete
|
|
215
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
216
|
+
|
|
217
|
+
const logsDir = join(tempDir, ".agentplate", "logs", "test-worker");
|
|
218
|
+
const markerPath = join(logsDir, ".current-session");
|
|
219
|
+
const sessionDir = (await Bun.file(markerPath).text()).trim();
|
|
220
|
+
|
|
221
|
+
// Events file should contain both tool-start and tool-end events
|
|
222
|
+
const eventsFile = Bun.file(join(sessionDir, "events.ndjson"));
|
|
223
|
+
const eventsContent = await eventsFile.text();
|
|
224
|
+
|
|
225
|
+
expect(eventsContent).toContain("tool.start");
|
|
226
|
+
expect(eventsContent).toContain("tool.end");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("session-end transitions agent state to completed in sessions.db", async () => {
|
|
230
|
+
// Create sessions.db with a test agent
|
|
231
|
+
const dbPath = join(tempDir, ".agentplate", "sessions.db");
|
|
232
|
+
const session: AgentSession = {
|
|
233
|
+
id: "session-001",
|
|
234
|
+
agentName: "test-agent",
|
|
235
|
+
capability: "builder",
|
|
236
|
+
worktreePath: "/tmp/test",
|
|
237
|
+
branchName: "test-branch",
|
|
238
|
+
taskId: "bead-001",
|
|
239
|
+
tmuxSession: "test-tmux",
|
|
240
|
+
state: "working",
|
|
241
|
+
pid: 12345,
|
|
242
|
+
parentAgent: null,
|
|
243
|
+
depth: 0,
|
|
244
|
+
runId: null,
|
|
245
|
+
startedAt: new Date().toISOString(),
|
|
246
|
+
lastActivity: new Date().toISOString(),
|
|
247
|
+
escalationLevel: 0,
|
|
248
|
+
stalledSince: null,
|
|
249
|
+
transcriptPath: null,
|
|
250
|
+
};
|
|
251
|
+
const store = createSessionStore(dbPath);
|
|
252
|
+
store.upsert(session);
|
|
253
|
+
store.close();
|
|
254
|
+
|
|
255
|
+
await logCommand(["session-end", "--agent", "test-agent"]);
|
|
256
|
+
|
|
257
|
+
// Read sessions.db and verify state changed to completed
|
|
258
|
+
const readStore = createSessionStore(dbPath);
|
|
259
|
+
const updatedSession = readStore.getByName("test-agent");
|
|
260
|
+
readStore.close();
|
|
261
|
+
|
|
262
|
+
expect(updatedSession).toBeDefined();
|
|
263
|
+
expect(updatedSession?.state).toBe("completed");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("session-end clears the .current-session marker", async () => {
|
|
267
|
+
// First create a session with tool-start
|
|
268
|
+
await logCommand(["tool-start", "--agent", "test-cleanup", "--tool-name", "Read"]);
|
|
269
|
+
|
|
270
|
+
const logsDir = join(tempDir, ".agentplate", "logs", "test-cleanup");
|
|
271
|
+
const markerPath = join(logsDir, ".current-session");
|
|
272
|
+
|
|
273
|
+
// Verify marker exists before session-end
|
|
274
|
+
let markerFile = Bun.file(markerPath);
|
|
275
|
+
expect(await markerFile.exists()).toBe(true);
|
|
276
|
+
|
|
277
|
+
// Now end the session
|
|
278
|
+
await logCommand(["session-end", "--agent", "test-cleanup"]);
|
|
279
|
+
|
|
280
|
+
// Marker should be removed - need to create a new Bun.file reference
|
|
281
|
+
markerFile = Bun.file(markerPath);
|
|
282
|
+
expect(await markerFile.exists()).toBe(false);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("session-end records metrics when agent session exists in sessions.db", async () => {
|
|
286
|
+
// Create sessions.db with a test agent
|
|
287
|
+
const sessionsDbPath = join(tempDir, ".agentplate", "sessions.db");
|
|
288
|
+
const session: AgentSession = {
|
|
289
|
+
id: "session-002",
|
|
290
|
+
agentName: "metrics-agent",
|
|
291
|
+
capability: "scout",
|
|
292
|
+
worktreePath: "/tmp/metrics",
|
|
293
|
+
branchName: "metrics-branch",
|
|
294
|
+
taskId: "bead-002",
|
|
295
|
+
tmuxSession: "metrics-tmux",
|
|
296
|
+
state: "working",
|
|
297
|
+
pid: 54321,
|
|
298
|
+
parentAgent: "parent-agent",
|
|
299
|
+
depth: 1,
|
|
300
|
+
runId: null,
|
|
301
|
+
startedAt: new Date(Date.now() - 60_000).toISOString(), // 1 minute ago
|
|
302
|
+
lastActivity: new Date().toISOString(),
|
|
303
|
+
escalationLevel: 0,
|
|
304
|
+
stalledSince: null,
|
|
305
|
+
transcriptPath: null,
|
|
306
|
+
};
|
|
307
|
+
const sessStore = createSessionStore(sessionsDbPath);
|
|
308
|
+
sessStore.upsert(session);
|
|
309
|
+
sessStore.close();
|
|
310
|
+
|
|
311
|
+
await logCommand(["session-end", "--agent", "metrics-agent"]);
|
|
312
|
+
|
|
313
|
+
// Verify metrics.db was created and has the session record
|
|
314
|
+
const metricsDbPath = join(tempDir, ".agentplate", "metrics.db");
|
|
315
|
+
const metricsStore = createMetricsStore(metricsDbPath);
|
|
316
|
+
const metrics = metricsStore.getRecentSessions(1);
|
|
317
|
+
metricsStore.close();
|
|
318
|
+
|
|
319
|
+
expect(metrics).toHaveLength(1);
|
|
320
|
+
expect(metrics[0]?.agentName).toBe("metrics-agent");
|
|
321
|
+
expect(metrics[0]?.taskId).toBe("bead-002");
|
|
322
|
+
expect(metrics[0]?.capability).toBe("scout");
|
|
323
|
+
expect(metrics[0]?.parentAgent).toBe("parent-agent");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("session-end does NOT transition coordinator to completed (persistent agent)", async () => {
|
|
327
|
+
// Create sessions.db with a coordinator agent
|
|
328
|
+
const dbPath = join(tempDir, ".agentplate", "sessions.db");
|
|
329
|
+
const session: AgentSession = {
|
|
330
|
+
id: "session-coord",
|
|
331
|
+
agentName: "coordinator",
|
|
332
|
+
capability: "coordinator",
|
|
333
|
+
worktreePath: tempDir,
|
|
334
|
+
branchName: "main",
|
|
335
|
+
taskId: "",
|
|
336
|
+
tmuxSession: "agentplate-coordinator",
|
|
337
|
+
state: "working",
|
|
338
|
+
pid: 11111,
|
|
339
|
+
parentAgent: null,
|
|
340
|
+
depth: 0,
|
|
341
|
+
runId: null,
|
|
342
|
+
startedAt: new Date().toISOString(),
|
|
343
|
+
lastActivity: new Date(Date.now() - 60_000).toISOString(),
|
|
344
|
+
escalationLevel: 0,
|
|
345
|
+
stalledSince: null,
|
|
346
|
+
transcriptPath: null,
|
|
347
|
+
};
|
|
348
|
+
const store = createSessionStore(dbPath);
|
|
349
|
+
store.upsert(session);
|
|
350
|
+
store.close();
|
|
351
|
+
|
|
352
|
+
await logCommand(["session-end", "--agent", "coordinator"]);
|
|
353
|
+
|
|
354
|
+
// Coordinator should remain 'working', not transition to 'completed'
|
|
355
|
+
const readStore = createSessionStore(dbPath);
|
|
356
|
+
const updatedSession = readStore.getByName("coordinator");
|
|
357
|
+
readStore.close();
|
|
358
|
+
|
|
359
|
+
expect(updatedSession).toBeDefined();
|
|
360
|
+
expect(updatedSession?.state).toBe("working");
|
|
361
|
+
// But lastActivity should be updated
|
|
362
|
+
expect(new Date(updatedSession?.lastActivity ?? "").getTime()).toBeGreaterThan(
|
|
363
|
+
new Date(session.lastActivity).getTime(),
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("session-end does NOT transition monitor to completed (persistent agent)", async () => {
|
|
368
|
+
const dbPath = join(tempDir, ".agentplate", "sessions.db");
|
|
369
|
+
const session: AgentSession = {
|
|
370
|
+
id: "session-mon",
|
|
371
|
+
agentName: "monitor",
|
|
372
|
+
capability: "monitor",
|
|
373
|
+
worktreePath: tempDir,
|
|
374
|
+
branchName: "main",
|
|
375
|
+
taskId: "",
|
|
376
|
+
tmuxSession: "agentplate-monitor",
|
|
377
|
+
state: "working",
|
|
378
|
+
pid: 22222,
|
|
379
|
+
parentAgent: null,
|
|
380
|
+
depth: 0,
|
|
381
|
+
runId: null,
|
|
382
|
+
startedAt: new Date().toISOString(),
|
|
383
|
+
lastActivity: new Date(Date.now() - 60_000).toISOString(),
|
|
384
|
+
escalationLevel: 0,
|
|
385
|
+
stalledSince: null,
|
|
386
|
+
transcriptPath: null,
|
|
387
|
+
};
|
|
388
|
+
const store = createSessionStore(dbPath);
|
|
389
|
+
store.upsert(session);
|
|
390
|
+
store.close();
|
|
391
|
+
|
|
392
|
+
await logCommand(["session-end", "--agent", "monitor"]);
|
|
393
|
+
|
|
394
|
+
const readStore = createSessionStore(dbPath);
|
|
395
|
+
const updatedSession = readStore.getByName("monitor");
|
|
396
|
+
readStore.close();
|
|
397
|
+
|
|
398
|
+
expect(updatedSession).toBeDefined();
|
|
399
|
+
expect(updatedSession?.state).toBe("working");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("session-end does NOT transition orchestrator to completed (persistent agent)", async () => {
|
|
403
|
+
const dbPath = join(tempDir, ".agentplate", "sessions.db");
|
|
404
|
+
const session: AgentSession = {
|
|
405
|
+
id: "session-orch",
|
|
406
|
+
agentName: "orchestrator",
|
|
407
|
+
capability: "orchestrator",
|
|
408
|
+
worktreePath: tempDir,
|
|
409
|
+
branchName: "main",
|
|
410
|
+
taskId: "",
|
|
411
|
+
tmuxSession: "agentplate-orchestrator",
|
|
412
|
+
state: "working",
|
|
413
|
+
pid: 33333,
|
|
414
|
+
parentAgent: null,
|
|
415
|
+
depth: 0,
|
|
416
|
+
runId: null,
|
|
417
|
+
startedAt: new Date().toISOString(),
|
|
418
|
+
lastActivity: new Date(Date.now() - 60_000).toISOString(),
|
|
419
|
+
escalationLevel: 0,
|
|
420
|
+
stalledSince: null,
|
|
421
|
+
transcriptPath: null,
|
|
422
|
+
};
|
|
423
|
+
const store = createSessionStore(dbPath);
|
|
424
|
+
store.upsert(session);
|
|
425
|
+
store.close();
|
|
426
|
+
|
|
427
|
+
await logCommand(["session-end", "--agent", "orchestrator"]);
|
|
428
|
+
|
|
429
|
+
const readStore = createSessionStore(dbPath);
|
|
430
|
+
const updatedSession = readStore.getByName("orchestrator");
|
|
431
|
+
readStore.close();
|
|
432
|
+
|
|
433
|
+
expect(updatedSession).toBeDefined();
|
|
434
|
+
expect(updatedSession?.state).toBe("working");
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
describe("session-end coordinator run completion", () => {
|
|
438
|
+
test("session-end does NOT auto-complete the active run for coordinator agent (per-turn Stop hook guard)", async () => {
|
|
439
|
+
// Regression test for agentplate-adc5:
|
|
440
|
+
// The coordinator's Stop hook fires on every turn boundary, not just at true session exit.
|
|
441
|
+
// session-end must NOT auto-complete the run, or the coordinator dies after its first turn.
|
|
442
|
+
const dbPath = join(tempDir, ".agentplate", "sessions.db");
|
|
443
|
+
const sessionStoreLocal = createSessionStore(dbPath);
|
|
444
|
+
sessionStoreLocal.upsert({
|
|
445
|
+
id: "session-coord-run",
|
|
446
|
+
agentName: "coordinator",
|
|
447
|
+
capability: "coordinator",
|
|
448
|
+
worktreePath: tempDir,
|
|
449
|
+
branchName: "main",
|
|
450
|
+
taskId: "",
|
|
451
|
+
tmuxSession: "agentplate-coordinator",
|
|
452
|
+
state: "working",
|
|
453
|
+
pid: 11111,
|
|
454
|
+
parentAgent: null,
|
|
455
|
+
depth: 0,
|
|
456
|
+
runId: "run-test-001",
|
|
457
|
+
startedAt: new Date().toISOString(),
|
|
458
|
+
lastActivity: new Date().toISOString(),
|
|
459
|
+
escalationLevel: 0,
|
|
460
|
+
stalledSince: null,
|
|
461
|
+
transcriptPath: null,
|
|
462
|
+
});
|
|
463
|
+
sessionStoreLocal.close();
|
|
464
|
+
|
|
465
|
+
// Create the run
|
|
466
|
+
const runStore = createRunStore(dbPath);
|
|
467
|
+
runStore.createRun({
|
|
468
|
+
id: "run-test-001",
|
|
469
|
+
startedAt: new Date().toISOString(),
|
|
470
|
+
coordinatorSessionId: "session-coord-run",
|
|
471
|
+
status: "active",
|
|
472
|
+
});
|
|
473
|
+
runStore.close();
|
|
474
|
+
|
|
475
|
+
// Write current-run.txt
|
|
476
|
+
const currentRunPath = join(tempDir, ".agentplate", "current-run.txt");
|
|
477
|
+
await Bun.write(currentRunPath, "run-test-001");
|
|
478
|
+
|
|
479
|
+
// Call session-end (simulates per-turn Stop hook)
|
|
480
|
+
await logCommand(["session-end", "--agent", "coordinator"]);
|
|
481
|
+
|
|
482
|
+
// Verify: run status remains "active" — session-end must NOT auto-complete the run
|
|
483
|
+
const runStoreRead = createRunStore(dbPath);
|
|
484
|
+
const run = runStoreRead.getRun("run-test-001");
|
|
485
|
+
runStoreRead.close();
|
|
486
|
+
|
|
487
|
+
expect(run).toBeDefined();
|
|
488
|
+
expect(run?.status).toBe("active");
|
|
489
|
+
expect(run?.completedAt).toBeNull();
|
|
490
|
+
|
|
491
|
+
// Verify: current-run.txt is NOT deleted (coordinator is still running)
|
|
492
|
+
expect(await Bun.file(currentRunPath).exists()).toBe(true);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("session-end does not fail when no active run for coordinator", async () => {
|
|
496
|
+
// Create a coordinator session but no current-run.txt
|
|
497
|
+
const dbPath = join(tempDir, ".agentplate", "sessions.db");
|
|
498
|
+
const sessionStoreLocal = createSessionStore(dbPath);
|
|
499
|
+
sessionStoreLocal.upsert({
|
|
500
|
+
id: "session-coord-no-run",
|
|
501
|
+
agentName: "coordinator-no-run",
|
|
502
|
+
capability: "coordinator",
|
|
503
|
+
worktreePath: tempDir,
|
|
504
|
+
branchName: "main",
|
|
505
|
+
taskId: "",
|
|
506
|
+
tmuxSession: "agentplate-coordinator-no-run",
|
|
507
|
+
state: "working",
|
|
508
|
+
pid: 11112,
|
|
509
|
+
parentAgent: null,
|
|
510
|
+
depth: 0,
|
|
511
|
+
runId: null,
|
|
512
|
+
startedAt: new Date().toISOString(),
|
|
513
|
+
lastActivity: new Date().toISOString(),
|
|
514
|
+
escalationLevel: 0,
|
|
515
|
+
stalledSince: null,
|
|
516
|
+
transcriptPath: null,
|
|
517
|
+
});
|
|
518
|
+
sessionStoreLocal.close();
|
|
519
|
+
|
|
520
|
+
// Call session-end (should not throw)
|
|
521
|
+
await expect(async () => {
|
|
522
|
+
await logCommand(["session-end", "--agent", "coordinator-no-run"]);
|
|
523
|
+
}).not.toThrow();
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("session-end does not complete run for non-coordinator agents", async () => {
|
|
527
|
+
// Create a builder session, create a run, write current-run.txt
|
|
528
|
+
const dbPath = join(tempDir, ".agentplate", "sessions.db");
|
|
529
|
+
const sessionStoreLocal = createSessionStore(dbPath);
|
|
530
|
+
sessionStoreLocal.upsert({
|
|
531
|
+
id: "session-builder-run",
|
|
532
|
+
agentName: "test-builder",
|
|
533
|
+
capability: "builder",
|
|
534
|
+
worktreePath: tempDir,
|
|
535
|
+
branchName: "builder-branch",
|
|
536
|
+
taskId: "bead-builder-001",
|
|
537
|
+
tmuxSession: "agentplate-builder",
|
|
538
|
+
state: "working",
|
|
539
|
+
pid: 11113,
|
|
540
|
+
parentAgent: null,
|
|
541
|
+
depth: 2,
|
|
542
|
+
runId: "run-test-002",
|
|
543
|
+
startedAt: new Date().toISOString(),
|
|
544
|
+
lastActivity: new Date().toISOString(),
|
|
545
|
+
escalationLevel: 0,
|
|
546
|
+
stalledSince: null,
|
|
547
|
+
transcriptPath: null,
|
|
548
|
+
});
|
|
549
|
+
sessionStoreLocal.close();
|
|
550
|
+
|
|
551
|
+
// Create the run
|
|
552
|
+
const runStore = createRunStore(dbPath);
|
|
553
|
+
runStore.createRun({
|
|
554
|
+
id: "run-test-002",
|
|
555
|
+
startedAt: new Date().toISOString(),
|
|
556
|
+
coordinatorSessionId: "session-coord-run",
|
|
557
|
+
status: "active",
|
|
558
|
+
});
|
|
559
|
+
runStore.close();
|
|
560
|
+
|
|
561
|
+
// Write current-run.txt
|
|
562
|
+
await Bun.write(join(tempDir, ".agentplate", "current-run.txt"), "run-test-002");
|
|
563
|
+
|
|
564
|
+
// Call session-end for builder
|
|
565
|
+
await logCommand(["session-end", "--agent", "test-builder"]);
|
|
566
|
+
|
|
567
|
+
// Verify: run status remains "active"
|
|
568
|
+
const runStoreRead = createRunStore(dbPath);
|
|
569
|
+
const run = runStoreRead.getRun("run-test-002");
|
|
570
|
+
runStoreRead.close();
|
|
571
|
+
|
|
572
|
+
expect(run).toBeDefined();
|
|
573
|
+
expect(run?.status).toBe("active");
|
|
574
|
+
expect(run?.completedAt).toBeNull();
|
|
575
|
+
|
|
576
|
+
// Verify: current-run.txt still exists
|
|
577
|
+
const currentRunFile = Bun.file(join(tempDir, ".agentplate", "current-run.txt"));
|
|
578
|
+
expect(await currentRunFile.exists()).toBe(true);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test("session-end handles already-completed run gracefully", async () => {
|
|
582
|
+
// Create a coordinator session, create a run that is already completed
|
|
583
|
+
const dbPath = join(tempDir, ".agentplate", "sessions.db");
|
|
584
|
+
const sessionStoreLocal = createSessionStore(dbPath);
|
|
585
|
+
sessionStoreLocal.upsert({
|
|
586
|
+
id: "session-coord-completed",
|
|
587
|
+
agentName: "coordinator-completed",
|
|
588
|
+
capability: "coordinator",
|
|
589
|
+
worktreePath: tempDir,
|
|
590
|
+
branchName: "main",
|
|
591
|
+
taskId: "",
|
|
592
|
+
tmuxSession: "agentplate-coordinator-completed",
|
|
593
|
+
state: "working",
|
|
594
|
+
pid: 11114,
|
|
595
|
+
parentAgent: null,
|
|
596
|
+
depth: 0,
|
|
597
|
+
runId: "run-test-003",
|
|
598
|
+
startedAt: new Date().toISOString(),
|
|
599
|
+
lastActivity: new Date().toISOString(),
|
|
600
|
+
escalationLevel: 0,
|
|
601
|
+
stalledSince: null,
|
|
602
|
+
transcriptPath: null,
|
|
603
|
+
});
|
|
604
|
+
sessionStoreLocal.close();
|
|
605
|
+
|
|
606
|
+
// Create the run already completed
|
|
607
|
+
const runStore = createRunStore(dbPath);
|
|
608
|
+
runStore.createRun({
|
|
609
|
+
id: "run-test-003",
|
|
610
|
+
startedAt: new Date().toISOString(),
|
|
611
|
+
coordinatorSessionId: "session-coord-completed",
|
|
612
|
+
status: "active",
|
|
613
|
+
});
|
|
614
|
+
// Complete it immediately
|
|
615
|
+
runStore.completeRun("run-test-003", "completed");
|
|
616
|
+
runStore.close();
|
|
617
|
+
|
|
618
|
+
// Write current-run.txt
|
|
619
|
+
await Bun.write(join(tempDir, ".agentplate", "current-run.txt"), "run-test-003");
|
|
620
|
+
|
|
621
|
+
// Call session-end (should not throw — completeRun is idempotent)
|
|
622
|
+
await expect(async () => {
|
|
623
|
+
await logCommand(["session-end", "--agent", "coordinator-completed"]);
|
|
624
|
+
}).not.toThrow();
|
|
625
|
+
|
|
626
|
+
// Verify: run is still completed
|
|
627
|
+
const runStoreRead = createRunStore(dbPath);
|
|
628
|
+
const run = runStoreRead.getRun("run-test-003");
|
|
629
|
+
runStoreRead.close();
|
|
630
|
+
|
|
631
|
+
expect(run).toBeDefined();
|
|
632
|
+
expect(run?.status).toBe("completed");
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
test("session-end does NOT transition lead to completed (persistent agent)", async () => {
|
|
637
|
+
// Regression test for agentplate-49a7:
|
|
638
|
+
// The lead's Stop hook fires every turn (interactive Claude Code), not just at
|
|
639
|
+
// true session end. session-end must NOT mark leads completed, or they vanish
|
|
640
|
+
// from getActive() after their first turn while their tmux is still alive.
|
|
641
|
+
const dbPath = join(tempDir, ".agentplate", "sessions.db");
|
|
642
|
+
const session: AgentSession = {
|
|
643
|
+
id: "session-lead",
|
|
644
|
+
agentName: "lead-alpha",
|
|
645
|
+
capability: "lead",
|
|
646
|
+
worktreePath: tempDir,
|
|
647
|
+
branchName: "lead-alpha-branch",
|
|
648
|
+
taskId: "bead-lead-001",
|
|
649
|
+
tmuxSession: "agentplate-lead-alpha",
|
|
650
|
+
state: "working",
|
|
651
|
+
pid: 33333,
|
|
652
|
+
parentAgent: null,
|
|
653
|
+
depth: 0,
|
|
654
|
+
runId: null,
|
|
655
|
+
startedAt: new Date().toISOString(),
|
|
656
|
+
lastActivity: new Date(Date.now() - 60_000).toISOString(),
|
|
657
|
+
escalationLevel: 0,
|
|
658
|
+
stalledSince: null,
|
|
659
|
+
transcriptPath: null,
|
|
660
|
+
};
|
|
661
|
+
const store = createSessionStore(dbPath);
|
|
662
|
+
store.upsert(session);
|
|
663
|
+
store.close();
|
|
664
|
+
|
|
665
|
+
await logCommand(["session-end", "--agent", "lead-alpha"]);
|
|
666
|
+
|
|
667
|
+
// Lead should remain 'working', not transition to 'completed'
|
|
668
|
+
const readStore = createSessionStore(dbPath);
|
|
669
|
+
const updatedSession = readStore.getByName("lead-alpha");
|
|
670
|
+
readStore.close();
|
|
671
|
+
|
|
672
|
+
expect(updatedSession).toBeDefined();
|
|
673
|
+
expect(updatedSession?.state).toBe("working");
|
|
674
|
+
// But lastActivity should be updated
|
|
675
|
+
expect(new Date(updatedSession?.lastActivity ?? "").getTime()).toBeGreaterThan(
|
|
676
|
+
new Date(session.lastActivity).getTime(),
|
|
677
|
+
);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
test("session-end does NOT write pending-nudge marker for leads (moved to ap stop)", async () => {
|
|
681
|
+
// Regression test for agentplate-49a7:
|
|
682
|
+
// The lead_completed nudge used to fire from the per-turn Stop hook, spamming
|
|
683
|
+
// the coordinator with false completion signals every turn. It is now emitted
|
|
684
|
+
// only by `ap stop <lead>` (the real completion signal).
|
|
685
|
+
const dbPath = join(tempDir, ".agentplate", "sessions.db");
|
|
686
|
+
const session: AgentSession = {
|
|
687
|
+
id: "session-lead",
|
|
688
|
+
agentName: "lead-alpha",
|
|
689
|
+
capability: "lead",
|
|
690
|
+
worktreePath: tempDir,
|
|
691
|
+
branchName: "lead-alpha-branch",
|
|
692
|
+
taskId: "bead-lead-001",
|
|
693
|
+
tmuxSession: "agentplate-lead-alpha",
|
|
694
|
+
state: "working",
|
|
695
|
+
pid: 33333,
|
|
696
|
+
parentAgent: null,
|
|
697
|
+
depth: 0,
|
|
698
|
+
runId: null,
|
|
699
|
+
startedAt: new Date().toISOString(),
|
|
700
|
+
lastActivity: new Date().toISOString(),
|
|
701
|
+
escalationLevel: 0,
|
|
702
|
+
stalledSince: null,
|
|
703
|
+
transcriptPath: null,
|
|
704
|
+
};
|
|
705
|
+
const store = createSessionStore(dbPath);
|
|
706
|
+
store.upsert(session);
|
|
707
|
+
store.close();
|
|
708
|
+
|
|
709
|
+
await logCommand(["session-end", "--agent", "lead-alpha"]);
|
|
710
|
+
|
|
711
|
+
// No pending-nudge marker should be written from session-end
|
|
712
|
+
const markerPath = join(tempDir, ".agentplate", "pending-nudges", "coordinator.json");
|
|
713
|
+
const markerFile = Bun.file(markerPath);
|
|
714
|
+
expect(await markerFile.exists()).toBe(false);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test("session-end does NOT write pending-nudge marker for non-lead agents", async () => {
|
|
718
|
+
// Create sessions.db with a builder agent (not a lead)
|
|
719
|
+
const dbPath = join(tempDir, ".agentplate", "sessions.db");
|
|
720
|
+
const session: AgentSession = {
|
|
721
|
+
id: "session-builder",
|
|
722
|
+
agentName: "builder-beta",
|
|
723
|
+
capability: "builder",
|
|
724
|
+
worktreePath: tempDir,
|
|
725
|
+
branchName: "builder-beta-branch",
|
|
726
|
+
taskId: "bead-builder-001",
|
|
727
|
+
tmuxSession: "agentplate-builder-beta",
|
|
728
|
+
state: "working",
|
|
729
|
+
pid: 44444,
|
|
730
|
+
parentAgent: null,
|
|
731
|
+
depth: 0,
|
|
732
|
+
runId: null,
|
|
733
|
+
startedAt: new Date().toISOString(),
|
|
734
|
+
lastActivity: new Date().toISOString(),
|
|
735
|
+
escalationLevel: 0,
|
|
736
|
+
stalledSince: null,
|
|
737
|
+
transcriptPath: null,
|
|
738
|
+
};
|
|
739
|
+
const store = createSessionStore(dbPath);
|
|
740
|
+
store.upsert(session);
|
|
741
|
+
store.close();
|
|
742
|
+
|
|
743
|
+
await logCommand(["session-end", "--agent", "builder-beta"]);
|
|
744
|
+
|
|
745
|
+
// Verify no pending-nudge marker was written
|
|
746
|
+
const markerPath = join(tempDir, ".agentplate", "pending-nudges", "coordinator.json");
|
|
747
|
+
const markerFile = Bun.file(markerPath);
|
|
748
|
+
expect(await markerFile.exists()).toBe(false);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
test("session-end does not crash when sessions.db does not exist", async () => {
|
|
752
|
+
// No sessions.db file exists
|
|
753
|
+
// session-end should complete without throwing
|
|
754
|
+
await expect(
|
|
755
|
+
logCommand(["session-end", "--agent", "nonexistent-agent"]),
|
|
756
|
+
).resolves.toBeUndefined();
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
test("tool-start updates lastActivity timestamp in sessions.db", async () => {
|
|
760
|
+
// Create sessions.db with a test agent
|
|
761
|
+
const dbPath = join(tempDir, ".agentplate", "sessions.db");
|
|
762
|
+
const oldTimestamp = new Date(Date.now() - 120_000).toISOString(); // 2 minutes ago
|
|
763
|
+
const session: AgentSession = {
|
|
764
|
+
id: "session-003",
|
|
765
|
+
agentName: "activity-agent",
|
|
766
|
+
capability: "builder",
|
|
767
|
+
worktreePath: "/tmp/activity",
|
|
768
|
+
branchName: "activity-branch",
|
|
769
|
+
taskId: "bead-003",
|
|
770
|
+
tmuxSession: "activity-tmux",
|
|
771
|
+
state: "working",
|
|
772
|
+
pid: 99999,
|
|
773
|
+
parentAgent: null,
|
|
774
|
+
depth: 0,
|
|
775
|
+
runId: null,
|
|
776
|
+
startedAt: oldTimestamp,
|
|
777
|
+
lastActivity: oldTimestamp,
|
|
778
|
+
escalationLevel: 0,
|
|
779
|
+
stalledSince: null,
|
|
780
|
+
transcriptPath: null,
|
|
781
|
+
};
|
|
782
|
+
const store = createSessionStore(dbPath);
|
|
783
|
+
store.upsert(session);
|
|
784
|
+
store.close();
|
|
785
|
+
|
|
786
|
+
await logCommand(["tool-start", "--agent", "activity-agent", "--tool-name", "Glob"]);
|
|
787
|
+
|
|
788
|
+
// Read sessions.db and verify lastActivity was updated
|
|
789
|
+
const readStore = createSessionStore(dbPath);
|
|
790
|
+
const updatedSession = readStore.getByName("activity-agent");
|
|
791
|
+
readStore.close();
|
|
792
|
+
|
|
793
|
+
expect(updatedSession).toBeDefined();
|
|
794
|
+
expect(updatedSession?.lastActivity).not.toBe(oldTimestamp);
|
|
795
|
+
expect(new Date(updatedSession?.lastActivity ?? "").getTime()).toBeGreaterThan(
|
|
796
|
+
new Date(oldTimestamp).getTime(),
|
|
797
|
+
);
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
test("tool-start transitions state from booting to working", async () => {
|
|
801
|
+
// Create sessions.db with agent in 'booting' state
|
|
802
|
+
const dbPath = join(tempDir, ".agentplate", "sessions.db");
|
|
803
|
+
const session: AgentSession = {
|
|
804
|
+
id: "session-004",
|
|
805
|
+
agentName: "booting-agent",
|
|
806
|
+
capability: "builder",
|
|
807
|
+
worktreePath: "/tmp/booting",
|
|
808
|
+
branchName: "booting-branch",
|
|
809
|
+
taskId: "bead-004",
|
|
810
|
+
tmuxSession: "booting-tmux",
|
|
811
|
+
state: "booting",
|
|
812
|
+
pid: 11111,
|
|
813
|
+
parentAgent: null,
|
|
814
|
+
depth: 0,
|
|
815
|
+
runId: null,
|
|
816
|
+
startedAt: new Date().toISOString(),
|
|
817
|
+
lastActivity: new Date().toISOString(),
|
|
818
|
+
escalationLevel: 0,
|
|
819
|
+
stalledSince: null,
|
|
820
|
+
transcriptPath: null,
|
|
821
|
+
};
|
|
822
|
+
const store = createSessionStore(dbPath);
|
|
823
|
+
store.upsert(session);
|
|
824
|
+
store.close();
|
|
825
|
+
|
|
826
|
+
await logCommand(["tool-start", "--agent", "booting-agent", "--tool-name", "Read"]);
|
|
827
|
+
|
|
828
|
+
// Read sessions.db and verify state changed to working
|
|
829
|
+
const readStore = createSessionStore(dbPath);
|
|
830
|
+
const updatedSession = readStore.getByName("booting-agent");
|
|
831
|
+
readStore.close();
|
|
832
|
+
|
|
833
|
+
expect(updatedSession).toBeDefined();
|
|
834
|
+
expect(updatedSession?.state).toBe("working");
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
test("tool-start defaults to unknown when --tool-name not provided", async () => {
|
|
838
|
+
// Should not throw when --tool-name is missing
|
|
839
|
+
await expect(
|
|
840
|
+
logCommand(["tool-start", "--agent", "default-tool-agent"]),
|
|
841
|
+
).resolves.toBeUndefined();
|
|
842
|
+
|
|
843
|
+
// Verify log was created
|
|
844
|
+
const logsDir = join(tempDir, ".agentplate", "logs", "default-tool-agent");
|
|
845
|
+
const markerPath = join(logsDir, ".current-session");
|
|
846
|
+
const markerFile = Bun.file(markerPath);
|
|
847
|
+
|
|
848
|
+
expect(await markerFile.exists()).toBe(true);
|
|
849
|
+
|
|
850
|
+
// Wait for async file writes to complete (logger uses fire-and-forget appendFile)
|
|
851
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
852
|
+
|
|
853
|
+
const sessionDir = (await markerFile.text()).trim();
|
|
854
|
+
const eventsFile = Bun.file(join(sessionDir, "events.ndjson"));
|
|
855
|
+
const eventsContent = await eventsFile.text();
|
|
856
|
+
|
|
857
|
+
// Should contain "unknown" as the tool name
|
|
858
|
+
expect(eventsContent).toContain("unknown");
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
test("tool-end defaults to unknown when --tool-name not provided", async () => {
|
|
862
|
+
await logCommand(["tool-start", "--agent", "default-end-agent"]);
|
|
863
|
+
|
|
864
|
+
// tool-end without --tool-name should not throw
|
|
865
|
+
await expect(logCommand(["tool-end", "--agent", "default-end-agent"])).resolves.toBeUndefined();
|
|
866
|
+
|
|
867
|
+
// Wait for async file writes to complete
|
|
868
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
869
|
+
|
|
870
|
+
const logsDir = join(tempDir, ".agentplate", "logs", "default-end-agent");
|
|
871
|
+
const markerPath = join(logsDir, ".current-session");
|
|
872
|
+
const sessionDir = (await Bun.file(markerPath).text()).trim();
|
|
873
|
+
const eventsFile = Bun.file(join(sessionDir, "events.ndjson"));
|
|
874
|
+
const eventsContent = await eventsFile.text();
|
|
875
|
+
|
|
876
|
+
expect(eventsContent).toContain("unknown");
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
test("tool-start writes to EventStore without --stdin flag (Pi runtime path)", async () => {
|
|
880
|
+
await logCommand(["tool-start", "--agent", "pi-agent", "--tool-name", "Read"]);
|
|
881
|
+
|
|
882
|
+
const eventsDbPath = join(tempDir, ".agentplate", "events.db");
|
|
883
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
884
|
+
const events = eventStore.getByAgent("pi-agent");
|
|
885
|
+
eventStore.close();
|
|
886
|
+
|
|
887
|
+
expect(events).toHaveLength(1);
|
|
888
|
+
expect(events[0]?.eventType).toBe("tool_start");
|
|
889
|
+
expect(events[0]?.toolName).toBe("Read");
|
|
890
|
+
expect(events[0]?.sessionId).toBeNull();
|
|
891
|
+
expect(events[0]?.agentName).toBe("pi-agent");
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
test("tool-end writes to EventStore without --stdin flag (Pi runtime path)", async () => {
|
|
895
|
+
await logCommand(["tool-start", "--agent", "pi-end-agent", "--tool-name", "Write"]);
|
|
896
|
+
await logCommand(["tool-end", "--agent", "pi-end-agent", "--tool-name", "Write"]);
|
|
897
|
+
|
|
898
|
+
const eventsDbPath = join(tempDir, ".agentplate", "events.db");
|
|
899
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
900
|
+
const events = eventStore.getByAgent("pi-end-agent");
|
|
901
|
+
eventStore.close();
|
|
902
|
+
|
|
903
|
+
expect(events).toHaveLength(2);
|
|
904
|
+
const startEv = events.find((e) => e.eventType === "tool_start");
|
|
905
|
+
const endEv = events.find((e) => e.eventType === "tool_end");
|
|
906
|
+
expect(startEv).toBeDefined();
|
|
907
|
+
expect(endEv).toBeDefined();
|
|
908
|
+
expect(startEv?.toolName).toBe("Write");
|
|
909
|
+
expect(endEv?.toolName).toBe("Write");
|
|
910
|
+
expect(startEv?.sessionId).toBeNull();
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
test("session-end writes to EventStore without --stdin flag (Pi runtime path)", async () => {
|
|
914
|
+
await logCommand(["session-end", "--agent", "pi-session-agent"]);
|
|
915
|
+
|
|
916
|
+
const eventsDbPath = join(tempDir, ".agentplate", "events.db");
|
|
917
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
918
|
+
const events = eventStore.getByAgent("pi-session-agent");
|
|
919
|
+
eventStore.close();
|
|
920
|
+
|
|
921
|
+
expect(events).toHaveLength(1);
|
|
922
|
+
expect(events[0]?.eventType).toBe("session_end");
|
|
923
|
+
expect(events[0]?.sessionId).toBeNull();
|
|
924
|
+
expect(events[0]?.agentName).toBe("pi-session-agent");
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
test("--help includes --stdin option in output", async () => {
|
|
928
|
+
await logCommand(["--help"]);
|
|
929
|
+
const out = output();
|
|
930
|
+
|
|
931
|
+
expect(out).toContain("--stdin");
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
test("session-end does not crash when loam learn/record fails", async () => {
|
|
935
|
+
// Create sessions.db with a builder agent (non-persistent)
|
|
936
|
+
const dbPath = join(tempDir, ".agentplate", "sessions.db");
|
|
937
|
+
const session: AgentSession = {
|
|
938
|
+
id: "session-loam-fail",
|
|
939
|
+
agentName: "loam-fail-agent",
|
|
940
|
+
capability: "builder",
|
|
941
|
+
worktreePath: tempDir,
|
|
942
|
+
branchName: "loam-fail-branch",
|
|
943
|
+
taskId: "bead-loam-001",
|
|
944
|
+
tmuxSession: "agentplate-loam-fail",
|
|
945
|
+
state: "working",
|
|
946
|
+
pid: 55555,
|
|
947
|
+
parentAgent: "parent-agent",
|
|
948
|
+
depth: 1,
|
|
949
|
+
runId: null,
|
|
950
|
+
startedAt: new Date().toISOString(),
|
|
951
|
+
lastActivity: new Date().toISOString(),
|
|
952
|
+
escalationLevel: 0,
|
|
953
|
+
stalledSince: null,
|
|
954
|
+
transcriptPath: null,
|
|
955
|
+
};
|
|
956
|
+
const store = createSessionStore(dbPath);
|
|
957
|
+
store.upsert(session);
|
|
958
|
+
store.close();
|
|
959
|
+
|
|
960
|
+
// session-end should complete without throwing even if loam learn/record fails
|
|
961
|
+
await expect(
|
|
962
|
+
logCommand(["session-end", "--agent", "loam-fail-agent"]),
|
|
963
|
+
).resolves.toBeUndefined();
|
|
964
|
+
|
|
965
|
+
// Verify state transitioned to completed
|
|
966
|
+
const readStore = createSessionStore(dbPath);
|
|
967
|
+
const updatedSession = readStore.getByName("loam-fail-agent");
|
|
968
|
+
readStore.close();
|
|
969
|
+
|
|
970
|
+
expect(updatedSession).toBeDefined();
|
|
971
|
+
expect(updatedSession?.state).toBe("completed");
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
test("session-end skips loam auto-record for coordinator (persistent agent)", async () => {
|
|
975
|
+
// Create sessions.db with a coordinator agent
|
|
976
|
+
const dbPath = join(tempDir, ".agentplate", "sessions.db");
|
|
977
|
+
const session: AgentSession = {
|
|
978
|
+
id: "session-coord-loam",
|
|
979
|
+
agentName: "coordinator-loam",
|
|
980
|
+
capability: "coordinator",
|
|
981
|
+
worktreePath: tempDir,
|
|
982
|
+
branchName: "main",
|
|
983
|
+
taskId: "",
|
|
984
|
+
tmuxSession: "agentplate-coordinator-loam",
|
|
985
|
+
state: "working",
|
|
986
|
+
pid: 66666,
|
|
987
|
+
parentAgent: null,
|
|
988
|
+
depth: 0,
|
|
989
|
+
runId: null,
|
|
990
|
+
startedAt: new Date().toISOString(),
|
|
991
|
+
lastActivity: new Date().toISOString(),
|
|
992
|
+
escalationLevel: 0,
|
|
993
|
+
stalledSince: null,
|
|
994
|
+
transcriptPath: null,
|
|
995
|
+
};
|
|
996
|
+
const store = createSessionStore(dbPath);
|
|
997
|
+
store.upsert(session);
|
|
998
|
+
store.close();
|
|
999
|
+
|
|
1000
|
+
await logCommand(["session-end", "--agent", "coordinator-loam"]);
|
|
1001
|
+
|
|
1002
|
+
// Verify no mail.db was created (loam auto-record was skipped)
|
|
1003
|
+
const mailDbPath = join(tempDir, ".agentplate", "mail.db");
|
|
1004
|
+
const mailDbFile = Bun.file(mailDbPath);
|
|
1005
|
+
expect(await mailDbFile.exists()).toBe(false);
|
|
1006
|
+
|
|
1007
|
+
// Coordinator should remain working (persistent agent)
|
|
1008
|
+
const readStore = createSessionStore(dbPath);
|
|
1009
|
+
const updatedSession = readStore.getByName("coordinator-loam");
|
|
1010
|
+
readStore.close();
|
|
1011
|
+
|
|
1012
|
+
expect(updatedSession).toBeDefined();
|
|
1013
|
+
expect(updatedSession?.state).toBe("working");
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
test("autoRecordExpertise calls record for each suggested domain", async () => {
|
|
1017
|
+
const learnResult: LoamLearnResult = {
|
|
1018
|
+
success: true,
|
|
1019
|
+
command: "loam learn",
|
|
1020
|
+
changedFiles: ["src/foo.ts", "src/bar.ts"],
|
|
1021
|
+
suggestedDomains: ["typescript", "cli"],
|
|
1022
|
+
unmatchedFiles: [],
|
|
1023
|
+
};
|
|
1024
|
+
const { client, recordCalls } = createFakeLoamClient(learnResult);
|
|
1025
|
+
const mailDbPath = join(tempDir, ".agentplate", "auto-record-mail.db");
|
|
1026
|
+
|
|
1027
|
+
const result = await autoRecordExpertise({
|
|
1028
|
+
loamClient: client,
|
|
1029
|
+
agentName: "test-builder",
|
|
1030
|
+
capability: "builder",
|
|
1031
|
+
taskId: "bead-123",
|
|
1032
|
+
mailDbPath,
|
|
1033
|
+
parentAgent: "parent-lead",
|
|
1034
|
+
projectRoot: tempDir,
|
|
1035
|
+
sessionStartedAt: new Date().toISOString(),
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
expect(result).toEqual(["typescript", "cli"]);
|
|
1039
|
+
expect(recordCalls).toHaveLength(2);
|
|
1040
|
+
expect(recordCalls[0]?.domain).toBe("typescript");
|
|
1041
|
+
expect(recordCalls[0]?.options).toMatchObject({
|
|
1042
|
+
type: "reference",
|
|
1043
|
+
tags: ["auto-session-end", "builder"],
|
|
1044
|
+
evidenceBead: "bead-123",
|
|
1045
|
+
});
|
|
1046
|
+
expect(recordCalls[1]?.domain).toBe("cli");
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
test("autoRecordExpertise sends mail with auto-recorded subject", async () => {
|
|
1050
|
+
const learnResult: LoamLearnResult = {
|
|
1051
|
+
success: true,
|
|
1052
|
+
command: "loam learn",
|
|
1053
|
+
changedFiles: ["src/foo.ts"],
|
|
1054
|
+
suggestedDomains: ["typescript"],
|
|
1055
|
+
unmatchedFiles: [],
|
|
1056
|
+
};
|
|
1057
|
+
const { client } = createFakeLoamClient(learnResult);
|
|
1058
|
+
const mailDbPath = join(tempDir, ".agentplate", "auto-record-mail2.db");
|
|
1059
|
+
|
|
1060
|
+
await autoRecordExpertise({
|
|
1061
|
+
loamClient: client,
|
|
1062
|
+
agentName: "test-builder",
|
|
1063
|
+
capability: "builder",
|
|
1064
|
+
taskId: "bead-456",
|
|
1065
|
+
mailDbPath,
|
|
1066
|
+
parentAgent: "parent-lead",
|
|
1067
|
+
projectRoot: tempDir,
|
|
1068
|
+
sessionStartedAt: new Date().toISOString(),
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
const mailStore = createMailStore(mailDbPath);
|
|
1072
|
+
const mailClient = createMailClient(mailStore);
|
|
1073
|
+
const messages = mailClient.list({ to: "parent-lead" });
|
|
1074
|
+
mailClient.close();
|
|
1075
|
+
|
|
1076
|
+
expect(messages).toHaveLength(1);
|
|
1077
|
+
expect(messages[0]?.subject).toBe("loam: auto-recorded insights in typescript");
|
|
1078
|
+
expect(messages[0]?.body).toContain("Auto-recorded expertise in: typescript");
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
test("autoRecordExpertise continues when individual record calls fail", async () => {
|
|
1082
|
+
const learnResult: LoamLearnResult = {
|
|
1083
|
+
success: true,
|
|
1084
|
+
command: "loam learn",
|
|
1085
|
+
changedFiles: ["src/foo.ts"],
|
|
1086
|
+
suggestedDomains: ["typescript", "cli"],
|
|
1087
|
+
unmatchedFiles: [],
|
|
1088
|
+
};
|
|
1089
|
+
const { client } = createFakeLoamClient(learnResult, { recordShouldFail: true });
|
|
1090
|
+
const mailDbPath = join(tempDir, ".agentplate", "auto-record-fail.db");
|
|
1091
|
+
|
|
1092
|
+
const result = await autoRecordExpertise({
|
|
1093
|
+
loamClient: client,
|
|
1094
|
+
agentName: "test-builder",
|
|
1095
|
+
capability: "builder",
|
|
1096
|
+
taskId: null,
|
|
1097
|
+
mailDbPath,
|
|
1098
|
+
parentAgent: null,
|
|
1099
|
+
projectRoot: tempDir,
|
|
1100
|
+
sessionStartedAt: new Date().toISOString(),
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
// All records failed, so no domains recorded and no mail sent
|
|
1104
|
+
expect(result).toEqual([]);
|
|
1105
|
+
const mailFile = Bun.file(mailDbPath);
|
|
1106
|
+
expect(await mailFile.exists()).toBe(false);
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
test("autoRecordExpertise returns empty when no domains suggested", async () => {
|
|
1110
|
+
const learnResult: LoamLearnResult = {
|
|
1111
|
+
success: true,
|
|
1112
|
+
command: "loam learn",
|
|
1113
|
+
changedFiles: ["src/foo.ts"],
|
|
1114
|
+
suggestedDomains: [],
|
|
1115
|
+
unmatchedFiles: [],
|
|
1116
|
+
};
|
|
1117
|
+
const { client, recordCalls } = createFakeLoamClient(learnResult);
|
|
1118
|
+
const mailDbPath = join(tempDir, ".agentplate", "auto-record-empty.db");
|
|
1119
|
+
|
|
1120
|
+
const result = await autoRecordExpertise({
|
|
1121
|
+
loamClient: client,
|
|
1122
|
+
agentName: "test-builder",
|
|
1123
|
+
capability: "builder",
|
|
1124
|
+
taskId: null,
|
|
1125
|
+
mailDbPath,
|
|
1126
|
+
parentAgent: null,
|
|
1127
|
+
projectRoot: tempDir,
|
|
1128
|
+
sessionStartedAt: new Date().toISOString(),
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
expect(result).toEqual([]);
|
|
1132
|
+
expect(recordCalls).toHaveLength(0);
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
test("autoRecordExpertise records pattern insights when EventStore has tool data", async () => {
|
|
1136
|
+
const learnResult: LoamLearnResult = {
|
|
1137
|
+
success: true,
|
|
1138
|
+
command: "loam learn",
|
|
1139
|
+
changedFiles: ["src/mail/store.ts"],
|
|
1140
|
+
suggestedDomains: ["messaging"],
|
|
1141
|
+
unmatchedFiles: [],
|
|
1142
|
+
};
|
|
1143
|
+
const { client, recordCalls } = createFakeLoamClient(learnResult);
|
|
1144
|
+
const mailDbPath = join(tempDir, ".agentplate", "insight-analysis-mail.db");
|
|
1145
|
+
|
|
1146
|
+
// Create EventStore with test data
|
|
1147
|
+
const eventsDbPath = join(tempDir, ".agentplate", "events.db");
|
|
1148
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
1149
|
+
|
|
1150
|
+
const sessionStartedAt = new Date(Date.now() - 60_000).toISOString(); // 1 minute ago
|
|
1151
|
+
|
|
1152
|
+
// Insert tool events: 15 tool calls total (10+ triggers workflow insight)
|
|
1153
|
+
// Read-heavy: 12 Read, 3 Edit → should classify as read-heavy
|
|
1154
|
+
for (let i = 0; i < 12; i++) {
|
|
1155
|
+
eventStore.insert({
|
|
1156
|
+
runId: null,
|
|
1157
|
+
agentName: "insight-agent",
|
|
1158
|
+
sessionId: "sess-insight",
|
|
1159
|
+
eventType: "tool_start",
|
|
1160
|
+
toolName: "Read",
|
|
1161
|
+
toolArgs: JSON.stringify({ file_path: `/src/file${i}.ts` }),
|
|
1162
|
+
toolDurationMs: null,
|
|
1163
|
+
level: "info",
|
|
1164
|
+
data: JSON.stringify({ summary: `read: /src/file${i}.ts` }),
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Add 4 edits to same file → hot file
|
|
1169
|
+
for (let i = 0; i < 4; i++) {
|
|
1170
|
+
eventStore.insert({
|
|
1171
|
+
runId: null,
|
|
1172
|
+
agentName: "insight-agent",
|
|
1173
|
+
sessionId: "sess-insight",
|
|
1174
|
+
eventType: "tool_start",
|
|
1175
|
+
toolName: "Edit",
|
|
1176
|
+
toolArgs: JSON.stringify({ file_path: "src/mail/store.ts" }),
|
|
1177
|
+
toolDurationMs: null,
|
|
1178
|
+
level: "info",
|
|
1179
|
+
data: JSON.stringify({ summary: "edit: src/mail/store.ts" }),
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Add 1 error event → error pattern
|
|
1184
|
+
eventStore.insert({
|
|
1185
|
+
runId: null,
|
|
1186
|
+
agentName: "insight-agent",
|
|
1187
|
+
sessionId: "sess-insight",
|
|
1188
|
+
eventType: "tool_start",
|
|
1189
|
+
toolName: "Bash",
|
|
1190
|
+
toolArgs: JSON.stringify({ command: "bun test" }),
|
|
1191
|
+
toolDurationMs: null,
|
|
1192
|
+
level: "error",
|
|
1193
|
+
data: "Test failed",
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
eventStore.close();
|
|
1197
|
+
|
|
1198
|
+
// Run autoRecordExpertise
|
|
1199
|
+
const result = await autoRecordExpertise({
|
|
1200
|
+
loamClient: client,
|
|
1201
|
+
agentName: "insight-agent",
|
|
1202
|
+
capability: "builder",
|
|
1203
|
+
taskId: "bead-insight",
|
|
1204
|
+
mailDbPath,
|
|
1205
|
+
parentAgent: "parent-agent",
|
|
1206
|
+
projectRoot: tempDir,
|
|
1207
|
+
sessionStartedAt,
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
// Verify reference + insights were recorded
|
|
1211
|
+
expect(recordCalls.length).toBeGreaterThanOrEqual(2); // At least reference + 1 insight
|
|
1212
|
+
|
|
1213
|
+
// Verify reference entry
|
|
1214
|
+
const referenceCall = recordCalls.find((c) => c.options.type === "reference");
|
|
1215
|
+
expect(referenceCall).toBeDefined();
|
|
1216
|
+
expect(referenceCall?.domain).toBe("messaging");
|
|
1217
|
+
|
|
1218
|
+
// Verify pattern insights
|
|
1219
|
+
const patternCalls = recordCalls.filter((c) => c.options.type === "pattern");
|
|
1220
|
+
expect(patternCalls.length).toBeGreaterThanOrEqual(2);
|
|
1221
|
+
|
|
1222
|
+
// Verify workflow insight
|
|
1223
|
+
const workflowInsight = patternCalls.find((c) => {
|
|
1224
|
+
const desc = c.options.description;
|
|
1225
|
+
return typeof desc === "string" && desc.includes("read-heavy workflow");
|
|
1226
|
+
});
|
|
1227
|
+
expect(workflowInsight).toBeDefined();
|
|
1228
|
+
|
|
1229
|
+
// Verify hot file insight
|
|
1230
|
+
const hotFileInsight = patternCalls.find((c) => {
|
|
1231
|
+
const desc = c.options.description;
|
|
1232
|
+
return (
|
|
1233
|
+
typeof desc === "string" && desc.includes("src/mail/store.ts") && desc.includes("4 edits")
|
|
1234
|
+
);
|
|
1235
|
+
});
|
|
1236
|
+
expect(hotFileInsight).toBeDefined();
|
|
1237
|
+
expect(hotFileInsight?.domain).toBe("messaging"); // Inferred from src/mail/
|
|
1238
|
+
|
|
1239
|
+
// Verify failure insight
|
|
1240
|
+
const failureCall = recordCalls.find((c) => c.options.type === "failure");
|
|
1241
|
+
expect(failureCall).toBeDefined();
|
|
1242
|
+
|
|
1243
|
+
// Verify recorded domains includes unique domains from insights
|
|
1244
|
+
expect(result).toContain("messaging");
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
test("autoRecordExpertise includes insight summary in notification mail", async () => {
|
|
1248
|
+
const learnResult: LoamLearnResult = {
|
|
1249
|
+
success: true,
|
|
1250
|
+
command: "loam learn",
|
|
1251
|
+
changedFiles: ["src/config.ts"],
|
|
1252
|
+
suggestedDomains: ["typescript"],
|
|
1253
|
+
unmatchedFiles: [],
|
|
1254
|
+
};
|
|
1255
|
+
const { client } = createFakeLoamClient(learnResult);
|
|
1256
|
+
const mailDbPath = join(tempDir, ".agentplate", "insight-mail-summary.db");
|
|
1257
|
+
|
|
1258
|
+
// Create EventStore with 10+ tool calls to trigger workflow insight
|
|
1259
|
+
const eventsDbPath = join(tempDir, ".agentplate", "events.db");
|
|
1260
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
1261
|
+
const sessionStartedAt = new Date(Date.now() - 60_000).toISOString();
|
|
1262
|
+
|
|
1263
|
+
for (let i = 0; i < 10; i++) {
|
|
1264
|
+
eventStore.insert({
|
|
1265
|
+
runId: null,
|
|
1266
|
+
agentName: "mail-insight-agent",
|
|
1267
|
+
sessionId: "sess-mail",
|
|
1268
|
+
eventType: "tool_start",
|
|
1269
|
+
toolName: "Read",
|
|
1270
|
+
toolArgs: JSON.stringify({ file_path: `/src/file${i}.ts` }),
|
|
1271
|
+
toolDurationMs: null,
|
|
1272
|
+
level: "info",
|
|
1273
|
+
data: JSON.stringify({ summary: `read: /src/file${i}.ts` }),
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
eventStore.close();
|
|
1278
|
+
|
|
1279
|
+
await autoRecordExpertise({
|
|
1280
|
+
loamClient: client,
|
|
1281
|
+
agentName: "mail-insight-agent",
|
|
1282
|
+
capability: "scout",
|
|
1283
|
+
taskId: "bead-mail",
|
|
1284
|
+
mailDbPath,
|
|
1285
|
+
parentAgent: "parent-agent",
|
|
1286
|
+
projectRoot: tempDir,
|
|
1287
|
+
sessionStartedAt,
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
// Verify mail was sent with insight summary
|
|
1291
|
+
const mailStore = createMailStore(mailDbPath);
|
|
1292
|
+
const mailClient = createMailClient(mailStore);
|
|
1293
|
+
const messages = mailClient.list({ to: "parent-agent" });
|
|
1294
|
+
mailClient.close();
|
|
1295
|
+
|
|
1296
|
+
expect(messages).toHaveLength(1);
|
|
1297
|
+
const mail = messages[0];
|
|
1298
|
+
expect(mail?.body).toContain("Auto-insights:");
|
|
1299
|
+
expect(mail?.body).toContain("10 tool calls");
|
|
1300
|
+
expect(mail?.body).toContain("pattern"); // At least 1 pattern insight
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
test("threads outcomeStatus into per-domain reference and per-insight records", async () => {
|
|
1304
|
+
const learnResult: LoamLearnResult = {
|
|
1305
|
+
success: true,
|
|
1306
|
+
command: "loam learn",
|
|
1307
|
+
changedFiles: ["src/foo.ts"],
|
|
1308
|
+
suggestedDomains: ["typescript"],
|
|
1309
|
+
unmatchedFiles: [],
|
|
1310
|
+
};
|
|
1311
|
+
const { client, recordCalls } = createFakeLoamClient(learnResult);
|
|
1312
|
+
const mailDbPath = join(tempDir, ".agentplate", "auto-record-outcome.db");
|
|
1313
|
+
|
|
1314
|
+
// Seed events so analyzer emits at least one insight (10+ tool calls).
|
|
1315
|
+
const eventsDbPath = join(tempDir, ".agentplate", "events.db");
|
|
1316
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
1317
|
+
const sessionStartedAt = new Date(Date.now() - 60_000).toISOString();
|
|
1318
|
+
for (let i = 0; i < 10; i++) {
|
|
1319
|
+
eventStore.insert({
|
|
1320
|
+
runId: null,
|
|
1321
|
+
agentName: "outcome-agent",
|
|
1322
|
+
sessionId: "sess-outcome",
|
|
1323
|
+
eventType: "tool_start",
|
|
1324
|
+
toolName: "Read",
|
|
1325
|
+
toolArgs: JSON.stringify({ file_path: `/src/file${i}.ts` }),
|
|
1326
|
+
toolDurationMs: null,
|
|
1327
|
+
level: "info",
|
|
1328
|
+
data: JSON.stringify({ summary: `read: /src/file${i}.ts` }),
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
eventStore.close();
|
|
1332
|
+
|
|
1333
|
+
await autoRecordExpertise({
|
|
1334
|
+
loamClient: client,
|
|
1335
|
+
agentName: "outcome-agent",
|
|
1336
|
+
capability: "builder",
|
|
1337
|
+
taskId: "bead-outcome",
|
|
1338
|
+
mailDbPath,
|
|
1339
|
+
parentAgent: "parent-agent",
|
|
1340
|
+
projectRoot: tempDir,
|
|
1341
|
+
sessionStartedAt,
|
|
1342
|
+
outcomeStatus: "partial",
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
expect(recordCalls.length).toBeGreaterThanOrEqual(2);
|
|
1346
|
+
for (const call of recordCalls) {
|
|
1347
|
+
expect(call.options.outcomeStatus).toBe("partial");
|
|
1348
|
+
expect(call.options.outcomeAgent).toBe("outcome-agent");
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1352
|
+
test("omits outcomeStatus when caller does not supply one", async () => {
|
|
1353
|
+
const learnResult: LoamLearnResult = {
|
|
1354
|
+
success: true,
|
|
1355
|
+
command: "loam learn",
|
|
1356
|
+
changedFiles: ["src/foo.ts"],
|
|
1357
|
+
suggestedDomains: ["typescript"],
|
|
1358
|
+
unmatchedFiles: [],
|
|
1359
|
+
};
|
|
1360
|
+
const { client, recordCalls } = createFakeLoamClient(learnResult);
|
|
1361
|
+
const mailDbPath = join(tempDir, ".agentplate", "auto-record-no-outcome.db");
|
|
1362
|
+
|
|
1363
|
+
await autoRecordExpertise({
|
|
1364
|
+
loamClient: client,
|
|
1365
|
+
agentName: "no-outcome-agent",
|
|
1366
|
+
capability: "builder",
|
|
1367
|
+
taskId: null,
|
|
1368
|
+
mailDbPath,
|
|
1369
|
+
parentAgent: null,
|
|
1370
|
+
projectRoot: tempDir,
|
|
1371
|
+
sessionStartedAt: new Date().toISOString(),
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
expect(recordCalls).toHaveLength(1);
|
|
1375
|
+
expect(recordCalls[0]?.options.outcomeStatus).toBeUndefined();
|
|
1376
|
+
});
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* Tests for `agentplate log` with --stdin flag.
|
|
1381
|
+
*
|
|
1382
|
+
* Uses Bun.spawn to invoke the log command as a subprocess with piped stdin,
|
|
1383
|
+
* because Bun.stdin.stream() cannot be injected in-process.
|
|
1384
|
+
* Real filesystem + real SQLite for EventStore verification.
|
|
1385
|
+
*/
|
|
1386
|
+
describe("logCommand --stdin integration", () => {
|
|
1387
|
+
let tempDir: string;
|
|
1388
|
+
|
|
1389
|
+
beforeEach(async () => {
|
|
1390
|
+
tempDir = await mkdtemp(join(tmpdir(), "log-stdin-test-"));
|
|
1391
|
+
const agentplateDir = join(tempDir, ".agentplate");
|
|
1392
|
+
await Bun.write(
|
|
1393
|
+
join(agentplateDir, "config.yaml"),
|
|
1394
|
+
`project:\n name: test\n root: ${tempDir}\n canonicalBranch: main\n`,
|
|
1395
|
+
);
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
afterEach(async () => {
|
|
1399
|
+
await cleanupTempDir(tempDir);
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
/**
|
|
1403
|
+
* Helper: run `agentplate log` as a subprocess with stdin piped.
|
|
1404
|
+
* Uses bun to run the CLI entry point directly.
|
|
1405
|
+
*/
|
|
1406
|
+
async function runLogWithStdin(
|
|
1407
|
+
event: string,
|
|
1408
|
+
agentName: string,
|
|
1409
|
+
stdinJson: Record<string, unknown>,
|
|
1410
|
+
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
1411
|
+
// Inline script that calls logCommand with --stdin and reads from stdin
|
|
1412
|
+
const scriptPath = join(tempDir, "_run-log.ts");
|
|
1413
|
+
const scriptContent = `
|
|
1414
|
+
import { logCommand } from "${join(import.meta.dir, "log.ts").replace(/\\/g, "/")}";
|
|
1415
|
+
const args = process.argv.slice(2);
|
|
1416
|
+
try {
|
|
1417
|
+
await logCommand(args);
|
|
1418
|
+
} catch (e) {
|
|
1419
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
1420
|
+
process.exit(1);
|
|
1421
|
+
}
|
|
1422
|
+
`;
|
|
1423
|
+
await Bun.write(scriptPath, scriptContent);
|
|
1424
|
+
|
|
1425
|
+
const proc = Bun.spawn(["bun", "run", scriptPath, event, "--agent", agentName, "--stdin"], {
|
|
1426
|
+
cwd: tempDir,
|
|
1427
|
+
stdin: "pipe",
|
|
1428
|
+
stdout: "pipe",
|
|
1429
|
+
stderr: "pipe",
|
|
1430
|
+
// Pin project root to tempDir. Without this, a subprocess started from
|
|
1431
|
+
// inside an `ap sling`-spawned worktree inherits AGENTPLATE_PROJECT_ROOT
|
|
1432
|
+
// pointing at the parent project, and writes events to prod's events.db.
|
|
1433
|
+
env: { ...process.env, AGENTPLATE_PROJECT_ROOT: tempDir },
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
// Write the JSON payload to stdin and close
|
|
1437
|
+
proc.stdin.write(JSON.stringify(stdinJson));
|
|
1438
|
+
proc.stdin.end();
|
|
1439
|
+
|
|
1440
|
+
const exitCode = await proc.exited;
|
|
1441
|
+
const stdout = await new Response(proc.stdout).text();
|
|
1442
|
+
const stderr = await new Response(proc.stderr).text();
|
|
1443
|
+
|
|
1444
|
+
return { exitCode, stdout, stderr };
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
test("tool-start with --stdin writes to EventStore", async () => {
|
|
1448
|
+
const payload = {
|
|
1449
|
+
tool_name: "Read",
|
|
1450
|
+
tool_input: { file_path: "/src/index.ts" },
|
|
1451
|
+
session_id: "sess-test-001",
|
|
1452
|
+
};
|
|
1453
|
+
|
|
1454
|
+
const result = await runLogWithStdin("tool-start", "stdin-builder", payload);
|
|
1455
|
+
expect(result.exitCode).toBe(0);
|
|
1456
|
+
|
|
1457
|
+
// Verify EventStore has the event
|
|
1458
|
+
const eventsDbPath = join(tempDir, ".agentplate", "events.db");
|
|
1459
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
1460
|
+
const events = eventStore.getByAgent("stdin-builder");
|
|
1461
|
+
eventStore.close();
|
|
1462
|
+
|
|
1463
|
+
expect(events).toHaveLength(1);
|
|
1464
|
+
const event = events[0] as StoredEvent;
|
|
1465
|
+
expect(event.eventType).toBe("tool_start");
|
|
1466
|
+
expect(event.toolName).toBe("Read");
|
|
1467
|
+
expect(event.sessionId).toBe("sess-test-001");
|
|
1468
|
+
expect(event.agentName).toBe("stdin-builder");
|
|
1469
|
+
|
|
1470
|
+
// Verify filtered tool args were stored
|
|
1471
|
+
const toolArgs = JSON.parse(event.toolArgs ?? "{}");
|
|
1472
|
+
expect(toolArgs.file_path).toBe("/src/index.ts");
|
|
1473
|
+
|
|
1474
|
+
// Verify summary in data
|
|
1475
|
+
const data = JSON.parse(event.data ?? "{}");
|
|
1476
|
+
expect(data.summary).toBe("read: /src/index.ts");
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
test("tool-end with --stdin writes to EventStore and correlates with tool-start", async () => {
|
|
1480
|
+
// First create a tool-start event
|
|
1481
|
+
const startPayload = {
|
|
1482
|
+
tool_name: "Bash",
|
|
1483
|
+
tool_input: { command: "bun test" },
|
|
1484
|
+
session_id: "sess-test-002",
|
|
1485
|
+
};
|
|
1486
|
+
const startResult = await runLogWithStdin("tool-start", "correlate-agent", startPayload);
|
|
1487
|
+
expect(startResult.exitCode).toBe(0);
|
|
1488
|
+
|
|
1489
|
+
// Small delay to ensure measurable duration
|
|
1490
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1491
|
+
|
|
1492
|
+
// Now send tool-end
|
|
1493
|
+
const endPayload = {
|
|
1494
|
+
tool_name: "Bash",
|
|
1495
|
+
tool_input: { command: "bun test" },
|
|
1496
|
+
session_id: "sess-test-002",
|
|
1497
|
+
};
|
|
1498
|
+
const endResult = await runLogWithStdin("tool-end", "correlate-agent", endPayload);
|
|
1499
|
+
expect(endResult.exitCode).toBe(0);
|
|
1500
|
+
|
|
1501
|
+
// Verify EventStore has both events
|
|
1502
|
+
const eventsDbPath = join(tempDir, ".agentplate", "events.db");
|
|
1503
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
1504
|
+
const events = eventStore.getByAgent("correlate-agent");
|
|
1505
|
+
eventStore.close();
|
|
1506
|
+
|
|
1507
|
+
expect(events).toHaveLength(2);
|
|
1508
|
+
|
|
1509
|
+
const startEvent = events.find((e) => e.eventType === "tool_start");
|
|
1510
|
+
const endEvent = events.find((e) => e.eventType === "tool_end");
|
|
1511
|
+
expect(startEvent).toBeDefined();
|
|
1512
|
+
expect(endEvent).toBeDefined();
|
|
1513
|
+
|
|
1514
|
+
// The start event should have tool_duration_ms set by correlateToolEnd()
|
|
1515
|
+
// (value may be affected by SQLite timestamp vs Date.now() timezone behavior,
|
|
1516
|
+
// so we only assert it was populated — not the exact value)
|
|
1517
|
+
expect(startEvent?.toolDurationMs).not.toBeNull();
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
test("tool-start with --stdin filters large tool_input", async () => {
|
|
1521
|
+
const payload = {
|
|
1522
|
+
tool_name: "Write",
|
|
1523
|
+
tool_input: {
|
|
1524
|
+
file_path: "/src/new-file.ts",
|
|
1525
|
+
content: "x".repeat(50_000), // 50KB of content — should be dropped
|
|
1526
|
+
},
|
|
1527
|
+
session_id: "sess-test-003",
|
|
1528
|
+
};
|
|
1529
|
+
|
|
1530
|
+
const result = await runLogWithStdin("tool-start", "filter-agent", payload);
|
|
1531
|
+
expect(result.exitCode).toBe(0);
|
|
1532
|
+
|
|
1533
|
+
const eventsDbPath = join(tempDir, ".agentplate", "events.db");
|
|
1534
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
1535
|
+
const events = eventStore.getByAgent("filter-agent");
|
|
1536
|
+
eventStore.close();
|
|
1537
|
+
|
|
1538
|
+
expect(events).toHaveLength(1);
|
|
1539
|
+
const event = events[0] as StoredEvent;
|
|
1540
|
+
|
|
1541
|
+
// The Write filter keeps file_path but drops content
|
|
1542
|
+
const toolArgs = JSON.parse(event.toolArgs ?? "{}");
|
|
1543
|
+
expect(toolArgs.file_path).toBe("/src/new-file.ts");
|
|
1544
|
+
expect(toolArgs).not.toHaveProperty("content");
|
|
1545
|
+
|
|
1546
|
+
// Verify summary
|
|
1547
|
+
const data = JSON.parse(event.data ?? "{}");
|
|
1548
|
+
expect(data.summary).toBe("write: /src/new-file.ts");
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
test("session-end with --stdin writes to EventStore with transcript_path", async () => {
|
|
1552
|
+
const payload = {
|
|
1553
|
+
session_id: "sess-test-004",
|
|
1554
|
+
transcript_path: "/tmp/transcript.jsonl",
|
|
1555
|
+
};
|
|
1556
|
+
|
|
1557
|
+
const result = await runLogWithStdin("session-end", "session-end-agent", payload);
|
|
1558
|
+
expect(result.exitCode).toBe(0);
|
|
1559
|
+
|
|
1560
|
+
const eventsDbPath = join(tempDir, ".agentplate", "events.db");
|
|
1561
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
1562
|
+
const events = eventStore.getByAgent("session-end-agent");
|
|
1563
|
+
eventStore.close();
|
|
1564
|
+
|
|
1565
|
+
expect(events).toHaveLength(1);
|
|
1566
|
+
const event = events[0] as StoredEvent;
|
|
1567
|
+
expect(event.eventType).toBe("session_end");
|
|
1568
|
+
expect(event.sessionId).toBe("sess-test-004");
|
|
1569
|
+
|
|
1570
|
+
// Verify transcript path stored in data
|
|
1571
|
+
const data = JSON.parse(event.data ?? "{}");
|
|
1572
|
+
expect(data.transcriptPath).toBe("/tmp/transcript.jsonl");
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
test("tool-start with --stdin still writes to legacy log files", async () => {
|
|
1576
|
+
const payload = {
|
|
1577
|
+
tool_name: "Grep",
|
|
1578
|
+
tool_input: { pattern: "TODO", path: "/src" },
|
|
1579
|
+
session_id: "sess-test-005",
|
|
1580
|
+
};
|
|
1581
|
+
|
|
1582
|
+
const result = await runLogWithStdin("tool-start", "legacy-compat-agent", payload);
|
|
1583
|
+
expect(result.exitCode).toBe(0);
|
|
1584
|
+
|
|
1585
|
+
// Wait for async file writes to complete
|
|
1586
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1587
|
+
|
|
1588
|
+
// Verify legacy log files exist
|
|
1589
|
+
const logsDir = join(tempDir, ".agentplate", "logs", "legacy-compat-agent");
|
|
1590
|
+
const markerPath = join(logsDir, ".current-session");
|
|
1591
|
+
const markerFile = Bun.file(markerPath);
|
|
1592
|
+
expect(await markerFile.exists()).toBe(true);
|
|
1593
|
+
|
|
1594
|
+
const sessionDir = (await markerFile.text()).trim();
|
|
1595
|
+
const eventsFile = Bun.file(join(sessionDir, "events.ndjson"));
|
|
1596
|
+
expect(await eventsFile.exists()).toBe(true);
|
|
1597
|
+
|
|
1598
|
+
const eventsContent = await eventsFile.text();
|
|
1599
|
+
expect(eventsContent).toContain("tool.start");
|
|
1600
|
+
expect(eventsContent).toContain("Grep");
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
test("tool-start with --stdin handles empty stdin gracefully", async () => {
|
|
1604
|
+
// Send empty JSON object — should still work (falls back to "unknown" tool name)
|
|
1605
|
+
const scriptPath = join(tempDir, "_run-log-empty.ts");
|
|
1606
|
+
const scriptContent = `
|
|
1607
|
+
import { logCommand } from "${join(import.meta.dir, "log.ts").replace(/\\/g, "/")}";
|
|
1608
|
+
|
|
1609
|
+
try {
|
|
1610
|
+
await logCommand(["tool-start", "--agent", "empty-stdin-agent", "--stdin"]);
|
|
1611
|
+
} catch (e) {
|
|
1612
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
1613
|
+
process.exit(1);
|
|
1614
|
+
}
|
|
1615
|
+
`;
|
|
1616
|
+
await Bun.write(scriptPath, scriptContent);
|
|
1617
|
+
|
|
1618
|
+
const proc = Bun.spawn(["bun", "run", scriptPath], {
|
|
1619
|
+
cwd: tempDir,
|
|
1620
|
+
stdin: "pipe",
|
|
1621
|
+
stdout: "pipe",
|
|
1622
|
+
stderr: "pipe",
|
|
1623
|
+
env: { ...process.env, AGENTPLATE_PROJECT_ROOT: tempDir },
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
// Write empty string and close immediately
|
|
1627
|
+
proc.stdin.end();
|
|
1628
|
+
|
|
1629
|
+
const exitCode = await proc.exited;
|
|
1630
|
+
expect(exitCode).toBe(0);
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
test("tool-start with --stdin and unknown tool name uses fallback filter", async () => {
|
|
1634
|
+
const payload = {
|
|
1635
|
+
tool_name: "SomeCustomTool",
|
|
1636
|
+
tool_input: { custom_key: "custom_value" },
|
|
1637
|
+
session_id: "sess-test-006",
|
|
1638
|
+
};
|
|
1639
|
+
|
|
1640
|
+
const result = await runLogWithStdin("tool-start", "custom-tool-agent", payload);
|
|
1641
|
+
expect(result.exitCode).toBe(0);
|
|
1642
|
+
|
|
1643
|
+
const eventsDbPath = join(tempDir, ".agentplate", "events.db");
|
|
1644
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
1645
|
+
const events = eventStore.getByAgent("custom-tool-agent");
|
|
1646
|
+
eventStore.close();
|
|
1647
|
+
|
|
1648
|
+
expect(events).toHaveLength(1);
|
|
1649
|
+
const event = events[0] as StoredEvent;
|
|
1650
|
+
expect(event.toolName).toBe("SomeCustomTool");
|
|
1651
|
+
|
|
1652
|
+
// Unknown tools get empty args from filterToolArgs
|
|
1653
|
+
const toolArgs = JSON.parse(event.toolArgs ?? "{}");
|
|
1654
|
+
expect(toolArgs).toEqual({});
|
|
1655
|
+
|
|
1656
|
+
const data = JSON.parse(event.data ?? "{}");
|
|
1657
|
+
expect(data.summary).toBe("SomeCustomTool");
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
test("tool-end with --stdin handles large payloads (>64KB)", async () => {
|
|
1661
|
+
const payload = {
|
|
1662
|
+
tool_name: "Bash",
|
|
1663
|
+
tool_input: { command: "cat /some/file" },
|
|
1664
|
+
tool_result: "x".repeat(100_000), // 100KB payload
|
|
1665
|
+
session_id: "sess-large-payload",
|
|
1666
|
+
};
|
|
1667
|
+
|
|
1668
|
+
const result = await runLogWithStdin("tool-end", "large-payload-agent", payload);
|
|
1669
|
+
expect(result.exitCode).toBe(0);
|
|
1670
|
+
|
|
1671
|
+
// Verify EventStore received the event with correct tool name
|
|
1672
|
+
const eventsDbPath = join(tempDir, ".agentplate", "events.db");
|
|
1673
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
1674
|
+
const events = eventStore.getByAgent("large-payload-agent");
|
|
1675
|
+
eventStore.close();
|
|
1676
|
+
|
|
1677
|
+
expect(events).toHaveLength(1);
|
|
1678
|
+
const event = events[0] as StoredEvent;
|
|
1679
|
+
expect(event.eventType).toBe("tool_end");
|
|
1680
|
+
expect(event.toolName).toBe("Bash");
|
|
1681
|
+
// tool_result is not stored in EventStore (filtered out), but tool_name was parsed correctly
|
|
1682
|
+
});
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
describe("appendOutcomeToAppliedRecords", () => {
|
|
1686
|
+
let tempDir: string;
|
|
1687
|
+
|
|
1688
|
+
/** Minimal fake LoamClient for appendOutcomeToAppliedRecords tests. */
|
|
1689
|
+
function makeOutcomeClient(opts?: { appendOutcomeShouldFail?: boolean }): {
|
|
1690
|
+
client: LoamClient;
|
|
1691
|
+
appendOutcomeCalls: Array<{ domain: string; id: string; outcome: Record<string, unknown> }>;
|
|
1692
|
+
} {
|
|
1693
|
+
const appendOutcomeCalls: Array<{
|
|
1694
|
+
domain: string;
|
|
1695
|
+
id: string;
|
|
1696
|
+
outcome: Record<string, unknown>;
|
|
1697
|
+
}> = [];
|
|
1698
|
+
const client = {
|
|
1699
|
+
async appendOutcome(domain: string, id: string, outcome: Record<string, unknown>) {
|
|
1700
|
+
if (opts?.appendOutcomeShouldFail) throw new Error("loam appendOutcome failed");
|
|
1701
|
+
appendOutcomeCalls.push({ domain, id, outcome });
|
|
1702
|
+
},
|
|
1703
|
+
} as unknown as LoamClient;
|
|
1704
|
+
return { client, appendOutcomeCalls };
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
beforeEach(async () => {
|
|
1708
|
+
tempDir = await mkdtemp(join(tmpdir(), "outcome-test-"));
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1711
|
+
afterEach(async () => {
|
|
1712
|
+
await cleanupTempDir(tempDir);
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
test("returns 0 when applied-records.json does not exist (backward compat)", async () => {
|
|
1716
|
+
const { client } = makeOutcomeClient();
|
|
1717
|
+
const count = await appendOutcomeToAppliedRecords({
|
|
1718
|
+
loamClient: client,
|
|
1719
|
+
agentName: "test-agent",
|
|
1720
|
+
capability: "builder",
|
|
1721
|
+
taskId: "bead-001",
|
|
1722
|
+
projectRoot: tempDir,
|
|
1723
|
+
});
|
|
1724
|
+
expect(count).toBe(0);
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
test("returns 0 when records array is empty", async () => {
|
|
1728
|
+
const agentDir = join(tempDir, ".agentplate", "agents", "test-agent");
|
|
1729
|
+
await mkdir(agentDir, { recursive: true });
|
|
1730
|
+
await Bun.write(
|
|
1731
|
+
join(agentDir, "applied-records.json"),
|
|
1732
|
+
JSON.stringify({
|
|
1733
|
+
taskId: "bead-001",
|
|
1734
|
+
agentName: "test-agent",
|
|
1735
|
+
capability: "builder",
|
|
1736
|
+
records: [],
|
|
1737
|
+
}),
|
|
1738
|
+
);
|
|
1739
|
+
|
|
1740
|
+
const { client } = makeOutcomeClient();
|
|
1741
|
+
const count = await appendOutcomeToAppliedRecords({
|
|
1742
|
+
loamClient: client,
|
|
1743
|
+
agentName: "test-agent",
|
|
1744
|
+
capability: "builder",
|
|
1745
|
+
taskId: "bead-001",
|
|
1746
|
+
projectRoot: tempDir,
|
|
1747
|
+
});
|
|
1748
|
+
expect(count).toBe(0);
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
test("calls appendOutcome for each record and returns count", async () => {
|
|
1752
|
+
const agentDir = join(tempDir, ".agentplate", "agents", "test-agent");
|
|
1753
|
+
await mkdir(agentDir, { recursive: true });
|
|
1754
|
+
const records = [
|
|
1755
|
+
{ id: "mx-aaa111", domain: "agents" },
|
|
1756
|
+
{ id: "mx-bbb222", domain: "typescript" },
|
|
1757
|
+
];
|
|
1758
|
+
await Bun.write(
|
|
1759
|
+
join(agentDir, "applied-records.json"),
|
|
1760
|
+
JSON.stringify({
|
|
1761
|
+
taskId: "bead-001",
|
|
1762
|
+
agentName: "test-agent",
|
|
1763
|
+
capability: "builder",
|
|
1764
|
+
records,
|
|
1765
|
+
}),
|
|
1766
|
+
);
|
|
1767
|
+
|
|
1768
|
+
const { client, appendOutcomeCalls } = makeOutcomeClient();
|
|
1769
|
+
const count = await appendOutcomeToAppliedRecords({
|
|
1770
|
+
loamClient: client,
|
|
1771
|
+
agentName: "test-agent",
|
|
1772
|
+
capability: "builder",
|
|
1773
|
+
taskId: "bead-001",
|
|
1774
|
+
projectRoot: tempDir,
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
expect(count).toBe(2);
|
|
1778
|
+
expect(appendOutcomeCalls).toHaveLength(2);
|
|
1779
|
+
expect(appendOutcomeCalls[0]).toMatchObject({ id: "mx-aaa111", domain: "agents" });
|
|
1780
|
+
expect(appendOutcomeCalls[1]).toMatchObject({ id: "mx-bbb222", domain: "typescript" });
|
|
1781
|
+
expect(appendOutcomeCalls[0]?.outcome).toMatchObject({
|
|
1782
|
+
status: "success",
|
|
1783
|
+
agent: "test-agent",
|
|
1784
|
+
});
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
test("cleans up applied-records.json after processing", async () => {
|
|
1788
|
+
const agentDir = join(tempDir, ".agentplate", "agents", "test-agent");
|
|
1789
|
+
await mkdir(agentDir, { recursive: true });
|
|
1790
|
+
const appliedPath = join(agentDir, "applied-records.json");
|
|
1791
|
+
await Bun.write(
|
|
1792
|
+
appliedPath,
|
|
1793
|
+
JSON.stringify({
|
|
1794
|
+
taskId: "bead-001",
|
|
1795
|
+
agentName: "test-agent",
|
|
1796
|
+
capability: "builder",
|
|
1797
|
+
records: [{ id: "mx-abc123", domain: "agents" }],
|
|
1798
|
+
}),
|
|
1799
|
+
);
|
|
1800
|
+
|
|
1801
|
+
const { client } = makeOutcomeClient();
|
|
1802
|
+
await appendOutcomeToAppliedRecords({
|
|
1803
|
+
loamClient: client,
|
|
1804
|
+
agentName: "test-agent",
|
|
1805
|
+
capability: "builder",
|
|
1806
|
+
taskId: "bead-001",
|
|
1807
|
+
projectRoot: tempDir,
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
expect(await Bun.file(appliedPath).exists()).toBe(false);
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
test("continues when individual appendOutcome calls fail (non-fatal per record)", async () => {
|
|
1814
|
+
const agentDir = join(tempDir, ".agentplate", "agents", "test-agent");
|
|
1815
|
+
await mkdir(agentDir, { recursive: true });
|
|
1816
|
+
const records = [
|
|
1817
|
+
{ id: "mx-fail111", domain: "agents" },
|
|
1818
|
+
{ id: "mx-fail222", domain: "typescript" },
|
|
1819
|
+
];
|
|
1820
|
+
await Bun.write(
|
|
1821
|
+
join(agentDir, "applied-records.json"),
|
|
1822
|
+
JSON.stringify({
|
|
1823
|
+
taskId: "bead-002",
|
|
1824
|
+
agentName: "test-agent",
|
|
1825
|
+
capability: "builder",
|
|
1826
|
+
records,
|
|
1827
|
+
}),
|
|
1828
|
+
);
|
|
1829
|
+
|
|
1830
|
+
// appendOutcomeShouldFail=true makes all calls throw — should return 0 but not throw
|
|
1831
|
+
const { client } = makeOutcomeClient({ appendOutcomeShouldFail: true });
|
|
1832
|
+
const count = await appendOutcomeToAppliedRecords({
|
|
1833
|
+
loamClient: client,
|
|
1834
|
+
agentName: "test-agent",
|
|
1835
|
+
capability: "builder",
|
|
1836
|
+
taskId: "bead-002",
|
|
1837
|
+
projectRoot: tempDir,
|
|
1838
|
+
});
|
|
1839
|
+
expect(count).toBe(0);
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
test("returns 0 for malformed JSON", async () => {
|
|
1843
|
+
const agentDir = join(tempDir, ".agentplate", "agents", "test-agent");
|
|
1844
|
+
await mkdir(agentDir, { recursive: true });
|
|
1845
|
+
await Bun.write(join(agentDir, "applied-records.json"), "not-valid-json{{{");
|
|
1846
|
+
|
|
1847
|
+
const { client } = makeOutcomeClient();
|
|
1848
|
+
const count = await appendOutcomeToAppliedRecords({
|
|
1849
|
+
loamClient: client,
|
|
1850
|
+
agentName: "test-agent",
|
|
1851
|
+
capability: "builder",
|
|
1852
|
+
taskId: null,
|
|
1853
|
+
projectRoot: tempDir,
|
|
1854
|
+
});
|
|
1855
|
+
expect(count).toBe(0);
|
|
1856
|
+
});
|
|
1857
|
+
|
|
1858
|
+
test("uses supplied outcomeStatus when provided", async () => {
|
|
1859
|
+
const agentDir = join(tempDir, ".agentplate", "agents", "test-agent");
|
|
1860
|
+
await mkdir(agentDir, { recursive: true });
|
|
1861
|
+
await Bun.write(
|
|
1862
|
+
join(agentDir, "applied-records.json"),
|
|
1863
|
+
JSON.stringify({
|
|
1864
|
+
taskId: "bead-outcome",
|
|
1865
|
+
agentName: "test-agent",
|
|
1866
|
+
capability: "builder",
|
|
1867
|
+
records: [{ id: "mx-aaa111", domain: "agents" }],
|
|
1868
|
+
}),
|
|
1869
|
+
);
|
|
1870
|
+
|
|
1871
|
+
const { client, appendOutcomeCalls } = makeOutcomeClient();
|
|
1872
|
+
await appendOutcomeToAppliedRecords({
|
|
1873
|
+
loamClient: client,
|
|
1874
|
+
agentName: "test-agent",
|
|
1875
|
+
capability: "builder",
|
|
1876
|
+
taskId: "bead-outcome",
|
|
1877
|
+
projectRoot: tempDir,
|
|
1878
|
+
outcomeStatus: "failure",
|
|
1879
|
+
});
|
|
1880
|
+
|
|
1881
|
+
expect(appendOutcomeCalls).toHaveLength(1);
|
|
1882
|
+
expect(appendOutcomeCalls[0]?.outcome).toMatchObject({ status: "failure" });
|
|
1883
|
+
expect(appendOutcomeCalls[0]?.outcome.notes).toContain("Quality gates: failure");
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
test("falls back to 'success' when outcomeStatus is undefined (backward compat)", async () => {
|
|
1887
|
+
const agentDir = join(tempDir, ".agentplate", "agents", "test-agent");
|
|
1888
|
+
await mkdir(agentDir, { recursive: true });
|
|
1889
|
+
await Bun.write(
|
|
1890
|
+
join(agentDir, "applied-records.json"),
|
|
1891
|
+
JSON.stringify({
|
|
1892
|
+
taskId: "bead-default",
|
|
1893
|
+
agentName: "test-agent",
|
|
1894
|
+
capability: "builder",
|
|
1895
|
+
records: [{ id: "mx-bbb222", domain: "agents" }],
|
|
1896
|
+
}),
|
|
1897
|
+
);
|
|
1898
|
+
|
|
1899
|
+
const { client, appendOutcomeCalls } = makeOutcomeClient();
|
|
1900
|
+
await appendOutcomeToAppliedRecords({
|
|
1901
|
+
loamClient: client,
|
|
1902
|
+
agentName: "test-agent",
|
|
1903
|
+
capability: "builder",
|
|
1904
|
+
taskId: "bead-default",
|
|
1905
|
+
projectRoot: tempDir,
|
|
1906
|
+
});
|
|
1907
|
+
|
|
1908
|
+
expect(appendOutcomeCalls).toHaveLength(1);
|
|
1909
|
+
expect(appendOutcomeCalls[0]?.outcome.status).toBe("success");
|
|
1910
|
+
// No "Quality gates:" annotation when caller didn't provide outcomeStatus
|
|
1911
|
+
expect(appendOutcomeCalls[0]?.outcome.notes).not.toContain("Quality gates:");
|
|
1912
|
+
});
|
|
1913
|
+
});
|