@ashlr/hub 2.2.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/CHANGELOG.md +1158 -0
- package/LICENSE +21 -0
- package/README.md +833 -0
- package/bin/ashlr +8 -0
- package/dist/api/core.d.ts +28 -0
- package/dist/api/core.js +42 -0
- package/dist/api/core.js.map +1 -0
- package/dist/api/index.d.ts +9 -0
- package/dist/api/index.js +9 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/plugin.d.ts +12 -0
- package/dist/api/plugin.js +13 -0
- package/dist/api/plugin.js.map +1 -0
- package/dist/api/types.d.ts +9 -0
- package/dist/api/types.js +9 -0
- package/dist/api/types.js.map +1 -0
- package/dist/cli/args.d.ts +20 -0
- package/dist/cli/args.js +23 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/ask.d.ts +22 -0
- package/dist/cli/ask.js +240 -0
- package/dist/cli/ask.js.map +1 -0
- package/dist/cli/audit.d.ts +26 -0
- package/dist/cli/audit.js +199 -0
- package/dist/cli/audit.js.map +1 -0
- package/dist/cli/backlog.d.ts +26 -0
- package/dist/cli/backlog.js +322 -0
- package/dist/cli/backlog.js.map +1 -0
- package/dist/cli/completions.d.ts +18 -0
- package/dist/cli/completions.js +156 -0
- package/dist/cli/completions.js.map +1 -0
- package/dist/cli/daemon.d.ts +31 -0
- package/dist/cli/daemon.js +334 -0
- package/dist/cli/daemon.js.map +1 -0
- package/dist/cli/demo-sandbox.d.ts +78 -0
- package/dist/cli/demo-sandbox.js +227 -0
- package/dist/cli/demo-sandbox.js.map +1 -0
- package/dist/cli/demo.d.ts +76 -0
- package/dist/cli/demo.js +285 -0
- package/dist/cli/demo.js.map +1 -0
- package/dist/cli/digest.d.ts +46 -0
- package/dist/cli/digest.js +352 -0
- package/dist/cli/digest.js.map +1 -0
- package/dist/cli/doctor-init.d.ts +28 -0
- package/dist/cli/doctor-init.js +335 -0
- package/dist/cli/doctor-init.js.map +1 -0
- package/dist/cli/genome.d.ts +62 -0
- package/dist/cli/genome.js +1061 -0
- package/dist/cli/genome.js.map +1 -0
- package/dist/cli/gh.d.ts +27 -0
- package/dist/cli/gh.js +322 -0
- package/dist/cli/gh.js.map +1 -0
- package/dist/cli/goals.d.ts +55 -0
- package/dist/cli/goals.js +667 -0
- package/dist/cli/goals.js.map +1 -0
- package/dist/cli/health.d.ts +56 -0
- package/dist/cli/health.js +407 -0
- package/dist/cli/health.js.map +1 -0
- package/dist/cli/help.d.ts +51 -0
- package/dist/cli/help.js +436 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/inbox.d.ts +28 -0
- package/dist/cli/inbox.js +589 -0
- package/dist/cli/inbox.js.map +1 -0
- package/dist/cli/index.d.ts +41 -0
- package/dist/cli/index.js +1208 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/knowledge.d.ts +33 -0
- package/dist/cli/knowledge.js +575 -0
- package/dist/cli/knowledge.js.map +1 -0
- package/dist/cli/mcp.d.ts +26 -0
- package/dist/cli/mcp.js +487 -0
- package/dist/cli/mcp.js.map +1 -0
- package/dist/cli/models.d.ts +26 -0
- package/dist/cli/models.js +435 -0
- package/dist/cli/models.js.map +1 -0
- package/dist/cli/new.d.ts +32 -0
- package/dist/cli/new.js +565 -0
- package/dist/cli/new.js.map +1 -0
- package/dist/cli/notify.d.ts +20 -0
- package/dist/cli/notify.js +188 -0
- package/dist/cli/notify.js.map +1 -0
- package/dist/cli/onboard.d.ts +85 -0
- package/dist/cli/onboard.js +305 -0
- package/dist/cli/onboard.js.map +1 -0
- package/dist/cli/open.d.ts +38 -0
- package/dist/cli/open.js +86 -0
- package/dist/cli/open.js.map +1 -0
- package/dist/cli/orient.d.ts +14 -0
- package/dist/cli/orient.js +110 -0
- package/dist/cli/orient.js.map +1 -0
- package/dist/cli/picker.d.ts +21 -0
- package/dist/cli/picker.js +177 -0
- package/dist/cli/picker.js.map +1 -0
- package/dist/cli/plugins.d.ts +21 -0
- package/dist/cli/plugins.js +394 -0
- package/dist/cli/plugins.js.map +1 -0
- package/dist/cli/preflight.d.ts +29 -0
- package/dist/cli/preflight.js +131 -0
- package/dist/cli/preflight.js.map +1 -0
- package/dist/cli/pulse.d.ts +26 -0
- package/dist/cli/pulse.js +482 -0
- package/dist/cli/pulse.js.map +1 -0
- package/dist/cli/reflect.d.ts +42 -0
- package/dist/cli/reflect.js +381 -0
- package/dist/cli/reflect.js.map +1 -0
- package/dist/cli/run.d.ts +29 -0
- package/dist/cli/run.js +687 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/sandbox.d.ts +20 -0
- package/dist/cli/sandbox.js +345 -0
- package/dist/cli/sandbox.js.map +1 -0
- package/dist/cli/seams.d.ts +41 -0
- package/dist/cli/seams.js +167 -0
- package/dist/cli/seams.js.map +1 -0
- package/dist/cli/serve.d.ts +25 -0
- package/dist/cli/serve.js +249 -0
- package/dist/cli/serve.js.map +1 -0
- package/dist/cli/ship.d.ts +33 -0
- package/dist/cli/ship.js +433 -0
- package/dist/cli/ship.js.map +1 -0
- package/dist/cli/spec.d.ts +19 -0
- package/dist/cli/spec.js +478 -0
- package/dist/cli/spec.js.map +1 -0
- package/dist/cli/swarm.d.ts +42 -0
- package/dist/cli/swarm.js +1365 -0
- package/dist/cli/swarm.js.map +1 -0
- package/dist/cli/telemetry.d.ts +26 -0
- package/dist/cli/telemetry.js +364 -0
- package/dist/cli/telemetry.js.map +1 -0
- package/dist/cli/tui.d.ts +17 -0
- package/dist/cli/tui.js +52 -0
- package/dist/cli/tui.js.map +1 -0
- package/dist/cli/ui.d.ts +53 -0
- package/dist/cli/ui.js +66 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/cli/update.d.ts +38 -0
- package/dist/cli/update.js +682 -0
- package/dist/cli/update.js.map +1 -0
- package/dist/cli/vercel.d.ts +20 -0
- package/dist/cli/vercel.js +193 -0
- package/dist/cli/vercel.js.map +1 -0
- package/dist/cli/verify-safety.d.ts +96 -0
- package/dist/cli/verify-safety.js +509 -0
- package/dist/cli/verify-safety.js.map +1 -0
- package/dist/cli/wire.d.ts +22 -0
- package/dist/cli/wire.js +205 -0
- package/dist/cli/wire.js.map +1 -0
- package/dist/core/classify.d.ts +51 -0
- package/dist/core/classify.js +450 -0
- package/dist/core/classify.js.map +1 -0
- package/dist/core/config.d.ts +63 -0
- package/dist/core/config.js +474 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/daemon/loop.d.ts +74 -0
- package/dist/core/daemon/loop.js +618 -0
- package/dist/core/daemon/loop.js.map +1 -0
- package/dist/core/daemon/state.d.ts +66 -0
- package/dist/core/daemon/state.js +197 -0
- package/dist/core/daemon/state.js.map +1 -0
- package/dist/core/dashboard.d.ts +40 -0
- package/dist/core/dashboard.js +463 -0
- package/dist/core/dashboard.js.map +1 -0
- package/dist/core/digest/build.d.ts +51 -0
- package/dist/core/digest/build.js +230 -0
- package/dist/core/digest/build.js.map +1 -0
- package/dist/core/digest/deliver.d.ts +47 -0
- package/dist/core/digest/deliver.js +230 -0
- package/dist/core/digest/deliver.js.map +1 -0
- package/dist/core/digest/store.d.ts +57 -0
- package/dist/core/digest/store.js +223 -0
- package/dist/core/digest/store.js.map +1 -0
- package/dist/core/doctor-fix.d.ts +21 -0
- package/dist/core/doctor-fix.js +440 -0
- package/dist/core/doctor-fix.js.map +1 -0
- package/dist/core/doctor.d.ts +18 -0
- package/dist/core/doctor.js +806 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/env-bridge.d.ts +64 -0
- package/dist/core/env-bridge.js +103 -0
- package/dist/core/env-bridge.js.map +1 -0
- package/dist/core/genome/capture.d.ts +49 -0
- package/dist/core/genome/capture.js +352 -0
- package/dist/core/genome/capture.js.map +1 -0
- package/dist/core/genome/consolidate.d.ts +38 -0
- package/dist/core/genome/consolidate.js +426 -0
- package/dist/core/genome/consolidate.js.map +1 -0
- package/dist/core/genome/export.d.ts +29 -0
- package/dist/core/genome/export.js +102 -0
- package/dist/core/genome/export.js.map +1 -0
- package/dist/core/genome/playbook.d.ts +33 -0
- package/dist/core/genome/playbook.js +320 -0
- package/dist/core/genome/playbook.js.map +1 -0
- package/dist/core/genome/recall.d.ts +45 -0
- package/dist/core/genome/recall.js +293 -0
- package/dist/core/genome/recall.js.map +1 -0
- package/dist/core/genome/store.d.ts +62 -0
- package/dist/core/genome/store.js +711 -0
- package/dist/core/genome/store.js.map +1 -0
- package/dist/core/git.d.ts +34 -0
- package/dist/core/git.js +115 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/goals/advance.d.ts +82 -0
- package/dist/core/goals/advance.js +263 -0
- package/dist/core/goals/advance.js.map +1 -0
- package/dist/core/goals/planner.d.ts +58 -0
- package/dist/core/goals/planner.js +233 -0
- package/dist/core/goals/planner.js.map +1 -0
- package/dist/core/goals/store.d.ts +128 -0
- package/dist/core/goals/store.js +442 -0
- package/dist/core/goals/store.js.map +1 -0
- package/dist/core/inbox/apply.d.ts +36 -0
- package/dist/core/inbox/apply.js +407 -0
- package/dist/core/inbox/apply.js.map +1 -0
- package/dist/core/inbox/notify-proposal.d.ts +13 -0
- package/dist/core/inbox/notify-proposal.js +21 -0
- package/dist/core/inbox/notify-proposal.js.map +1 -0
- package/dist/core/inbox/store.d.ts +67 -0
- package/dist/core/inbox/store.js +258 -0
- package/dist/core/inbox/store.js.map +1 -0
- package/dist/core/index-engine.d.ts +62 -0
- package/dist/core/index-engine.js +486 -0
- package/dist/core/index-engine.js.map +1 -0
- package/dist/core/integrations/desktop-notify.d.ts +18 -0
- package/dist/core/integrations/desktop-notify.js +42 -0
- package/dist/core/integrations/desktop-notify.js.map +1 -0
- package/dist/core/integrations/editors.d.ts +50 -0
- package/dist/core/integrations/editors.js +216 -0
- package/dist/core/integrations/editors.js.map +1 -0
- package/dist/core/integrations/github.d.ts +66 -0
- package/dist/core/integrations/github.js +332 -0
- package/dist/core/integrations/github.js.map +1 -0
- package/dist/core/integrations/identity.d.ts +29 -0
- package/dist/core/integrations/identity.js +359 -0
- package/dist/core/integrations/identity.js.map +1 -0
- package/dist/core/integrations/notify.d.ts +23 -0
- package/dist/core/integrations/notify.js +96 -0
- package/dist/core/integrations/notify.js.map +1 -0
- package/dist/core/integrations/vercel.d.ts +37 -0
- package/dist/core/integrations/vercel.js +192 -0
- package/dist/core/integrations/vercel.js.map +1 -0
- package/dist/core/knowledge/ask.d.ts +34 -0
- package/dist/core/knowledge/ask.js +325 -0
- package/dist/core/knowledge/ask.js.map +1 -0
- package/dist/core/knowledge/graph.d.ts +51 -0
- package/dist/core/knowledge/graph.js +564 -0
- package/dist/core/knowledge/graph.js.map +1 -0
- package/dist/core/knowledge/index.d.ts +74 -0
- package/dist/core/knowledge/index.js +586 -0
- package/dist/core/knowledge/index.js.map +1 -0
- package/dist/core/learn/playbooks.d.ts +84 -0
- package/dist/core/learn/playbooks.js +241 -0
- package/dist/core/learn/playbooks.js.map +1 -0
- package/dist/core/learn/reflect.d.ts +87 -0
- package/dist/core/learn/reflect.js +435 -0
- package/dist/core/learn/reflect.js.map +1 -0
- package/dist/core/learn/store.d.ts +51 -0
- package/dist/core/learn/store.js +165 -0
- package/dist/core/learn/store.js.map +1 -0
- package/dist/core/learn/tuning.d.ts +48 -0
- package/dist/core/learn/tuning.js +201 -0
- package/dist/core/learn/tuning.js.map +1 -0
- package/dist/core/lifecycle/scaffold.d.ts +43 -0
- package/dist/core/lifecycle/scaffold.js +260 -0
- package/dist/core/lifecycle/scaffold.js.map +1 -0
- package/dist/core/lifecycle/ship.d.ts +48 -0
- package/dist/core/lifecycle/ship.js +513 -0
- package/dist/core/lifecycle/ship.js.map +1 -0
- package/dist/core/lifecycle/templates.d.ts +20 -0
- package/dist/core/lifecycle/templates.js +605 -0
- package/dist/core/lifecycle/templates.js.map +1 -0
- package/dist/core/mcp-gateway.d.ts +59 -0
- package/dist/core/mcp-gateway.js +385 -0
- package/dist/core/mcp-gateway.js.map +1 -0
- package/dist/core/mcp-native.d.ts +53 -0
- package/dist/core/mcp-native.js +507 -0
- package/dist/core/mcp-native.js.map +1 -0
- package/dist/core/mcp-registry.d.ts +36 -0
- package/dist/core/mcp-registry.js +180 -0
- package/dist/core/mcp-registry.js.map +1 -0
- package/dist/core/observability/budget-alert.d.ts +18 -0
- package/dist/core/observability/budget-alert.js +90 -0
- package/dist/core/observability/budget-alert.js.map +1 -0
- package/dist/core/observability/estimate.d.ts +27 -0
- package/dist/core/observability/estimate.js +188 -0
- package/dist/core/observability/estimate.js.map +1 -0
- package/dist/core/observability/forecast.d.ts +19 -0
- package/dist/core/observability/forecast.js +101 -0
- package/dist/core/observability/forecast.js.map +1 -0
- package/dist/core/observability/governance.d.ts +28 -0
- package/dist/core/observability/governance.js +101 -0
- package/dist/core/observability/governance.js.map +1 -0
- package/dist/core/observability/otlp.d.ts +88 -0
- package/dist/core/observability/otlp.js +217 -0
- package/dist/core/observability/otlp.js.map +1 -0
- package/dist/core/observability/rollup.d.ts +35 -0
- package/dist/core/observability/rollup.js +311 -0
- package/dist/core/observability/rollup.js.map +1 -0
- package/dist/core/observability/telemetry-sink.d.ts +63 -0
- package/dist/core/observability/telemetry-sink.js +350 -0
- package/dist/core/observability/telemetry-sink.js.map +1 -0
- package/dist/core/observability/usage-source.d.ts +60 -0
- package/dist/core/observability/usage-source.js +347 -0
- package/dist/core/observability/usage-source.js.map +1 -0
- package/dist/core/onboard.d.ts +27 -0
- package/dist/core/onboard.js +288 -0
- package/dist/core/onboard.js.map +1 -0
- package/dist/core/orient.d.ts +23 -0
- package/dist/core/orient.js +135 -0
- package/dist/core/orient.js.map +1 -0
- package/dist/core/phantom.d.ts +23 -0
- package/dist/core/phantom.js +279 -0
- package/dist/core/phantom.js.map +1 -0
- package/dist/core/plugins/host-api.d.ts +29 -0
- package/dist/core/plugins/host-api.js +113 -0
- package/dist/core/plugins/host-api.js.map +1 -0
- package/dist/core/plugins/integrity.d.ts +36 -0
- package/dist/core/plugins/integrity.js +73 -0
- package/dist/core/plugins/integrity.js.map +1 -0
- package/dist/core/plugins/manifest.d.ts +31 -0
- package/dist/core/plugins/manifest.js +316 -0
- package/dist/core/plugins/manifest.js.map +1 -0
- package/dist/core/plugins/registry.d.ts +87 -0
- package/dist/core/plugins/registry.js +415 -0
- package/dist/core/plugins/registry.js.map +1 -0
- package/dist/core/plugins/types.d.ts +182 -0
- package/dist/core/plugins/types.js +40 -0
- package/dist/core/plugins/types.js.map +1 -0
- package/dist/core/plugins/wrappers.d.ts +40 -0
- package/dist/core/plugins/wrappers.js +229 -0
- package/dist/core/plugins/wrappers.js.map +1 -0
- package/dist/core/portfolio/backlog.d.ts +40 -0
- package/dist/core/portfolio/backlog.js +177 -0
- package/dist/core/portfolio/backlog.js.map +1 -0
- package/dist/core/portfolio/scanners.d.ts +21 -0
- package/dist/core/portfolio/scanners.js +600 -0
- package/dist/core/portfolio/scanners.js.map +1 -0
- package/dist/core/providers.d.ts +34 -0
- package/dist/core/providers.js +250 -0
- package/dist/core/providers.js.map +1 -0
- package/dist/core/quality/conventions.d.ts +35 -0
- package/dist/core/quality/conventions.js +267 -0
- package/dist/core/quality/conventions.js.map +1 -0
- package/dist/core/quality/fixes.d.ts +56 -0
- package/dist/core/quality/fixes.js +209 -0
- package/dist/core/quality/fixes.js.map +1 -0
- package/dist/core/quality/health.d.ts +69 -0
- package/dist/core/quality/health.js +350 -0
- package/dist/core/quality/health.js.map +1 -0
- package/dist/core/quality/store.d.ts +56 -0
- package/dist/core/quality/store.js +195 -0
- package/dist/core/quality/store.js.map +1 -0
- package/dist/core/readiness.d.ts +112 -0
- package/dist/core/readiness.js +431 -0
- package/dist/core/readiness.js.map +1 -0
- package/dist/core/run/agent-loop.d.ts +40 -0
- package/dist/core/run/agent-loop.js +291 -0
- package/dist/core/run/agent-loop.js.map +1 -0
- package/dist/core/run/budget.d.ts +47 -0
- package/dist/core/run/budget.js +113 -0
- package/dist/core/run/budget.js.map +1 -0
- package/dist/core/run/engines.d.ts +75 -0
- package/dist/core/run/engines.js +199 -0
- package/dist/core/run/engines.js.map +1 -0
- package/dist/core/run/model-manager.d.ts +64 -0
- package/dist/core/run/model-manager.js +339 -0
- package/dist/core/run/model-manager.js.map +1 -0
- package/dist/core/run/orchestrator.d.ts +100 -0
- package/dist/core/run/orchestrator.js +1515 -0
- package/dist/core/run/orchestrator.js.map +1 -0
- package/dist/core/run/provider-client.d.ts +46 -0
- package/dist/core/run/provider-client.js +796 -0
- package/dist/core/run/provider-client.js.map +1 -0
- package/dist/core/run/retry.d.ts +19 -0
- package/dist/core/run/retry.js +68 -0
- package/dist/core/run/retry.js.map +1 -0
- package/dist/core/run/router.d.ts +50 -0
- package/dist/core/run/router.js +257 -0
- package/dist/core/run/router.js.map +1 -0
- package/dist/core/run/self-heal.d.ts +52 -0
- package/dist/core/run/self-heal.js +181 -0
- package/dist/core/run/self-heal.js.map +1 -0
- package/dist/core/run/streaming.d.ts +31 -0
- package/dist/core/run/streaming.js +122 -0
- package/dist/core/run/streaming.js.map +1 -0
- package/dist/core/run/verify.d.ts +30 -0
- package/dist/core/run/verify.js +204 -0
- package/dist/core/run/verify.js.map +1 -0
- package/dist/core/sandbox/audit.d.ts +31 -0
- package/dist/core/sandbox/audit.js +169 -0
- package/dist/core/sandbox/audit.js.map +1 -0
- package/dist/core/sandbox/policy.d.ts +61 -0
- package/dist/core/sandbox/policy.js +211 -0
- package/dist/core/sandbox/policy.js.map +1 -0
- package/dist/core/sandbox/worktree.d.ts +175 -0
- package/dist/core/sandbox/worktree.js +673 -0
- package/dist/core/sandbox/worktree.js.map +1 -0
- package/dist/core/seams/backlog.d.ts +47 -0
- package/dist/core/seams/backlog.js +47 -0
- package/dist/core/seams/backlog.js.map +1 -0
- package/dist/core/seams/daemon-coordinator.d.ts +72 -0
- package/dist/core/seams/daemon-coordinator.js +76 -0
- package/dist/core/seams/daemon-coordinator.js.map +1 -0
- package/dist/core/seams/genome.d.ts +45 -0
- package/dist/core/seams/genome.js +53 -0
- package/dist/core/seams/genome.js.map +1 -0
- package/dist/core/seams/identity.d.ts +40 -0
- package/dist/core/seams/identity.js +44 -0
- package/dist/core/seams/identity.js.map +1 -0
- package/dist/core/seams/inbox.d.ts +60 -0
- package/dist/core/seams/inbox.js +66 -0
- package/dist/core/seams/inbox.js.map +1 -0
- package/dist/core/seams/index.d.ts +20 -0
- package/dist/core/seams/index.js +21 -0
- package/dist/core/seams/index.js.map +1 -0
- package/dist/core/seams/portfolio.d.ts +50 -0
- package/dist/core/seams/portfolio.js +61 -0
- package/dist/core/seams/portfolio.js.map +1 -0
- package/dist/core/seams/registry.d.ts +42 -0
- package/dist/core/seams/registry.js +128 -0
- package/dist/core/seams/registry.js.map +1 -0
- package/dist/core/seams/run-swarm.d.ts +66 -0
- package/dist/core/seams/run-swarm.js +74 -0
- package/dist/core/seams/run-swarm.js.map +1 -0
- package/dist/core/seams/types.d.ts +123 -0
- package/dist/core/seams/types.js +35 -0
- package/dist/core/seams/types.js.map +1 -0
- package/dist/core/spec/spec-store.d.ts +62 -0
- package/dist/core/spec/spec-store.js +359 -0
- package/dist/core/spec/spec-store.js.map +1 -0
- package/dist/core/swarm/gate.d.ts +45 -0
- package/dist/core/swarm/gate.js +114 -0
- package/dist/core/swarm/gate.js.map +1 -0
- package/dist/core/swarm/planner.d.ts +32 -0
- package/dist/core/swarm/planner.js +293 -0
- package/dist/core/swarm/planner.js.map +1 -0
- package/dist/core/swarm/rollback.d.ts +56 -0
- package/dist/core/swarm/rollback.js +266 -0
- package/dist/core/swarm/rollback.js.map +1 -0
- package/dist/core/swarm/runner.d.ts +62 -0
- package/dist/core/swarm/runner.js +1263 -0
- package/dist/core/swarm/runner.js.map +1 -0
- package/dist/core/swarm/sign.d.ts +71 -0
- package/dist/core/swarm/sign.js +362 -0
- package/dist/core/swarm/sign.js.map +1 -0
- package/dist/core/swarm/store.d.ts +52 -0
- package/dist/core/swarm/store.js +195 -0
- package/dist/core/swarm/store.js.map +1 -0
- package/dist/core/tidy.d.ts +32 -0
- package/dist/core/tidy.js +354 -0
- package/dist/core/tidy.js.map +1 -0
- package/dist/core/tools-registry.d.ts +16 -0
- package/dist/core/tools-registry.js +308 -0
- package/dist/core/tools-registry.js.map +1 -0
- package/dist/core/types.d.ts +2545 -0
- package/dist/core/types.js +9 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/web/api.d.ts +53 -0
- package/dist/core/web/api.js +698 -0
- package/dist/core/web/api.js.map +1 -0
- package/dist/core/web/public/app.js +1906 -0
- package/dist/core/web/public/index.html +721 -0
- package/dist/core/web/public/styles.css +2007 -0
- package/dist/core/web/server.d.ts +18 -0
- package/dist/core/web/server.js +122 -0
- package/dist/core/web/server.js.map +1 -0
- package/dist/core/web/static.d.ts +18 -0
- package/dist/core/web/static.js +122 -0
- package/dist/core/web/static.js.map +1 -0
- package/dist/tui/app.d.ts +33 -0
- package/dist/tui/app.js +350 -0
- package/dist/tui/app.js.map +1 -0
- package/dist/tui/render.d.ts +20 -0
- package/dist/tui/render.js +558 -0
- package/dist/tui/render.js.map +1 -0
- package/package.json +80 -0
- package/schema/config.schema.json +223 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree.ts — isolated git-worktree sandbox (M21 SAFETY FOUNDATION).
|
|
3
|
+
*
|
|
4
|
+
* Creates an ISOLATED git worktree of a source repo on a NEW scratch branch
|
|
5
|
+
* under ~/.ashlr/sandboxes/<id>/, so autonomous edits NEVER touch the user's
|
|
6
|
+
* working tree, index, HEAD, or their checked-out branch. The worktree is
|
|
7
|
+
* created, used, then discarded (git worktree remove + scratch-branch delete).
|
|
8
|
+
*
|
|
9
|
+
* ISOLATION + DESTRUCTIVE-SAFETY (the whole point):
|
|
10
|
+
* - Sandbox worktrees live ONLY under ~/.ashlr/sandboxes/.
|
|
11
|
+
* - Creating/removing them MUST NOT modify the source repo's working tree,
|
|
12
|
+
* index, HEAD, or the user's branches.
|
|
13
|
+
* - `git worktree add -b <branch> <path> <baseHead>` off the current HEAD;
|
|
14
|
+
* `git worktree remove --force` + `git branch -D` on cleanup.
|
|
15
|
+
* - NEVER `git reset --hard` / `git checkout` in the source repo. NEVER push.
|
|
16
|
+
* NEVER delete user branches. NEVER touch a repo that is not ENROLLED
|
|
17
|
+
* (except via the explicit allowAnyRepo test hatch on a tmp repo).
|
|
18
|
+
* - Every mutating op (and every refused/errored attempt) is audited.
|
|
19
|
+
* - All git invoked via node:child_process arg ARRAYS (execFile, no shell).
|
|
20
|
+
* - No new runtime deps; node builtins only.
|
|
21
|
+
*/
|
|
22
|
+
import { execFileSync } from 'node:child_process';
|
|
23
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, rmSync, } from 'node:fs';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
import { join, resolve, sep } from 'node:path';
|
|
26
|
+
import { randomBytes } from 'node:crypto';
|
|
27
|
+
import { isRepo } from '../git.js';
|
|
28
|
+
import { assertMayMutate } from './policy.js';
|
|
29
|
+
import { audit } from './audit.js';
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Constants
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
const GIT_TIMEOUT = 30_000; // ms — generous; worktree add can touch many files
|
|
34
|
+
const BRANCH_PREFIX = 'ashlr/sandbox/';
|
|
35
|
+
const META_FILE = 'sandbox.json';
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// H5 — bounded sandbox lifecycle (LOCAL-ONLY resource guards)
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
/**
|
|
40
|
+
* H5 CHANGE 4 — hard cap on concurrent on-disk sandboxes. Conservative default:
|
|
41
|
+
* the daemon caps swarm concurrency at 8 (loop.ts) + runner MAX_PARALLEL=8, so
|
|
42
|
+
* 16 leaves ~2x headroom for concurrent daemon swarms + a manual `ashlr swarm`
|
|
43
|
+
* + transient overlap, while still bounding unbounded accumulation. createSandbox
|
|
44
|
+
* sweeps STALE orphans first, then REFUSES (clean audited error) if still over.
|
|
45
|
+
*
|
|
46
|
+
* CONFIGURABLE: an operator (or a deterministic test) may override the default
|
|
47
|
+
* via ASHLR_MAX_SANDBOXES (a positive integer). Resolved at CALL TIME — like the
|
|
48
|
+
* HOME-sensitive path helpers — so a test can set a small cap without restarting
|
|
49
|
+
* the process. An absent/blank/invalid/non-positive value falls back to the
|
|
50
|
+
* conservative default; the override can only ever SET a finite positive bound
|
|
51
|
+
* (it can never disable the cap), keeping this a strictly LOCAL resource guard.
|
|
52
|
+
*/
|
|
53
|
+
const MAX_SANDBOXES_DEFAULT = 16;
|
|
54
|
+
/** Resolve the effective sandbox cap (env override, else conservative default). */
|
|
55
|
+
function maxSandboxes() {
|
|
56
|
+
const raw = process.env.ASHLR_MAX_SANDBOXES;
|
|
57
|
+
if (raw !== undefined && raw.trim() !== '') {
|
|
58
|
+
const n = Number.parseInt(raw, 10);
|
|
59
|
+
// Only a finite, positive integer override takes effect; anything else
|
|
60
|
+
// (NaN, <=0, non-numeric) falls back to the safe default. The override can
|
|
61
|
+
// never remove the bound — it can only set a different finite positive one.
|
|
62
|
+
if (Number.isInteger(n) && n > 0)
|
|
63
|
+
return n;
|
|
64
|
+
}
|
|
65
|
+
return MAX_SANDBOXES_DEFAULT;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* H5 CHANGE 1 — orphan-sweep staleness threshold (ms). This is the FALLBACK
|
|
69
|
+
* liveness proxy used ONLY for a sandbox that carries no usable `ownerPid`
|
|
70
|
+
* marker (older metadata, or a crash fixture that models a GONE owner). The
|
|
71
|
+
* PRIMARY liveness signal is the positive `ownerPid` marker (see Sandbox.ownerPid
|
|
72
|
+
* + ownerAlive() below): a sandbox whose owner pid is still alive is skipped by
|
|
73
|
+
* the sweep / disk-cap pre-sweep REGARDLESS of age, so a live in-flight worktree
|
|
74
|
+
* is never reclaimed out from under a running swarm — even a cross-process one.
|
|
75
|
+
*
|
|
76
|
+
* Because there is NO hard wall-clock cap on a swarm (the runner bounds a swarm
|
|
77
|
+
* by step count <= 200 and the token budget only — there is no elapsed-time
|
|
78
|
+
* deadline), createdAt-age is NOT by itself a sound liveness proxy. So this
|
|
79
|
+
* threshold is set FAR above any plausible 200-step run (6 hours) as a
|
|
80
|
+
* belt-and-suspenders bound for the only residual gap: the rare pid-reuse case
|
|
81
|
+
* where a crashed swarm's recorded pid was recycled by an unrelated live process
|
|
82
|
+
* (ownerAlive() would then falsely read 'alive', so age must also have elapsed
|
|
83
|
+
* before we'd ever reclaim). The cost of a too-LARGE value (a stale orphan
|
|
84
|
+
* lingering a few hours longer) is far cheaper than the cost of a too-SMALL one
|
|
85
|
+
* (force-removing a live worktree), so we err large. Shared by the daemon-start
|
|
86
|
+
* sweep wiring (loop.ts) and the disk-cap pre-sweep below.
|
|
87
|
+
*
|
|
88
|
+
* CROSS-PROCESS LIVENESS LIMITATION (documented): the only way a live worktree
|
|
89
|
+
* could still be reclaimed is if BOTH (a) its owner process crashed AND (b) the
|
|
90
|
+
* OS recycled its exact pid for a new live process AND (c) >6h elapsed — a
|
|
91
|
+
* vanishingly small window that, even if hit, only reclaims a worktree whose
|
|
92
|
+
* original owner is provably dead.
|
|
93
|
+
*/
|
|
94
|
+
export const ORPHAN_STALE_MS = 6 * 60 * 60_000;
|
|
95
|
+
/**
|
|
96
|
+
* Positive liveness check for a sandbox's recorded owner process. Returns true
|
|
97
|
+
* ONLY when `ownerPid` is a positive integer AND `process.kill(ownerPid, 0)`
|
|
98
|
+
* succeeds (the process exists and is signalable by us) — i.e. a swarm is
|
|
99
|
+
* provably still running and holding this worktree. Any throw (ESRCH = gone,
|
|
100
|
+
* EPERM = exists-but-not-ours, EINVAL, etc.) or an absent/invalid pid returns
|
|
101
|
+
* false, falling back to the conservative createdAt-age staleMs guard. NEVER
|
|
102
|
+
* throws and NEVER sends a real signal (signal 0 is error-check-only).
|
|
103
|
+
*/
|
|
104
|
+
function ownerAlive(sb) {
|
|
105
|
+
const pid = sb.ownerPid;
|
|
106
|
+
if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
process.kill(pid, 0);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Path helpers (re-resolved at call time so tests can relocate HOME)
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
/** Absolute path to the sandboxes root: ~/.ashlr/sandboxes. Created lazily. */
|
|
121
|
+
export function sandboxesDir() {
|
|
122
|
+
return join(homedir(), '.ashlr', 'sandboxes');
|
|
123
|
+
}
|
|
124
|
+
/** Directory holding one sandbox's worktree + metadata. */
|
|
125
|
+
function sandboxHome(id) {
|
|
126
|
+
return join(sandboxesDir(), id);
|
|
127
|
+
}
|
|
128
|
+
/** Path to a sandbox's persisted metadata file. */
|
|
129
|
+
function metaPath(id) {
|
|
130
|
+
return join(sandboxHome(id), META_FILE);
|
|
131
|
+
}
|
|
132
|
+
/** Path to the isolated worktree checkout for a sandbox. */
|
|
133
|
+
function worktreePathFor(id) {
|
|
134
|
+
return join(sandboxHome(id), 'worktree');
|
|
135
|
+
}
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// git helper — execFile arg arrays, no shell
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
/**
|
|
140
|
+
* Run a git command inside `cwd`. Throws on failure (callers decide whether
|
|
141
|
+
* to tolerate). Always uses an arg array — never a shell string.
|
|
142
|
+
*/
|
|
143
|
+
function gitRun(cwd, args) {
|
|
144
|
+
return execFileSync('git', args, {
|
|
145
|
+
cwd,
|
|
146
|
+
timeout: GIT_TIMEOUT,
|
|
147
|
+
stdio: 'pipe',
|
|
148
|
+
encoding: 'utf8',
|
|
149
|
+
}).trim();
|
|
150
|
+
}
|
|
151
|
+
/** Run a git command, returning null on any error (never throws). */
|
|
152
|
+
function gitTry(cwd, args) {
|
|
153
|
+
try {
|
|
154
|
+
return gitRun(cwd, args);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Metadata persistence
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
function writeMeta(sb) {
|
|
164
|
+
const home = sandboxHome(sb.id);
|
|
165
|
+
if (!existsSync(home)) {
|
|
166
|
+
mkdirSync(home, { recursive: true });
|
|
167
|
+
}
|
|
168
|
+
writeFileSync(metaPath(sb.id), JSON.stringify(sb, null, 2) + '\n', 'utf8');
|
|
169
|
+
}
|
|
170
|
+
/** Read + validate one sandbox's metadata. Returns null on missing/malformed. */
|
|
171
|
+
function readMeta(id) {
|
|
172
|
+
const p = metaPath(id);
|
|
173
|
+
if (!existsSync(p))
|
|
174
|
+
return null;
|
|
175
|
+
try {
|
|
176
|
+
const parsed = JSON.parse(readFileSync(p, 'utf8'));
|
|
177
|
+
if (parsed !== null &&
|
|
178
|
+
typeof parsed === 'object' &&
|
|
179
|
+
!Array.isArray(parsed)) {
|
|
180
|
+
const o = parsed;
|
|
181
|
+
if (typeof o['id'] === 'string' &&
|
|
182
|
+
typeof o['sourceRepo'] === 'string' &&
|
|
183
|
+
typeof o['worktreePath'] === 'string' &&
|
|
184
|
+
typeof o['branch'] === 'string' &&
|
|
185
|
+
typeof o['baseHead'] === 'string' &&
|
|
186
|
+
typeof o['createdAt'] === 'string') {
|
|
187
|
+
// ownerPid is OPTIONAL (back-compat): only carried through when it is a
|
|
188
|
+
// positive integer; anything else (absent/malformed/<=0) is dropped so
|
|
189
|
+
// ownerAlive() falls back to the createdAt-age guard for that sandbox.
|
|
190
|
+
const rawPid = o['ownerPid'];
|
|
191
|
+
const ownerPid = typeof rawPid === 'number' && Number.isInteger(rawPid) && rawPid > 0
|
|
192
|
+
? rawPid
|
|
193
|
+
: undefined;
|
|
194
|
+
return {
|
|
195
|
+
id: o['id'],
|
|
196
|
+
sourceRepo: o['sourceRepo'],
|
|
197
|
+
worktreePath: o['worktreePath'],
|
|
198
|
+
branch: o['branch'],
|
|
199
|
+
baseHead: o['baseHead'],
|
|
200
|
+
createdAt: o['createdAt'],
|
|
201
|
+
...(ownerPid !== undefined ? { ownerPid } : {}),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// malformed — treat as absent
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// createSandbox
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
/**
|
|
215
|
+
* Create an isolated git-worktree sandbox of `sourceRepo` on a NEW scratch
|
|
216
|
+
* branch under ~/.ashlr/sandboxes/<id>/.
|
|
217
|
+
*
|
|
218
|
+
* FIRST calls assertMayMutate(sourceRepo, opts) — refuses (throws + audits
|
|
219
|
+
* result:'refused') if the kill switch is on OR the repo is not enrolled and
|
|
220
|
+
* opts.allowAnyRepo is not set. Verifies sourceRepo is a git repo. Reads the
|
|
221
|
+
* source HEAD WITHOUT mutating it. Adds the worktree via
|
|
222
|
+
* `git worktree add -b <branch> <path> <baseHead>` run in sourceRepo — this
|
|
223
|
+
* MUST NOT modify the source working tree, index, HEAD, or any user branch.
|
|
224
|
+
*/
|
|
225
|
+
export function createSandbox(sourceRepo, opts) {
|
|
226
|
+
// Gate FIRST — kill switch / enrollment. Audit refusals.
|
|
227
|
+
//
|
|
228
|
+
// H5 CHANGE 3 (env-gate allowAnyRepo) EDIT SITE — integration applies the real
|
|
229
|
+
// edit. The env-gate lives in assertMayMutate (policy.ts) as the single source
|
|
230
|
+
// of truth: `opts.allowAnyRepo` is honored ONLY when ASHLR_TEST_ALLOW_ANY_REPO
|
|
231
|
+
// ==='1' (mirrors advance.ts:156). createSandbox passes opts straight through,
|
|
232
|
+
// so it inherits the gate transitively — NO separate env check is needed here.
|
|
233
|
+
// The kill switch ALWAYS wins (assertMayMutate checks it first, unconditional).
|
|
234
|
+
try {
|
|
235
|
+
assertMayMutate(sourceRepo, opts);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
audit({
|
|
239
|
+
action: 'sandbox:create',
|
|
240
|
+
repo: sourceRepo,
|
|
241
|
+
sandboxId: null,
|
|
242
|
+
summary: 'refused by policy gate',
|
|
243
|
+
result: 'refused',
|
|
244
|
+
});
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
247
|
+
// Must be a real git repo.
|
|
248
|
+
if (!isRepo(sourceRepo)) {
|
|
249
|
+
audit({
|
|
250
|
+
action: 'sandbox:create',
|
|
251
|
+
repo: sourceRepo,
|
|
252
|
+
sandboxId: null,
|
|
253
|
+
summary: 'sourceRepo is not a git repository',
|
|
254
|
+
result: 'error',
|
|
255
|
+
});
|
|
256
|
+
throw new Error(`not a git repository: ${sourceRepo}`);
|
|
257
|
+
}
|
|
258
|
+
// Read the source HEAD commit WITHOUT mutating it.
|
|
259
|
+
const baseHead = gitTry(sourceRepo, ['rev-parse', 'HEAD']);
|
|
260
|
+
if (!baseHead) {
|
|
261
|
+
audit({
|
|
262
|
+
action: 'sandbox:create',
|
|
263
|
+
repo: sourceRepo,
|
|
264
|
+
sandboxId: null,
|
|
265
|
+
summary: 'could not resolve source HEAD',
|
|
266
|
+
result: 'error',
|
|
267
|
+
});
|
|
268
|
+
throw new Error(`could not resolve HEAD in repo: ${sourceRepo}`);
|
|
269
|
+
}
|
|
270
|
+
// -------------------------------------------------------------------------
|
|
271
|
+
// H5 CHANGE 4 — DISK/COUNT CAP (LOCAL-ONLY resource guard). When the live
|
|
272
|
+
// sandbox count is at/over the cap, FIRST sweep crash-leftover orphans (NEVER a
|
|
273
|
+
// live one — sweepOrphanSandboxes skips any sandbox whose ownerPid is still
|
|
274
|
+
// alive regardless of age, and falls back to the ORPHAN_STALE_MS age guard only
|
|
275
|
+
// for ones with no live owner; so a same-process in-flight worktree held by a
|
|
276
|
+
// concurrent daemon swarm is protected here), then re-count; if STILL at/over
|
|
277
|
+
// the cap, REFUSE (audit result:'refused' + throw a
|
|
278
|
+
// clean error) rather than accumulate unboundedly. This removes NOTHING in-use
|
|
279
|
+
// and opens no outward capability — it only bounds on-disk worktree growth so a
|
|
280
|
+
// pathological crash/restart loop can never fill the disk. The cap is resolved
|
|
281
|
+
// at call time (env-overridable; conservative default) — see maxSandboxes().
|
|
282
|
+
const cap = maxSandboxes();
|
|
283
|
+
if (listSandboxes().length >= cap) {
|
|
284
|
+
// Reclaim genuine crash leftovers first (stale-guarded — a live sandbox is
|
|
285
|
+
// skipped). A healthy install with a transient burst self-heals here.
|
|
286
|
+
sweepOrphanSandboxes({ staleMs: ORPHAN_STALE_MS });
|
|
287
|
+
if (listSandboxes().length >= cap) {
|
|
288
|
+
audit({
|
|
289
|
+
action: 'sandbox:create',
|
|
290
|
+
repo: sourceRepo,
|
|
291
|
+
sandboxId: null,
|
|
292
|
+
summary: `sandbox cap reached (MAX_SANDBOXES=${cap})`,
|
|
293
|
+
result: 'refused',
|
|
294
|
+
});
|
|
295
|
+
throw new Error(`sandbox cap reached (MAX_SANDBOXES=${cap})`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Generate a unique id; the scratch branch contains it.
|
|
299
|
+
const id = randomBytes(6).toString('hex');
|
|
300
|
+
const branch = `${BRANCH_PREFIX}${id}`;
|
|
301
|
+
const worktreePath = worktreePathFor(id);
|
|
302
|
+
// Ensure the per-sandbox home exists (parent of the worktree). The worktree
|
|
303
|
+
// dir itself must NOT pre-exist — git worktree add creates it.
|
|
304
|
+
const home = sandboxHome(id);
|
|
305
|
+
if (!existsSync(home)) {
|
|
306
|
+
mkdirSync(home, { recursive: true });
|
|
307
|
+
}
|
|
308
|
+
// Add the isolated worktree on a NEW scratch branch off baseHead, run IN the
|
|
309
|
+
// source repo. This does NOT touch the source working tree, index, HEAD, or
|
|
310
|
+
// any user branch — it only creates a new ref + a separate checkout.
|
|
311
|
+
try {
|
|
312
|
+
gitRun(sourceRepo, [
|
|
313
|
+
'worktree',
|
|
314
|
+
'add',
|
|
315
|
+
'-b',
|
|
316
|
+
branch,
|
|
317
|
+
worktreePath,
|
|
318
|
+
baseHead,
|
|
319
|
+
]);
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
// Best-effort cleanup of the partially-created sandbox home, then audit.
|
|
323
|
+
// A partial `worktree add` (ref created, then checkout failed) can leave an
|
|
324
|
+
// orphan scratch branch and/or a dangling worktree registration in the
|
|
325
|
+
// SOURCE repo. Both are namespaced/harmless, but the sandbox invariant is
|
|
326
|
+
// "created, used, then discarded — bounded", so we prune them defensively.
|
|
327
|
+
// Guarded by the BRANCH_PREFIX assertion so we can NEVER `branch -D` a user
|
|
328
|
+
// branch even if `branch` were somehow corrupted.
|
|
329
|
+
try {
|
|
330
|
+
rmSync(home, { recursive: true, force: true });
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
/* ignore */
|
|
334
|
+
}
|
|
335
|
+
gitTry(sourceRepo, ['worktree', 'prune']);
|
|
336
|
+
if (branch.startsWith(BRANCH_PREFIX)) {
|
|
337
|
+
gitTry(sourceRepo, ['branch', '-D', branch]);
|
|
338
|
+
}
|
|
339
|
+
audit({
|
|
340
|
+
action: 'sandbox:create',
|
|
341
|
+
repo: sourceRepo,
|
|
342
|
+
sandboxId: id,
|
|
343
|
+
summary: 'git worktree add failed',
|
|
344
|
+
result: 'error',
|
|
345
|
+
});
|
|
346
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
347
|
+
}
|
|
348
|
+
const sb = {
|
|
349
|
+
id,
|
|
350
|
+
sourceRepo,
|
|
351
|
+
worktreePath,
|
|
352
|
+
branch,
|
|
353
|
+
baseHead,
|
|
354
|
+
createdAt: new Date().toISOString(),
|
|
355
|
+
// H5 — stamp the OWNING process pid as a positive liveness marker so the
|
|
356
|
+
// orphan sweep / disk-cap pre-sweep never force-remove this worktree while
|
|
357
|
+
// this process (or any process holding the same pid) is still alive.
|
|
358
|
+
ownerPid: process.pid,
|
|
359
|
+
};
|
|
360
|
+
writeMeta(sb);
|
|
361
|
+
audit({
|
|
362
|
+
action: 'sandbox:create',
|
|
363
|
+
repo: sourceRepo,
|
|
364
|
+
sandboxId: id,
|
|
365
|
+
summary: `worktree on ${branch} @ ${baseHead.slice(0, 8)}`,
|
|
366
|
+
result: 'ok',
|
|
367
|
+
});
|
|
368
|
+
return sb;
|
|
369
|
+
}
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
// sandboxDiff
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
/**
|
|
374
|
+
* Capture the git diff of the sandbox worktree vs its base HEAD. Read-only —
|
|
375
|
+
* never mutates the worktree or the source repo. Counts come from --numstat;
|
|
376
|
+
* the unified patch from a plain `git diff <baseHead>` inside the worktree.
|
|
377
|
+
*/
|
|
378
|
+
export function sandboxDiff(sb) {
|
|
379
|
+
const cwd = sb.worktreePath;
|
|
380
|
+
// Stage everything (including UNTRACKED new files) against the worktree's OWN
|
|
381
|
+
// index so plain `git diff` captures new files too. `git diff` ignores
|
|
382
|
+
// untracked files by design, and the autonomous write path writes files
|
|
383
|
+
// without `git add`/`commit` — so without this, brand-new files an autonomous
|
|
384
|
+
// run creates would be silently omitted from the captured proposal diff and
|
|
385
|
+
// then DESTROYED by removeSandbox's `worktree remove --force`. Staging in the
|
|
386
|
+
// SANDBOX worktree is safe: it touches only the worktree's index, never the
|
|
387
|
+
// source repo's index/working-tree/HEAD (the worktree has its own index).
|
|
388
|
+
// `add -A` records adds/mods/deletes; we then diff the index vs baseHead.
|
|
389
|
+
gitTry(cwd, ['add', '-A']);
|
|
390
|
+
// numstat: one line per file "<ins>\t<del>\t<path>" (binary => "-\t-\t..").
|
|
391
|
+
// `--staged` diffs the index (now including new files) against baseHead.
|
|
392
|
+
const numstat = gitTry(cwd, ['diff', '--staged', '--numstat', sb.baseHead]) ?? '';
|
|
393
|
+
let files = 0;
|
|
394
|
+
let insertions = 0;
|
|
395
|
+
let deletions = 0;
|
|
396
|
+
for (const line of numstat.split('\n')) {
|
|
397
|
+
const trimmed = line.trim();
|
|
398
|
+
if (!trimmed)
|
|
399
|
+
continue;
|
|
400
|
+
const parts = trimmed.split('\t');
|
|
401
|
+
if (parts.length < 3)
|
|
402
|
+
continue;
|
|
403
|
+
files += 1;
|
|
404
|
+
const ins = parseInt(parts[0] ?? '0', 10);
|
|
405
|
+
const del = parseInt(parts[1] ?? '0', 10);
|
|
406
|
+
if (!Number.isNaN(ins))
|
|
407
|
+
insertions += ins;
|
|
408
|
+
if (!Number.isNaN(del))
|
|
409
|
+
deletions += del;
|
|
410
|
+
}
|
|
411
|
+
// Full unified patch — staged vs baseHead so new files are included.
|
|
412
|
+
const patch = gitTry(cwd, ['diff', '--staged', sb.baseHead]) ?? '';
|
|
413
|
+
return {
|
|
414
|
+
sandboxId: sb.id,
|
|
415
|
+
files,
|
|
416
|
+
insertions,
|
|
417
|
+
deletions,
|
|
418
|
+
patch,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
// removeSandbox
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
/**
|
|
425
|
+
* Remove a sandbox: `git worktree remove --force` then delete the scratch
|
|
426
|
+
* branch (`git branch -D`), both run against the source repo; then clean up
|
|
427
|
+
* persisted metadata. MUST NOT touch the source working tree/index/HEAD/user
|
|
428
|
+
* branches. Idempotent — tolerates an already-removed worktree, never throws
|
|
429
|
+
* on a missing dir.
|
|
430
|
+
*/
|
|
431
|
+
export function removeSandbox(sb) {
|
|
432
|
+
const home = sandboxHome(sb.id);
|
|
433
|
+
// ---- Defense-in-depth: NEVER trust on-disk metadata to drive a destructive
|
|
434
|
+
// git op. Metadata can be tampered/corrupted/future-format, so before ANY
|
|
435
|
+
// mutating git call we (a) re-derive the safe branch + worktree path from the
|
|
436
|
+
// sandbox id, (b) require the stored values to match those safe values, and
|
|
437
|
+
// (c) require the branch to be inside our namespace and the worktree path to
|
|
438
|
+
// be contained under sandboxesDir(). A trip on any guard refuses the git ops
|
|
439
|
+
// (audited result:'refused') and falls through to local-dir cleanup only —
|
|
440
|
+
// we still rmSync the sandbox home, but we NEVER run `branch -D` / `worktree
|
|
441
|
+
// remove` against an arbitrary branch/path in an arbitrary repo.
|
|
442
|
+
const safeBranch = `${BRANCH_PREFIX}${sb.id}`;
|
|
443
|
+
const safeWorktree = worktreePathFor(sb.id);
|
|
444
|
+
const sandboxesRoot = sandboxesDir() + sep;
|
|
445
|
+
const branchInNamespace = sb.branch.startsWith(BRANCH_PREFIX);
|
|
446
|
+
const branchMatches = sb.branch === safeBranch;
|
|
447
|
+
const worktreeContained = resolve(sb.worktreePath).startsWith(sandboxesRoot) &&
|
|
448
|
+
resolve(sb.worktreePath) === resolve(safeWorktree);
|
|
449
|
+
const guardsPass = branchInNamespace && branchMatches && worktreeContained;
|
|
450
|
+
if (!guardsPass) {
|
|
451
|
+
audit({
|
|
452
|
+
action: 'sandbox:remove',
|
|
453
|
+
repo: sb.sourceRepo,
|
|
454
|
+
sandboxId: sb.id,
|
|
455
|
+
summary: 'refused git cleanup: metadata failed branch-prefix/containment guard',
|
|
456
|
+
result: 'refused',
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
// 1. Remove the worktree registration from the source repo (best-effort).
|
|
460
|
+
// --force handles a dirty / committed-in worktree. Only attempted when the
|
|
461
|
+
// source repo still exists as a git repo AND the guards passed. We target
|
|
462
|
+
// the RE-DERIVED safe values, not the raw metadata.
|
|
463
|
+
if (guardsPass && isRepo(sb.sourceRepo)) {
|
|
464
|
+
// `git worktree remove` also deletes the worktree directory. If the dir is
|
|
465
|
+
// already gone, prune first so git's bookkeeping is consistent, then ignore.
|
|
466
|
+
gitTry(sb.sourceRepo, ['worktree', 'remove', '--force', safeWorktree]);
|
|
467
|
+
gitTry(sb.sourceRepo, ['worktree', 'prune']);
|
|
468
|
+
// 2. Delete the scratch branch (a sandbox-only ref — never a user branch).
|
|
469
|
+
// safeBranch is guaranteed to start with BRANCH_PREFIX.
|
|
470
|
+
gitTry(sb.sourceRepo, ['branch', '-D', safeBranch]);
|
|
471
|
+
}
|
|
472
|
+
// 3. Clean up the per-sandbox home (worktree leftovers + metadata).
|
|
473
|
+
// rmSync with force never throws on a missing path.
|
|
474
|
+
try {
|
|
475
|
+
rmSync(home, { recursive: true, force: true });
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
// already gone / unremovable — idempotent cleanup, never throw
|
|
479
|
+
}
|
|
480
|
+
audit({
|
|
481
|
+
action: 'sandbox:remove',
|
|
482
|
+
repo: sb.sourceRepo,
|
|
483
|
+
sandboxId: sb.id,
|
|
484
|
+
summary: guardsPass
|
|
485
|
+
? `removed worktree + branch ${safeBranch}`
|
|
486
|
+
: `local cleanup only (git ops refused) for ${sb.id}`,
|
|
487
|
+
result: 'ok',
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
// listSandboxes
|
|
492
|
+
// ---------------------------------------------------------------------------
|
|
493
|
+
/**
|
|
494
|
+
* Enumerate persisted sandbox metadata under sandboxesDir(). Returns [] when
|
|
495
|
+
* none. Never throws on a malformed/partial entry (skips it).
|
|
496
|
+
*/
|
|
497
|
+
export function listSandboxes() {
|
|
498
|
+
const dir = sandboxesDir();
|
|
499
|
+
if (!existsSync(dir))
|
|
500
|
+
return [];
|
|
501
|
+
let ids;
|
|
502
|
+
try {
|
|
503
|
+
ids = readdirSync(dir, { withFileTypes: true })
|
|
504
|
+
.filter((e) => e.isDirectory())
|
|
505
|
+
.map((e) => e.name);
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
return [];
|
|
509
|
+
}
|
|
510
|
+
const out = [];
|
|
511
|
+
for (const id of ids) {
|
|
512
|
+
const sb = readMeta(id);
|
|
513
|
+
if (sb)
|
|
514
|
+
out.push(sb);
|
|
515
|
+
}
|
|
516
|
+
return out;
|
|
517
|
+
}
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
// sweepOrphanSandboxes (H2 — crash-recovery, LOCAL-ONLY)
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
/**
|
|
522
|
+
* Sweep ORPHANED sandboxes on restart (H2 crash-recovery). A swarm creates a
|
|
523
|
+
* worktree, uses it, then `removeSandbox`es it — all within a single in-memory
|
|
524
|
+
* `runSwarm` call. The sandbox lifetime is NOT persisted on the SwarmRun (there
|
|
525
|
+
* is no `sandboxId` field on a SwarmRun; the only `sandboxId` references are
|
|
526
|
+
* audit-log entries), so a process killed AFTER `git worktree add` but BEFORE
|
|
527
|
+
* `removeSandbox` leaves a worktree on disk under ~/.ashlr/sandboxes/<id>/ with
|
|
528
|
+
* no record linking it back to any swarm. Such a leftover would otherwise
|
|
529
|
+
* accumulate forever.
|
|
530
|
+
*
|
|
531
|
+
* LIVENESS (important — read before wiring a caller): a persisted sandbox is NOT
|
|
532
|
+
* necessarily an orphan. A `runSwarm` keeps its sandbox metadata on disk for the
|
|
533
|
+
* WHOLE run (createSandbox writes it at start, removeSandbox deletes it at the
|
|
534
|
+
* end), and the daemon runs several sandboxed swarms CONCURRENTLY, each with a
|
|
535
|
+
* live on-disk sandbox; a separately-launched `ashlr swarm` in another process is
|
|
536
|
+
* also possible (the re-entrancy guards are per-process). To NEVER force-remove a
|
|
537
|
+
* live worktree, the sweep uses TWO guards, in order:
|
|
538
|
+
*
|
|
539
|
+
* 1. POSITIVE liveness (primary, age-independent): a sandbox whose recorded
|
|
540
|
+
* `ownerPid` is still alive (ownerAlive()) is SKIPPED regardless of age — so
|
|
541
|
+
* a long-running swarm (same- OR cross-process) older than any staleMs is
|
|
542
|
+
* never reclaimed out from under its owner. This closes the central gap that
|
|
543
|
+
* createdAt-age alone could not (there is no wall-clock cap on a swarm).
|
|
544
|
+
* 2. AGE fallback (secondary): for a sandbox with NO usable ownerPid (older
|
|
545
|
+
* metadata, or a crash fixture that models a GONE owner), the OPTIONAL
|
|
546
|
+
* `staleMs` guard applies — a sandbox whose `createdAt` is younger than
|
|
547
|
+
* `staleMs` is SKIPPED (conservatively assumed possibly-live). With NO
|
|
548
|
+
* `staleMs` (the default, used by recovery PROOF tests where every sandbox is
|
|
549
|
+
* a deliberately-dropped orphan with a dead/absent owner) it sweeps every
|
|
550
|
+
* listed sandbox not protected by guard 1.
|
|
551
|
+
*
|
|
552
|
+
* This sweep composes the two EXISTING safe primitives — `listSandboxes()` to
|
|
553
|
+
* enumerate persisted sandbox metadata, then `removeSandbox()` for each — so it
|
|
554
|
+
* inherits all of removeSandbox's containment guards verbatim: it only ever runs
|
|
555
|
+
* `git worktree remove` / `git branch -D` against a path RE-DERIVED to live under
|
|
556
|
+
* `sandboxesDir()` and a branch RE-DERIVED into the `ashlr/sandbox/<id>` namespace
|
|
557
|
+
* (a metadata mismatch falls through to LOCAL dir cleanup only — never a git op on
|
|
558
|
+
* an arbitrary branch/path). It therefore can NEVER touch a user's working tree,
|
|
559
|
+
* index, HEAD, or any user branch.
|
|
560
|
+
*
|
|
561
|
+
* LOCAL-ONLY by construction: it adds NO outward capability (cleanup is purely
|
|
562
|
+
* inward — it removes only ashlr/sandbox/* worktrees + scratch refs, pushes
|
|
563
|
+
* nothing, opens no PR, applies no proposal), weakens NO guard (every removal
|
|
564
|
+
* goes through removeSandbox's full guard set), and changes NO happy-path
|
|
565
|
+
* behavior (a clean run already removes its own sandbox, so a healthy install
|
|
566
|
+
* has nothing to sweep). H5 wires this into daemon start (loop.ts, staleMs=
|
|
567
|
+
* ORPHAN_STALE_MS), the disk-cap pre-sweep above, and `ashlr sandbox gc` — every
|
|
568
|
+
* caller passes a staleMs and is additionally protected by the age-independent
|
|
569
|
+
* ownerAlive() guard, so no LIVE worktree is ever force-removed. Idempotent
|
|
570
|
+
* and never throws — a removal failure on one id is swallowed so the sweep always
|
|
571
|
+
* makes maximal progress. Returns the ids it swept so a caller / test can assert
|
|
572
|
+
* what was reclaimed.
|
|
573
|
+
*
|
|
574
|
+
* @param opts.staleMs Optional age guard (ms). Skip any sandbox whose `createdAt`
|
|
575
|
+
* is younger than this — a conservative liveness proxy so a concurrently-live
|
|
576
|
+
* sandbox is never force-removed. Omit to sweep all listed sandboxes.
|
|
577
|
+
*/
|
|
578
|
+
export function sweepOrphanSandboxes(opts) {
|
|
579
|
+
const staleMs = opts?.staleMs;
|
|
580
|
+
const now = Date.now();
|
|
581
|
+
const swept = [];
|
|
582
|
+
for (const sb of listSandboxes()) {
|
|
583
|
+
// GUARD 1 — POSITIVE liveness (age-independent): never reclaim a worktree
|
|
584
|
+
// whose owning process is still alive, even if it is older than staleMs.
|
|
585
|
+
// This is what makes the sweep safe despite the absence of a swarm
|
|
586
|
+
// wall-clock cap — a live in-flight worktree (any process, any age) is
|
|
587
|
+
// protected by its still-alive owner pid.
|
|
588
|
+
if (ownerAlive(sb)) {
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
// GUARD 2 — AGE fallback for a sandbox with no usable ownerPid (owner gone
|
|
592
|
+
// or older metadata).
|
|
593
|
+
if (staleMs !== undefined) {
|
|
594
|
+
const createdMs = Date.parse(sb.createdAt);
|
|
595
|
+
// Skip not-yet-stale sandboxes (possibly a live owner whose pid we could
|
|
596
|
+
// not read). An unparseable createdAt is treated as stale=0
|
|
597
|
+
// (Number.isNaN) so a corrupt timestamp is still reclaimable rather than
|
|
598
|
+
// stranded forever.
|
|
599
|
+
if (!Number.isNaN(createdMs) && now - createdMs < staleMs) {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
try {
|
|
604
|
+
removeSandbox(sb);
|
|
605
|
+
swept.push(sb.id);
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
// removeSandbox is already best-effort/idempotent and should not throw;
|
|
609
|
+
// guard anyway so one bad entry never aborts the whole restart sweep.
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return swept;
|
|
613
|
+
}
|
|
614
|
+
// ---------------------------------------------------------------------------
|
|
615
|
+
// sweepRepoSandboxes (H7 — scoped one-command rollback cleanup, LOCAL-ONLY)
|
|
616
|
+
// ---------------------------------------------------------------------------
|
|
617
|
+
/**
|
|
618
|
+
* Reclaim the on-disk sandboxes belonging to ONE specific source repo — the
|
|
619
|
+
* scoped cleanup behind `ashlr onboard --rollback <repo>` (H7 BUILD ITEM 4).
|
|
620
|
+
*
|
|
621
|
+
* WHY a distinct sweep (not sweepOrphanSandboxes): the generic orphan sweep is a
|
|
622
|
+
* BACKGROUND restart heuristic — it protects a possibly-live owner whose pid is
|
|
623
|
+
* unreadable by ALSO requiring `createdAt` to be older than `staleMs` (6h). But a
|
|
624
|
+
* crash-leftover worktree from the VERY activation the user is now explicitly
|
|
625
|
+
* undoing is, by definition, fresh (< 6h old), so the generic sweep would SKIP it
|
|
626
|
+
* and the rollback would leave it on disk — contradicting "one-command undo of a
|
|
627
|
+
* first activation". For an explicit, user-requested cleanup of a NAMED repo that
|
|
628
|
+
* the user just unenrolled, the age heuristic is unwarranted.
|
|
629
|
+
*
|
|
630
|
+
* SAFETY (the real boundary is preserved): this drops ONLY the age guard, never
|
|
631
|
+
* the LIVE-OWNER guard. It still SKIPS any sandbox whose recorded `ownerPid` is
|
|
632
|
+
* still alive (ownerAlive()), so a running in-flight swarm's worktree is NEVER
|
|
633
|
+
* force-removed — even a fresh one. It is SCOPED: only sandboxes whose
|
|
634
|
+
* `sourceRepo` resolves to `resolve(repo)` are considered, so it never touches a
|
|
635
|
+
* different repo's sandboxes. Each removal goes through removeSandbox(), so it
|
|
636
|
+
* inherits the full branch-prefix / path-containment guards verbatim and can
|
|
637
|
+
* NEVER touch a user's working tree, index, HEAD, or any user branch.
|
|
638
|
+
*
|
|
639
|
+
* LOCAL-ONLY by construction: adds NO outward capability (inward cleanup only —
|
|
640
|
+
* removes only ashlr/sandbox/* worktrees + scratch refs), weakens NO guard.
|
|
641
|
+
* Idempotent; never throws — a failure on one id is swallowed so the sweep makes
|
|
642
|
+
* maximal progress. Returns the ids it reclaimed.
|
|
643
|
+
*
|
|
644
|
+
* @param repo The source repo whose sandboxes to reclaim (resolved internally).
|
|
645
|
+
*/
|
|
646
|
+
export function sweepRepoSandboxes(repo) {
|
|
647
|
+
const target = resolve(repo);
|
|
648
|
+
const swept = [];
|
|
649
|
+
for (const sb of listSandboxes()) {
|
|
650
|
+
// SCOPE: only this repo's sandboxes.
|
|
651
|
+
if (resolve(sb.sourceRepo) !== target) {
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
// GUARD — POSITIVE liveness (the one guard we KEEP): never reclaim a
|
|
655
|
+
// worktree whose owning process is still alive, regardless of age. A live
|
|
656
|
+
// in-flight swarm (any age) is protected; the age guard is intentionally
|
|
657
|
+
// dropped because this is an explicit user-requested cleanup of a just-
|
|
658
|
+
// unenrolled repo, not a background restart heuristic.
|
|
659
|
+
if (ownerAlive(sb)) {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
try {
|
|
663
|
+
removeSandbox(sb);
|
|
664
|
+
swept.push(sb.id);
|
|
665
|
+
}
|
|
666
|
+
catch {
|
|
667
|
+
// removeSandbox is best-effort/idempotent; guard so one bad entry never
|
|
668
|
+
// aborts the whole scoped sweep.
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return swept;
|
|
672
|
+
}
|
|
673
|
+
//# sourceMappingURL=worktree.js.map
|