@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,804 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { DEFAULT_QUALITY_GATES } from "../config.ts";
|
|
4
|
+
import { AgentError } from "../errors.ts";
|
|
5
|
+
import type { QualityGate } from "../types.ts";
|
|
6
|
+
import {
|
|
7
|
+
DANGEROUS_BASH_PATTERNS,
|
|
8
|
+
INTERACTIVE_TOOLS,
|
|
9
|
+
NATIVE_TEAM_TOOLS,
|
|
10
|
+
SAFE_BASH_PREFIXES,
|
|
11
|
+
WRITE_TOOLS,
|
|
12
|
+
} from "./guard-rules.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Capabilities that must never modify project files.
|
|
16
|
+
* Includes read-only roles (scout, reviewer) and coordination roles (lead).
|
|
17
|
+
* Only "builder" and "merger" are allowed to modify files.
|
|
18
|
+
*/
|
|
19
|
+
const NON_IMPLEMENTATION_CAPABILITIES = new Set([
|
|
20
|
+
"scout",
|
|
21
|
+
"reviewer",
|
|
22
|
+
"lead",
|
|
23
|
+
"orchestrator",
|
|
24
|
+
"coordinator",
|
|
25
|
+
"supervisor",
|
|
26
|
+
"monitor",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Capabilities that coordinate work and need git add/commit for syncing
|
|
31
|
+
* tasks, loam, and other metadata — but must NOT git push.
|
|
32
|
+
*/
|
|
33
|
+
const COORDINATION_CAPABILITIES = new Set(["coordinator", "orchestrator", "supervisor", "monitor"]);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Additional safe Bash prefixes for coordination capabilities.
|
|
37
|
+
* Allows git add/commit for task sync, loam records, etc.
|
|
38
|
+
* git push remains blocked via DANGEROUS_BASH_PATTERNS.
|
|
39
|
+
*/
|
|
40
|
+
const COORDINATION_SAFE_PREFIXES = ["git add", "git commit"];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extract command prefixes from quality gate configurations.
|
|
44
|
+
*
|
|
45
|
+
* Each gate's command is used as a safe prefix so non-implementation agents
|
|
46
|
+
* can still run quality gate commands (e.g., reviewers running tests).
|
|
47
|
+
* This makes the safe prefix list configurable instead of hardcoding
|
|
48
|
+
* specific tool commands like "bun test".
|
|
49
|
+
*/
|
|
50
|
+
export function extractQualityGatePrefixes(gates: QualityGate[]): string[] {
|
|
51
|
+
return gates.map((g) => g.command);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Hook entry shape matching Claude Code's settings.local.json format. */
|
|
55
|
+
interface HookEntry {
|
|
56
|
+
matcher: string;
|
|
57
|
+
hooks: Array<{ type: string; command: string }>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the path to the hooks template file.
|
|
62
|
+
* The template lives at `templates/hooks.json.tmpl` relative to the repo root.
|
|
63
|
+
*/
|
|
64
|
+
function getTemplatePath(): string {
|
|
65
|
+
// src/agents/hooks-deployer.ts -> repo root is ../../
|
|
66
|
+
return join(dirname(import.meta.dir), "..", "templates", "hooks.json.tmpl");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Env var guard prefix for hook commands.
|
|
71
|
+
*
|
|
72
|
+
* When hooks are deployed to the project root (e.g. for the coordinator),
|
|
73
|
+
* they affect ALL Claude Code sessions in that directory. This prefix
|
|
74
|
+
* ensures hooks only activate for agentplate-managed agent sessions
|
|
75
|
+
* (which have AGENTPLATE_AGENT_NAME set in their environment) and are
|
|
76
|
+
* no-ops for the user's own Claude Code session.
|
|
77
|
+
*/
|
|
78
|
+
const ENV_GUARD = '[ -z "$AGENTPLATE_AGENT_NAME" ] && exit 0;';
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* PATH setup prefix for hook commands.
|
|
82
|
+
*
|
|
83
|
+
* Claude Code executes hook commands via /bin/sh with a minimal PATH
|
|
84
|
+
* (/usr/bin:/bin:/usr/sbin:/sbin). Bun-installed CLIs — ap, lm, sr, tl, bd —
|
|
85
|
+
* live in ~/.bun/bin which is absent from that PATH, causing hooks like
|
|
86
|
+
* `ap prime` (SessionStart) and `lm learn` (Stop) to fail with
|
|
87
|
+
* "command not found".
|
|
88
|
+
*
|
|
89
|
+
* Prepend this to any hook command that invokes one of those CLIs so they
|
|
90
|
+
* resolve correctly regardless of how Claude Code was launched.
|
|
91
|
+
*
|
|
92
|
+
* Exported so tests can verify the exact prefix value.
|
|
93
|
+
*/
|
|
94
|
+
export const PATH_PREFIX = 'export PATH="$HOME/.bun/bin:/usr/local/bin:/opt/homebrew/bin:$PATH";';
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build a PreToolUse guard script that validates file paths are within
|
|
98
|
+
* the agent's worktree boundary.
|
|
99
|
+
*
|
|
100
|
+
* Applied to Write, Edit, and NotebookEdit tools. Uses the
|
|
101
|
+
* AGENTPLATE_WORKTREE_PATH env var set during tmux session creation
|
|
102
|
+
* to determine the allowed path boundary.
|
|
103
|
+
*
|
|
104
|
+
* @param filePathField - The JSON field name containing the file path
|
|
105
|
+
* ("file_path" for Write/Edit, "notebook_path" for NotebookEdit)
|
|
106
|
+
*/
|
|
107
|
+
export function buildPathBoundaryGuardScript(filePathField: string): string {
|
|
108
|
+
const script = [
|
|
109
|
+
// Only enforce for agentplate agent sessions
|
|
110
|
+
ENV_GUARD,
|
|
111
|
+
// Skip if worktree path is not set (e.g., orchestrator)
|
|
112
|
+
'[ -z "$AGENTPLATE_WORKTREE_PATH" ] && exit 0;',
|
|
113
|
+
"read -r INPUT;",
|
|
114
|
+
// Extract file path from JSON (sed -n + p = empty if no match)
|
|
115
|
+
`FILE_PATH=$(echo "$INPUT" | sed -n 's/.*"${filePathField}": *"\\([^"]*\\)".*/\\1/p');`,
|
|
116
|
+
// No path extracted — fail open (tool may be called differently)
|
|
117
|
+
'[ -z "$FILE_PATH" ] && exit 0;',
|
|
118
|
+
// Resolve relative paths against cwd
|
|
119
|
+
'case "$FILE_PATH" in /*) ;; *) FILE_PATH="$(pwd)/$FILE_PATH" ;; esac;',
|
|
120
|
+
// Allow if path is inside the worktree (exact match or subpath)
|
|
121
|
+
'case "$FILE_PATH" in "$AGENTPLATE_WORKTREE_PATH"/*) exit 0 ;; "$AGENTPLATE_WORKTREE_PATH") exit 0 ;; esac;',
|
|
122
|
+
// Block: path is outside the worktree boundary
|
|
123
|
+
'echo \'{"decision":"block","reason":"Path boundary violation: file is outside your assigned worktree. All writes must target files within your worktree."}\';',
|
|
124
|
+
].join(" ");
|
|
125
|
+
return script;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Generate PreToolUse guards that enforce worktree path boundaries.
|
|
130
|
+
*
|
|
131
|
+
* Returns guards for Write (file_path), Edit (file_path), and
|
|
132
|
+
* NotebookEdit (notebook_path). Applied to ALL agent capabilities
|
|
133
|
+
* as defense-in-depth (non-implementation agents already have these
|
|
134
|
+
* tools blocked, but the path guard catches any bypass).
|
|
135
|
+
*/
|
|
136
|
+
export function getPathBoundaryGuards(): HookEntry[] {
|
|
137
|
+
return [
|
|
138
|
+
{
|
|
139
|
+
matcher: "Write",
|
|
140
|
+
hooks: [{ type: "command", command: buildPathBoundaryGuardScript("file_path") }],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
matcher: "Edit",
|
|
144
|
+
hooks: [{ type: "command", command: buildPathBoundaryGuardScript("file_path") }],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
matcher: "NotebookEdit",
|
|
148
|
+
hooks: [{ type: "command", command: buildPathBoundaryGuardScript("notebook_path") }],
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Escape a string for use inside a single-quoted POSIX shell string.
|
|
155
|
+
*
|
|
156
|
+
* POSIX single-quoted strings cannot contain single quotes at all.
|
|
157
|
+
* The standard technique is to end the single-quoted segment, emit an escaped
|
|
158
|
+
* single quote using $'\'', then start a new single-quoted segment:
|
|
159
|
+
* 'it'\''s fine' → it's fine
|
|
160
|
+
*
|
|
161
|
+
* Exported so tests can verify escaping directly.
|
|
162
|
+
*/
|
|
163
|
+
export function escapeForSingleQuotedShell(str: string): string {
|
|
164
|
+
return str.replace(/'/g, "'\\''");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Build a PreToolUse guard that blocks a specific tool.
|
|
169
|
+
*
|
|
170
|
+
* Returns a JSON response with decision=block so Claude Code rejects
|
|
171
|
+
* the tool call before execution.
|
|
172
|
+
*/
|
|
173
|
+
function blockGuard(toolName: string, reason: string): HookEntry {
|
|
174
|
+
const response = JSON.stringify({ decision: "block", reason });
|
|
175
|
+
return {
|
|
176
|
+
matcher: toolName,
|
|
177
|
+
hooks: [
|
|
178
|
+
{
|
|
179
|
+
type: "command",
|
|
180
|
+
command: `${ENV_GUARD} echo '${escapeForSingleQuotedShell(response)}'`,
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Build a Bash guard script that inspects the command from stdin JSON.
|
|
188
|
+
*
|
|
189
|
+
* Claude Code PreToolUse hooks receive `{"tool_name": "Bash", "tool_input": {"command": "..."}, ...}` on stdin.
|
|
190
|
+
* This builds a bash script that reads stdin, extracts the command, and checks for
|
|
191
|
+
* dangerous patterns (push to canonical branch, hard reset, wrong branch naming).
|
|
192
|
+
*/
|
|
193
|
+
function buildBashGuardScript(agentName: string): string {
|
|
194
|
+
// The script reads JSON from stdin, extracts the command field, then checks patterns.
|
|
195
|
+
// Uses parameter expansion to avoid requiring jq (zero runtime deps).
|
|
196
|
+
const script = [
|
|
197
|
+
// Only enforce for agentplate agent sessions (skip for user's own Claude Code)
|
|
198
|
+
ENV_GUARD,
|
|
199
|
+
"read -r INPUT;",
|
|
200
|
+
// Extract command value from JSON — grab everything after "command": (with optional space)
|
|
201
|
+
'CMD=$(echo "$INPUT" | sed \'s/.*"command": *"\\([^"]*\\)".*/\\1/\');',
|
|
202
|
+
// Check 1: Block all git push — agents must never push to remote
|
|
203
|
+
"if echo \"$CMD\" | grep -qE '\\bgit\\s+push\\b'; then",
|
|
204
|
+
' echo \'{"decision":"block","reason":"git push is blocked — use ap merge to integrate changes, push manually when ready"}\';',
|
|
205
|
+
" exit 0;",
|
|
206
|
+
"fi;",
|
|
207
|
+
// Check 2: Block git reset --hard
|
|
208
|
+
"if echo \"$CMD\" | grep -qE 'git\\s+reset\\s+--hard'; then",
|
|
209
|
+
' echo \'{"decision":"block","reason":"git reset --hard is not allowed — it destroys uncommitted work"}\';',
|
|
210
|
+
" exit 0;",
|
|
211
|
+
"fi;",
|
|
212
|
+
// Check 3: Warn on git checkout -b with wrong naming convention
|
|
213
|
+
"if echo \"$CMD\" | grep -qE 'git\\s+checkout\\s+-b\\s'; then",
|
|
214
|
+
` BRANCH=$(echo "$CMD" | sed 's/.*git\\s*checkout\\s*-b\\s*\\([^ ]*\\).*/\\1/');`,
|
|
215
|
+
` if ! echo "$BRANCH" | grep -qE '^agentplate/${agentName}/'; then`,
|
|
216
|
+
` echo '{"decision":"block","reason":"Branch must follow agentplate/${agentName}/{task-id} convention"}';`,
|
|
217
|
+
" exit 0;",
|
|
218
|
+
" fi;",
|
|
219
|
+
"fi;",
|
|
220
|
+
].join(" ");
|
|
221
|
+
return script;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Generate Bash-level PreToolUse guards for dangerous operations.
|
|
226
|
+
*
|
|
227
|
+
* Applied to ALL agent capabilities. Inspects Bash tool commands for:
|
|
228
|
+
* - `git push` to canonical branches (main/master) — blocked
|
|
229
|
+
* - `git reset --hard` — blocked
|
|
230
|
+
* - `git checkout -b` with non-standard branch naming — blocked
|
|
231
|
+
*
|
|
232
|
+
* @param agentName - The agent name, used for branch naming validation
|
|
233
|
+
*/
|
|
234
|
+
export function getDangerGuards(agentName: string): HookEntry[] {
|
|
235
|
+
return [
|
|
236
|
+
{
|
|
237
|
+
matcher: "Bash",
|
|
238
|
+
hooks: [
|
|
239
|
+
{
|
|
240
|
+
type: "command",
|
|
241
|
+
command: buildBashGuardScript(agentName),
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Build a Bash guard script that blocks file-modifying commands for non-implementation agents.
|
|
250
|
+
*
|
|
251
|
+
* Uses a whitelist-first approach: if the command matches a known-safe prefix, it passes.
|
|
252
|
+
* Otherwise, it checks against dangerous patterns and blocks if any match.
|
|
253
|
+
*
|
|
254
|
+
* @param capability - The agent capability, included in block reason messages
|
|
255
|
+
* @param extraSafePrefixes - Additional safe prefixes for this capability (e.g. git add/commit for coordinators)
|
|
256
|
+
*/
|
|
257
|
+
export function buildBashFileGuardScript(
|
|
258
|
+
capability: string,
|
|
259
|
+
extraSafePrefixes: string[] = [],
|
|
260
|
+
): string {
|
|
261
|
+
// Build the safe prefix check: if command starts with any safe prefix, allow it
|
|
262
|
+
const allSafePrefixes = [...SAFE_BASH_PREFIXES, ...extraSafePrefixes];
|
|
263
|
+
const safePrefixChecks = allSafePrefixes
|
|
264
|
+
.map((prefix) => `if echo "$CMD" | grep -qE '^\\s*${prefix}'; then exit 0; fi;`)
|
|
265
|
+
.join(" ");
|
|
266
|
+
|
|
267
|
+
// Build the dangerous pattern check
|
|
268
|
+
const dangerPattern = DANGEROUS_BASH_PATTERNS.join("|");
|
|
269
|
+
|
|
270
|
+
const script = [
|
|
271
|
+
// Only enforce for agentplate agent sessions (skip for user's own Claude Code)
|
|
272
|
+
ENV_GUARD,
|
|
273
|
+
"read -r INPUT;",
|
|
274
|
+
// Extract command value from JSON (with optional space after colon)
|
|
275
|
+
'CMD=$(echo "$INPUT" | sed \'s/.*"command": *"\\([^"]*\\)".*/\\1/\');',
|
|
276
|
+
// First: whitelist safe commands
|
|
277
|
+
safePrefixChecks,
|
|
278
|
+
// Then: check for dangerous patterns
|
|
279
|
+
`if echo "$CMD" | grep -qE '${dangerPattern}'; then`,
|
|
280
|
+
` echo '{"decision":"block","reason":"${capability} agents cannot modify files — this command is not allowed"}';`,
|
|
281
|
+
" exit 0;",
|
|
282
|
+
"fi;",
|
|
283
|
+
].join(" ");
|
|
284
|
+
return script;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Build a PreToolUse guard script that prevents agents from closing or updating
|
|
289
|
+
* issues they don't own.
|
|
290
|
+
*
|
|
291
|
+
* Guards against two patterns:
|
|
292
|
+
* - `sr/bd close <id>` — blocks if <id> != $AGENTPLATE_TASK_ID
|
|
293
|
+
* - `sr/bd update <id> --status` — blocks if <id> != $AGENTPLATE_TASK_ID
|
|
294
|
+
*
|
|
295
|
+
* Agents without AGENTPLATE_TASK_ID (coordinator, monitor) exit early and are unaffected.
|
|
296
|
+
*/
|
|
297
|
+
export function buildTrackerCloseGuardScript(): string {
|
|
298
|
+
const script = [
|
|
299
|
+
// Only enforce for agentplate agent sessions
|
|
300
|
+
ENV_GUARD,
|
|
301
|
+
// Skip if task ID is not set (coordinator/monitor have no task)
|
|
302
|
+
'[ -z "$AGENTPLATE_TASK_ID" ] && exit 0;',
|
|
303
|
+
"read -r INPUT;",
|
|
304
|
+
// Extract command value from JSON
|
|
305
|
+
'CMD=$(echo "$INPUT" | sed \'s/.*"command": *"\\([^"]*\\)".*/\\1/\');',
|
|
306
|
+
// Check for sr/bd close <id>
|
|
307
|
+
"if echo \"$CMD\" | grep -qE '^\\s*(sr|bd)\\s+close\\s'; then",
|
|
308
|
+
" ISSUE_ID=$(echo \"$CMD\" | sed -E 's/^[[:space:]]*(sr|bd)[[:space:]]+close[[:space:]]+([^ ]+).*/\\2/');",
|
|
309
|
+
' if [ "$ISSUE_ID" != "$AGENTPLATE_TASK_ID" ]; then',
|
|
310
|
+
' echo "{\\"decision\\":\\"block\\",\\"reason\\":\\"Cannot close issue $ISSUE_ID — agents may only close their own task ($AGENTPLATE_TASK_ID). Report completion via worker_done mail to your parent instead.\\"}";',
|
|
311
|
+
" exit 0;",
|
|
312
|
+
" fi;",
|
|
313
|
+
"fi;",
|
|
314
|
+
// Check for sr/bd update <id> --status
|
|
315
|
+
"if echo \"$CMD\" | grep -qE '^\\s*(sr|bd)\\s+update\\s.*--status'; then",
|
|
316
|
+
" ISSUE_ID=$(echo \"$CMD\" | sed -E 's/^[[:space:]]*(sr|bd)[[:space:]]+update[[:space:]]+([^ ]+).*/\\2/');",
|
|
317
|
+
' if [ "$ISSUE_ID" != "$AGENTPLATE_TASK_ID" ]; then',
|
|
318
|
+
' echo "{\\"decision\\":\\"block\\",\\"reason\\":\\"Cannot update issue $ISSUE_ID — agents may only update their own task ($AGENTPLATE_TASK_ID).\\"}";',
|
|
319
|
+
" exit 0;",
|
|
320
|
+
" fi;",
|
|
321
|
+
"fi;",
|
|
322
|
+
].join(" ");
|
|
323
|
+
return script;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Generate a PreToolUse guard that blocks tracker close/update for foreign issues.
|
|
328
|
+
*
|
|
329
|
+
* Returns a single Bash matcher entry. Applied to ALL agent capabilities
|
|
330
|
+
* so that no agent can accidentally close the coordinator's dispatch issue.
|
|
331
|
+
* Agents without AGENTPLATE_TASK_ID (coordinator, monitor) are unaffected.
|
|
332
|
+
*/
|
|
333
|
+
export function getTrackerCloseGuards(): HookEntry[] {
|
|
334
|
+
return [
|
|
335
|
+
{
|
|
336
|
+
matcher: "Bash",
|
|
337
|
+
hooks: [{ type: "command", command: buildTrackerCloseGuardScript() }],
|
|
338
|
+
},
|
|
339
|
+
];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Build a PreToolUse guard script that enforces the merge_ready gate on lead
|
|
344
|
+
* agents (agentplate-3899, agentplate-da9b): a lead may not run
|
|
345
|
+
* `sr/bd close $AGENTPLATE_TASK_ID` unless (a) it has sent at least one
|
|
346
|
+
* `merge_ready` mail AND has sent at least one `merge_ready` per `worker_done`
|
|
347
|
+
* it has received, AND (b) the lead's branch (worktree HEAD) is reachable
|
|
348
|
+
* from the merge target (session-branch.txt > "main") via
|
|
349
|
+
* `git merge-base --is-ancestor`. (a) proves the lead reported completion;
|
|
350
|
+
* (b) proves the coordinator actually merged the work.
|
|
351
|
+
*
|
|
352
|
+
* Counts are derived by querying `ap mail list --json` and grep-counting
|
|
353
|
+
* `"id":"` occurrences in the JSON response (no jq dependency). The gate
|
|
354
|
+
* is a no-op for non-lead agents because it is only deployed to leads via
|
|
355
|
+
* `getLeadCloseGateGuards()`, but it still self-protects: the script
|
|
356
|
+
* exits early when AGENTPLATE_AGENT_NAME or AGENTPLATE_TASK_ID is unset.
|
|
357
|
+
* The merge-ancestor check fails open when AGENTPLATE_WORKTREE_PATH is unset
|
|
358
|
+
* or the target ref cannot be resolved locally — in those cases we cannot
|
|
359
|
+
* make a definitive claim, so we don't block.
|
|
360
|
+
*
|
|
361
|
+
* Foreign-task closes are caught earlier by `buildTrackerCloseGuardScript`,
|
|
362
|
+
* so this gate only fires when the issue ID matches AGENTPLATE_TASK_ID.
|
|
363
|
+
*/
|
|
364
|
+
export function buildLeadCloseGateScript(): string {
|
|
365
|
+
const blockNoMergeReady = JSON.stringify({
|
|
366
|
+
decision: "block",
|
|
367
|
+
reason:
|
|
368
|
+
'merge_ready gate: cannot close your task — you have not sent a merge_ready mail to coordinator. Required: ap mail send --to coordinator --subject "merge_ready: <task>" --body "<branch + files>" --type merge_ready --from $AGENTPLATE_AGENT_NAME. Then retry the close.',
|
|
369
|
+
});
|
|
370
|
+
const blockUnderCount = JSON.stringify({
|
|
371
|
+
decision: "block",
|
|
372
|
+
reason:
|
|
373
|
+
"merge_ready gate: cannot close your task — merge_ready count is less than worker_done received. Send one merge_ready per worker_done before closing.",
|
|
374
|
+
});
|
|
375
|
+
const blockNotMerged = JSON.stringify({
|
|
376
|
+
decision: "block",
|
|
377
|
+
reason:
|
|
378
|
+
"merge_ready gate: cannot close your task — your branch is not yet merged into the target (session-branch.txt or main). Wait for the coordinator to merge before closing. The merge step is what makes the work real.",
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const script = [
|
|
382
|
+
// Only enforce for agentplate agent sessions
|
|
383
|
+
ENV_GUARD,
|
|
384
|
+
// Skip if task ID is not set (coordinator/monitor have no task)
|
|
385
|
+
'[ -z "$AGENTPLATE_TASK_ID" ] && exit 0;',
|
|
386
|
+
"read -r INPUT;",
|
|
387
|
+
// Extract command value from JSON
|
|
388
|
+
'CMD=$(echo "$INPUT" | sed \'s/.*"command": *"\\([^"]*\\)".*/\\1/\');',
|
|
389
|
+
// Only inspect sr/bd close commands
|
|
390
|
+
"if ! echo \"$CMD\" | grep -qE '^\\s*(sr|bd)\\s+close\\s'; then exit 0; fi;",
|
|
391
|
+
// Extract the issue ID being closed
|
|
392
|
+
"ISSUE_ID=$(echo \"$CMD\" | sed -E 's/^[[:space:]]*(sr|bd)[[:space:]]+close[[:space:]]+([^ ]+).*/\\2/');",
|
|
393
|
+
// Only gate when the lead is closing its own task. Foreign closes are blocked by buildTrackerCloseGuardScript.
|
|
394
|
+
'[ "$ISSUE_ID" != "$AGENTPLATE_TASK_ID" ] && exit 0;',
|
|
395
|
+
// Count merge_ready mails sent by this agent
|
|
396
|
+
'MR=$(ap mail list --json --from "$AGENTPLATE_AGENT_NAME" --type merge_ready 2>/dev/null | grep -o \'"id":"\' | wc -l | tr -d \' \');',
|
|
397
|
+
// Count worker_done mails received by this agent
|
|
398
|
+
'WD=$(ap mail list --json --to "$AGENTPLATE_AGENT_NAME" --type worker_done 2>/dev/null | grep -o \'"id":"\' | wc -l | tr -d \' \');',
|
|
399
|
+
// Default to 0 if the count failed for any reason.
|
|
400
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
|
|
401
|
+
"MR=${MR:-0}; WD=${WD:-0};",
|
|
402
|
+
// Block if no merge_ready was ever sent
|
|
403
|
+
'if [ "$MR" -eq 0 ]; then',
|
|
404
|
+
` echo '${escapeForSingleQuotedShell(blockNoMergeReady)}';`,
|
|
405
|
+
" exit 0;",
|
|
406
|
+
"fi;",
|
|
407
|
+
// Block if not enough merge_ready for the worker_done count
|
|
408
|
+
'if [ "$MR" -lt "$WD" ]; then',
|
|
409
|
+
` echo '${escapeForSingleQuotedShell(blockUnderCount)}';`,
|
|
410
|
+
" exit 0;",
|
|
411
|
+
"fi;",
|
|
412
|
+
// Verify the lead's branch is actually merged into the target (agentplate-da9b).
|
|
413
|
+
// merge_ready alone doesn't prove the work landed — the coordinator may still be
|
|
414
|
+
// verifying or the merge may have failed.
|
|
415
|
+
// Skip if worktree path is missing (test envs etc.) — fail open.
|
|
416
|
+
'[ -z "$AGENTPLATE_WORKTREE_PATH" ] && exit 0;',
|
|
417
|
+
// Resolve target branch: $AGENTPLATE_PROJECT_ROOT/.agentplate/session-branch.txt > "main"
|
|
418
|
+
'TARGET="";',
|
|
419
|
+
'if [ -n "$AGENTPLATE_PROJECT_ROOT" ] && [ -f "$AGENTPLATE_PROJECT_ROOT/.agentplate/session-branch.txt" ]; then',
|
|
420
|
+
' TARGET=$(tr -d "[:space:]" < "$AGENTPLATE_PROJECT_ROOT/.agentplate/session-branch.txt" 2>/dev/null);',
|
|
421
|
+
"fi;",
|
|
422
|
+
'[ -z "$TARGET" ] && TARGET=main;',
|
|
423
|
+
// If the target ref doesn't exist locally, we can't verify — fail open.
|
|
424
|
+
'if ! git -C "$AGENTPLATE_WORKTREE_PATH" rev-parse --verify "$TARGET" >/dev/null 2>&1; then exit 0; fi;',
|
|
425
|
+
// Block if HEAD is not yet an ancestor of the target.
|
|
426
|
+
'if ! git -C "$AGENTPLATE_WORKTREE_PATH" merge-base --is-ancestor HEAD "$TARGET" >/dev/null 2>&1; then',
|
|
427
|
+
` echo '${escapeForSingleQuotedShell(blockNotMerged)}';`,
|
|
428
|
+
" exit 0;",
|
|
429
|
+
"fi;",
|
|
430
|
+
].join(" ");
|
|
431
|
+
return script;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Generate the lead-only PreToolUse guard that gates `sr/bd close <own-task>`
|
|
436
|
+
* on merge_ready emission. Wraps `buildLeadCloseGateScript` with the standard
|
|
437
|
+
* PATH_PREFIX so `ap` resolves under Claude Code's minimal hook PATH.
|
|
438
|
+
*
|
|
439
|
+
* Only deployed to lead agents (see getCapabilityGuards).
|
|
440
|
+
*/
|
|
441
|
+
export function getLeadCloseGateGuards(): HookEntry[] {
|
|
442
|
+
return [
|
|
443
|
+
{
|
|
444
|
+
matcher: "Bash",
|
|
445
|
+
hooks: [{ type: "command", command: `${PATH_PREFIX} ${buildLeadCloseGateScript()}` }],
|
|
446
|
+
},
|
|
447
|
+
];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Capabilities that are allowed to modify files via Bash commands.
|
|
452
|
+
* These get the Bash path boundary guard instead of a blanket file-modification block.
|
|
453
|
+
*/
|
|
454
|
+
const IMPLEMENTATION_CAPABILITIES = new Set(["builder", "merger"]);
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Bash patterns that modify files and require path boundary validation.
|
|
458
|
+
* Each entry is a regex fragment matched against the extracted command.
|
|
459
|
+
* When matched, all absolute paths in the command are checked against the worktree boundary.
|
|
460
|
+
*/
|
|
461
|
+
const FILE_MODIFYING_BASH_PATTERNS = [
|
|
462
|
+
"sed\\s+-i",
|
|
463
|
+
"sed\\s+--in-place",
|
|
464
|
+
"echo\\s+.*>",
|
|
465
|
+
"printf\\s+.*>",
|
|
466
|
+
"cat\\s+.*>",
|
|
467
|
+
"tee\\s",
|
|
468
|
+
"\\bmv\\s",
|
|
469
|
+
"\\bcp\\s",
|
|
470
|
+
"\\brm\\s",
|
|
471
|
+
"\\bmkdir\\s",
|
|
472
|
+
"\\btouch\\s",
|
|
473
|
+
"\\bchmod\\s",
|
|
474
|
+
"\\bchown\\s",
|
|
475
|
+
">>",
|
|
476
|
+
"\\binstall\\s",
|
|
477
|
+
"\\brsync\\s",
|
|
478
|
+
];
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Build a Bash PreToolUse guard script that validates file-modifying commands
|
|
482
|
+
* keep their target paths within the agent's worktree boundary.
|
|
483
|
+
*
|
|
484
|
+
* Applied to builder/merger agents. For file-modifying Bash commands (sed -i,
|
|
485
|
+
* echo >, cp, mv, tee, install, rsync, etc.), extracts all absolute paths
|
|
486
|
+
* from the command and verifies they resolve within the worktree.
|
|
487
|
+
*
|
|
488
|
+
* Limitations (documented by design):
|
|
489
|
+
* - Cannot detect paths constructed via variable expansion ($VAR/file)
|
|
490
|
+
* - Cannot detect paths reached via cd + relative path
|
|
491
|
+
* - Cannot detect paths inside subshells or backtick evaluation
|
|
492
|
+
* - Relative paths are assumed safe (tmux cwd IS the worktree)
|
|
493
|
+
*
|
|
494
|
+
* Uses AGENTPLATE_WORKTREE_PATH env var set during tmux session creation.
|
|
495
|
+
*/
|
|
496
|
+
export function buildBashPathBoundaryScript(): string {
|
|
497
|
+
const fileModifyPattern = FILE_MODIFYING_BASH_PATTERNS.join("|");
|
|
498
|
+
|
|
499
|
+
const script = [
|
|
500
|
+
// Only enforce for agentplate agent sessions
|
|
501
|
+
ENV_GUARD,
|
|
502
|
+
// Skip if worktree path is not set (e.g., orchestrator)
|
|
503
|
+
'[ -z "$AGENTPLATE_WORKTREE_PATH" ] && exit 0;',
|
|
504
|
+
"read -r INPUT;",
|
|
505
|
+
// Extract command value from JSON (with optional space after colon)
|
|
506
|
+
'CMD=$(echo "$INPUT" | sed \'s/.*"command": *"\\([^"]*\\)".*/\\1/\');',
|
|
507
|
+
// Only check file-modifying commands — non-modifying commands pass through
|
|
508
|
+
`if ! echo "$CMD" | grep -qE '${fileModifyPattern}'; then exit 0; fi;`,
|
|
509
|
+
// Extract all absolute paths (tokens starting with /) from the command.
|
|
510
|
+
// Uses tr to split on whitespace, grep to find /paths, sed to strip trailing quotes/semicolons.
|
|
511
|
+
"PATHS=$(echo \"$CMD\" | tr ' \\t' '\\n\\n' | grep '^/' | sed 's/[\";>]*$//');",
|
|
512
|
+
// If no absolute paths found, allow (relative paths resolve from worktree cwd)
|
|
513
|
+
'[ -z "$PATHS" ] && exit 0;',
|
|
514
|
+
// Check each absolute path against the worktree boundary
|
|
515
|
+
'echo "$PATHS" | while IFS= read -r P; do',
|
|
516
|
+
' case "$P" in',
|
|
517
|
+
' "$AGENTPLATE_WORKTREE_PATH"/*) ;;',
|
|
518
|
+
' "$AGENTPLATE_WORKTREE_PATH") ;;',
|
|
519
|
+
" /dev/*) ;;",
|
|
520
|
+
" /tmp/*) ;;",
|
|
521
|
+
' *) echo \'{"decision":"block","reason":"Bash path boundary violation: command targets a path outside your worktree. All file modifications must stay within your assigned worktree."}\'; exit 0; ;;',
|
|
522
|
+
" esac;",
|
|
523
|
+
"done;",
|
|
524
|
+
].join(" ");
|
|
525
|
+
return script;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Generate Bash path boundary guards for implementation capabilities.
|
|
530
|
+
*
|
|
531
|
+
* Returns a single Bash PreToolUse guard that checks file-modifying commands
|
|
532
|
+
* for absolute paths outside the worktree boundary.
|
|
533
|
+
*
|
|
534
|
+
* Only applied to builder/merger agents (implementation capabilities).
|
|
535
|
+
* Non-implementation agents already have all file-modifying Bash commands
|
|
536
|
+
* blocked via buildBashFileGuardScript().
|
|
537
|
+
*/
|
|
538
|
+
export function getBashPathBoundaryGuards(): HookEntry[] {
|
|
539
|
+
return [
|
|
540
|
+
{
|
|
541
|
+
matcher: "Bash",
|
|
542
|
+
hooks: [{ type: "command", command: buildBashPathBoundaryScript() }],
|
|
543
|
+
},
|
|
544
|
+
];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Generate capability-specific PreToolUse guards.
|
|
549
|
+
*
|
|
550
|
+
* Non-implementation capabilities (scout, reviewer, lead, coordinator, supervisor, monitor) get:
|
|
551
|
+
* - Write, Edit, NotebookEdit tool blocks
|
|
552
|
+
* - Bash file-modification command guards (sed -i, echo >, mv, rm, etc.)
|
|
553
|
+
* - Coordination capabilities (coordinator, supervisor) get git add/commit whitelisted
|
|
554
|
+
*
|
|
555
|
+
* Implementation capabilities (builder, merger) get:
|
|
556
|
+
* - Bash path boundary guards (validates absolute paths stay in worktree)
|
|
557
|
+
*
|
|
558
|
+
* All agentplate-managed agents get:
|
|
559
|
+
* - Claude Code native team/task tool blocks (Task, TeamCreate, SendMessage, etc.)
|
|
560
|
+
* to ensure delegation goes through agentplate sling
|
|
561
|
+
*
|
|
562
|
+
* Note: All capabilities also receive Bash danger guards via getDangerGuards().
|
|
563
|
+
*/
|
|
564
|
+
export function getCapabilityGuards(capability: string, qualityGates?: QualityGate[]): HookEntry[] {
|
|
565
|
+
const guards: HookEntry[] = [];
|
|
566
|
+
const gates = qualityGates ?? DEFAULT_QUALITY_GATES;
|
|
567
|
+
const gatePrefixes = extractQualityGatePrefixes(gates);
|
|
568
|
+
|
|
569
|
+
// Block Claude Code native team/task tools for ALL agentplate agents.
|
|
570
|
+
// Agents must use `agentplate sling` for delegation, not native Task/Team tools.
|
|
571
|
+
const teamToolGuards = NATIVE_TEAM_TOOLS.map((tool) =>
|
|
572
|
+
blockGuard(
|
|
573
|
+
tool,
|
|
574
|
+
`Agentplate agents must use 'ap sling' for delegation — ${tool} is not allowed`,
|
|
575
|
+
),
|
|
576
|
+
);
|
|
577
|
+
guards.push(...teamToolGuards);
|
|
578
|
+
|
|
579
|
+
// Block interactive tools for ALL agentplate agents.
|
|
580
|
+
// These tools require a human to respond and block indefinitely in tmux sessions.
|
|
581
|
+
// Agents must use agentplate mail (--type question) to escalate instead.
|
|
582
|
+
const interactiveGuards = INTERACTIVE_TOOLS.map((tool) =>
|
|
583
|
+
blockGuard(
|
|
584
|
+
tool,
|
|
585
|
+
`${tool} requires human interaction -- agents run non-interactively. Use ap mail (--type question) to escalate`,
|
|
586
|
+
),
|
|
587
|
+
);
|
|
588
|
+
guards.push(...interactiveGuards);
|
|
589
|
+
|
|
590
|
+
if (NON_IMPLEMENTATION_CAPABILITIES.has(capability)) {
|
|
591
|
+
const toolGuards = WRITE_TOOLS.map((tool) =>
|
|
592
|
+
blockGuard(tool, `${capability} agents cannot modify files — ${tool} is not allowed`),
|
|
593
|
+
);
|
|
594
|
+
guards.push(...toolGuards);
|
|
595
|
+
|
|
596
|
+
// Coordination capabilities get git add/commit whitelisted for task/loam sync
|
|
597
|
+
const extraSafe = COORDINATION_CAPABILITIES.has(capability)
|
|
598
|
+
? [...COORDINATION_SAFE_PREFIXES, ...gatePrefixes]
|
|
599
|
+
: gatePrefixes;
|
|
600
|
+
const bashFileGuard: HookEntry = {
|
|
601
|
+
matcher: "Bash",
|
|
602
|
+
hooks: [
|
|
603
|
+
{
|
|
604
|
+
type: "command",
|
|
605
|
+
command: buildBashFileGuardScript(capability, extraSafe),
|
|
606
|
+
},
|
|
607
|
+
],
|
|
608
|
+
};
|
|
609
|
+
guards.push(bashFileGuard);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Implementation capabilities get Bash path boundary validation
|
|
613
|
+
// (non-implementation agents already block all file-modifying Bash commands)
|
|
614
|
+
if (IMPLEMENTATION_CAPABILITIES.has(capability)) {
|
|
615
|
+
guards.push(...getBashPathBoundaryGuards());
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Lead agents get the merge_ready gate on sr/bd close (agentplate-3899).
|
|
619
|
+
// Blocks closing the lead's own task unless at least one merge_ready mail
|
|
620
|
+
// has been sent and the count covers all worker_done received.
|
|
621
|
+
if (capability === "lead") {
|
|
622
|
+
guards.push(...getLeadCloseGateGuards());
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return guards;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Check whether a hook entry is agentplate-managed.
|
|
630
|
+
*
|
|
631
|
+
* Agentplate hook commands always reference either `ap ` / `agentplate` (CLI commands)
|
|
632
|
+
* or `AGENTPLATE_` (env var guards like AGENTPLATE_AGENT_NAME, AGENTPLATE_WORKTREE_PATH).
|
|
633
|
+
* User hooks will not contain these patterns.
|
|
634
|
+
*/
|
|
635
|
+
export function isAgentplateHookEntry(entry: HookEntry): boolean {
|
|
636
|
+
return entry.hooks.some(
|
|
637
|
+
(h) =>
|
|
638
|
+
h.command.includes("ap ") ||
|
|
639
|
+
h.command.includes("agentplate") ||
|
|
640
|
+
h.command.includes("AGENTPLATE_"),
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Deploy hooks config to an agent's worktree as `.claude/settings.local.json`.
|
|
646
|
+
*
|
|
647
|
+
* Reads `templates/hooks.json.tmpl`, replaces `{{AGENT_NAME}}`, then merges
|
|
648
|
+
* capability-specific PreToolUse guards into the resulting config.
|
|
649
|
+
*
|
|
650
|
+
* When the target file already exists (e.g. at the project root for coordinator/
|
|
651
|
+
* supervisor/monitor), preserves non-hooks keys and user-defined hook entries.
|
|
652
|
+
* Stale agentplate hook entries are stripped and replaced with the new set.
|
|
653
|
+
* Agentplate hooks are placed before user hooks per event type so security
|
|
654
|
+
* guards run first.
|
|
655
|
+
*
|
|
656
|
+
* In `headlessOnly` mode, only PreToolUse hooks are deployed (agentplate-e24b).
|
|
657
|
+
* Headless Claude Code (`-p --output-format stream-json`) DOES dispatch hooks
|
|
658
|
+
* from settings.local.json, so PreToolUse security guards (path boundary,
|
|
659
|
+
* capability blocks, bash danger patterns, tracker close, lead close gate)
|
|
660
|
+
* are required to keep parity with tmux mode. The other hook types are dropped
|
|
661
|
+
* because they have headless equivalents already wired up:
|
|
662
|
+
* - SessionStart → buildInitialHeadlessPrompt() in sling.ts
|
|
663
|
+
* - UserPromptSubmit → mail injection loop owned by `ap serve`
|
|
664
|
+
* - PostToolUse → stream-json parser captures tool_use/tool_result
|
|
665
|
+
* - Stop → stream-json parser captures the `result` event
|
|
666
|
+
* - PreCompact → deferred (tracked separately)
|
|
667
|
+
*
|
|
668
|
+
* @param worktreePath - Absolute path to the agent's git worktree (or project root)
|
|
669
|
+
* @param agentName - The unique name of the agent
|
|
670
|
+
* @param capability - Agent capability (builder, scout, reviewer, lead, merger)
|
|
671
|
+
* @param qualityGates - Quality gates whose commands are whitelisted as safe Bash prefixes
|
|
672
|
+
* @param headlessOnly - When true, deploy only PreToolUse entries (agentplate-e24b)
|
|
673
|
+
* @throws {AgentError} If the template is not found or the write fails
|
|
674
|
+
*/
|
|
675
|
+
export async function deployHooks(
|
|
676
|
+
worktreePath: string,
|
|
677
|
+
agentName: string,
|
|
678
|
+
capability = "builder",
|
|
679
|
+
qualityGates?: QualityGate[],
|
|
680
|
+
headlessOnly = false,
|
|
681
|
+
): Promise<void> {
|
|
682
|
+
const templatePath = getTemplatePath();
|
|
683
|
+
const file = Bun.file(templatePath);
|
|
684
|
+
const exists = await file.exists();
|
|
685
|
+
|
|
686
|
+
if (!exists) {
|
|
687
|
+
throw new AgentError(`Hooks template not found: ${templatePath}`, {
|
|
688
|
+
agentName,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
let template: string;
|
|
693
|
+
try {
|
|
694
|
+
template = await file.text();
|
|
695
|
+
} catch (err) {
|
|
696
|
+
throw new AgentError(`Failed to read hooks template: ${templatePath}`, {
|
|
697
|
+
agentName,
|
|
698
|
+
cause: err instanceof Error ? err : undefined,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Replace all occurrences of {{AGENT_NAME}}
|
|
703
|
+
let content = template;
|
|
704
|
+
while (content.includes("{{AGENT_NAME}}")) {
|
|
705
|
+
content = content.replace("{{AGENT_NAME}}", agentName);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Parse the base config from the template
|
|
709
|
+
const config = JSON.parse(content) as { hooks: Record<string, HookEntry[]> };
|
|
710
|
+
|
|
711
|
+
// Headless mode: drop all template-derived hook entries.
|
|
712
|
+
// Under spawn-per-turn (Phase 3, agentplate-2cf9), the turn-runner provides
|
|
713
|
+
// the user prompt and emits its own observability events for every turn;
|
|
714
|
+
// the template's SessionStart/UserPromptSubmit/PostToolUse/Stop/PreCompact
|
|
715
|
+
// hooks would either double-deliver mail (UserPromptSubmit re-injects on top
|
|
716
|
+
// of the runner's prompt) or duplicate session_end / per-tool events.
|
|
717
|
+
// Only the dynamic PreToolUse security guards added below are retained.
|
|
718
|
+
if (headlessOnly) {
|
|
719
|
+
config.hooks = {};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Extend PATH in all template hook commands.
|
|
723
|
+
// Claude Code invokes hooks with PATH=/usr/bin:/bin:/usr/sbin:/sbin — ~/.bun/bin
|
|
724
|
+
// (where ap, lm, sr, etc. live) is not included. Prepend PATH_PREFIX so CLIs resolve.
|
|
725
|
+
for (const entries of Object.values(config.hooks)) {
|
|
726
|
+
for (const entry of entries) {
|
|
727
|
+
for (const hook of entry.hooks) {
|
|
728
|
+
hook.command = `${PATH_PREFIX} ${hook.command}`;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Merge capability-specific PreToolUse guards into the config.
|
|
734
|
+
// Guards are generated scripts using only shell built-ins (grep, sed, echo, exit)
|
|
735
|
+
// and do not require PATH extension.
|
|
736
|
+
const pathGuards = getPathBoundaryGuards();
|
|
737
|
+
const dangerGuards = getDangerGuards(agentName);
|
|
738
|
+
const capabilityGuards = getCapabilityGuards(capability, qualityGates);
|
|
739
|
+
const trackerCloseGuards = getTrackerCloseGuards();
|
|
740
|
+
const allGuards = [...pathGuards, ...dangerGuards, ...capabilityGuards, ...trackerCloseGuards];
|
|
741
|
+
|
|
742
|
+
if (allGuards.length > 0) {
|
|
743
|
+
const preToolUse = config.hooks.PreToolUse ?? [];
|
|
744
|
+
config.hooks.PreToolUse = [...allGuards, ...preToolUse];
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const claudeDir = join(worktreePath, ".claude");
|
|
748
|
+
const outputPath = join(claudeDir, "settings.local.json");
|
|
749
|
+
|
|
750
|
+
try {
|
|
751
|
+
await mkdir(claudeDir, { recursive: true });
|
|
752
|
+
} catch (err) {
|
|
753
|
+
throw new AgentError(`Failed to create .claude/ directory at: ${claudeDir}`, {
|
|
754
|
+
agentName,
|
|
755
|
+
cause: err instanceof Error ? err : undefined,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Read existing settings.local.json to preserve user hooks and non-hooks keys
|
|
760
|
+
let existingConfig: Record<string, unknown> = {};
|
|
761
|
+
const existingFile = Bun.file(outputPath);
|
|
762
|
+
if (await existingFile.exists()) {
|
|
763
|
+
try {
|
|
764
|
+
const existingContent = await existingFile.text();
|
|
765
|
+
existingConfig = JSON.parse(existingContent) as Record<string, unknown>;
|
|
766
|
+
} catch {
|
|
767
|
+
// Malformed existing file — start fresh
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Separate non-hooks keys (permissions, env, $schema, etc.) from hooks
|
|
772
|
+
const { hooks: existingHooksRaw, ...nonHooksKeys } = existingConfig;
|
|
773
|
+
|
|
774
|
+
// Partition existing hooks: keep user entries, discard stale agentplate entries
|
|
775
|
+
const existingHooks = (existingHooksRaw ?? {}) as Record<string, HookEntry[]>;
|
|
776
|
+
const userHooks: Record<string, HookEntry[]> = {};
|
|
777
|
+
for (const [eventType, entries] of Object.entries(existingHooks)) {
|
|
778
|
+
const userEntries = entries.filter((e) => !isAgentplateHookEntry(e));
|
|
779
|
+
if (userEntries.length > 0) {
|
|
780
|
+
userHooks[eventType] = userEntries;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Merge: agentplate hooks first (security guards must run first), then user hooks
|
|
785
|
+
const mergedHooks: Record<string, HookEntry[]> = {};
|
|
786
|
+
const allEventTypes = new Set([...Object.keys(config.hooks), ...Object.keys(userHooks)]);
|
|
787
|
+
for (const eventType of allEventTypes) {
|
|
788
|
+
const agentplateEntries = config.hooks[eventType] ?? [];
|
|
789
|
+
const userEntries = userHooks[eventType] ?? [];
|
|
790
|
+
mergedHooks[eventType] = [...agentplateEntries, ...userEntries];
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const finalConfig = { ...nonHooksKeys, hooks: mergedHooks };
|
|
794
|
+
const finalContent = `${JSON.stringify(finalConfig, null, "\t")}\n`;
|
|
795
|
+
|
|
796
|
+
try {
|
|
797
|
+
await Bun.write(outputPath, finalContent);
|
|
798
|
+
} catch (err) {
|
|
799
|
+
throw new AgentError(`Failed to write hooks config to: ${outputPath}`, {
|
|
800
|
+
agentName,
|
|
801
|
+
cause: err instanceof Error ? err : undefined,
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
}
|