@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,926 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiered conflict resolution for merging agent branches.
|
|
3
|
+
*
|
|
4
|
+
* Implements a 4-tier escalation strategy:
|
|
5
|
+
* 1. Clean merge — git merge with no conflicts
|
|
6
|
+
* 2. Auto-resolve — parse conflict markers, keep incoming (agent) changes
|
|
7
|
+
* 3. AI-resolve — use Claude to resolve remaining conflicts
|
|
8
|
+
* 4. Re-imagine — abort merge and reimplement changes from scratch
|
|
9
|
+
*
|
|
10
|
+
* Each tier is attempted in order. If a tier fails, the next is tried.
|
|
11
|
+
* Disabled tiers are skipped. Uses Bun.spawn for all subprocess calls.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { unlinkSync } from "node:fs";
|
|
15
|
+
import { MergeError } from "../errors.ts";
|
|
16
|
+
import type { LoamClient } from "../loam/client.ts";
|
|
17
|
+
import { getRuntime } from "../runtimes/registry.ts";
|
|
18
|
+
import type {
|
|
19
|
+
AgentplateConfig,
|
|
20
|
+
ConflictHistory,
|
|
21
|
+
MergeEntry,
|
|
22
|
+
MergeResult,
|
|
23
|
+
ParsedConflictPattern,
|
|
24
|
+
ResolutionTier,
|
|
25
|
+
} from "../types.ts";
|
|
26
|
+
|
|
27
|
+
export interface MergeResolver {
|
|
28
|
+
/** Attempt to merge the entry's branch into the canonical branch with tiered resolution. */
|
|
29
|
+
resolve(entry: MergeEntry, canonicalBranch: string, repoRoot: string): Promise<MergeResult>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Run a git command in the given repo root. Returns stdout, stderr, and exit code.
|
|
34
|
+
*/
|
|
35
|
+
async function runGit(
|
|
36
|
+
repoRoot: string,
|
|
37
|
+
args: string[],
|
|
38
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
39
|
+
const proc = Bun.spawn(["git", ...args], {
|
|
40
|
+
cwd: repoRoot,
|
|
41
|
+
stdout: "pipe",
|
|
42
|
+
stderr: "pipe",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
46
|
+
new Response(proc.stdout).text(),
|
|
47
|
+
new Response(proc.stderr).text(),
|
|
48
|
+
proc.exited,
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
return { stdout, stderr, exitCode };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* ag-eco runtime state path prefixes and exact filenames.
|
|
56
|
+
* Files matching these are bookkeeping artifacts that change during normal
|
|
57
|
+
* orchestration and should be auto-committed rather than blocking merges.
|
|
58
|
+
*/
|
|
59
|
+
const OS_ECO_STATE_PREFIXES = [
|
|
60
|
+
".sprout/",
|
|
61
|
+
".agentplate/",
|
|
62
|
+
".greenhouse/",
|
|
63
|
+
".loam/",
|
|
64
|
+
".trellis/",
|
|
65
|
+
".claude/",
|
|
66
|
+
];
|
|
67
|
+
const OS_ECO_STATE_FILES = ["CLAUDE.md"];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Returns true if a file path is an ag-eco runtime state file
|
|
71
|
+
* (issue tracker, groups, expertise, prompts, etc.).
|
|
72
|
+
*/
|
|
73
|
+
function isOsEcoStateFile(filePath: string): boolean {
|
|
74
|
+
if (OS_ECO_STATE_FILES.includes(filePath)) return true;
|
|
75
|
+
return OS_ECO_STATE_PREFIXES.some((prefix) => filePath.startsWith(prefix));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get the list of tracked files with uncommitted changes (unstaged or staged).
|
|
80
|
+
* Returns deduplicated list of file paths. An empty list means the working tree is clean.
|
|
81
|
+
*/
|
|
82
|
+
async function checkDirtyWorkingTree(repoRoot: string): Promise<string[]> {
|
|
83
|
+
const { stdout: unstaged } = await runGit(repoRoot, ["diff", "--name-only"]);
|
|
84
|
+
const { stdout: staged } = await runGit(repoRoot, ["diff", "--name-only", "--cached"]);
|
|
85
|
+
const files = [
|
|
86
|
+
...unstaged
|
|
87
|
+
.trim()
|
|
88
|
+
.split("\n")
|
|
89
|
+
.filter((l) => l.length > 0),
|
|
90
|
+
...staged
|
|
91
|
+
.trim()
|
|
92
|
+
.split("\n")
|
|
93
|
+
.filter((l) => l.length > 0),
|
|
94
|
+
];
|
|
95
|
+
return [...new Set(files)];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Auto-commit ag-eco runtime state files so they don't block merges.
|
|
100
|
+
* Returns true if a commit was made, false if there was nothing to commit.
|
|
101
|
+
*/
|
|
102
|
+
async function autoCommitStateFiles(repoRoot: string, stateFiles: string[]): Promise<boolean> {
|
|
103
|
+
if (stateFiles.length === 0) return false;
|
|
104
|
+
|
|
105
|
+
const { exitCode: addCode } = await runGit(repoRoot, ["add", ...stateFiles]);
|
|
106
|
+
if (addCode !== 0) return false;
|
|
107
|
+
|
|
108
|
+
const { exitCode: commitCode } = await runGit(repoRoot, [
|
|
109
|
+
"commit",
|
|
110
|
+
"-m",
|
|
111
|
+
"chore: sync ag-eco runtime state",
|
|
112
|
+
]);
|
|
113
|
+
return commitCode === 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get the list of conflicted files from `git diff --name-only --diff-filter=U`.
|
|
118
|
+
*/
|
|
119
|
+
async function getConflictedFiles(repoRoot: string): Promise<string[]> {
|
|
120
|
+
const { stdout } = await runGit(repoRoot, ["diff", "--name-only", "--diff-filter=U"]);
|
|
121
|
+
return stdout
|
|
122
|
+
.trim()
|
|
123
|
+
.split("\n")
|
|
124
|
+
.filter((line) => line.length > 0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Parse conflict markers in file content and keep the incoming (agent) changes.
|
|
129
|
+
*
|
|
130
|
+
* A conflict block looks like:
|
|
131
|
+
* ```
|
|
132
|
+
* <<<<<<< HEAD
|
|
133
|
+
* canonical content
|
|
134
|
+
* =======
|
|
135
|
+
* incoming content
|
|
136
|
+
* >>>>>>> branch
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* This function replaces each conflict block with only the incoming content.
|
|
140
|
+
* Returns the resolved content, or null if no conflict markers were found.
|
|
141
|
+
*/
|
|
142
|
+
function resolveConflictsKeepIncoming(content: string): string | null {
|
|
143
|
+
const conflictPattern = /^<{7} .+\n([\s\S]*?)^={7}\n([\s\S]*?)^>{7} .+\n?/gm;
|
|
144
|
+
|
|
145
|
+
if (!conflictPattern.test(content)) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Reset regex lastIndex after test()
|
|
150
|
+
conflictPattern.lastIndex = 0;
|
|
151
|
+
|
|
152
|
+
return content.replace(conflictPattern, (_match, _canonical: string, incoming: string) => {
|
|
153
|
+
return incoming;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Parse conflict markers in file content and keep ALL lines from both sides.
|
|
159
|
+
* Used when the file has `merge=union` gitattribute — dedup-on-read handles duplicates.
|
|
160
|
+
*
|
|
161
|
+
* A conflict block looks like:
|
|
162
|
+
* ```
|
|
163
|
+
* <<<<<<< HEAD
|
|
164
|
+
* canonical content
|
|
165
|
+
* =======
|
|
166
|
+
* incoming content
|
|
167
|
+
* >>>>>>> branch
|
|
168
|
+
* ```
|
|
169
|
+
*
|
|
170
|
+
* This function replaces each conflict block with canonical + incoming content concatenated.
|
|
171
|
+
* Returns the resolved content, or null if no conflict markers were found.
|
|
172
|
+
*/
|
|
173
|
+
export function resolveConflictsUnion(content: string): string | null {
|
|
174
|
+
const conflictPattern = /^<{7} .+\n([\s\S]*?)^={7}\n([\s\S]*?)^>{7} .+\n?/gm;
|
|
175
|
+
|
|
176
|
+
if (!conflictPattern.test(content)) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Reset regex lastIndex after test()
|
|
181
|
+
conflictPattern.lastIndex = 0;
|
|
182
|
+
|
|
183
|
+
return content.replace(conflictPattern, (_match, canonical: string, incoming: string) => {
|
|
184
|
+
return canonical + incoming;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Detect if any conflict block has non-whitespace content on the canonical (HEAD) side.
|
|
190
|
+
* Returns true if auto-resolving with keep-incoming would silently discard canonical content.
|
|
191
|
+
* Use this before calling resolveConflictsKeepIncoming to prevent data loss.
|
|
192
|
+
*/
|
|
193
|
+
export function hasContentfulCanonical(content: string): boolean {
|
|
194
|
+
const conflictPattern = /^<{7} .+\n([\s\S]*?)^={7}\n([\s\S]*?)^>{7} .+\n?/gm;
|
|
195
|
+
let match = conflictPattern.exec(content);
|
|
196
|
+
while (match !== null) {
|
|
197
|
+
const canonical = match[1] ?? "";
|
|
198
|
+
if (canonical.trim().length > 0) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
match = conflictPattern.exec(content);
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check if a file has the `merge=union` gitattribute set.
|
|
208
|
+
* Returns true if `git check-attr merge -- <file>` ends with ": merge: union".
|
|
209
|
+
*/
|
|
210
|
+
export async function checkMergeUnion(repoRoot: string, filePath: string): Promise<boolean> {
|
|
211
|
+
const { stdout, exitCode } = await runGit(repoRoot, ["check-attr", "merge", "--", filePath]);
|
|
212
|
+
if (exitCode !== 0) return false;
|
|
213
|
+
return stdout.trim().endsWith(": merge: union");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Read a file's content using Bun.file().
|
|
218
|
+
*/
|
|
219
|
+
async function readFile(filePath: string): Promise<string> {
|
|
220
|
+
const file = Bun.file(filePath);
|
|
221
|
+
return file.text();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Write content to a file using Bun.write().
|
|
226
|
+
*/
|
|
227
|
+
async function writeFile(filePath: string, content: string): Promise<void> {
|
|
228
|
+
await Bun.write(filePath, content);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Tier 1: Attempt a clean merge (git merge --no-edit).
|
|
233
|
+
* Returns true if the merge succeeds with no conflicts.
|
|
234
|
+
*/
|
|
235
|
+
async function tryCleanMerge(
|
|
236
|
+
entry: MergeEntry,
|
|
237
|
+
repoRoot: string,
|
|
238
|
+
): Promise<{ success: boolean; conflictFiles: string[] }> {
|
|
239
|
+
const { exitCode } = await runGit(repoRoot, ["merge", "--no-edit", entry.branchName]);
|
|
240
|
+
|
|
241
|
+
if (exitCode === 0) {
|
|
242
|
+
return { success: true, conflictFiles: [] };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Merge failed — get the list of conflicted files
|
|
246
|
+
const conflictFiles = await getConflictedFiles(repoRoot);
|
|
247
|
+
return { success: false, conflictFiles };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Tier 2: Auto-resolve conflicts by keeping incoming (agent) changes.
|
|
252
|
+
* Parses conflict markers and keeps the content between ======= and >>>>>>>.
|
|
253
|
+
* Skips files where the canonical side has non-whitespace content to prevent
|
|
254
|
+
* silent data loss — those files are escalated to higher tiers.
|
|
255
|
+
*/
|
|
256
|
+
async function tryAutoResolve(
|
|
257
|
+
conflictFiles: string[],
|
|
258
|
+
repoRoot: string,
|
|
259
|
+
): Promise<{ success: boolean; remainingConflicts: string[]; contentDropWarnings: string[] }> {
|
|
260
|
+
const remainingConflicts: string[] = [];
|
|
261
|
+
const contentDropWarnings: string[] = [];
|
|
262
|
+
|
|
263
|
+
for (const file of conflictFiles) {
|
|
264
|
+
const filePath = `${repoRoot}/${file}`;
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const content = await readFile(filePath);
|
|
268
|
+
const isUnion = await checkMergeUnion(repoRoot, file);
|
|
269
|
+
|
|
270
|
+
// For non-union files, check if the canonical side has content.
|
|
271
|
+
// If it does, auto-resolving would silently discard that content.
|
|
272
|
+
// Escalate to a higher tier instead.
|
|
273
|
+
if (!isUnion && hasContentfulCanonical(content)) {
|
|
274
|
+
contentDropWarnings.push(
|
|
275
|
+
`auto-resolve skipped for ${file}: canonical side has content that would be discarded`,
|
|
276
|
+
);
|
|
277
|
+
remainingConflicts.push(file);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const resolved = isUnion
|
|
282
|
+
? resolveConflictsUnion(content)
|
|
283
|
+
: resolveConflictsKeepIncoming(content);
|
|
284
|
+
|
|
285
|
+
if (resolved === null) {
|
|
286
|
+
// No conflict markers found (shouldn't happen but be defensive)
|
|
287
|
+
remainingConflicts.push(file);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
await writeFile(filePath, resolved);
|
|
292
|
+
const { exitCode } = await runGit(repoRoot, ["add", file]);
|
|
293
|
+
if (exitCode !== 0) {
|
|
294
|
+
remainingConflicts.push(file);
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
remainingConflicts.push(file);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (remainingConflicts.length > 0) {
|
|
302
|
+
return { success: false, remainingConflicts, contentDropWarnings };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// All files resolved — commit
|
|
306
|
+
const { exitCode } = await runGit(repoRoot, ["commit", "--no-edit"]);
|
|
307
|
+
return { success: exitCode === 0, remainingConflicts, contentDropWarnings };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Check if text looks like conversational prose rather than code.
|
|
312
|
+
* Returns true if the output is likely prose from the LLM rather than resolved code.
|
|
313
|
+
*/
|
|
314
|
+
export function looksLikeProse(text: string): boolean {
|
|
315
|
+
const trimmed = text.trim();
|
|
316
|
+
if (trimmed.length === 0) return true;
|
|
317
|
+
|
|
318
|
+
// Common conversational opening patterns from LLMs
|
|
319
|
+
const prosePatterns = [
|
|
320
|
+
/^(I |I'[a-z]+ |Here |Here's |The |This |Let me |Sure|Unfortunately|Apologies|Sorry)/i,
|
|
321
|
+
/^(To resolve|Looking at|Based on|After reviewing|The conflict)/i,
|
|
322
|
+
/^```/m, // Markdown fencing — the model wrapped the code
|
|
323
|
+
/I need permission/i,
|
|
324
|
+
/I cannot/i,
|
|
325
|
+
/I don't have/i,
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
for (const pattern of prosePatterns) {
|
|
329
|
+
if (pattern.test(trimmed)) return true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Tier 3: AI-assisted conflict resolution using Claude.
|
|
337
|
+
* Spawns `claude --print` for each conflicted file with the conflict content.
|
|
338
|
+
* Validates that output looks like code, not conversational prose.
|
|
339
|
+
*/
|
|
340
|
+
async function tryAiResolve(
|
|
341
|
+
conflictFiles: string[],
|
|
342
|
+
repoRoot: string,
|
|
343
|
+
pastResolutions?: string[],
|
|
344
|
+
config?: AgentplateConfig,
|
|
345
|
+
): Promise<{ success: boolean; remainingConflicts: string[] }> {
|
|
346
|
+
const remainingConflicts: string[] = [];
|
|
347
|
+
|
|
348
|
+
for (const file of conflictFiles) {
|
|
349
|
+
const filePath = `${repoRoot}/${file}`;
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
const content = await readFile(filePath);
|
|
353
|
+
const historyContext =
|
|
354
|
+
pastResolutions && pastResolutions.length > 0
|
|
355
|
+
? `\n\nHistorical context from past merges:\n${pastResolutions.join("\n")}\n`
|
|
356
|
+
: "";
|
|
357
|
+
const prompt = [
|
|
358
|
+
"You are a merge conflict resolver. Output ONLY the resolved file content.",
|
|
359
|
+
"Rules: NO explanation, NO markdown fencing, NO conversation, NO preamble.",
|
|
360
|
+
"Output the raw file content as it should appear on disk.",
|
|
361
|
+
"Choose the best combination of both sides of this conflict:",
|
|
362
|
+
historyContext,
|
|
363
|
+
"\n\n",
|
|
364
|
+
content,
|
|
365
|
+
].join(" ");
|
|
366
|
+
|
|
367
|
+
const runtime = getRuntime(config?.runtime?.printCommand ?? config?.runtime?.default, config);
|
|
368
|
+
const argv = runtime.buildPrintCommand(prompt);
|
|
369
|
+
const proc = Bun.spawn(argv, {
|
|
370
|
+
cwd: repoRoot,
|
|
371
|
+
stdout: "pipe",
|
|
372
|
+
stderr: "pipe",
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const [resolved, , exitCode] = await Promise.all([
|
|
376
|
+
new Response(proc.stdout).text(),
|
|
377
|
+
new Response(proc.stderr).text(),
|
|
378
|
+
proc.exited,
|
|
379
|
+
]);
|
|
380
|
+
|
|
381
|
+
if (exitCode !== 0 || resolved.trim() === "") {
|
|
382
|
+
remainingConflicts.push(file);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Validate output is code, not prose — fall back to next tier if not
|
|
387
|
+
if (looksLikeProse(resolved)) {
|
|
388
|
+
remainingConflicts.push(file);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
await writeFile(filePath, resolved);
|
|
393
|
+
const { exitCode: addExitCode } = await runGit(repoRoot, ["add", file]);
|
|
394
|
+
if (addExitCode !== 0) {
|
|
395
|
+
remainingConflicts.push(file);
|
|
396
|
+
}
|
|
397
|
+
} catch {
|
|
398
|
+
remainingConflicts.push(file);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (remainingConflicts.length > 0) {
|
|
403
|
+
return { success: false, remainingConflicts };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// All files resolved — commit
|
|
407
|
+
const { exitCode } = await runGit(repoRoot, ["commit", "--no-edit"]);
|
|
408
|
+
return { success: exitCode === 0, remainingConflicts };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Tier 4: Re-imagine — abort the merge and reimplement changes from scratch.
|
|
413
|
+
* Uses Claude to reimplement the agent's changes on top of the canonical version.
|
|
414
|
+
*/
|
|
415
|
+
async function tryReimagine(
|
|
416
|
+
entry: MergeEntry,
|
|
417
|
+
canonicalBranch: string,
|
|
418
|
+
repoRoot: string,
|
|
419
|
+
config?: AgentplateConfig,
|
|
420
|
+
): Promise<{ success: boolean }> {
|
|
421
|
+
// Abort the current merge
|
|
422
|
+
await runGit(repoRoot, ["merge", "--abort"]);
|
|
423
|
+
|
|
424
|
+
for (const file of entry.filesModified) {
|
|
425
|
+
try {
|
|
426
|
+
// Get the canonical version
|
|
427
|
+
const { stdout: canonicalContent, exitCode: catCanonicalCode } = await runGit(repoRoot, [
|
|
428
|
+
"show",
|
|
429
|
+
`${canonicalBranch}:${file}`,
|
|
430
|
+
]);
|
|
431
|
+
|
|
432
|
+
// Get the branch version
|
|
433
|
+
const { stdout: branchContent, exitCode: catBranchCode } = await runGit(repoRoot, [
|
|
434
|
+
"show",
|
|
435
|
+
`${entry.branchName}:${file}`,
|
|
436
|
+
]);
|
|
437
|
+
|
|
438
|
+
if (catCanonicalCode !== 0 || catBranchCode !== 0) {
|
|
439
|
+
return { success: false };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const prompt = [
|
|
443
|
+
"You are a merge conflict resolver. Output ONLY the final file content.",
|
|
444
|
+
"Rules: NO explanation, NO markdown fencing, NO conversation, NO preamble.",
|
|
445
|
+
"Output the raw file content as it should appear on disk.",
|
|
446
|
+
"Reimplement the changes from the branch version onto the canonical version.",
|
|
447
|
+
`\n\n=== CANONICAL VERSION (${canonicalBranch}) ===\n`,
|
|
448
|
+
canonicalContent,
|
|
449
|
+
`\n\n=== BRANCH VERSION (${entry.branchName}) ===\n`,
|
|
450
|
+
branchContent,
|
|
451
|
+
].join("");
|
|
452
|
+
|
|
453
|
+
const runtime = getRuntime(config?.runtime?.printCommand ?? config?.runtime?.default, config);
|
|
454
|
+
const argv = runtime.buildPrintCommand(prompt);
|
|
455
|
+
const proc = Bun.spawn(argv, {
|
|
456
|
+
cwd: repoRoot,
|
|
457
|
+
stdout: "pipe",
|
|
458
|
+
stderr: "pipe",
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const [reimagined, , exitCode] = await Promise.all([
|
|
462
|
+
new Response(proc.stdout).text(),
|
|
463
|
+
new Response(proc.stderr).text(),
|
|
464
|
+
proc.exited,
|
|
465
|
+
]);
|
|
466
|
+
|
|
467
|
+
if (exitCode !== 0 || reimagined.trim() === "") {
|
|
468
|
+
return { success: false };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Validate output is code, not prose
|
|
472
|
+
if (looksLikeProse(reimagined)) {
|
|
473
|
+
return { success: false };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const filePath = `${repoRoot}/${file}`;
|
|
477
|
+
await writeFile(filePath, reimagined);
|
|
478
|
+
const { exitCode: addExitCode } = await runGit(repoRoot, ["add", file]);
|
|
479
|
+
if (addExitCode !== 0) {
|
|
480
|
+
return { success: false };
|
|
481
|
+
}
|
|
482
|
+
} catch {
|
|
483
|
+
return { success: false };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Commit the reimagined changes
|
|
488
|
+
const { exitCode } = await runGit(repoRoot, [
|
|
489
|
+
"commit",
|
|
490
|
+
"-m",
|
|
491
|
+
`Reimagine merge: ${entry.branchName} onto ${canonicalBranch}`,
|
|
492
|
+
]);
|
|
493
|
+
|
|
494
|
+
return { success: exitCode === 0 };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Parse loam search output for conflict patterns.
|
|
499
|
+
* Extracts structured data from pattern descriptions recorded by recordConflictPattern().
|
|
500
|
+
*/
|
|
501
|
+
export function parseConflictPatterns(searchOutput: string): ParsedConflictPattern[] {
|
|
502
|
+
const patterns: ParsedConflictPattern[] = [];
|
|
503
|
+
// Simple approach: match to end of line/sentence and manually strip trailing period
|
|
504
|
+
const regex =
|
|
505
|
+
/Merge conflict (resolved|failed) at tier (clean-merge|auto-resolve|ai-resolve|reimagine)\.\s*Branch:\s*(\S+)\.\s*Agent:\s*(\S+)\.\s*Conflicting files:\s*(.+?)(?=\.(?:\s|$))/g;
|
|
506
|
+
|
|
507
|
+
let match = regex.exec(searchOutput);
|
|
508
|
+
while (match !== null) {
|
|
509
|
+
const outcome = match[1];
|
|
510
|
+
const tier = match[2];
|
|
511
|
+
const branch = match[3];
|
|
512
|
+
const agent = match[4];
|
|
513
|
+
const filesStr = match[5];
|
|
514
|
+
|
|
515
|
+
if (!outcome || !tier || !branch || !agent || !filesStr) {
|
|
516
|
+
match = regex.exec(searchOutput);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
patterns.push({
|
|
521
|
+
tier: tier as ResolutionTier,
|
|
522
|
+
success: outcome === "resolved",
|
|
523
|
+
files: filesStr
|
|
524
|
+
.split(",")
|
|
525
|
+
.map((f) => f.trim())
|
|
526
|
+
.filter((f) => f.length > 0),
|
|
527
|
+
agent: agent.trim(),
|
|
528
|
+
branch: branch.trim(),
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
match = regex.exec(searchOutput);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return patterns;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Build conflict history from parsed patterns, scoped to the files in the current merge entry.
|
|
539
|
+
*
|
|
540
|
+
* Skip-tier logic: if a tier has failed >= 2 times for any overlapping file
|
|
541
|
+
* and never succeeded for those files, add it to skipTiers.
|
|
542
|
+
*
|
|
543
|
+
* Past resolutions: collect descriptions of successful resolutions involving
|
|
544
|
+
* overlapping files to enrich AI prompts.
|
|
545
|
+
*
|
|
546
|
+
* Predicted conflicts: files from historical patterns that overlap with the
|
|
547
|
+
* current entry files.
|
|
548
|
+
*/
|
|
549
|
+
export function buildConflictHistory(
|
|
550
|
+
patterns: ParsedConflictPattern[],
|
|
551
|
+
entryFiles: string[],
|
|
552
|
+
): ConflictHistory {
|
|
553
|
+
const entryFileSet = new Set(entryFiles);
|
|
554
|
+
|
|
555
|
+
// Filter patterns to those that share files with the current entry
|
|
556
|
+
const relevantPatterns = patterns.filter((p) => p.files.some((f) => entryFileSet.has(f)));
|
|
557
|
+
|
|
558
|
+
if (relevantPatterns.length === 0) {
|
|
559
|
+
return { skipTiers: [], pastResolutions: [], predictedConflictFiles: [] };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Build tier success/failure counts
|
|
563
|
+
const tierCounts = new Map<ResolutionTier, { successes: number; failures: number }>();
|
|
564
|
+
for (const p of relevantPatterns) {
|
|
565
|
+
const counts = tierCounts.get(p.tier) ?? { successes: 0, failures: 0 };
|
|
566
|
+
if (p.success) {
|
|
567
|
+
counts.successes++;
|
|
568
|
+
} else {
|
|
569
|
+
counts.failures++;
|
|
570
|
+
}
|
|
571
|
+
tierCounts.set(p.tier, counts);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Skip tiers that have failed >= 2 times and never succeeded
|
|
575
|
+
const skipTiers: ResolutionTier[] = [];
|
|
576
|
+
for (const [tier, counts] of tierCounts) {
|
|
577
|
+
if (counts.failures >= 2 && counts.successes === 0) {
|
|
578
|
+
skipTiers.push(tier);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Collect past successful resolutions
|
|
583
|
+
const pastResolutions: string[] = [];
|
|
584
|
+
for (const p of relevantPatterns) {
|
|
585
|
+
if (p.success) {
|
|
586
|
+
pastResolutions.push(
|
|
587
|
+
`Previously resolved at tier ${p.tier} for files: ${p.files.join(", ")}`,
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Predict conflict files: all files from relevant historical patterns
|
|
593
|
+
const predictedFileSet = new Set<string>();
|
|
594
|
+
for (const p of relevantPatterns) {
|
|
595
|
+
for (const f of p.files) {
|
|
596
|
+
predictedFileSet.add(f);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
const predictedConflictFiles = [...predictedFileSet].sort();
|
|
600
|
+
|
|
601
|
+
return { skipTiers, pastResolutions, predictedConflictFiles };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Query loam for historical conflict patterns related to the merge entry.
|
|
606
|
+
* Returns empty history if loam is unavailable or search fails (fire-and-forget).
|
|
607
|
+
*/
|
|
608
|
+
async function queryConflictHistory(
|
|
609
|
+
loamClient: LoamClient,
|
|
610
|
+
entry: MergeEntry,
|
|
611
|
+
): Promise<ConflictHistory> {
|
|
612
|
+
try {
|
|
613
|
+
const searchOutput = await loamClient.search("merge-conflict", { sortByScore: true });
|
|
614
|
+
const patterns = parseConflictPatterns(searchOutput);
|
|
615
|
+
return buildConflictHistory(patterns, entry.filesModified);
|
|
616
|
+
} catch {
|
|
617
|
+
return { skipTiers: [], pastResolutions: [], predictedConflictFiles: [] };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Record a merge conflict pattern to loam for future learning.
|
|
623
|
+
* Uses fire-and-forget (try/catch swallowing errors) so recording
|
|
624
|
+
* never blocks or fails the merge itself.
|
|
625
|
+
*/
|
|
626
|
+
function recordConflictPattern(
|
|
627
|
+
loamClient: LoamClient,
|
|
628
|
+
entry: MergeEntry,
|
|
629
|
+
tier: ResolutionTier,
|
|
630
|
+
conflictFiles: string[],
|
|
631
|
+
success: boolean,
|
|
632
|
+
): void {
|
|
633
|
+
const outcome = success ? "resolved" : "failed";
|
|
634
|
+
const description = [
|
|
635
|
+
`Merge conflict ${outcome} at tier ${tier}.`,
|
|
636
|
+
`Branch: ${entry.branchName}.`,
|
|
637
|
+
`Agent: ${entry.agentName}.`,
|
|
638
|
+
`Conflicting files: ${conflictFiles.join(", ")}.`,
|
|
639
|
+
].join(" ");
|
|
640
|
+
|
|
641
|
+
// Fire-and-forget per convention mx-09e10f
|
|
642
|
+
loamClient
|
|
643
|
+
.record("architecture", {
|
|
644
|
+
type: "pattern",
|
|
645
|
+
description,
|
|
646
|
+
tags: ["merge-conflict"],
|
|
647
|
+
evidenceBead: entry.taskId,
|
|
648
|
+
})
|
|
649
|
+
.catch(() => {});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Create a MergeResolver with configurable tier enablement.
|
|
654
|
+
*
|
|
655
|
+
* @param options.aiResolveEnabled - Enable tier 3 (AI-assisted resolution)
|
|
656
|
+
* @param options.reimagineEnabled - Enable tier 4 (full reimagine)
|
|
657
|
+
* @param options.loamClient - Optional LoamClient for conflict pattern recording
|
|
658
|
+
*/
|
|
659
|
+
export function createMergeResolver(options: {
|
|
660
|
+
aiResolveEnabled: boolean;
|
|
661
|
+
reimagineEnabled: boolean;
|
|
662
|
+
loamClient?: LoamClient;
|
|
663
|
+
config?: AgentplateConfig;
|
|
664
|
+
onMergeSuccess?: (entry: MergeEntry) => Promise<void>;
|
|
665
|
+
}): MergeResolver {
|
|
666
|
+
return {
|
|
667
|
+
async resolve(
|
|
668
|
+
entry: MergeEntry,
|
|
669
|
+
canonicalBranch: string,
|
|
670
|
+
repoRoot: string,
|
|
671
|
+
): Promise<MergeResult> {
|
|
672
|
+
// Check current branch — skip checkout if already on canonical.
|
|
673
|
+
// Avoids "already checked out" error when worktrees exist.
|
|
674
|
+
const { stdout: currentRef, exitCode: refCode } = await runGit(repoRoot, [
|
|
675
|
+
"symbolic-ref",
|
|
676
|
+
"--short",
|
|
677
|
+
"HEAD",
|
|
678
|
+
]);
|
|
679
|
+
const needsCheckout = refCode !== 0 || currentRef.trim() !== canonicalBranch;
|
|
680
|
+
|
|
681
|
+
if (needsCheckout) {
|
|
682
|
+
const { exitCode: checkoutCode, stderr: checkoutErr } = await runGit(repoRoot, [
|
|
683
|
+
"checkout",
|
|
684
|
+
canonicalBranch,
|
|
685
|
+
]);
|
|
686
|
+
if (checkoutCode !== 0) {
|
|
687
|
+
throw new MergeError(`Failed to checkout ${canonicalBranch}: ${checkoutErr.trim()}`, {
|
|
688
|
+
branchName: canonicalBranch,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Pre-check: auto-commit ag-eco state files, stash any remaining dirty tracked files.
|
|
694
|
+
// When dirty tracked files exist, git merge refuses to start (exit 1, no conflict markers),
|
|
695
|
+
// causing all tiers to cascade with empty conflict lists and a misleading final error.
|
|
696
|
+
const dirtyFiles = await checkDirtyWorkingTree(repoRoot);
|
|
697
|
+
if (dirtyFiles.length > 0) {
|
|
698
|
+
const stateFiles = dirtyFiles.filter(isOsEcoStateFile);
|
|
699
|
+
|
|
700
|
+
// Auto-commit ag-eco runtime state files so they don't block merges
|
|
701
|
+
if (stateFiles.length > 0) {
|
|
702
|
+
await autoCommitStateFiles(repoRoot, stateFiles);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Re-check after auto-commit: any remaining dirty tracked files get stashed
|
|
707
|
+
// so clean-merge-eligible branches can proceed without manual intervention.
|
|
708
|
+
let didStash = false;
|
|
709
|
+
const remainingDirty = await checkDirtyWorkingTree(repoRoot);
|
|
710
|
+
if (remainingDirty.length > 0) {
|
|
711
|
+
const { exitCode: stashCode } = await runGit(repoRoot, [
|
|
712
|
+
"stash",
|
|
713
|
+
"push",
|
|
714
|
+
"-m",
|
|
715
|
+
"ap-merge: auto-stash dirty files",
|
|
716
|
+
]);
|
|
717
|
+
if (stashCode !== 0) {
|
|
718
|
+
throw new MergeError(
|
|
719
|
+
`Working tree has uncommitted changes to tracked files: ${remainingDirty.join(", ")}. Commit or stash changes before running ap merge.`,
|
|
720
|
+
{ branchName: entry.branchName },
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
didStash = true;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const warnings: string[] = [];
|
|
727
|
+
let lastTier: ResolutionTier = "clean-merge";
|
|
728
|
+
let conflictFiles: string[] = [];
|
|
729
|
+
|
|
730
|
+
try {
|
|
731
|
+
// Delete untracked files overlapping entry.filesModified before merging.
|
|
732
|
+
// git merge refuses to run if untracked files in the working tree would
|
|
733
|
+
// be overwritten by the incoming branch. Deleting them lets the merge
|
|
734
|
+
// proceed and bring in the branch version.
|
|
735
|
+
const { stdout: untrackedOut } = await runGit(repoRoot, [
|
|
736
|
+
"ls-files",
|
|
737
|
+
"--others",
|
|
738
|
+
"--exclude-standard",
|
|
739
|
+
]);
|
|
740
|
+
const untrackedFiles = untrackedOut
|
|
741
|
+
.trim()
|
|
742
|
+
.split("\n")
|
|
743
|
+
.filter((f) => f.length > 0);
|
|
744
|
+
const entryFileSet = new Set(entry.filesModified);
|
|
745
|
+
const overlappingUntracked = untrackedFiles.filter((f) => entryFileSet.has(f));
|
|
746
|
+
for (const file of overlappingUntracked) {
|
|
747
|
+
const filePath = `${repoRoot}/${file}`;
|
|
748
|
+
try {
|
|
749
|
+
if (await Bun.file(filePath).exists()) {
|
|
750
|
+
unlinkSync(filePath);
|
|
751
|
+
}
|
|
752
|
+
warnings.push(
|
|
753
|
+
`untracked file deleted before merge: ${file} (branch version will be used)`,
|
|
754
|
+
);
|
|
755
|
+
} catch {
|
|
756
|
+
// Ignore errors removing untracked files
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Tier 1: Clean merge
|
|
761
|
+
const cleanResult = await tryCleanMerge(entry, repoRoot);
|
|
762
|
+
if (cleanResult.success) {
|
|
763
|
+
if (options.onMergeSuccess) {
|
|
764
|
+
try {
|
|
765
|
+
await options.onMergeSuccess({
|
|
766
|
+
...entry,
|
|
767
|
+
status: "merged",
|
|
768
|
+
resolvedTier: "clean-merge",
|
|
769
|
+
});
|
|
770
|
+
} catch {
|
|
771
|
+
// callback failures must not fail the merge
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
entry: { ...entry, status: "merged", resolvedTier: "clean-merge" },
|
|
776
|
+
success: true,
|
|
777
|
+
tier: "clean-merge",
|
|
778
|
+
conflictFiles: [],
|
|
779
|
+
errorMessage: null,
|
|
780
|
+
warnings,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
conflictFiles = cleanResult.conflictFiles;
|
|
784
|
+
|
|
785
|
+
// Query conflict history (if loamClient available)
|
|
786
|
+
let history: ConflictHistory = {
|
|
787
|
+
skipTiers: [],
|
|
788
|
+
pastResolutions: [],
|
|
789
|
+
predictedConflictFiles: [],
|
|
790
|
+
};
|
|
791
|
+
if (options.loamClient) {
|
|
792
|
+
history = await queryConflictHistory(options.loamClient, entry);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Tier 2: Auto-resolve (keep incoming)
|
|
796
|
+
if (!history.skipTiers.includes("auto-resolve")) {
|
|
797
|
+
lastTier = "auto-resolve";
|
|
798
|
+
const autoResult = await tryAutoResolve(conflictFiles, repoRoot);
|
|
799
|
+
if (autoResult.contentDropWarnings.length > 0) {
|
|
800
|
+
warnings.push(...autoResult.contentDropWarnings);
|
|
801
|
+
}
|
|
802
|
+
if (autoResult.success) {
|
|
803
|
+
if (options.loamClient) {
|
|
804
|
+
recordConflictPattern(options.loamClient, entry, "auto-resolve", conflictFiles, true);
|
|
805
|
+
}
|
|
806
|
+
if (options.onMergeSuccess) {
|
|
807
|
+
try {
|
|
808
|
+
await options.onMergeSuccess({
|
|
809
|
+
...entry,
|
|
810
|
+
status: "merged",
|
|
811
|
+
resolvedTier: "auto-resolve",
|
|
812
|
+
});
|
|
813
|
+
} catch {
|
|
814
|
+
// callback failures must not fail the merge
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
entry: { ...entry, status: "merged", resolvedTier: "auto-resolve" },
|
|
819
|
+
success: true,
|
|
820
|
+
tier: "auto-resolve",
|
|
821
|
+
conflictFiles,
|
|
822
|
+
errorMessage: null,
|
|
823
|
+
warnings,
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
conflictFiles = autoResult.remainingConflicts;
|
|
827
|
+
} // If skipped, fall through to next tier
|
|
828
|
+
|
|
829
|
+
// Tier 3: AI-resolve
|
|
830
|
+
if (options.aiResolveEnabled && !history.skipTiers.includes("ai-resolve")) {
|
|
831
|
+
lastTier = "ai-resolve";
|
|
832
|
+
const aiResult = await tryAiResolve(
|
|
833
|
+
conflictFiles,
|
|
834
|
+
repoRoot,
|
|
835
|
+
history.pastResolutions,
|
|
836
|
+
options.config,
|
|
837
|
+
);
|
|
838
|
+
if (aiResult.success) {
|
|
839
|
+
if (options.loamClient) {
|
|
840
|
+
recordConflictPattern(options.loamClient, entry, "ai-resolve", conflictFiles, true);
|
|
841
|
+
}
|
|
842
|
+
if (options.onMergeSuccess) {
|
|
843
|
+
try {
|
|
844
|
+
await options.onMergeSuccess({
|
|
845
|
+
...entry,
|
|
846
|
+
status: "merged",
|
|
847
|
+
resolvedTier: "ai-resolve",
|
|
848
|
+
});
|
|
849
|
+
} catch {
|
|
850
|
+
// callback failures must not fail the merge
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return {
|
|
854
|
+
entry: { ...entry, status: "merged", resolvedTier: "ai-resolve" },
|
|
855
|
+
success: true,
|
|
856
|
+
tier: "ai-resolve",
|
|
857
|
+
conflictFiles,
|
|
858
|
+
errorMessage: null,
|
|
859
|
+
warnings,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
conflictFiles = aiResult.remainingConflicts;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Tier 4: Re-imagine
|
|
866
|
+
if (options.reimagineEnabled && !history.skipTiers.includes("reimagine")) {
|
|
867
|
+
lastTier = "reimagine";
|
|
868
|
+
const reimagineResult = await tryReimagine(
|
|
869
|
+
entry,
|
|
870
|
+
canonicalBranch,
|
|
871
|
+
repoRoot,
|
|
872
|
+
options.config,
|
|
873
|
+
);
|
|
874
|
+
if (reimagineResult.success) {
|
|
875
|
+
if (options.loamClient) {
|
|
876
|
+
recordConflictPattern(options.loamClient, entry, "reimagine", conflictFiles, true);
|
|
877
|
+
}
|
|
878
|
+
if (options.onMergeSuccess) {
|
|
879
|
+
try {
|
|
880
|
+
await options.onMergeSuccess({
|
|
881
|
+
...entry,
|
|
882
|
+
status: "merged",
|
|
883
|
+
resolvedTier: "reimagine",
|
|
884
|
+
});
|
|
885
|
+
} catch {
|
|
886
|
+
// callback failures must not fail the merge
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return {
|
|
890
|
+
entry: { ...entry, status: "merged", resolvedTier: "reimagine" },
|
|
891
|
+
success: true,
|
|
892
|
+
tier: "reimagine",
|
|
893
|
+
conflictFiles: [],
|
|
894
|
+
errorMessage: null,
|
|
895
|
+
warnings,
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// All enabled tiers failed — abort any in-progress merge
|
|
901
|
+
try {
|
|
902
|
+
await runGit(repoRoot, ["merge", "--abort"]);
|
|
903
|
+
} catch {
|
|
904
|
+
// merge --abort may fail if there's no merge in progress (e.g., after reimagine)
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (options.loamClient) {
|
|
908
|
+
recordConflictPattern(options.loamClient, entry, lastTier, conflictFiles, false);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
return {
|
|
912
|
+
entry: { ...entry, status: "failed", resolvedTier: null },
|
|
913
|
+
success: false,
|
|
914
|
+
tier: lastTier,
|
|
915
|
+
conflictFiles,
|
|
916
|
+
errorMessage: `All enabled resolution tiers failed (last attempted: ${lastTier})`,
|
|
917
|
+
warnings,
|
|
918
|
+
};
|
|
919
|
+
} finally {
|
|
920
|
+
if (didStash) {
|
|
921
|
+
await runGit(repoRoot, ["stash", "pop"]);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
},
|
|
925
|
+
};
|
|
926
|
+
}
|