@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,50 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function getCurrentCommit(cwd?: string): string | undefined {
|
|
4
|
+
try {
|
|
5
|
+
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
6
|
+
cwd: cwd ?? process.cwd(),
|
|
7
|
+
encoding: "utf-8",
|
|
8
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
9
|
+
}).trim();
|
|
10
|
+
} catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getContextFiles(cwd?: string): string[] {
|
|
16
|
+
const dir = cwd ?? process.cwd();
|
|
17
|
+
const files = new Set<string>();
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const staged = execFileSync("git", ["diff", "--name-only", "--cached"], {
|
|
21
|
+
cwd: dir,
|
|
22
|
+
encoding: "utf-8",
|
|
23
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
24
|
+
}).trim();
|
|
25
|
+
if (staged) {
|
|
26
|
+
for (const f of staged.split("\n")) {
|
|
27
|
+
if (f) files.add(f);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// not a git repo or no staged changes
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const unstaged = execFileSync("git", ["diff", "--name-only"], {
|
|
36
|
+
cwd: dir,
|
|
37
|
+
encoding: "utf-8",
|
|
38
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
39
|
+
}).trim();
|
|
40
|
+
if (unstaged) {
|
|
41
|
+
for (const f of unstaged.split("\n")) {
|
|
42
|
+
if (f) files.add(f);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// ignore
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return [...files].sort();
|
|
50
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import type { ExpertiseRecord } from "../schemas/record.ts";
|
|
3
|
+
import type { TrackerName } from "./active-work.ts";
|
|
4
|
+
import { fileLivesUnderDir } from "./dir-anchors.ts";
|
|
5
|
+
|
|
6
|
+
export function isGitRepo(cwd: string): boolean {
|
|
7
|
+
try {
|
|
8
|
+
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
9
|
+
cwd,
|
|
10
|
+
stdio: "pipe",
|
|
11
|
+
});
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getChangedFiles(cwd: string, since: string): string[] {
|
|
19
|
+
const files = new Set<string>();
|
|
20
|
+
|
|
21
|
+
// Committed changes (since ref)
|
|
22
|
+
try {
|
|
23
|
+
const committed = execFileSync("git", ["diff", "--name-only", since], {
|
|
24
|
+
cwd,
|
|
25
|
+
encoding: "utf-8",
|
|
26
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
27
|
+
}).trim();
|
|
28
|
+
if (committed) {
|
|
29
|
+
for (const f of committed.split("\n")) {
|
|
30
|
+
if (f) files.add(f);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// ref might not exist (e.g., first commit) — fall through
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Staged but uncommitted changes
|
|
38
|
+
try {
|
|
39
|
+
const staged = execFileSync("git", ["diff", "--name-only", "--cached"], {
|
|
40
|
+
cwd,
|
|
41
|
+
encoding: "utf-8",
|
|
42
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
43
|
+
}).trim();
|
|
44
|
+
if (staged) {
|
|
45
|
+
for (const f of staged.split("\n")) {
|
|
46
|
+
if (f) files.add(f);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// ignore
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Unstaged working tree changes
|
|
54
|
+
try {
|
|
55
|
+
const unstaged = execFileSync("git", ["diff", "--name-only"], {
|
|
56
|
+
cwd,
|
|
57
|
+
encoding: "utf-8",
|
|
58
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
59
|
+
}).trim();
|
|
60
|
+
if (unstaged) {
|
|
61
|
+
for (const f of unstaged.split("\n")) {
|
|
62
|
+
if (f) files.add(f);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// ignore
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return [...files].sort();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Files reported by `git status --porcelain` — staged, unstaged, and untracked
|
|
73
|
+
// (excluding ignored). Includes rename destinations rather than sources. Used
|
|
74
|
+
// by prime's auto-context-scope (slice 2) which mirrors V1_PLAN's "what the
|
|
75
|
+
// agent is about to work on" framing rather than `git diff HEAD~1`'s "what
|
|
76
|
+
// recently shipped."
|
|
77
|
+
export function getActiveFiles(cwd: string): string[] {
|
|
78
|
+
const files = new Set<string>();
|
|
79
|
+
try {
|
|
80
|
+
// `-uall` lists every untracked file individually instead of collapsing
|
|
81
|
+
// directories to a single `??` entry — the prime context-scope needs
|
|
82
|
+
// per-file granularity to match record `files`/`dir_anchors`.
|
|
83
|
+
const out = execFileSync("git", ["status", "--porcelain", "-uall"], {
|
|
84
|
+
cwd,
|
|
85
|
+
encoding: "utf-8",
|
|
86
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
87
|
+
});
|
|
88
|
+
for (const raw of out.split("\n")) {
|
|
89
|
+
if (!raw) continue;
|
|
90
|
+
// Porcelain v1 lines are `XY <path>` where XY is two status chars.
|
|
91
|
+
// Renames are `R old -> new`; we keep the destination only.
|
|
92
|
+
const path = raw.slice(3);
|
|
93
|
+
const arrow = path.indexOf(" -> ");
|
|
94
|
+
if (arrow >= 0) {
|
|
95
|
+
files.add(path.slice(arrow + 4));
|
|
96
|
+
} else if (path) {
|
|
97
|
+
files.add(path);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// not a repo, git missing, or status failed — caller treats empty as
|
|
102
|
+
// "no signal" and falls back to unscoped output.
|
|
103
|
+
}
|
|
104
|
+
return [...files].sort();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function fileMatchesAny(file: string, changedFiles: string[]): boolean {
|
|
108
|
+
return changedFiles.some(
|
|
109
|
+
(changed) => changed === file || changed.endsWith(file) || file.endsWith(changed),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function filterByContext(
|
|
114
|
+
records: ExpertiseRecord[],
|
|
115
|
+
changedFiles: string[],
|
|
116
|
+
): ExpertiseRecord[] {
|
|
117
|
+
return records.filter((r) => {
|
|
118
|
+
const hasFiles = "files" in r && Array.isArray(r.files) && r.files.length > 0;
|
|
119
|
+
const hasDirAnchors = Array.isArray(r.dir_anchors) && r.dir_anchors.length > 0;
|
|
120
|
+
|
|
121
|
+
// No anchors at all → always relevant (conventions, decisions, failures,
|
|
122
|
+
// guides; or named records with no scoping declared).
|
|
123
|
+
if (!hasFiles && !hasDirAnchors) return true;
|
|
124
|
+
|
|
125
|
+
if (hasFiles && r.files !== undefined && r.files.some((f) => fileMatchesAny(f, changedFiles))) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
if (hasDirAnchors && r.dir_anchors !== undefined) {
|
|
129
|
+
for (const dir of r.dir_anchors) {
|
|
130
|
+
if (changedFiles.some((cf) => fileLivesUnderDir(cf, dir))) return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export type ActiveTrackers = Partial<Record<TrackerName, string>>;
|
|
138
|
+
|
|
139
|
+
export interface ActiveContext {
|
|
140
|
+
changedFiles: string[];
|
|
141
|
+
trackers: ActiveTrackers;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function activeContextHasSignal(ctx: ActiveContext): boolean {
|
|
145
|
+
if (ctx.changedFiles.length > 0) return true;
|
|
146
|
+
for (const v of Object.values(ctx.trackers)) {
|
|
147
|
+
if (typeof v === "string" && v !== "") return true;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Slice-2 auto-scope: union of filterByContext + evidence-tracker match. A
|
|
153
|
+
// record with no file/dir anchors AND no tracker anchors is treated as
|
|
154
|
+
// universal (applies everywhere); anchored records require a match on at
|
|
155
|
+
// least one signal.
|
|
156
|
+
export function filterByActiveContext(
|
|
157
|
+
records: ExpertiseRecord[],
|
|
158
|
+
ctx: ActiveContext,
|
|
159
|
+
): ExpertiseRecord[] {
|
|
160
|
+
const { changedFiles, trackers } = ctx;
|
|
161
|
+
return records.filter((r) => {
|
|
162
|
+
const hasFiles = "files" in r && Array.isArray(r.files) && r.files.length > 0;
|
|
163
|
+
const hasDirAnchors = Array.isArray(r.dir_anchors) && r.dir_anchors.length > 0;
|
|
164
|
+
const ev = r.evidence;
|
|
165
|
+
const sproutMatch = !!(ev?.sprout && trackers.sprout && ev.sprout === trackers.sprout);
|
|
166
|
+
const ghMatch = !!(ev?.gh && trackers.gh && ev.gh === trackers.gh);
|
|
167
|
+
const linearMatch = !!(ev?.linear && trackers.linear && ev.linear === trackers.linear);
|
|
168
|
+
const beadMatch = !!(ev?.bead && trackers.bead && ev.bead === trackers.bead);
|
|
169
|
+
if (sproutMatch || ghMatch || linearMatch || beadMatch) return true;
|
|
170
|
+
|
|
171
|
+
if (!hasFiles && !hasDirAnchors) return true;
|
|
172
|
+
|
|
173
|
+
if (hasFiles && r.files !== undefined && r.files.some((f) => fileMatchesAny(f, changedFiles))) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
if (hasDirAnchors && r.dir_anchors !== undefined) {
|
|
177
|
+
for (const dir of r.dir_anchors) {
|
|
178
|
+
if (changedFiles.some((cf) => fileLivesUnderDir(cf, dir))) return true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { log } from "../log.ts";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_HOOK_TIMEOUT_MS,
|
|
5
|
+
HOOK_EVENTS,
|
|
6
|
+
type HookEvent,
|
|
7
|
+
type LoamConfig,
|
|
8
|
+
} from "../schemas/config.ts";
|
|
9
|
+
import { readConfig } from "./config.ts";
|
|
10
|
+
|
|
11
|
+
// Events whose hooks may mutate the payload via stdout JSON. `pre-record` and
|
|
12
|
+
// `pre-prime` carry the original R-02 mutable payloads; `pre-compact` lets a
|
|
13
|
+
// hook substitute the merged replacement record (loam-184b). `pre-prune` is
|
|
14
|
+
// block-or-allow only (a hook can prevent the prune but not reshape the
|
|
15
|
+
// candidate set).
|
|
16
|
+
const MUTABLE_EVENTS: ReadonlySet<HookEvent> = new Set<HookEvent>([
|
|
17
|
+
"pre-record",
|
|
18
|
+
"pre-prime",
|
|
19
|
+
"pre-compact",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const BLOCKING_EVENTS: ReadonlySet<HookEvent> = new Set<HookEvent>([
|
|
23
|
+
"pre-record",
|
|
24
|
+
"pre-prime",
|
|
25
|
+
"pre-prune",
|
|
26
|
+
"pre-compact",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export interface HookExecution {
|
|
30
|
+
command: string;
|
|
31
|
+
exitCode: number;
|
|
32
|
+
stderr: string;
|
|
33
|
+
stdout: string;
|
|
34
|
+
durationMs: number;
|
|
35
|
+
timedOut: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface HookResult<T> {
|
|
39
|
+
// Whether at least one hook script was executed.
|
|
40
|
+
ranAny: boolean;
|
|
41
|
+
// True iff a `pre-*` hook exited non-zero (or timed out). The caller MUST
|
|
42
|
+
// abort the parent command when blocked is true.
|
|
43
|
+
blocked: boolean;
|
|
44
|
+
// Human-readable reason when blocked (script command + exit code/stderr).
|
|
45
|
+
blockReason?: string;
|
|
46
|
+
// Warnings collected from post-* failures or pre-* hooks whose stdout
|
|
47
|
+
// could not be parsed as JSON. Callers should surface these to the user.
|
|
48
|
+
warnings: string[];
|
|
49
|
+
// Possibly-mutated payload. For non-mutable events or when no hook altered
|
|
50
|
+
// stdout, this equals the input payload.
|
|
51
|
+
payload: T;
|
|
52
|
+
// Per-script execution diagnostics (in invocation order).
|
|
53
|
+
executions: HookExecution[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface HookSettings {
|
|
57
|
+
timeoutMs: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveSettings(config: LoamConfig): HookSettings {
|
|
61
|
+
const raw = config.hook_settings?.timeout_ms;
|
|
62
|
+
const timeoutMs = typeof raw === "number" && raw > 0 ? raw : DEFAULT_HOOK_TIMEOUT_MS;
|
|
63
|
+
return { timeoutMs };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getHookList(config: LoamConfig, event: HookEvent): string[] {
|
|
67
|
+
const list = config.hooks?.[event] ?? [];
|
|
68
|
+
return Array.isArray(list)
|
|
69
|
+
? list.filter((s) => typeof s === "string" && s.trim().length > 0)
|
|
70
|
+
: [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface RunOneResult {
|
|
74
|
+
exitCode: number;
|
|
75
|
+
stdout: string;
|
|
76
|
+
stderr: string;
|
|
77
|
+
durationMs: number;
|
|
78
|
+
timedOut: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function runOne(
|
|
82
|
+
command: string,
|
|
83
|
+
stdinJson: string,
|
|
84
|
+
cwd: string,
|
|
85
|
+
timeoutMs: number,
|
|
86
|
+
): Promise<RunOneResult> {
|
|
87
|
+
const start = Date.now();
|
|
88
|
+
// Run the hook in its own process group (POSIX session) so a timeout can
|
|
89
|
+
// SIGKILL every descendant — not just the immediate `sh`. Bun.spawn's
|
|
90
|
+
// `timeout` option only signals the direct child; a forked exec like
|
|
91
|
+
// `sleep 30 & wait` orphans the sleep, which keeps stdout/stderr open and
|
|
92
|
+
// hangs `Promise.all([Response.text(), Response.text(), proc.exited])`
|
|
93
|
+
// indefinitely (R-02 stress finding). Node's child_process supports
|
|
94
|
+
// `detached: true` (calls setsid on POSIX, putting the child in a new pgid
|
|
95
|
+
// equal to its pid), and `process.kill(-pid, "SIGKILL")` reaches the whole
|
|
96
|
+
// group. Bun re-exports node:child_process so this works on the Bun runtime.
|
|
97
|
+
const child = spawn("sh", ["-c", command], {
|
|
98
|
+
cwd,
|
|
99
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
100
|
+
env: { ...process.env, LOAM_HOOK: "1" },
|
|
101
|
+
detached: true,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
let timedOut = false;
|
|
105
|
+
const timer = setTimeout(() => {
|
|
106
|
+
timedOut = true;
|
|
107
|
+
if (typeof child.pid === "number") {
|
|
108
|
+
try {
|
|
109
|
+
// Negative pid → process group. SIGKILL is unblockable so this
|
|
110
|
+
// reaches every descendant even if the hook script ignores SIGTERM.
|
|
111
|
+
process.kill(-child.pid, "SIGKILL");
|
|
112
|
+
} catch {
|
|
113
|
+
// ESRCH if the group already died; ignore.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}, timeoutMs);
|
|
117
|
+
|
|
118
|
+
let stdout = "";
|
|
119
|
+
let stderr = "";
|
|
120
|
+
child.stdout?.on("data", (b: Buffer) => {
|
|
121
|
+
stdout += b.toString();
|
|
122
|
+
});
|
|
123
|
+
child.stderr?.on("data", (b: Buffer) => {
|
|
124
|
+
stderr += b.toString();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (child.stdin) {
|
|
128
|
+
try {
|
|
129
|
+
child.stdin.write(stdinJson);
|
|
130
|
+
child.stdin.end();
|
|
131
|
+
} catch {
|
|
132
|
+
// Script may have closed stdin early — not fatal.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
137
|
+
child.on("close", (code: number | null) => resolve(code ?? 0));
|
|
138
|
+
child.on("error", () => resolve(1));
|
|
139
|
+
});
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
exitCode: timedOut ? 124 : exitCode,
|
|
144
|
+
stdout,
|
|
145
|
+
stderr,
|
|
146
|
+
durationMs: Date.now() - start,
|
|
147
|
+
timedOut,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Run all hook scripts registered for `event` in declaration order. Each script
|
|
153
|
+
* receives the current payload as JSON on stdin. For mutable events
|
|
154
|
+
* (`pre-record`, `pre-prime`), a script may print modified JSON on stdout; that
|
|
155
|
+
* JSON becomes the input to the next script and the final `result.payload`
|
|
156
|
+
* returned to the caller. `pre-prune` is block-or-allow only — its stdout is
|
|
157
|
+
* ignored even though it is a `pre-*` event, so a hook cannot reshape the
|
|
158
|
+
* candidate set.
|
|
159
|
+
*
|
|
160
|
+
* Semantics summary:
|
|
161
|
+
* - `pre-*` non-zero exit (or timeout) → blocks: subsequent scripts skipped,
|
|
162
|
+
* caller must abort. `result.blocked` is true with a reason string.
|
|
163
|
+
* - `post-*` non-zero exit → warning only, all scripts still run.
|
|
164
|
+
* - Empty / whitespace stdout from a mutable hook → payload unchanged.
|
|
165
|
+
* - Non-JSON stdout from a mutable hook → payload unchanged + warning.
|
|
166
|
+
* - Stderr from any script is forwarded to the user's stderr unless captured.
|
|
167
|
+
*/
|
|
168
|
+
export async function runHooks<T>(
|
|
169
|
+
event: HookEvent,
|
|
170
|
+
payload: T,
|
|
171
|
+
opts: { cwd?: string; config?: LoamConfig; forwardStderr?: boolean } = {},
|
|
172
|
+
): Promise<HookResult<T>> {
|
|
173
|
+
if (!HOOK_EVENTS.includes(event)) {
|
|
174
|
+
throw new Error(`Unknown hook event: "${event}".`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
178
|
+
|
|
179
|
+
let config: LoamConfig;
|
|
180
|
+
try {
|
|
181
|
+
config = opts.config ?? (await readConfig(cwd));
|
|
182
|
+
} catch {
|
|
183
|
+
// No config available (e.g., before `lm init`). Treat as no hooks.
|
|
184
|
+
return {
|
|
185
|
+
ranAny: false,
|
|
186
|
+
blocked: false,
|
|
187
|
+
warnings: [],
|
|
188
|
+
payload,
|
|
189
|
+
executions: [],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const scripts = getHookList(config, event);
|
|
194
|
+
if (scripts.length === 0) {
|
|
195
|
+
return {
|
|
196
|
+
ranAny: false,
|
|
197
|
+
blocked: false,
|
|
198
|
+
warnings: [],
|
|
199
|
+
payload,
|
|
200
|
+
executions: [],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const settings = resolveSettings(config);
|
|
205
|
+
const isMutable = MUTABLE_EVENTS.has(event);
|
|
206
|
+
const isBlocking = BLOCKING_EVENTS.has(event);
|
|
207
|
+
const forwardStderr = opts.forwardStderr !== false;
|
|
208
|
+
|
|
209
|
+
const warnings: string[] = [];
|
|
210
|
+
const executions: HookExecution[] = [];
|
|
211
|
+
let currentPayload: T = payload;
|
|
212
|
+
|
|
213
|
+
for (const command of scripts) {
|
|
214
|
+
const stdinJson = JSON.stringify({ event, payload: currentPayload });
|
|
215
|
+
const res = await runOne(command, stdinJson, cwd, settings.timeoutMs);
|
|
216
|
+
|
|
217
|
+
executions.push({
|
|
218
|
+
command,
|
|
219
|
+
exitCode: res.exitCode,
|
|
220
|
+
stderr: res.stderr,
|
|
221
|
+
stdout: res.stdout,
|
|
222
|
+
durationMs: res.durationMs,
|
|
223
|
+
timedOut: res.timedOut,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Diagnostic trace of hook execution. Silent at the default `info`
|
|
227
|
+
// level; surfaces under LOAM_DEBUG. Runs outside the expertise-file
|
|
228
|
+
// write lock (record.ts runs pre-/post-hooks around the locked write),
|
|
229
|
+
// so logging here can never contend with a concurrent writer.
|
|
230
|
+
log.debug(
|
|
231
|
+
{
|
|
232
|
+
event,
|
|
233
|
+
command,
|
|
234
|
+
exitCode: res.exitCode,
|
|
235
|
+
durationMs: res.durationMs,
|
|
236
|
+
timedOut: res.timedOut,
|
|
237
|
+
},
|
|
238
|
+
"hook executed",
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
if (forwardStderr && res.stderr.length > 0) {
|
|
242
|
+
process.stderr.write(res.stderr);
|
|
243
|
+
if (!res.stderr.endsWith("\n")) process.stderr.write("\n");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (res.exitCode !== 0) {
|
|
247
|
+
const reason = res.timedOut
|
|
248
|
+
? `hook \`${command}\` timed out after ${settings.timeoutMs}ms`
|
|
249
|
+
: `hook \`${command}\` exited with code ${res.exitCode}`;
|
|
250
|
+
log.warn(
|
|
251
|
+
{ event, command, exitCode: res.exitCode, timedOut: res.timedOut, blocking: isBlocking },
|
|
252
|
+
reason,
|
|
253
|
+
);
|
|
254
|
+
if (isBlocking) {
|
|
255
|
+
return {
|
|
256
|
+
ranAny: true,
|
|
257
|
+
blocked: true,
|
|
258
|
+
blockReason: reason,
|
|
259
|
+
warnings,
|
|
260
|
+
payload: currentPayload,
|
|
261
|
+
executions,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
warnings.push(reason);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (isMutable) {
|
|
269
|
+
const trimmed = res.stdout.trim();
|
|
270
|
+
if (trimmed.length === 0) continue;
|
|
271
|
+
try {
|
|
272
|
+
const parsed = JSON.parse(trimmed) as { payload?: T } | T;
|
|
273
|
+
if (
|
|
274
|
+
parsed !== null &&
|
|
275
|
+
typeof parsed === "object" &&
|
|
276
|
+
"payload" in (parsed as Record<string, unknown>)
|
|
277
|
+
) {
|
|
278
|
+
currentPayload = (parsed as { payload: T }).payload;
|
|
279
|
+
} else {
|
|
280
|
+
currentPayload = parsed as T;
|
|
281
|
+
}
|
|
282
|
+
} catch (err) {
|
|
283
|
+
warnings.push(
|
|
284
|
+
`hook \`${command}\` printed non-JSON on stdout (mutation ignored): ${
|
|
285
|
+
err instanceof Error ? err.message : String(err)
|
|
286
|
+
}`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
ranAny: true,
|
|
294
|
+
blocked: false,
|
|
295
|
+
warnings,
|
|
296
|
+
payload: currentPayload,
|
|
297
|
+
executions,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export {
|
|
2
|
+
getConfigPath,
|
|
3
|
+
getExpertiseDir,
|
|
4
|
+
getExpertisePath,
|
|
5
|
+
getLoamDir,
|
|
6
|
+
initLoamDir,
|
|
7
|
+
readConfig,
|
|
8
|
+
writeConfig,
|
|
9
|
+
} from "./config.ts";
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
appendRecord,
|
|
13
|
+
countRecords,
|
|
14
|
+
createExpertiseFile,
|
|
15
|
+
filterByType,
|
|
16
|
+
generateRecordId,
|
|
17
|
+
getFileModTime,
|
|
18
|
+
readExpertiseFile,
|
|
19
|
+
} from "./expertise.ts";
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
formatDomainExpertise,
|
|
23
|
+
formatPrimeOutput,
|
|
24
|
+
formatStatusOutput,
|
|
25
|
+
formatTimeAgo,
|
|
26
|
+
getRecordSummary,
|
|
27
|
+
} from "./format.ts";
|
|
28
|
+
export {
|
|
29
|
+
fileMatchesAny,
|
|
30
|
+
filterByContext,
|
|
31
|
+
getChangedFiles,
|
|
32
|
+
isGitRepo,
|
|
33
|
+
} from "./git.ts";
|
|
34
|
+
export {
|
|
35
|
+
outputJson,
|
|
36
|
+
outputJsonError,
|
|
37
|
+
} from "./json-output.ts";
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
hasMarkerSection,
|
|
41
|
+
MARKER_END,
|
|
42
|
+
MARKER_START,
|
|
43
|
+
removeMarkerSection,
|
|
44
|
+
replaceMarkerSection,
|
|
45
|
+
wrapInMarkers,
|
|
46
|
+
} from "./markers.ts";
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
compareSemver,
|
|
50
|
+
getCurrentVersion,
|
|
51
|
+
getLatestVersion,
|
|
52
|
+
} from "./version.ts";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface JsonResult {
|
|
2
|
+
success: boolean;
|
|
3
|
+
command: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function outputJson(result: JsonResult): void {
|
|
8
|
+
console.log(JSON.stringify(result, null, 2));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function outputJsonError(command: string, error: string): void {
|
|
12
|
+
console.error(JSON.stringify({ success: false, command, error }, null, 2));
|
|
13
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { lstat, open, unlink } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
const LOCK_STALE_MS = 30_000; // 30 seconds
|
|
5
|
+
const LOCK_RETRY_INTERVAL_MS = 50;
|
|
6
|
+
const LOCK_TIMEOUT_MS = 5_000; // 5 seconds
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Advisory file-level lock using O_CREAT | O_EXCL.
|
|
10
|
+
* Wraps an async function with lock acquisition and guaranteed cleanup.
|
|
11
|
+
*/
|
|
12
|
+
export async function withFileLock<T>(filePath: string, fn: () => Promise<T>): Promise<T> {
|
|
13
|
+
const lockPath = `${filePath}.lock`;
|
|
14
|
+
await acquireLock(lockPath);
|
|
15
|
+
try {
|
|
16
|
+
return await fn();
|
|
17
|
+
} finally {
|
|
18
|
+
await releaseLock(lockPath);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function acquireLock(lockPath: string): Promise<void> {
|
|
23
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
24
|
+
|
|
25
|
+
while (true) {
|
|
26
|
+
try {
|
|
27
|
+
const fd = await open(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
|
|
28
|
+
await fd.close();
|
|
29
|
+
return;
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if ((err as NodeJS.ErrnoException).code !== "EEXIST") {
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Lock file exists — check if it's stale
|
|
36
|
+
if (await isStaleLock(lockPath)) {
|
|
37
|
+
try {
|
|
38
|
+
await unlink(lockPath);
|
|
39
|
+
} catch {
|
|
40
|
+
// Another process may have already removed it
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (Date.now() >= deadline) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Timed out waiting for lock on ${lockPath}. If no other loam process is running, delete the lock file manually.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await sleep(LOCK_RETRY_INTERVAL_MS);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function isStaleLock(lockPath: string): Promise<boolean> {
|
|
57
|
+
try {
|
|
58
|
+
const stats = await lstat(lockPath);
|
|
59
|
+
return Date.now() - stats.mtimeMs > LOCK_STALE_MS;
|
|
60
|
+
} catch {
|
|
61
|
+
// Lock file disappeared between check — not stale, just gone
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function releaseLock(lockPath: string): Promise<void> {
|
|
67
|
+
try {
|
|
68
|
+
await unlink(lockPath);
|
|
69
|
+
} catch {
|
|
70
|
+
// Lock file already gone — acceptable
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sleep(ms: number): Promise<void> {
|
|
75
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
76
|
+
}
|