@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,1234 @@
|
|
|
1
|
+
import { getRegistry } from "../registry/type-registry.ts";
|
|
2
|
+
import type {
|
|
3
|
+
HookEvent,
|
|
4
|
+
LoamConfig,
|
|
5
|
+
SessionCloseConfig,
|
|
6
|
+
SessionCloseStyleName,
|
|
7
|
+
} from "../schemas/config.ts";
|
|
8
|
+
import { DEFAULT_SESSION_CLOSE_STYLE, HOOK_EVENTS } from "../schemas/config.ts";
|
|
9
|
+
import type { BuiltinRecordType, ExpertiseRecord } from "../schemas/record.ts";
|
|
10
|
+
import { formatLinks, formatTimeAgo, xmlAttrEscape, xmlEscape } from "./format-helpers.ts";
|
|
11
|
+
|
|
12
|
+
export { formatTimeAgo };
|
|
13
|
+
|
|
14
|
+
export function getRecordSummary(record: ExpertiseRecord): string {
|
|
15
|
+
const def = getRegistry().get(record.type);
|
|
16
|
+
if (!def) {
|
|
17
|
+
throw new Error(`Unknown record type: ${record.type}`);
|
|
18
|
+
}
|
|
19
|
+
return def.summary(record);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatDomainExpertiseCompact(
|
|
23
|
+
domain: string,
|
|
24
|
+
records: ExpertiseRecord[],
|
|
25
|
+
lastUpdated: Date | null,
|
|
26
|
+
annotations?: Map<string, string>,
|
|
27
|
+
): string {
|
|
28
|
+
const registry = getRegistry();
|
|
29
|
+
const updatedStr = lastUpdated ? `, updated ${formatTimeAgo(lastUpdated)}` : "";
|
|
30
|
+
const lines: string[] = [];
|
|
31
|
+
|
|
32
|
+
lines.push(`## ${domain} (${records.length} records${updatedStr})`);
|
|
33
|
+
for (const r of records) {
|
|
34
|
+
const def = registry.get(r.type);
|
|
35
|
+
if (!def) continue;
|
|
36
|
+
let line = def.formatCompactLine(r);
|
|
37
|
+
const why = annotations && r.id ? annotations.get(r.id) : undefined;
|
|
38
|
+
if (why) line += ` — ${why}`;
|
|
39
|
+
lines.push(line);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return lines.join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function formatPrimeOutputCompact(domainSections: string[]): string {
|
|
46
|
+
const lines: string[] = [];
|
|
47
|
+
|
|
48
|
+
lines.push("# Project Expertise (via Loam)");
|
|
49
|
+
lines.push("");
|
|
50
|
+
|
|
51
|
+
if (domainSections.length === 0) {
|
|
52
|
+
lines.push(
|
|
53
|
+
"No expertise recorded yet. Use `lm add <domain>` to create a domain, then `lm record` to add records.",
|
|
54
|
+
);
|
|
55
|
+
} else {
|
|
56
|
+
lines.push(domainSections.join("\n\n"));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
lines.push("");
|
|
60
|
+
lines.push("## Quick Reference");
|
|
61
|
+
lines.push("");
|
|
62
|
+
lines.push('- `lm search "query"` — find relevant records before implementing');
|
|
63
|
+
lines.push(
|
|
64
|
+
"- `lm prime --files src/foo.ts` — prime **before** editing a file, not just at session start",
|
|
65
|
+
);
|
|
66
|
+
lines.push("- `lm prime --context` — load records for git-changed files");
|
|
67
|
+
lines.push('- `lm record <domain> --type <type> --description "..."`');
|
|
68
|
+
lines.push(
|
|
69
|
+
" - Evidence: commit + files auto-populate from git. Trackers: `--evidence-sprout` / `--evidence-gh` / `--evidence-linear` / `--evidence-bead`. Override commit: `--evidence-commit <sha>`. Link records: `--relates-to <mx-id>`",
|
|
70
|
+
);
|
|
71
|
+
lines.push("- `lm doctor` — check record health");
|
|
72
|
+
lines.push("");
|
|
73
|
+
lines.push("**Record types and required flags:**");
|
|
74
|
+
lines.push("");
|
|
75
|
+
lines.push("| Type | Required flags |");
|
|
76
|
+
lines.push("|------|----------------|");
|
|
77
|
+
lines.push('| `convention` | `"<content>"` (positional) |');
|
|
78
|
+
lines.push('| `pattern` | `--name "..." --description "..."` |');
|
|
79
|
+
lines.push('| `failure` | `--description "..." --resolution "..."` |');
|
|
80
|
+
lines.push('| `decision` | `--title "..." --rationale "..."` |');
|
|
81
|
+
lines.push('| `reference` | `--name "..." --description "..."` |');
|
|
82
|
+
lines.push('| `guide` | `--name "..." --description "..."` |');
|
|
83
|
+
|
|
84
|
+
return lines.join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function formatDomainExpertise(
|
|
88
|
+
domain: string,
|
|
89
|
+
records: ExpertiseRecord[],
|
|
90
|
+
lastUpdated: Date | null,
|
|
91
|
+
options: { full?: boolean } = {},
|
|
92
|
+
annotations?: Map<string, string>,
|
|
93
|
+
): string {
|
|
94
|
+
const full = options.full ?? false;
|
|
95
|
+
const registry = getRegistry();
|
|
96
|
+
const updatedStr = lastUpdated ? `, updated ${formatTimeAgo(lastUpdated)}` : "";
|
|
97
|
+
const lines: string[] = [];
|
|
98
|
+
|
|
99
|
+
lines.push(`## ${domain} (${records.length} records${updatedStr})`);
|
|
100
|
+
lines.push("");
|
|
101
|
+
|
|
102
|
+
const sections: string[] = [];
|
|
103
|
+
for (const def of registry.enabled()) {
|
|
104
|
+
const subset = records.filter((r) => r.type === def.name);
|
|
105
|
+
const block = def.formatMarkdown(subset, full);
|
|
106
|
+
if (block.length > 0) sections.push(annotateMarkdownBlock(block, annotations));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
lines.push(sections.join("\n\n"));
|
|
110
|
+
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Append a "why surfaced" suffix to each bullet line that carries an id tag.
|
|
115
|
+
// def.formatMarkdown returns a multi-line section ("### Patterns\n- [mx-xxx]
|
|
116
|
+
// ...\n- [mx-yyy] ..."); we scan for `[mx-N]` at line start and append the
|
|
117
|
+
// annotation when present. Bullets without ids (older records) are left
|
|
118
|
+
// untouched.
|
|
119
|
+
const MARKDOWN_ID_TAG_RE = /\[mx-([0-9a-f]+)\]/;
|
|
120
|
+
|
|
121
|
+
function annotateMarkdownBlock(
|
|
122
|
+
block: string,
|
|
123
|
+
annotations: Map<string, string> | undefined,
|
|
124
|
+
): string {
|
|
125
|
+
if (!annotations || annotations.size === 0) return block;
|
|
126
|
+
const lines = block.split("\n");
|
|
127
|
+
for (let i = 0; i < lines.length; i++) {
|
|
128
|
+
const line = lines[i];
|
|
129
|
+
if (line === undefined || !line.startsWith("- ")) continue;
|
|
130
|
+
const m = MARKDOWN_ID_TAG_RE.exec(line);
|
|
131
|
+
if (!m) continue;
|
|
132
|
+
const id = `mx-${m[1]}`;
|
|
133
|
+
const why = annotations.get(id);
|
|
134
|
+
if (why) lines[i] = `${line} — ${why}`;
|
|
135
|
+
}
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function formatPrimeOutput(domainSections: string[]): string {
|
|
140
|
+
const lines: string[] = [];
|
|
141
|
+
|
|
142
|
+
lines.push("# Project Expertise (via Loam)");
|
|
143
|
+
lines.push("");
|
|
144
|
+
lines.push("> **Context Recovery**: Run `lm prime` after compaction, clear, or new session");
|
|
145
|
+
lines.push("");
|
|
146
|
+
lines.push("## Rules");
|
|
147
|
+
lines.push("");
|
|
148
|
+
lines.push(
|
|
149
|
+
"- **Record learnings**: When you discover a pattern, fix a bug, or make a design decision — record it with `lm record`",
|
|
150
|
+
);
|
|
151
|
+
lines.push(
|
|
152
|
+
"- **Check expertise first**: Before implementing, check if relevant expertise exists with `lm search` or `lm prime --context`",
|
|
153
|
+
);
|
|
154
|
+
lines.push(
|
|
155
|
+
"- **Targeted priming**: Use `lm prime --files src/foo.ts` to load only records relevant to specific files",
|
|
156
|
+
);
|
|
157
|
+
lines.push(
|
|
158
|
+
"- **Do NOT** store expertise in code comments, markdown files, or memory tools — use `lm record`",
|
|
159
|
+
);
|
|
160
|
+
lines.push("- Run `lm doctor` if you are unsure whether records are healthy");
|
|
161
|
+
lines.push("");
|
|
162
|
+
|
|
163
|
+
if (domainSections.length === 0) {
|
|
164
|
+
lines.push(
|
|
165
|
+
"No expertise recorded yet. Use `lm add <domain>` to create a domain, then `lm record` to add records.",
|
|
166
|
+
);
|
|
167
|
+
lines.push("");
|
|
168
|
+
} else {
|
|
169
|
+
lines.push(domainSections.join("\n\n"));
|
|
170
|
+
lines.push("");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
lines.push("");
|
|
174
|
+
lines.push("## Recording New Learnings");
|
|
175
|
+
lines.push("");
|
|
176
|
+
lines.push(
|
|
177
|
+
"When you discover a pattern, convention, failure, or make an architectural decision:",
|
|
178
|
+
);
|
|
179
|
+
lines.push("");
|
|
180
|
+
lines.push("```bash");
|
|
181
|
+
lines.push('lm record <domain> --type convention "description"');
|
|
182
|
+
lines.push('lm record <domain> --type failure --description "..." --resolution "..."');
|
|
183
|
+
lines.push('lm record <domain> --type decision --title "..." --rationale "..."');
|
|
184
|
+
lines.push('lm record <domain> --type pattern --name "..." --description "..." --files "..."');
|
|
185
|
+
lines.push('lm record <domain> --type reference --name "..." --description "..." --files "..."');
|
|
186
|
+
lines.push('lm record <domain> --type guide --name "..." --description "..."');
|
|
187
|
+
lines.push("```");
|
|
188
|
+
lines.push("");
|
|
189
|
+
lines.push("**Required fields by type:**");
|
|
190
|
+
lines.push("");
|
|
191
|
+
lines.push("| Type | Required flags |");
|
|
192
|
+
lines.push("|------|----------------|");
|
|
193
|
+
lines.push('| `convention` | `"<content>"` (positional) |');
|
|
194
|
+
lines.push('| `pattern` | `--name "..." --description "..."` |');
|
|
195
|
+
lines.push('| `failure` | `--description "..." --resolution "..."` |');
|
|
196
|
+
lines.push('| `decision` | `--title "..." --rationale "..."` |');
|
|
197
|
+
lines.push('| `reference` | `--name "..." --description "..."` |');
|
|
198
|
+
lines.push('| `guide` | `--name "..." --description "..."` |');
|
|
199
|
+
lines.push("");
|
|
200
|
+
lines.push(
|
|
201
|
+
"**Link evidence** to records. The current commit and changed files auto-populate from git; link trackers or related records explicitly:",
|
|
202
|
+
);
|
|
203
|
+
lines.push("");
|
|
204
|
+
lines.push("```bash");
|
|
205
|
+
lines.push(
|
|
206
|
+
'lm record <domain> --type pattern --name "..." --description "..." --evidence-sprout SEED-123',
|
|
207
|
+
);
|
|
208
|
+
lines.push(
|
|
209
|
+
'lm record <domain> --type decision --title "..." --rationale "..." --evidence-gh 42 --evidence-linear ENG-9',
|
|
210
|
+
);
|
|
211
|
+
lines.push(
|
|
212
|
+
'lm record <domain> --type convention "..." --relates-to mx-abc # link to related records',
|
|
213
|
+
);
|
|
214
|
+
lines.push("```");
|
|
215
|
+
lines.push("");
|
|
216
|
+
lines.push("**Batch record** multiple records at once:");
|
|
217
|
+
lines.push("");
|
|
218
|
+
lines.push("```bash");
|
|
219
|
+
lines.push("lm record <domain> --batch records.json # from file");
|
|
220
|
+
lines.push(
|
|
221
|
+
'echo \'[{"type":"convention","content":"..."}]\' | lm record <domain> --stdin # from stdin',
|
|
222
|
+
);
|
|
223
|
+
lines.push("```");
|
|
224
|
+
lines.push("");
|
|
225
|
+
lines.push("## Searching Expertise");
|
|
226
|
+
lines.push("");
|
|
227
|
+
lines.push(
|
|
228
|
+
"Use `lm search` to find relevant records across all domains. Results are ranked by relevance (BM25):",
|
|
229
|
+
);
|
|
230
|
+
lines.push("");
|
|
231
|
+
lines.push("```bash");
|
|
232
|
+
lines.push('lm search "file locking" # multi-word queries ranked by relevance');
|
|
233
|
+
lines.push('lm search "atomic" --domain cli # limit to a specific domain');
|
|
234
|
+
lines.push('lm search "ESM" --type convention # filter by record type');
|
|
235
|
+
lines.push('lm search "concurrency" --tag safety # filter by tag');
|
|
236
|
+
lines.push("```");
|
|
237
|
+
lines.push("");
|
|
238
|
+
lines.push("Search before implementing — existing expertise may already cover your use case.");
|
|
239
|
+
lines.push("");
|
|
240
|
+
lines.push("## Domain Maintenance");
|
|
241
|
+
lines.push("");
|
|
242
|
+
lines.push("When a domain grows large, compact it to keep expertise focused:");
|
|
243
|
+
lines.push("");
|
|
244
|
+
lines.push("```bash");
|
|
245
|
+
lines.push("lm compact --auto --dry-run # preview what would be merged");
|
|
246
|
+
lines.push("lm compact --auto # merge same-type record groups");
|
|
247
|
+
lines.push("```");
|
|
248
|
+
lines.push("");
|
|
249
|
+
lines.push("Use `lm diff` to review what expertise changed:");
|
|
250
|
+
lines.push("");
|
|
251
|
+
lines.push("```bash");
|
|
252
|
+
lines.push("lm diff HEAD~3 # see record changes over last 3 commits");
|
|
253
|
+
lines.push("```");
|
|
254
|
+
lines.push("");
|
|
255
|
+
lines.push("## Session End");
|
|
256
|
+
lines.push("");
|
|
257
|
+
lines.push("**IMPORTANT**: Before ending your session, record what you learned and sync:");
|
|
258
|
+
lines.push("");
|
|
259
|
+
lines.push("```");
|
|
260
|
+
lines.push("[ ] lm learn # see what files changed — decide what to record");
|
|
261
|
+
lines.push("[ ] lm record ... # record learnings (see above)");
|
|
262
|
+
lines.push("[ ] lm sync # validate, stage, and commit .loam/ changes");
|
|
263
|
+
lines.push("```");
|
|
264
|
+
lines.push("");
|
|
265
|
+
lines.push("Do NOT skip this. Unrecorded learnings are lost for the next session.");
|
|
266
|
+
|
|
267
|
+
return lines.join("\n");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export type PrimeFormat = "markdown" | "compact" | "xml" | "plain";
|
|
271
|
+
|
|
272
|
+
// --- XML format (optimized for Claude) ---
|
|
273
|
+
|
|
274
|
+
export function formatDomainExpertiseXml(
|
|
275
|
+
domain: string,
|
|
276
|
+
records: ExpertiseRecord[],
|
|
277
|
+
lastUpdated: Date | null,
|
|
278
|
+
annotations?: Map<string, string>,
|
|
279
|
+
): string {
|
|
280
|
+
const registry = getRegistry();
|
|
281
|
+
const updatedStr = lastUpdated ? ` updated="${formatTimeAgo(lastUpdated)}"` : "";
|
|
282
|
+
const lines: string[] = [];
|
|
283
|
+
|
|
284
|
+
lines.push(`<domain name="${xmlEscape(domain)}" entries="${records.length}"${updatedStr}>`);
|
|
285
|
+
|
|
286
|
+
for (const r of records) {
|
|
287
|
+
const def = registry.get(r.type);
|
|
288
|
+
if (!def) continue;
|
|
289
|
+
const idAttr = r.id ? ` id="${xmlEscape(r.id)}"` : "";
|
|
290
|
+
const why = annotations && r.id ? annotations.get(r.id) : undefined;
|
|
291
|
+
const whyAttr = why ? ` why="${xmlAttrEscape(why)}"` : "";
|
|
292
|
+
lines.push(` <${r.type}${idAttr} classification="${r.classification}"${whyAttr}>`);
|
|
293
|
+
|
|
294
|
+
for (const inner of def.formatXml(r)) {
|
|
295
|
+
lines.push(inner);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (r.tags && r.tags.length > 0) {
|
|
299
|
+
lines.push(` <tags>${r.tags.map(xmlEscape).join(", ")}</tags>`);
|
|
300
|
+
}
|
|
301
|
+
if (r.relates_to && r.relates_to.length > 0) {
|
|
302
|
+
lines.push(` <relates_to>${r.relates_to.join(", ")}</relates_to>`);
|
|
303
|
+
}
|
|
304
|
+
if (r.supersedes && r.supersedes.length > 0) {
|
|
305
|
+
lines.push(` <supersedes>${r.supersedes.join(", ")}</supersedes>`);
|
|
306
|
+
}
|
|
307
|
+
if (r.outcomes && r.outcomes.length > 0) {
|
|
308
|
+
for (const outcome of r.outcomes) {
|
|
309
|
+
const durationAttr =
|
|
310
|
+
outcome.duration !== undefined ? ` duration="${outcome.duration}"` : "";
|
|
311
|
+
const agentAttr = outcome.agent ? ` agent="${xmlEscape(outcome.agent)}"` : "";
|
|
312
|
+
const testResultsContent = outcome.test_results ? `${xmlEscape(outcome.test_results)}` : "";
|
|
313
|
+
lines.push(
|
|
314
|
+
` <outcome status="${outcome.status}"${durationAttr}${agentAttr}>${testResultsContent}</outcome>`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
lines.push(` </${r.type}>`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
lines.push("</domain>");
|
|
322
|
+
return lines.join("\n");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function formatPrimeOutputXml(domainSections: string[]): string {
|
|
326
|
+
const lines: string[] = [];
|
|
327
|
+
lines.push("<expertise>");
|
|
328
|
+
|
|
329
|
+
if (domainSections.length === 0) {
|
|
330
|
+
lines.push(
|
|
331
|
+
" <empty>No expertise recorded yet. Use lm add and lm record to get started.</empty>",
|
|
332
|
+
);
|
|
333
|
+
} else {
|
|
334
|
+
lines.push(domainSections.join("\n"));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
lines.push("</expertise>");
|
|
338
|
+
return lines.join("\n");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// --- Plain text format (optimized for Codex) ---
|
|
342
|
+
|
|
343
|
+
// Bespoke per-type body kept inline since TypeDefinition (Phase 1) doesn't
|
|
344
|
+
// expose a formatPlain hook. Iteration order comes from registry.enabled() so
|
|
345
|
+
// Phase 2 custom types can be plugged in by extending this switch (or by
|
|
346
|
+
// adding formatPlain to TypeDefinition).
|
|
347
|
+
function plainSection(
|
|
348
|
+
def: { name: string; sectionTitle: string },
|
|
349
|
+
records: ExpertiseRecord[],
|
|
350
|
+
): string[] {
|
|
351
|
+
const out: string[] = [];
|
|
352
|
+
switch (def.name) {
|
|
353
|
+
case "convention": {
|
|
354
|
+
out.push("Conventions:");
|
|
355
|
+
for (const r of records as Array<ExpertiseRecord & { type: "convention" }>) {
|
|
356
|
+
const id = r.id ? `[${r.id}] ` : "";
|
|
357
|
+
out.push(` - ${id}${r.content}${formatLinks(r)}`);
|
|
358
|
+
}
|
|
359
|
+
out.push("");
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
case "pattern": {
|
|
363
|
+
out.push("Patterns:");
|
|
364
|
+
for (const r of records as Array<ExpertiseRecord & { type: "pattern" }>) {
|
|
365
|
+
const id = r.id ? `[${r.id}] ` : "";
|
|
366
|
+
let line = ` - ${id}${r.name}: ${r.description}`;
|
|
367
|
+
if (r.files && r.files.length > 0) {
|
|
368
|
+
line += ` (${r.files.join(", ")})`;
|
|
369
|
+
}
|
|
370
|
+
line += formatLinks(r);
|
|
371
|
+
out.push(line);
|
|
372
|
+
}
|
|
373
|
+
out.push("");
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
case "failure": {
|
|
377
|
+
out.push("Known Failures:");
|
|
378
|
+
for (const r of records as Array<ExpertiseRecord & { type: "failure" }>) {
|
|
379
|
+
const id = r.id ? `[${r.id}] ` : "";
|
|
380
|
+
out.push(` - ${id}${r.description}${formatLinks(r)}`);
|
|
381
|
+
out.push(` Fix: ${r.resolution}`);
|
|
382
|
+
}
|
|
383
|
+
out.push("");
|
|
384
|
+
return out;
|
|
385
|
+
}
|
|
386
|
+
case "decision": {
|
|
387
|
+
out.push("Decisions:");
|
|
388
|
+
for (const r of records as Array<ExpertiseRecord & { type: "decision" }>) {
|
|
389
|
+
const id = r.id ? `[${r.id}] ` : "";
|
|
390
|
+
out.push(` - ${id}${r.title}: ${r.rationale}${formatLinks(r)}`);
|
|
391
|
+
}
|
|
392
|
+
out.push("");
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
395
|
+
case "reference": {
|
|
396
|
+
out.push("References:");
|
|
397
|
+
for (const r of records as Array<ExpertiseRecord & { type: "reference" }>) {
|
|
398
|
+
const id = r.id ? `[${r.id}] ` : "";
|
|
399
|
+
let line = ` - ${id}${r.name}: ${r.description}`;
|
|
400
|
+
if (r.files && r.files.length > 0) {
|
|
401
|
+
line += ` (${r.files.join(", ")})`;
|
|
402
|
+
}
|
|
403
|
+
line += formatLinks(r);
|
|
404
|
+
out.push(line);
|
|
405
|
+
}
|
|
406
|
+
out.push("");
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
409
|
+
case "guide": {
|
|
410
|
+
out.push("Guides:");
|
|
411
|
+
for (const r of records as Array<ExpertiseRecord & { type: "guide" }>) {
|
|
412
|
+
const id = r.id ? `[${r.id}] ` : "";
|
|
413
|
+
out.push(` - ${id}${r.name}: ${r.description}${formatLinks(r)}`);
|
|
414
|
+
}
|
|
415
|
+
out.push("");
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
default:
|
|
419
|
+
return out;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function formatDomainExpertisePlain(
|
|
424
|
+
domain: string,
|
|
425
|
+
records: ExpertiseRecord[],
|
|
426
|
+
lastUpdated: Date | null,
|
|
427
|
+
annotations?: Map<string, string>,
|
|
428
|
+
): string {
|
|
429
|
+
const registry = getRegistry();
|
|
430
|
+
const updatedStr = lastUpdated ? ` (updated ${formatTimeAgo(lastUpdated)})` : "";
|
|
431
|
+
const lines: string[] = [];
|
|
432
|
+
|
|
433
|
+
lines.push(`[${domain}] ${records.length} records${updatedStr}`);
|
|
434
|
+
lines.push("");
|
|
435
|
+
|
|
436
|
+
for (const def of registry.enabled()) {
|
|
437
|
+
const subset = records.filter((r) => r.type === def.name);
|
|
438
|
+
if (subset.length === 0) continue;
|
|
439
|
+
for (const line of plainSection(def, subset)) {
|
|
440
|
+
lines.push(annotatePlainLine(line, annotations));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return lines.join("\n").trimEnd();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Plain-format bullets carry `[mx-N]` after the leading " - " indent. Mirror
|
|
448
|
+
// the markdown post-processor: when an annotation exists for the id, append
|
|
449
|
+
// " (why: ...)" to the bullet so spawned codex agents see the same surface
|
|
450
|
+
// reason that the compact/markdown formats expose.
|
|
451
|
+
const PLAIN_ID_TAG_RE = /^\s+-\s+\[(mx-[0-9a-f]+)\]/;
|
|
452
|
+
|
|
453
|
+
function annotatePlainLine(line: string, annotations: Map<string, string> | undefined): string {
|
|
454
|
+
if (!annotations || annotations.size === 0) return line;
|
|
455
|
+
const m = PLAIN_ID_TAG_RE.exec(line);
|
|
456
|
+
if (!m) return line;
|
|
457
|
+
const id = m[1];
|
|
458
|
+
if (!id) return line;
|
|
459
|
+
const why = annotations.get(id);
|
|
460
|
+
return why ? `${line} (${why})` : line;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Plain format is the spawn-injection contract: clean text suitable for
|
|
464
|
+
// concatenation into another tool's system prompt. No decorative document
|
|
465
|
+
// title, no underlines, no markdown — only the per-domain sections (which
|
|
466
|
+
// already lead with `[domain] N records` metadata). Empty-state stays as a
|
|
467
|
+
// single bare line.
|
|
468
|
+
export function formatPrimeOutputPlain(domainSections: string[]): string {
|
|
469
|
+
if (domainSections.length === 0) {
|
|
470
|
+
return "No expertise recorded yet. Use `lm add <domain>` and `lm record` to get started.";
|
|
471
|
+
}
|
|
472
|
+
return domainSections.join("\n\n");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export interface JsonDomain {
|
|
476
|
+
domain: string;
|
|
477
|
+
entry_count: number;
|
|
478
|
+
records: ExpertiseRecord[];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function formatJsonOutput(domains: JsonDomain[]): string {
|
|
482
|
+
return JSON.stringify({ type: "expertise", domains }, null, 2);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Close-session footer prose. Audit (V1_PLAN §3) found 70-80% of conventions
|
|
486
|
+
// were ritual restatements driven by the prior "you MUST run this checklist"
|
|
487
|
+
// prose; the v0.10.0 reframe to "if you discovered ..." suppresses filler
|
|
488
|
+
// without losing the memory-anchor function of the 🚨 marker (V1_PLAN §5.2
|
|
489
|
+
// keeps it on the prime footer for agents whose context has filled with file
|
|
490
|
+
// edits since session start). Single helper feeds the prime footer
|
|
491
|
+
// (markdown/compact/xml/plain) and the onboard/cursor/codex snippets
|
|
492
|
+
// ("embedded"). Style preset and custom override come from `prime.session_close`
|
|
493
|
+
// in loam.config.yaml.
|
|
494
|
+
export type SessionCloseFormat = PrimeFormat | "embedded";
|
|
495
|
+
|
|
496
|
+
export function getSessionEndReminder(
|
|
497
|
+
format: SessionCloseFormat,
|
|
498
|
+
config?: SessionCloseConfig,
|
|
499
|
+
): string {
|
|
500
|
+
const custom = config?.custom;
|
|
501
|
+
if (typeof custom === "string" && custom.length > 0) {
|
|
502
|
+
return custom;
|
|
503
|
+
}
|
|
504
|
+
const style = config?.style ?? DEFAULT_SESSION_CLOSE_STYLE;
|
|
505
|
+
switch (style) {
|
|
506
|
+
case "none":
|
|
507
|
+
return "";
|
|
508
|
+
case "minimal":
|
|
509
|
+
return renderMinimalSessionClose(format);
|
|
510
|
+
case "directive":
|
|
511
|
+
return renderDirectiveSessionClose(format);
|
|
512
|
+
default:
|
|
513
|
+
return renderConditionalSessionClose(format);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function renderConditionalSessionClose(format: SessionCloseFormat): string {
|
|
518
|
+
switch (format) {
|
|
519
|
+
case "xml":
|
|
520
|
+
return [
|
|
521
|
+
"<session_close>",
|
|
522
|
+
" <instruction>If you discovered insights worth preserving — a new convention, a pattern that worked, a decision made, a failure encountered — record them before closing this session.</instruction>",
|
|
523
|
+
" <commands>",
|
|
524
|
+
" <command>lm learn — see what files changed</command>",
|
|
525
|
+
" <command>lm record <domain> --type <type> --description "..."</command>",
|
|
526
|
+
" <command>lm sync — validate, stage, commit</command>",
|
|
527
|
+
" </commands>",
|
|
528
|
+
" <note>Skip if no insight surfaced. Unrecorded learnings are lost; ritual filler records are also noise.</note>",
|
|
529
|
+
"</session_close>",
|
|
530
|
+
].join("\n");
|
|
531
|
+
case "plain":
|
|
532
|
+
return [
|
|
533
|
+
"=== \u{1F6A8} SESSION CLOSE \u{1F6A8} ===",
|
|
534
|
+
"",
|
|
535
|
+
"If you discovered insights worth preserving — a new convention, a pattern that worked,",
|
|
536
|
+
"a decision made, a failure encountered — record them before closing this session:",
|
|
537
|
+
"",
|
|
538
|
+
" lm learn (see what files changed)",
|
|
539
|
+
" lm record <domain> --type <type> ... (record the insight)",
|
|
540
|
+
" lm sync (validate, stage, commit)",
|
|
541
|
+
"",
|
|
542
|
+
"Skip if no insight surfaced. Unrecorded learnings are lost; ritual filler records are also noise.",
|
|
543
|
+
].join("\n");
|
|
544
|
+
case "embedded":
|
|
545
|
+
return [
|
|
546
|
+
"### Before You Finish",
|
|
547
|
+
"",
|
|
548
|
+
"If you discovered conventions, patterns, decisions, or failures worth preserving during",
|
|
549
|
+
"this session, record them before closing:",
|
|
550
|
+
"",
|
|
551
|
+
"```bash",
|
|
552
|
+
"lm learn # see what files changed",
|
|
553
|
+
'lm record <domain> --type <convention|pattern|failure|decision|reference|guide> --description "..."',
|
|
554
|
+
"lm sync # validate, stage, commit",
|
|
555
|
+
"```",
|
|
556
|
+
"",
|
|
557
|
+
"Skip if no insight surfaced. Unrecorded learnings are lost; ritual filler records are also noise.",
|
|
558
|
+
].join("\n");
|
|
559
|
+
default:
|
|
560
|
+
return [
|
|
561
|
+
"# \u{1F6A8} SESSION CLOSE \u{1F6A8}",
|
|
562
|
+
"",
|
|
563
|
+
"**If you discovered insights worth preserving** — a new convention, a pattern that worked, a decision made, a failure encountered — record them before closing this session:",
|
|
564
|
+
"",
|
|
565
|
+
"```bash",
|
|
566
|
+
"lm learn # see what files changed",
|
|
567
|
+
"lm record <domain> --type <type> ... # record the insight",
|
|
568
|
+
"lm sync # validate, stage, commit",
|
|
569
|
+
"```",
|
|
570
|
+
"",
|
|
571
|
+
"Skip if no insight surfaced. Unrecorded learnings are lost; ritual filler records are also noise.",
|
|
572
|
+
].join("\n");
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// One-line nudge for projects that want presence-only signaling — surfaces
|
|
577
|
+
// the three-step workflow without the type glossary, anti-filler guardrails,
|
|
578
|
+
// or memory-anchor marker. Embedded form drops "Before You Finish" framing
|
|
579
|
+
// because it lives at the top of CLAUDE.md, not at end-of-session.
|
|
580
|
+
function renderMinimalSessionClose(format: SessionCloseFormat): string {
|
|
581
|
+
switch (format) {
|
|
582
|
+
case "xml":
|
|
583
|
+
return [
|
|
584
|
+
"<session_close>",
|
|
585
|
+
" <instruction>Before closing, record any insight worth preserving: `lm learn` → `lm record <domain> --type <type> …` → `lm sync`. Skip if no insight surfaced.</instruction>",
|
|
586
|
+
"</session_close>",
|
|
587
|
+
].join("\n");
|
|
588
|
+
case "plain":
|
|
589
|
+
return "Before closing, record any insight worth preserving: lm learn → lm record <domain> --type <type> … → lm sync. Skip if no insight surfaced.";
|
|
590
|
+
case "embedded":
|
|
591
|
+
return "**Before you finish**: record any insight worth preserving with `lm learn` → `lm record <domain> --type <type> …` → `lm sync`. Skip if no insight surfaced.";
|
|
592
|
+
default:
|
|
593
|
+
return "**Before closing**: record any insight worth preserving — `lm learn` → `lm record <domain> --type <type> …` → `lm sync`. Skip if no insight surfaced.";
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Directive preset: numbered imperative + type glossary + dedup nudge +
|
|
598
|
+
// anti-filler examples. Directiveness comes from imperative voice and
|
|
599
|
+
// concrete negative examples, NOT from "you MUST" ritual prose (the audit
|
|
600
|
+
// data is clear that produces 70-80% filler conventions).
|
|
601
|
+
function renderDirectiveSessionClose(format: SessionCloseFormat): string {
|
|
602
|
+
switch (format) {
|
|
603
|
+
case "xml":
|
|
604
|
+
return [
|
|
605
|
+
"<session_close>",
|
|
606
|
+
" <heading>SESSION CLOSE — record insights before exiting</heading>",
|
|
607
|
+
" <steps>",
|
|
608
|
+
' <step n="1">Run `lm learn` to see changed files and suggested domains.</step>',
|
|
609
|
+
' <step n="2">Run `lm record <domain> --type <type> …` for each insight worth preserving.</step>',
|
|
610
|
+
' <step n="3">Run `lm sync` to validate, stage, and commit the .loam/ changes.</step>',
|
|
611
|
+
" </steps>",
|
|
612
|
+
" <types>",
|
|
613
|
+
' <type name="convention">a norm the project follows (e.g. naming, formatting)</type>',
|
|
614
|
+
' <type name="pattern">how-to: a repeatable approach to a class of problem</type>',
|
|
615
|
+
' <type name="failure">anti-pattern + fix: what broke and how you resolved it</type>',
|
|
616
|
+
' <type name="decision">we picked X over Y, and why</type>',
|
|
617
|
+
' <type name="reference">pointer to an external spec, doc, or codebase landmark</type>',
|
|
618
|
+
' <type name="guide">a walk-through of a multi-step workflow</type>',
|
|
619
|
+
" </types>",
|
|
620
|
+
" <dedup>`--name` upserts merge; re-recording is safe — outcomes accumulate, fields update in place.</dedup>",
|
|
621
|
+
" <do_not_record>",
|
|
622
|
+
" <item>Restatements of CLAUDE.md or prior records</item>",
|
|
623
|
+
' <item>Generic truisms ("use TypeScript", "write tests")</item>',
|
|
624
|
+
' <item>Single-PR-scoped facts ("this PR adds X")</item>',
|
|
625
|
+
" <item>Ritual confirmations with no new information</item>",
|
|
626
|
+
" </do_not_record>",
|
|
627
|
+
" <note>Skip if no insight surfaced. Unrecorded learnings are lost; ritual filler records are also noise.</note>",
|
|
628
|
+
"</session_close>",
|
|
629
|
+
].join("\n");
|
|
630
|
+
case "plain":
|
|
631
|
+
return [
|
|
632
|
+
"=== \u{1F6A8} SESSION CLOSE \u{1F6A8} ===",
|
|
633
|
+
"",
|
|
634
|
+
"Record insights before exiting:",
|
|
635
|
+
"",
|
|
636
|
+
" 1. lm learn (see changed files + suggested domains)",
|
|
637
|
+
" 2. lm record <domain> --type <type> ... (record each insight)",
|
|
638
|
+
" 3. lm sync (validate, stage, commit)",
|
|
639
|
+
"",
|
|
640
|
+
"Types:",
|
|
641
|
+
" - convention : a norm the project follows (naming, formatting, ...)",
|
|
642
|
+
" - pattern : how-to — a repeatable approach to a class of problem",
|
|
643
|
+
" - failure : anti-pattern + fix — what broke and how you resolved it",
|
|
644
|
+
" - decision : we picked X over Y, and why",
|
|
645
|
+
" - reference : pointer to an external spec / doc / codebase landmark",
|
|
646
|
+
" - guide : a walk-through of a multi-step workflow",
|
|
647
|
+
"",
|
|
648
|
+
"Dedup: --name upserts merge; re-recording is safe — outcomes accumulate, fields update in place.",
|
|
649
|
+
"",
|
|
650
|
+
"Do NOT record:",
|
|
651
|
+
" - Restatements of CLAUDE.md or prior records",
|
|
652
|
+
' - Generic truisms ("use TypeScript", "write tests")',
|
|
653
|
+
' - Single-PR-scoped facts ("this PR adds X")',
|
|
654
|
+
" - Ritual confirmations with no new information",
|
|
655
|
+
"",
|
|
656
|
+
"Skip if no insight surfaced. Unrecorded learnings are lost; ritual filler records are also noise.",
|
|
657
|
+
].join("\n");
|
|
658
|
+
case "embedded":
|
|
659
|
+
return [
|
|
660
|
+
"### Before You Finish",
|
|
661
|
+
"",
|
|
662
|
+
"Record insights worth preserving before closing this session:",
|
|
663
|
+
"",
|
|
664
|
+
"1. `lm learn` — see changed files and suggested domains",
|
|
665
|
+
"2. `lm record <domain> --type <type> …` — record each insight",
|
|
666
|
+
"3. `lm sync` — validate, stage, commit",
|
|
667
|
+
"",
|
|
668
|
+
"**Types**:",
|
|
669
|
+
"- `convention` — a norm the project follows (naming, formatting, …)",
|
|
670
|
+
"- `pattern` — how-to: a repeatable approach to a class of problem",
|
|
671
|
+
"- `failure` — anti-pattern + fix: what broke and how you resolved it",
|
|
672
|
+
"- `decision` — we picked X over Y, and why",
|
|
673
|
+
"- `reference` — pointer to an external spec / doc / codebase landmark",
|
|
674
|
+
"- `guide` — a walk-through of a multi-step workflow",
|
|
675
|
+
"",
|
|
676
|
+
"**Dedup**: `--name` upserts merge; re-recording is safe — outcomes accumulate, fields update in place.",
|
|
677
|
+
"",
|
|
678
|
+
'**Do not record**: restatements of CLAUDE.md or prior records, generic truisms ("use TypeScript"), single-PR-scoped facts ("this PR adds X"), ritual confirmations with no new information.',
|
|
679
|
+
"",
|
|
680
|
+
"Skip if no insight surfaced. Unrecorded learnings are lost; ritual filler records are also noise.",
|
|
681
|
+
].join("\n");
|
|
682
|
+
default:
|
|
683
|
+
return [
|
|
684
|
+
"# \u{1F6A8} SESSION CLOSE \u{1F6A8}",
|
|
685
|
+
"",
|
|
686
|
+
"**Record insights before exiting:**",
|
|
687
|
+
"",
|
|
688
|
+
"1. `lm learn` — see changed files and suggested domains",
|
|
689
|
+
"2. `lm record <domain> --type <type> …` — record each insight",
|
|
690
|
+
"3. `lm sync` — validate, stage, commit",
|
|
691
|
+
"",
|
|
692
|
+
"**Types**:",
|
|
693
|
+
"- `convention` — a norm the project follows (naming, formatting, …)",
|
|
694
|
+
"- `pattern` — how-to: a repeatable approach to a class of problem",
|
|
695
|
+
"- `failure` — anti-pattern + fix: what broke and how you resolved it",
|
|
696
|
+
"- `decision` — we picked X over Y, and why",
|
|
697
|
+
"- `reference` — pointer to an external spec / doc / codebase landmark",
|
|
698
|
+
"- `guide` — a walk-through of a multi-step workflow",
|
|
699
|
+
"",
|
|
700
|
+
"**Dedup**: `--name` upserts merge; re-recording is safe — outcomes accumulate, fields update in place.",
|
|
701
|
+
"",
|
|
702
|
+
'**Do not record**: restatements of CLAUDE.md or prior records, generic truisms ("use TypeScript"), single-PR-scoped facts ("this PR adds X"), ritual confirmations with no new information.',
|
|
703
|
+
"",
|
|
704
|
+
"Skip if no insight surfaced. Unrecorded learnings are lost; ritual filler records are also noise.",
|
|
705
|
+
].join("\n");
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
export type { SessionCloseStyleName };
|
|
710
|
+
|
|
711
|
+
export interface StatusDomainStat {
|
|
712
|
+
domain: string;
|
|
713
|
+
count: number;
|
|
714
|
+
lastUpdated: Date | null;
|
|
715
|
+
oldestRecorded?: Date | null;
|
|
716
|
+
newestRecorded?: Date | null;
|
|
717
|
+
rotting?: boolean;
|
|
718
|
+
rottingDays?: number | null;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
export function formatStatusOutput(
|
|
722
|
+
domainStats: StatusDomainStat[],
|
|
723
|
+
governance: { max_entries: number; warn_entries: number; hard_limit: number },
|
|
724
|
+
): string {
|
|
725
|
+
const lines: string[] = [];
|
|
726
|
+
lines.push("Loam Status");
|
|
727
|
+
lines.push("============");
|
|
728
|
+
lines.push("");
|
|
729
|
+
|
|
730
|
+
if (domainStats.length === 0) {
|
|
731
|
+
lines.push("No domains configured. Run `lm add <domain>` to get started.");
|
|
732
|
+
return lines.join("\n");
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
for (const stat of domainStats) {
|
|
736
|
+
const { domain, count, lastUpdated, oldestRecorded, newestRecorded, rotting, rottingDays } =
|
|
737
|
+
stat;
|
|
738
|
+
const updatedStr = lastUpdated ? formatTimeAgo(lastUpdated) : "never";
|
|
739
|
+
let governanceStatus = "";
|
|
740
|
+
if (count >= governance.hard_limit) {
|
|
741
|
+
governanceStatus = " ⚠ OVER HARD LIMIT — must decompose";
|
|
742
|
+
} else if (count >= governance.warn_entries) {
|
|
743
|
+
governanceStatus = " ⚠ consider splitting domain";
|
|
744
|
+
} else if (count >= governance.max_entries) {
|
|
745
|
+
governanceStatus = " — approaching limit";
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
let rangeStr = "";
|
|
749
|
+
if (oldestRecorded && newestRecorded) {
|
|
750
|
+
const oldestAgo = formatTimeAgo(oldestRecorded);
|
|
751
|
+
const newestAgo = formatTimeAgo(newestRecorded);
|
|
752
|
+
rangeStr =
|
|
753
|
+
oldestAgo === newestAgo
|
|
754
|
+
? ` — recorded ${oldestAgo}`
|
|
755
|
+
: ` — recorded ${oldestAgo} → ${newestAgo}`;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
let rottingStr = "";
|
|
759
|
+
if (rotting) {
|
|
760
|
+
rottingStr =
|
|
761
|
+
typeof rottingDays === "number"
|
|
762
|
+
? ` ⚠ ROTTING (no writes in ${rottingDays}d)`
|
|
763
|
+
: " ⚠ ROTTING";
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
lines.push(
|
|
767
|
+
` ${domain}: ${count} records (updated ${updatedStr})${rangeStr}${governanceStatus}${rottingStr}`,
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return lines.join("\n");
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// --- Manifest mode ---
|
|
775
|
+
|
|
776
|
+
export interface ManifestDomain {
|
|
777
|
+
domain: string;
|
|
778
|
+
count: number;
|
|
779
|
+
lastUpdated: Date | null;
|
|
780
|
+
typeCounts: Record<string, number>;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
export interface ManifestGovernance {
|
|
784
|
+
max_entries: number;
|
|
785
|
+
warn_entries: number;
|
|
786
|
+
hard_limit: number;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
interface QuickRef {
|
|
790
|
+
command: string;
|
|
791
|
+
description: string;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const MANIFEST_QUICK_REF: QuickRef[] = [
|
|
795
|
+
{ command: "lm prime <domain>", description: "load full records for one domain" },
|
|
796
|
+
{ command: "lm prime --files <path>", description: "load records relevant to specific files" },
|
|
797
|
+
{ command: 'lm search "<query>"', description: "search records across domains" },
|
|
798
|
+
{
|
|
799
|
+
command: 'lm record <domain> --type <type> --description "..."',
|
|
800
|
+
description: "store an insight",
|
|
801
|
+
},
|
|
802
|
+
{ command: "lm learn", description: "discover what to record from changed files" },
|
|
803
|
+
{ command: "lm sync", description: "validate, stage, and commit .loam/ changes" },
|
|
804
|
+
];
|
|
805
|
+
|
|
806
|
+
function manifestStatusSuffix(count: number, governance: ManifestGovernance): string {
|
|
807
|
+
if (count >= governance.hard_limit) return " ⚠ OVER HARD LIMIT — must decompose";
|
|
808
|
+
if (count >= governance.warn_entries) return " ⚠ consider splitting domain";
|
|
809
|
+
if (count >= governance.max_entries) return " — approaching limit";
|
|
810
|
+
return "";
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const TYPE_COUNT_ORDER: BuiltinRecordType[] = [
|
|
814
|
+
"pattern",
|
|
815
|
+
"convention",
|
|
816
|
+
"failure",
|
|
817
|
+
"decision",
|
|
818
|
+
"reference",
|
|
819
|
+
"guide",
|
|
820
|
+
];
|
|
821
|
+
|
|
822
|
+
function pluralize(n: number, singular: string): string {
|
|
823
|
+
return n === 1 ? singular : `${singular}s`;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function formatTypeCounts(typeCounts: Record<string, number>): string {
|
|
827
|
+
const parts: string[] = [];
|
|
828
|
+
const seen = new Set<string>();
|
|
829
|
+
// Built-ins in canonical order first, then any custom-type counts (Phase 2).
|
|
830
|
+
for (const t of TYPE_COUNT_ORDER) {
|
|
831
|
+
const n = typeCounts[t];
|
|
832
|
+
if (n && n > 0) parts.push(`${n} ${pluralize(n, t)}`);
|
|
833
|
+
seen.add(t);
|
|
834
|
+
}
|
|
835
|
+
for (const [t, n] of Object.entries(typeCounts)) {
|
|
836
|
+
if (seen.has(t)) continue;
|
|
837
|
+
if (n && n > 0) parts.push(`${n} ${pluralize(n, t)}`);
|
|
838
|
+
}
|
|
839
|
+
return parts.join(", ");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export function formatPrimeManifest(
|
|
843
|
+
domains: ManifestDomain[],
|
|
844
|
+
governance: ManifestGovernance,
|
|
845
|
+
format: PrimeFormat,
|
|
846
|
+
): string {
|
|
847
|
+
switch (format) {
|
|
848
|
+
case "xml":
|
|
849
|
+
return formatPrimeManifestXml(domains, governance);
|
|
850
|
+
case "plain":
|
|
851
|
+
return formatPrimeManifestPlain(domains, governance);
|
|
852
|
+
default:
|
|
853
|
+
return formatPrimeManifestMarkdown(domains, governance);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function formatPrimeManifestMarkdown(
|
|
858
|
+
domains: ManifestDomain[],
|
|
859
|
+
governance: ManifestGovernance,
|
|
860
|
+
): string {
|
|
861
|
+
const lines: string[] = [];
|
|
862
|
+
lines.push("# Project Expertise Manifest (via Loam)");
|
|
863
|
+
lines.push("");
|
|
864
|
+
lines.push(
|
|
865
|
+
"> Manifest mode lists available domains. Load records on demand with `lm prime <domain>` or `lm prime --files <path>`.",
|
|
866
|
+
);
|
|
867
|
+
lines.push("");
|
|
868
|
+
lines.push("## Quick Reference");
|
|
869
|
+
lines.push("");
|
|
870
|
+
for (const { command, description } of MANIFEST_QUICK_REF) {
|
|
871
|
+
lines.push(`- \`${command}\` — ${description}`);
|
|
872
|
+
}
|
|
873
|
+
lines.push("");
|
|
874
|
+
lines.push("## Available Domains");
|
|
875
|
+
lines.push("");
|
|
876
|
+
if (domains.length === 0) {
|
|
877
|
+
lines.push(
|
|
878
|
+
"No expertise recorded yet. Use `lm add <domain>` to create a domain, then `lm record` to add records.",
|
|
879
|
+
);
|
|
880
|
+
} else {
|
|
881
|
+
for (const { domain, count, lastUpdated, typeCounts } of domains) {
|
|
882
|
+
const typeStr = formatTypeCounts(typeCounts);
|
|
883
|
+
const typeSuffix = typeStr ? ` (${typeStr})` : "";
|
|
884
|
+
const updatedStr = lastUpdated ? ` — updated ${formatTimeAgo(lastUpdated)}` : "";
|
|
885
|
+
const status = manifestStatusSuffix(count, governance);
|
|
886
|
+
lines.push(
|
|
887
|
+
`- **${domain}**: ${count} ${pluralize(count, "record")}${typeSuffix}${updatedStr}${status}`,
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return lines.join("\n");
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function formatPrimeManifestPlain(
|
|
895
|
+
domains: ManifestDomain[],
|
|
896
|
+
governance: ManifestGovernance,
|
|
897
|
+
): string {
|
|
898
|
+
const lines: string[] = [];
|
|
899
|
+
lines.push("Project Expertise Manifest (via Loam)");
|
|
900
|
+
lines.push("======================================");
|
|
901
|
+
lines.push("");
|
|
902
|
+
lines.push(
|
|
903
|
+
"Manifest mode lists available domains. Load records on demand with `lm prime <domain>` or `lm prime --files <path>`.",
|
|
904
|
+
);
|
|
905
|
+
lines.push("");
|
|
906
|
+
lines.push("Quick Reference:");
|
|
907
|
+
for (const { command, description } of MANIFEST_QUICK_REF) {
|
|
908
|
+
lines.push(` - ${command} — ${description}`);
|
|
909
|
+
}
|
|
910
|
+
lines.push("");
|
|
911
|
+
lines.push("Available Domains:");
|
|
912
|
+
if (domains.length === 0) {
|
|
913
|
+
lines.push(
|
|
914
|
+
" No expertise recorded yet. Use `lm add <domain>` and `lm record` to get started.",
|
|
915
|
+
);
|
|
916
|
+
} else {
|
|
917
|
+
for (const { domain, count, lastUpdated, typeCounts } of domains) {
|
|
918
|
+
const typeStr = formatTypeCounts(typeCounts);
|
|
919
|
+
const typeSuffix = typeStr ? ` (${typeStr})` : "";
|
|
920
|
+
const updatedStr = lastUpdated ? ` — updated ${formatTimeAgo(lastUpdated)}` : "";
|
|
921
|
+
const status = manifestStatusSuffix(count, governance);
|
|
922
|
+
lines.push(
|
|
923
|
+
` - ${domain}: ${count} ${pluralize(count, "record")}${typeSuffix}${updatedStr}${status}`,
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return lines.join("\n");
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function formatPrimeManifestXml(domains: ManifestDomain[], governance: ManifestGovernance): string {
|
|
931
|
+
const lines: string[] = [];
|
|
932
|
+
lines.push("<manifest>");
|
|
933
|
+
lines.push(
|
|
934
|
+
" <description>Manifest mode lists available domains. Load records on demand with `lm prime <domain>` or `lm prime --files <path>`.</description>",
|
|
935
|
+
);
|
|
936
|
+
lines.push(" <quick_reference>");
|
|
937
|
+
for (const { command, description } of MANIFEST_QUICK_REF) {
|
|
938
|
+
lines.push(` <command name="${xmlAttrEscape(command)}">${xmlEscape(description)}</command>`);
|
|
939
|
+
}
|
|
940
|
+
lines.push(" </quick_reference>");
|
|
941
|
+
lines.push(" <domains>");
|
|
942
|
+
for (const { domain, count, lastUpdated, typeCounts } of domains) {
|
|
943
|
+
const updatedAttr = lastUpdated ? ` updated="${formatTimeAgo(lastUpdated)}"` : "";
|
|
944
|
+
const status = manifestStatusSuffix(count, governance).trim();
|
|
945
|
+
const statusAttr = status ? ` status="${xmlAttrEscape(status)}"` : "";
|
|
946
|
+
lines.push(
|
|
947
|
+
` <domain name="${xmlAttrEscape(domain)}" entries="${count}"${updatedAttr}${statusAttr}>`,
|
|
948
|
+
);
|
|
949
|
+
for (const t of TYPE_COUNT_ORDER) {
|
|
950
|
+
const n = typeCounts[t];
|
|
951
|
+
if (n && n > 0) lines.push(` <type_count type="${t}" count="${n}" />`);
|
|
952
|
+
}
|
|
953
|
+
lines.push(" </domain>");
|
|
954
|
+
}
|
|
955
|
+
lines.push(" </domains>");
|
|
956
|
+
lines.push("</manifest>");
|
|
957
|
+
return lines.join("\n");
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
export interface ManifestPayload {
|
|
961
|
+
type: "manifest";
|
|
962
|
+
quick_reference: QuickRef[];
|
|
963
|
+
domains: Array<{
|
|
964
|
+
domain: string;
|
|
965
|
+
count: number;
|
|
966
|
+
lastUpdated: string | null;
|
|
967
|
+
type_counts: Record<string, number>;
|
|
968
|
+
health: {
|
|
969
|
+
status: "ok" | "approaching_limit" | "over_warn_threshold" | "over_hard_limit";
|
|
970
|
+
max_entries: number;
|
|
971
|
+
warn_entries: number;
|
|
972
|
+
hard_limit: number;
|
|
973
|
+
};
|
|
974
|
+
}>;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function manifestHealthStatus(
|
|
978
|
+
count: number,
|
|
979
|
+
governance: ManifestGovernance,
|
|
980
|
+
): ManifestPayload["domains"][number]["health"]["status"] {
|
|
981
|
+
if (count >= governance.hard_limit) return "over_hard_limit";
|
|
982
|
+
if (count >= governance.warn_entries) return "over_warn_threshold";
|
|
983
|
+
if (count >= governance.max_entries) return "approaching_limit";
|
|
984
|
+
return "ok";
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
export function buildManifestPayload(
|
|
988
|
+
domains: ManifestDomain[],
|
|
989
|
+
governance: ManifestGovernance,
|
|
990
|
+
): ManifestPayload {
|
|
991
|
+
return {
|
|
992
|
+
type: "manifest",
|
|
993
|
+
quick_reference: MANIFEST_QUICK_REF,
|
|
994
|
+
domains: domains.map(({ domain, count, lastUpdated, typeCounts }) => ({
|
|
995
|
+
domain,
|
|
996
|
+
count,
|
|
997
|
+
lastUpdated: lastUpdated ? lastUpdated.toISOString() : null,
|
|
998
|
+
type_counts: typeCounts,
|
|
999
|
+
health: {
|
|
1000
|
+
status: manifestHealthStatus(count, governance),
|
|
1001
|
+
max_entries: governance.max_entries,
|
|
1002
|
+
warn_entries: governance.warn_entries,
|
|
1003
|
+
hard_limit: governance.hard_limit,
|
|
1004
|
+
},
|
|
1005
|
+
})),
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
export function computeTypeCounts(records: ExpertiseRecord[]): Record<string, number> {
|
|
1010
|
+
const counts: Record<string, number> = {};
|
|
1011
|
+
for (const r of records) {
|
|
1012
|
+
counts[r.type] = (counts[r.type] ?? 0) + 1;
|
|
1013
|
+
}
|
|
1014
|
+
return counts;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// --- Project Contract block (write-side gates from config) ---
|
|
1018
|
+
//
|
|
1019
|
+
// Slice 1 of the v0.10 prime overhaul leads with this so agents see the
|
|
1020
|
+
// project's write-side rules (`lm record` will enforce them) before any
|
|
1021
|
+
// record content. Surfaces custom types, disabled types, per-domain
|
|
1022
|
+
// allowed_types / required_fields, and active hooks. Returns null when the
|
|
1023
|
+
// project has no contract content worth surfacing — keeps minimal configs
|
|
1024
|
+
// clutter-free.
|
|
1025
|
+
|
|
1026
|
+
export interface ContractDomainEntry {
|
|
1027
|
+
domain: string;
|
|
1028
|
+
allowedTypes: string[];
|
|
1029
|
+
requiredFields: string[];
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
export interface ContractCustomType {
|
|
1033
|
+
name: string;
|
|
1034
|
+
extends: string | null;
|
|
1035
|
+
required: string[];
|
|
1036
|
+
optional: string[];
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
export interface ProjectContract {
|
|
1040
|
+
customTypes: ContractCustomType[];
|
|
1041
|
+
disabledTypes: string[];
|
|
1042
|
+
domains: ContractDomainEntry[];
|
|
1043
|
+
hooks: HookEvent[];
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
export function buildProjectContract(config: LoamConfig): ProjectContract {
|
|
1047
|
+
const registry = getRegistry();
|
|
1048
|
+
const customTypes: ContractCustomType[] = registry.customDefs().map((def) => ({
|
|
1049
|
+
name: def.name,
|
|
1050
|
+
extends: config.custom_types?.[def.name]?.extends ?? null,
|
|
1051
|
+
required: [...def.required],
|
|
1052
|
+
optional: [...def.optional],
|
|
1053
|
+
}));
|
|
1054
|
+
const disabledTypes = [...(config.disabled_types ?? [])];
|
|
1055
|
+
const domains: ContractDomainEntry[] = [];
|
|
1056
|
+
for (const [name, dconf] of Object.entries(config.domains ?? {})) {
|
|
1057
|
+
const allowed = dconf?.allowed_types ?? [];
|
|
1058
|
+
const required = dconf?.required_fields ?? [];
|
|
1059
|
+
if (allowed.length > 0 || required.length > 0) {
|
|
1060
|
+
domains.push({
|
|
1061
|
+
domain: name,
|
|
1062
|
+
allowedTypes: [...allowed],
|
|
1063
|
+
requiredFields: [...required],
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
const hooks: HookEvent[] = [];
|
|
1068
|
+
for (const event of HOOK_EVENTS) {
|
|
1069
|
+
const scripts = config.hooks?.[event] ?? [];
|
|
1070
|
+
if (scripts.length > 0) hooks.push(event);
|
|
1071
|
+
}
|
|
1072
|
+
return { customTypes, disabledTypes, domains, hooks };
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
export function hasContractContent(c: ProjectContract): boolean {
|
|
1076
|
+
return (
|
|
1077
|
+
c.customTypes.length > 0 ||
|
|
1078
|
+
c.disabledTypes.length > 0 ||
|
|
1079
|
+
c.domains.length > 0 ||
|
|
1080
|
+
c.hooks.length > 0
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function customTypeFieldsSuffix(t: ContractCustomType): string {
|
|
1085
|
+
const parts: string[] = [];
|
|
1086
|
+
if (t.required.length > 0) parts.push(`required: ${t.required.join(", ")}`);
|
|
1087
|
+
if (t.optional.length > 0) parts.push(`optional: ${t.optional.join(", ")}`);
|
|
1088
|
+
return parts.length > 0 ? `; ${parts.join("; ")}` : "";
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function formatProjectContractMarkdown(c: ProjectContract): string {
|
|
1092
|
+
const lines: string[] = [];
|
|
1093
|
+
lines.push("## Project Contract");
|
|
1094
|
+
lines.push("");
|
|
1095
|
+
lines.push("Write-side gates `lm record` enforces in this project.");
|
|
1096
|
+
lines.push("");
|
|
1097
|
+
|
|
1098
|
+
if (c.customTypes.length > 0) {
|
|
1099
|
+
lines.push("**Custom types**:");
|
|
1100
|
+
for (const t of c.customTypes) {
|
|
1101
|
+
const ext = t.extends ? ` (extends \`${t.extends}\`)` : "";
|
|
1102
|
+
lines.push(`- \`${t.name}\`${ext}${customTypeFieldsSuffix(t)}`);
|
|
1103
|
+
}
|
|
1104
|
+
lines.push("");
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (c.disabledTypes.length > 0) {
|
|
1108
|
+
const names = c.disabledTypes.map((t) => `\`${t}\``).join(", ");
|
|
1109
|
+
lines.push(`**Disabled types**: ${names} (writes emit a deprecation warning)`);
|
|
1110
|
+
lines.push("");
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (c.domains.length > 0) {
|
|
1114
|
+
lines.push("**Per-domain rules**:");
|
|
1115
|
+
for (const d of c.domains) {
|
|
1116
|
+
const parts: string[] = [];
|
|
1117
|
+
if (d.allowedTypes.length > 0) parts.push(`allowed types — ${d.allowedTypes.join(", ")}`);
|
|
1118
|
+
if (d.requiredFields.length > 0)
|
|
1119
|
+
parts.push(`required fields — ${d.requiredFields.join(", ")}`);
|
|
1120
|
+
lines.push(`- \`${d.domain}\`: ${parts.join("; ")}`);
|
|
1121
|
+
}
|
|
1122
|
+
lines.push("");
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
if (c.hooks.length > 0) {
|
|
1126
|
+
lines.push(`**Active hooks**: ${c.hooks.join(", ")}`);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
return lines.join("\n").trimEnd();
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function formatProjectContractPlain(c: ProjectContract): string {
|
|
1133
|
+
const lines: string[] = [];
|
|
1134
|
+
lines.push("Project Contract (write-side gates)");
|
|
1135
|
+
lines.push("===================================");
|
|
1136
|
+
|
|
1137
|
+
if (c.customTypes.length > 0) {
|
|
1138
|
+
lines.push("");
|
|
1139
|
+
lines.push("Custom types:");
|
|
1140
|
+
for (const t of c.customTypes) {
|
|
1141
|
+
const ext = t.extends ? ` (extends ${t.extends})` : "";
|
|
1142
|
+
lines.push(` - ${t.name}${ext}${customTypeFieldsSuffix(t)}`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (c.disabledTypes.length > 0) {
|
|
1147
|
+
lines.push("");
|
|
1148
|
+
lines.push(`Disabled types: ${c.disabledTypes.join(", ")} (writes emit a deprecation warning)`);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
if (c.domains.length > 0) {
|
|
1152
|
+
lines.push("");
|
|
1153
|
+
lines.push("Per-domain rules:");
|
|
1154
|
+
for (const d of c.domains) {
|
|
1155
|
+
const parts: string[] = [];
|
|
1156
|
+
if (d.allowedTypes.length > 0) parts.push(`allowed: ${d.allowedTypes.join(", ")}`);
|
|
1157
|
+
if (d.requiredFields.length > 0) parts.push(`required: ${d.requiredFields.join(", ")}`);
|
|
1158
|
+
lines.push(` - ${d.domain}: ${parts.join("; ")}`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
if (c.hooks.length > 0) {
|
|
1163
|
+
lines.push("");
|
|
1164
|
+
lines.push(`Active hooks: ${c.hooks.join(", ")}`);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
return lines.join("\n");
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function formatProjectContractXml(c: ProjectContract): string {
|
|
1171
|
+
const lines: string[] = [];
|
|
1172
|
+
lines.push("<contract>");
|
|
1173
|
+
if (c.customTypes.length > 0) {
|
|
1174
|
+
lines.push(" <custom_types>");
|
|
1175
|
+
for (const t of c.customTypes) {
|
|
1176
|
+
const extAttr = t.extends ? ` extends="${xmlAttrEscape(t.extends)}"` : "";
|
|
1177
|
+
lines.push(` <type name="${xmlAttrEscape(t.name)}"${extAttr}>`);
|
|
1178
|
+
if (t.required.length > 0)
|
|
1179
|
+
lines.push(` <required>${xmlEscape(t.required.join(", "))}</required>`);
|
|
1180
|
+
if (t.optional.length > 0)
|
|
1181
|
+
lines.push(` <optional>${xmlEscape(t.optional.join(", "))}</optional>`);
|
|
1182
|
+
lines.push(" </type>");
|
|
1183
|
+
}
|
|
1184
|
+
lines.push(" </custom_types>");
|
|
1185
|
+
}
|
|
1186
|
+
if (c.disabledTypes.length > 0) {
|
|
1187
|
+
lines.push(` <disabled_types>${xmlEscape(c.disabledTypes.join(", "))}</disabled_types>`);
|
|
1188
|
+
}
|
|
1189
|
+
if (c.domains.length > 0) {
|
|
1190
|
+
lines.push(" <domains>");
|
|
1191
|
+
for (const d of c.domains) {
|
|
1192
|
+
const allowedAttr =
|
|
1193
|
+
d.allowedTypes.length > 0 ? ` allowed="${xmlAttrEscape(d.allowedTypes.join(", "))}"` : "";
|
|
1194
|
+
const reqAttr =
|
|
1195
|
+
d.requiredFields.length > 0
|
|
1196
|
+
? ` required="${xmlAttrEscape(d.requiredFields.join(", "))}"`
|
|
1197
|
+
: "";
|
|
1198
|
+
lines.push(` <domain name="${xmlAttrEscape(d.domain)}"${allowedAttr}${reqAttr} />`);
|
|
1199
|
+
}
|
|
1200
|
+
lines.push(" </domains>");
|
|
1201
|
+
}
|
|
1202
|
+
if (c.hooks.length > 0) {
|
|
1203
|
+
lines.push(` <hooks>${xmlEscape(c.hooks.join(", "))}</hooks>`);
|
|
1204
|
+
}
|
|
1205
|
+
lines.push("</contract>");
|
|
1206
|
+
return lines.join("\n");
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
export function formatProjectContract(config: LoamConfig, format: PrimeFormat): string | null {
|
|
1210
|
+
const contract = buildProjectContract(config);
|
|
1211
|
+
if (!hasContractContent(contract)) return null;
|
|
1212
|
+
switch (format) {
|
|
1213
|
+
case "xml":
|
|
1214
|
+
return formatProjectContractXml(contract);
|
|
1215
|
+
case "plain":
|
|
1216
|
+
return formatProjectContractPlain(contract);
|
|
1217
|
+
default:
|
|
1218
|
+
return formatProjectContractMarkdown(contract);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Auto-flip thresholds for the `lm prime` default mode. When the project has
|
|
1223
|
+
// not declared `prime.default_mode` and the invocation isn't scoped, prime
|
|
1224
|
+
// flips to manifest output above either threshold so unscoped output doesn't
|
|
1225
|
+
// blow the context window. Strict greater-than: 100 records is full, 101 is
|
|
1226
|
+
// manifest; 5 domains is full, 6 is manifest.
|
|
1227
|
+
export const AUTO_MANIFEST_RECORD_THRESHOLD = 100;
|
|
1228
|
+
export const AUTO_MANIFEST_DOMAIN_THRESHOLD = 5;
|
|
1229
|
+
|
|
1230
|
+
export function shouldAutoFlipToManifest(totalRecords: number, totalDomains: number): boolean {
|
|
1231
|
+
return (
|
|
1232
|
+
totalRecords > AUTO_MANIFEST_RECORD_THRESHOLD || totalDomains > AUTO_MANIFEST_DOMAIN_THRESHOLD
|
|
1233
|
+
);
|
|
1234
|
+
}
|