@1aboveio/skills 0.10.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 +3 -0
- package/README.md +90 -0
- package/bin/1aboveio-skills.mjs +18 -0
- package/package.json +28 -0
- package/runtime/skills/distribution/generated/recipes.json +1189 -0
- package/runtime/skills/distribution/scripts/bundles.mjs +280 -0
- package/runtime/skills/engineering/engineering-runtime/scripts/main-module.mjs +80 -0
- package/skills/backend/airflow-dag-develop/LICENSE +3 -0
- package/skills/backend/airflow-dag-develop/SKILL.md +111 -0
- package/skills/backend/app-debug/LICENSE +3 -0
- package/skills/backend/app-debug/SKILL.md +109 -0
- package/skills/backend/app-debug/references/common-errors.md +128 -0
- package/skills/backend/python-backend/LICENSE +3 -0
- package/skills/backend/python-backend/SKILL.md +326 -0
- package/skills/cicd-pipeline/cloud-build/LICENSE +3 -0
- package/skills/cicd-pipeline/cloud-build/SKILL.md +707 -0
- package/skills/cicd-pipeline/cloud-debug/LICENSE +3 -0
- package/skills/cicd-pipeline/cloud-debug/SKILL.md +316 -0
- package/skills/cicd-pipeline/cloud-debug/references/build-failures.md +238 -0
- package/skills/cicd-pipeline/cloud-debug/references/deploy-failures.md +376 -0
- package/skills/cicd-pipeline/cloud-debug/references/pipeline-failures.md +378 -0
- package/skills/cicd-pipeline/cloud-deploy/LICENSE +3 -0
- package/skills/cicd-pipeline/cloud-deploy/SKILL.md +229 -0
- package/skills/cicd-pipeline/cloud-deploy/references/config-templates.md +257 -0
- package/skills/cicd-pipeline/docker/LICENSE +3 -0
- package/skills/cicd-pipeline/docker/SKILL.md +126 -0
- package/skills/cicd-pipeline/google-cloud/LICENSE +3 -0
- package/skills/cicd-pipeline/google-cloud/SKILL.md +118 -0
- package/skills/cicd-pipeline/google-cloud/references/gcs.md +469 -0
- package/skills/cicd-pipeline/google-cloud/references/iam.md +451 -0
- package/skills/cicd-pipeline/google-cloud/references/project.md +349 -0
- package/skills/cicd-pipeline/google-cloud/references/secrets.md +336 -0
- package/skills/cicd-pipeline/google-cloud/references/vpc.md +312 -0
- package/skills/cicd-pipeline/google-cloud/scripts/create-sa.sh +36 -0
- package/skills/cicd-pipeline/google-cloud/scripts/gcp-config.sh +31 -0
- package/skills/cicd-pipeline/google-cloud/scripts/grant-iap.sh +41 -0
- package/skills/cicd-pipeline/google-cloud/scripts/setup-secrets.sh +48 -0
- package/skills/cicd-pipeline/mergify/LICENSE +3 -0
- package/skills/cicd-pipeline/mergify/SKILL.md +138 -0
- package/skills/cicd-pipeline/mergify/assets/templates/mergify.yml +237 -0
- package/skills/cicd-pipeline/mergify/assets/templates/ruleset.json +46 -0
- package/skills/cicd-pipeline/mergify/references/branch-protection.md +277 -0
- package/skills/cicd-pipeline/mergify/references/configuration.md +183 -0
- package/skills/cicd-pipeline/mergify/references/diagnosis.md +73 -0
- package/skills/cicd-pipeline/mergify/references/traps.md +78 -0
- package/skills/cicd-pipeline/mergify/references/watch-contract.md +218 -0
- package/skills/cicd-pipeline/mergify/scripts/audit-core.mjs +131 -0
- package/skills/cicd-pipeline/mergify/scripts/audit.mjs +4 -0
- package/skills/cicd-pipeline/mergify/scripts/watch-pr-delivery-core.mjs +663 -0
- package/skills/cicd-pipeline/mergify/scripts/watch-pr-delivery.mjs +4 -0
- package/skills/cicd-pipeline/podman/LICENSE +3 -0
- package/skills/cicd-pipeline/podman/SKILL.md +70 -0
- package/skills/cicd-pipeline/podman/agents/openai.yaml +4 -0
- package/skills/cicd-pipeline/podman/assets/templates/podman-compose-socket-directory.yml +6 -0
- package/skills/cicd-pipeline/podman/assets/templates/podman-service-override.conf +3 -0
- package/skills/cicd-pipeline/podman/references/compose-compatibility.md +70 -0
- package/skills/cicd-pipeline/podman/references/networking-and-ports.md +74 -0
- package/skills/cicd-pipeline/podman/references/rootless-services-and-sockets.md +156 -0
- package/skills/cicd-pipeline/podman/references/troubleshooting.md +98 -0
- package/skills/engineering/e2e-test/LICENSE +3 -0
- package/skills/engineering/e2e-test/SKILL.md +156 -0
- package/skills/engineering/e2e-test/assets/ci-gates.cloudbuild.yaml +272 -0
- package/skills/engineering/e2e-test/assets/ci-gates.github.yml +451 -0
- package/skills/engineering/e2e-test/assets/e2e-workflow.yml +282 -0
- package/skills/engineering/e2e-test/references/authoring/auth-flows.md +159 -0
- package/skills/engineering/e2e-test/references/authoring/playwright-config.md +71 -0
- package/skills/engineering/e2e-test/references/authoring/playwright-patterns.md +219 -0
- package/skills/engineering/e2e-test/references/authoring/test-skipping.md +44 -0
- package/skills/engineering/e2e-test/references/ci-integration.md +121 -0
- package/skills/engineering/e2e-test/references/ci-playwright-container.md +280 -0
- package/skills/engineering/e2e-test/references/debugging.md +36 -0
- package/skills/engineering/e2e-test/references/presentation-sweep.md +131 -0
- package/skills/engineering/e2e-test/references/reviewing.md +39 -0
- package/skills/engineering/e2e-test/references/route-discovery.md +50 -0
- package/skills/engineering/e2e-test/references/route-manifest.md +44 -0
- package/skills/engineering/e2e-test/scripts/detect-routes-fastapi.py +290 -0
- package/skills/engineering/e2e-test/scripts/detect-routes-nextjs.mjs +200 -0
- package/skills/engineering/e2e-test/scripts/post-visual-evidence.mjs +158 -0
- package/skills/engineering/e2e-test/scripts/presentation-checks.mjs +171 -0
- package/skills/engineering/e2e-test/scripts/presentation-perceivability.mjs +179 -0
- package/skills/engineering/e2e-test/scripts/presentation-reachability.mjs +154 -0
- package/skills/engineering/e2e-test/scripts/presentation-render-health.mjs +141 -0
- package/skills/engineering/e2e-test/scripts/presentation-sweep.mjs +148 -0
- package/skills/engineering/e2e-test/scripts/presentation-temporal.mjs +127 -0
- package/skills/engineering/e2e-test/scripts/presentation-visual.mjs +84 -0
- package/skills/engineering/e2e-test/scripts/project-route-manifest.mjs +75 -0
- package/skills/engineering/e2e-test/scripts/validate-manifest.mjs +106 -0
- package/skills/engineering/engineering-runtime/LICENSE +3 -0
- package/skills/engineering/engineering-runtime/SKILL.md +10 -0
- package/skills/engineering/engineering-runtime/agents/openai.yaml +6 -0
- package/skills/engineering/engineering-runtime/coherence/workflow.json +553 -0
- package/skills/engineering/engineering-runtime/scripts/exact-head-artifact.mjs +131 -0
- package/skills/engineering/engineering-runtime/scripts/head-check-set.mjs +398 -0
- package/skills/engineering/engineering-runtime/scripts/main-module.mjs +80 -0
- package/skills/engineering/engineering-runtime/scripts/mergify-yaml.mjs +11 -0
- package/skills/engineering/engineering-runtime/scripts/package-lock.json +43 -0
- package/skills/engineering/engineering-runtime/scripts/package.json +10 -0
- package/skills/engineering/engineering-runtime/scripts/required-check-plan.mjs +223 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +576 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +166 -0
- package/skills/engineering/ensure-coverage/LICENSE +3 -0
- package/skills/engineering/ensure-coverage/SKILL.md +136 -0
- package/skills/engineering/ensure-coverage/evals/evals.json +125 -0
- package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +91 -0
- package/skills/engineering/ensure-coverage/references/breadth/inventory-contract.md +83 -0
- package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +44 -0
- package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +16 -0
- package/skills/engineering/ensure-coverage/references/depth/characterization.md +68 -0
- package/skills/engineering/ensure-coverage/references/depth/coverage.config.example.json +25 -0
- package/skills/engineering/ensure-coverage/references/depth/grading.md +35 -0
- package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +87 -0
- package/skills/engineering/ensure-coverage/references/depth/test-smells.md +23 -0
- package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +164 -0
- package/skills/engineering/ensure-coverage/references/enforcement/hooks.md +85 -0
- package/skills/engineering/ensure-coverage/references/examples/coverage-ledger.md +109 -0
- package/skills/engineering/ensure-coverage/references/examples/refund-flow.md +33 -0
- package/skills/engineering/ensure-coverage/references/presentation/axis.md +78 -0
- package/skills/engineering/ensure-coverage/references/presentation/runner-contract.md +74 -0
- package/skills/engineering/ensure-coverage/references/process/audit-mode.md +33 -0
- package/skills/engineering/ensure-coverage/references/process/output-template.md +139 -0
- package/skills/engineering/ensure-coverage/references/process/review-contract-template.md +119 -0
- package/skills/engineering/ensure-coverage/references/process/scope-class.md +178 -0
- package/skills/engineering/ensure-coverage/references/process/test-strategy.md +55 -0
- package/skills/engineering/ensure-coverage/schemas/coverage-config.schema.json +45 -0
- package/skills/engineering/ensure-coverage/schemas/coverage-file.schema.json +93 -0
- package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.mjs +178 -0
- package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.mjs +101 -0
- package/skills/engineering/ensure-coverage/scripts/ci-audit.mjs +358 -0
- package/skills/engineering/ensure-coverage/scripts/coverage-checklist.mjs +494 -0
- package/skills/engineering/ensure-coverage/scripts/coverage-ledger.mjs +663 -0
- package/skills/engineering/ensure-coverage/scripts/design-parity.mjs +591 -0
- package/skills/engineering/ensure-coverage/scripts/evidence-block.mjs +367 -0
- package/skills/engineering/ensure-coverage/scripts/lint-tests.mjs +269 -0
- package/skills/engineering/ensure-coverage/scripts/mock-policy-config.mjs +176 -0
- package/skills/engineering/ensure-coverage/scripts/package-lock.json +76 -0
- package/skills/engineering/ensure-coverage/scripts/package.json +19 -0
- package/skills/engineering/ensure-coverage/scripts/scope-class.mjs +554 -0
- package/skills/engineering/harness-runtime/LICENSE +3 -0
- package/skills/engineering/harness-runtime/SKILL.md +18 -0
- package/skills/engineering/harness-runtime/agents/openai.yaml +6 -0
- package/skills/engineering/harness-runtime/bin/discover-models.mjs +4 -0
- package/skills/engineering/harness-runtime/bin/model-catalog.mjs +4 -0
- package/skills/engineering/harness-runtime/contracts.md +15 -0
- package/skills/engineering/harness-runtime/discover-models.mjs +392 -0
- package/skills/engineering/harness-runtime/fixtures/native-question-schemas.json +33 -0
- package/skills/engineering/harness-runtime/fixtures/question-responses.json +54 -0
- package/skills/engineering/harness-runtime/index.mjs +767 -0
- package/skills/engineering/harness-runtime/model-catalog.mjs +787 -0
- package/skills/engineering/harness-runtime/native-question-contracts.md +37 -0
- package/skills/engineering/harness-runtime/references/model-catalog-seed.json +159 -0
- package/skills/engineering/harness-runtime/references/model-catalog.md +57 -0
- package/skills/engineering/implement-and-pr/LICENSE +3 -0
- package/skills/engineering/implement-and-pr/SKILL.md +176 -0
- package/skills/engineering/implement-and-pr/references/ci-iteration.md +10 -0
- package/skills/engineering/implement-and-pr/references/closeout.md +27 -0
- package/skills/engineering/implement-and-pr/references/contract-complete-fix-rounds.md +34 -0
- package/skills/engineering/implement-and-pr/references/evidence-rules.md +39 -0
- package/skills/engineering/implement-and-pr/references/incremental-plan.md +16 -0
- package/skills/engineering/implement-and-pr/references/self-review.md +23 -0
- package/skills/engineering/implement-and-pr/references/tdd-mode.md +18 -0
- package/skills/engineering/resolve-issues/LICENSE +3 -0
- package/skills/engineering/resolve-issues/SKILL.md +167 -0
- package/skills/engineering/resolve-issues/generated/workflow-repair-policy.json +448 -0
- package/skills/engineering/resolve-issues/references/breaker.md +82 -0
- package/skills/engineering/resolve-issues/references/deliverables.md +27 -0
- package/skills/engineering/resolve-issues/references/delivery.md +108 -0
- package/skills/engineering/resolve-issues/references/evidence-lane.md +21 -0
- package/skills/engineering/resolve-issues/references/exact-head-ci.md +287 -0
- package/skills/engineering/resolve-issues/references/fan-out.md +33 -0
- package/skills/engineering/resolve-issues/references/finalization.md +68 -0
- package/skills/engineering/resolve-issues/references/guarantees.md +10 -0
- package/skills/engineering/resolve-issues/references/high-risk.md +29 -0
- package/skills/engineering/resolve-issues/references/incidents/848/README.md +156 -0
- package/skills/engineering/resolve-issues/references/intake.md +86 -0
- package/skills/engineering/resolve-issues/references/integration-gate.md +53 -0
- package/skills/engineering/resolve-issues/references/interference.md +87 -0
- package/skills/engineering/resolve-issues/references/loop.md +134 -0
- package/skills/engineering/resolve-issues/references/model-catalog.md +9 -0
- package/skills/engineering/resolve-issues/references/postmortem.md +27 -0
- package/skills/engineering/resolve-issues/references/pre-flight-model-slots.md +41 -0
- package/skills/engineering/resolve-issues/references/pre-flight-recording-and-checkout.md +48 -0
- package/skills/engineering/resolve-issues/references/pre-flight.md +41 -0
- package/skills/engineering/resolve-issues/references/regression-checklist.md +26 -0
- package/skills/engineering/resolve-issues/references/run-state.md +288 -0
- package/skills/engineering/resolve-issues/references/sandboxed-testing.md +48 -0
- package/skills/engineering/resolve-issues/references/spawn-contract.md +96 -0
- package/skills/engineering/resolve-issues/references/terminal-evidence-journal.md +40 -0
- package/skills/engineering/resolve-issues/references/why.md +653 -0
- package/skills/engineering/resolve-issues/schemas/fix-round.schema.json +49 -0
- package/skills/engineering/resolve-issues/scripts/combine-and-verify.mjs +721 -0
- package/skills/engineering/resolve-issues/scripts/component-candidate.mjs +962 -0
- package/skills/engineering/resolve-issues/scripts/contract-revision.mjs +220 -0
- package/skills/engineering/resolve-issues/scripts/detect-delivery-mode.mjs +420 -0
- package/skills/engineering/resolve-issues/scripts/detect-target-branch.mjs +256 -0
- package/skills/engineering/resolve-issues/scripts/detect-workspace-mode.mjs +168 -0
- package/skills/engineering/resolve-issues/scripts/discover-models.mjs +9 -0
- package/skills/engineering/resolve-issues/scripts/doctrine.mjs +62 -0
- package/skills/engineering/resolve-issues/scripts/evidence-lifecycle-contract.mjs +191 -0
- package/skills/engineering/resolve-issues/scripts/exact-head-ci.mjs +413 -0
- package/skills/engineering/resolve-issues/scripts/exact-head-github-provider.mjs +332 -0
- package/skills/engineering/resolve-issues/scripts/finalize.mjs +488 -0
- package/skills/engineering/resolve-issues/scripts/fix-rounds.mjs +3307 -0
- package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-circular-1001.json +16 -0
- package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-valid-sequencing.json +51 -0
- package/skills/engineering/resolve-issues/scripts/fixtures/fmm-express-830-component-candidate.json +17 -0
- package/skills/engineering/resolve-issues/scripts/fixtures/head-check-set-1081.json +166 -0
- package/skills/engineering/resolve-issues/scripts/gate-value-series.mjs +92 -0
- package/skills/engineering/resolve-issues/scripts/guide-index.mjs +73 -0
- package/skills/engineering/resolve-issues/scripts/head-check-set.mjs +159 -0
- package/skills/engineering/resolve-issues/scripts/interference.mjs +427 -0
- package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +9 -0
- package/skills/engineering/resolve-issues/scripts/next-operations.mjs +419 -0
- package/skills/engineering/resolve-issues/scripts/postmortem.mjs +909 -0
- package/skills/engineering/resolve-issues/scripts/preflight-questions.mjs +322 -0
- package/skills/engineering/resolve-issues/scripts/reconcile-contained-unit-prs.mjs +415 -0
- package/skills/engineering/resolve-issues/scripts/release-state-contract.mjs +697 -0
- package/skills/engineering/resolve-issues/scripts/report.mjs +494 -0
- package/skills/engineering/resolve-issues/scripts/required-check-plan.mjs +172 -0
- package/skills/engineering/resolve-issues/scripts/round-metadata.mjs +79 -0
- package/skills/engineering/resolve-issues/scripts/run-state-review6-cases.mjs +334 -0
- package/skills/engineering/resolve-issues/scripts/run-state.mjs +4784 -0
- package/skills/engineering/resolve-issues/scripts/sandbox-selftest.mjs +395 -0
- package/skills/engineering/resolve-issues/scripts/spawn-contract.mjs +290 -0
- package/skills/engineering/resolve-issues/scripts/terminal-dispositions.mjs +170 -0
- package/skills/engineering/resolve-issues/scripts/terminal-evidence-journal.mjs +293 -0
- package/skills/engineering/resolve-issues/scripts/unit-kind.mjs +197 -0
- package/skills/engineering/resolve-issues/scripts/unit-lifecycle.mjs +127 -0
- package/skills/engineering/resolve-issues/scripts/watch-delivery.mjs +893 -0
- package/skills/engineering/resolve-issues/scripts/workspaces.mjs +829 -0
- package/skills/engineering/resolve-issues/workflows/independent-review.workflow.js +290 -0
- package/skills/engineering/resolve-issues/workflows/prior-art-scan.workflow.js +80 -0
- package/skills/engineering/resolve-issues/workflows/workflow-smoke.mjs +102 -0
- package/skills/engineering/resolve-release/LICENSE +3 -0
- package/skills/engineering/resolve-release/SKILL.md +112 -0
- package/skills/engineering/resolve-release/references/assembly.md +137 -0
- package/skills/engineering/resolve-release/references/auto-when-green.md +56 -0
- package/skills/engineering/resolve-release/references/candidate.md +167 -0
- package/skills/engineering/resolve-release/references/exposure.md +178 -0
- package/skills/engineering/resolve-release/references/handoff.md +24 -0
- package/skills/engineering/resolve-release/references/postmortem.md +230 -0
- package/skills/engineering/resolve-release/references/preflight.md +207 -0
- package/skills/engineering/resolve-release/references/principles.md +94 -0
- package/skills/engineering/resolve-release/references/regression-checklist.md +36 -0
- package/skills/engineering/resolve-release/references/related-skills.md +12 -0
- package/skills/engineering/resolve-release/references/routing.md +149 -0
- package/skills/engineering/resolve-release/references/verified-sha-github-flow.md +285 -0
- package/skills/engineering/resolve-release/references/versioning.md +202 -0
- package/skills/engineering/resolve-release/references/why.md +53 -0
- package/skills/engineering/resolve-release/scripts/adapter-completion-artifact.mjs +389 -0
- package/skills/engineering/resolve-release/scripts/build-changes.mjs +209 -0
- package/skills/engineering/resolve-release/scripts/candidate-hygiene.mjs +407 -0
- package/skills/engineering/resolve-release/scripts/candidate-identity.mjs +904 -0
- package/skills/engineering/resolve-release/scripts/candidate-traffic.mjs +81 -0
- package/skills/engineering/resolve-release/scripts/checked-adapter-loader.mjs +612 -0
- package/skills/engineering/resolve-release/scripts/close-attempt.mjs +135 -0
- package/skills/engineering/resolve-release/scripts/closeout-release.mjs +161 -0
- package/skills/engineering/resolve-release/scripts/doctrine.mjs +106 -0
- package/skills/engineering/resolve-release/scripts/durable-processing.mjs +522 -0
- package/skills/engineering/resolve-release/scripts/ensure-target-green.mjs +659 -0
- package/skills/engineering/resolve-release/scripts/evidence-bundle.mjs +1014 -0
- package/skills/engineering/resolve-release/scripts/finalize-release.mjs +526 -0
- package/skills/engineering/resolve-release/scripts/fixtures/durable-processing-adapter.mjs +169 -0
- package/skills/engineering/resolve-release/scripts/green-gate.mjs +599 -0
- package/skills/engineering/resolve-release/scripts/isolated-adapter-evaluator.mjs +752 -0
- package/skills/engineering/resolve-release/scripts/metadata-pr-status.mjs +56 -0
- package/skills/engineering/resolve-release/scripts/metadata-sync.mjs +1538 -0
- package/skills/engineering/resolve-release/scripts/postmortem.mjs +381 -0
- package/skills/engineering/resolve-release/scripts/preflight-probes.mjs +498 -0
- package/skills/engineering/resolve-release/scripts/production-endpoints.mjs +326 -0
- package/skills/engineering/resolve-release/scripts/rc-circuit-breaker.mjs +272 -0
- package/skills/engineering/resolve-release/scripts/report.mjs +417 -0
- package/skills/engineering/resolve-release/scripts/reprobe-credentials.mjs +114 -0
- package/skills/engineering/resolve-release/scripts/revalidate-candidate.mjs +238 -0
- package/skills/engineering/resolve-release/scripts/review-packet.mjs +503 -0
- package/skills/engineering/resolve-release/scripts/rollback-floor.mjs +263 -0
- package/skills/engineering/resolve-release/scripts/version-assert.mjs +339 -0
- package/skills/engineering/resolve-release/scripts/version-postmortem.mjs +485 -0
- package/skills/engineering/resolve-release/scripts/version.mjs +1199 -0
- package/skills/engineering/resolve-release/scripts/watch-candidate-delivery.mjs +449 -0
- package/skills/engineering/resolve-release/vendor/ACORN-LICENSE +21 -0
- package/skills/engineering/resolve-release/vendor/README.md +60 -0
- package/skills/engineering/resolve-release/vendor/acorn.mjs +6233 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/LICENSE +21 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/README.md +341 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/README.template.md +70 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs +7 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.mts +2033 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.ts +2033 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js +7 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/package.json +49 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/LICENSE +21 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/README.md +5 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.mts +549 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.ts +549 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/package.json +36 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/LICENSE +47 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/README.md +82 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.d.ts +11 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.mjs +22 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cjs +21 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.cjs +21 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.d.ts +11 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.d.ts +11 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.mjs +25 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.wasm +0 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.mts +85 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.ts +85 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.mts +20 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.ts +20 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/package.json +61 -0
- package/skills/engineering/review-pr/LICENSE +3 -0
- package/skills/engineering/review-pr/SKILL.md +123 -0
- package/skills/engineering/review-pr/references/adversarial-reviewer-prompt.md +42 -0
- package/skills/engineering/review-pr/references/code-correctness.md +5 -0
- package/skills/engineering/review-pr/references/contract-freshness.md +9 -0
- package/skills/engineering/review-pr/references/coordination.md +18 -0
- package/skills/engineering/review-pr/references/domain-hazards.md +123 -0
- package/skills/engineering/review-pr/references/finding-themes.md +7 -0
- package/skills/engineering/review-pr/references/github-posting.md +98 -0
- package/skills/engineering/review-pr/references/golden-path-smoke.md +5 -0
- package/skills/engineering/review-pr/references/incremental-output.md +16 -0
- package/skills/engineering/review-pr/references/inputs-and-discovery.md +31 -0
- package/skills/engineering/review-pr/references/output-format.md +99 -0
- package/skills/engineering/review-pr/references/over-mock-screen.md +7 -0
- package/skills/engineering/review-pr/references/promotion-prs.md +16 -0
- package/skills/engineering/review-pr/references/re-review.md +18 -0
- package/skills/engineering/review-pr/references/review-method.md +199 -0
- package/skills/engineering/review-pr/references/review-mode.md +30 -0
- package/skills/engineering/review-pr/references/review-posture.md +53 -0
- package/skills/engineering/review-pr/references/round1-depth.md +62 -0
- package/skills/engineering/review-pr/references/scripts.md +17 -0
- package/skills/engineering/review-pr/references/workflow.md +16 -0
- package/skills/engineering/review-pr/schemas/findings.schema.json +282 -0
- package/skills/engineering/review-pr/scripts/finding-contract.mjs +285 -0
- package/skills/engineering/review-pr/scripts/post-review.mjs +405 -0
- package/skills/engineering/review-pr/scripts/pr-context.mjs +207 -0
- package/skills/engineering/review-pr/scripts/scan-diff.mjs +365 -0
- package/skills/engineering/review-pr/scripts/theme-contract.mjs +57 -0
- package/skills/engineering/smoke/LICENSE +3 -0
- package/skills/engineering/smoke/SKILL.md +131 -0
- package/skills/engineering/smoke/assets/smoke.manifest.example.json +53 -0
- package/skills/engineering/smoke/references/manifest.md +192 -0
- package/skills/engineering/smoke/scripts/smoke.mjs +713 -0
- package/skills/fullstack/better-auth/LICENSE +3 -0
- package/skills/fullstack/better-auth/SKILL.md +601 -0
- package/skills/fullstack/better-auth/references/feishu-api.md +270 -0
- package/skills/fullstack/monorepo/LICENSE +3 -0
- package/skills/fullstack/monorepo/SKILL.md +465 -0
- package/skills/fullstack/nextjs-fullstack/LICENSE +3 -0
- package/skills/fullstack/nextjs-fullstack/SKILL.md +210 -0
- package/skills/fullstack/nextjs-fullstack/conventions.md +318 -0
- package/skills/fullstack/nextjs-fullstack/frontend-conventions.md +61 -0
- package/skills/fullstack/nextjs-fullstack/nextjs16.md +287 -0
- package/skills/fullstack/nextjs-fullstack/server-actions.md +409 -0
- package/skills/fullstack/prisma-setup/LICENSE +3 -0
- package/skills/fullstack/prisma-setup/SKILL.md +180 -0
- package/skills/fullstack/prisma-setup/nextjs.md +258 -0
- package/skills/fullstack/prisma-setup/turborepo.md +301 -0
- package/skills/fullstack/shadcn/LICENSE +3 -0
- package/skills/fullstack/shadcn/SKILL.md +119 -0
- package/skills/fullstack/shadcn/assets/shadcn-small.png +0 -0
- package/skills/fullstack/shadcn/assets/shadcn.png +0 -0
- package/skills/fullstack/shadcn/cli.md +411 -0
- package/skills/fullstack/shadcn/customization.md +224 -0
- package/skills/fullstack/shadcn/evals/evals.json +90 -0
- package/skills/fullstack/shadcn/mcp.md +101 -0
- package/skills/fullstack/shadcn/rules/base-vs-radix.md +323 -0
- package/skills/fullstack/shadcn/rules/component-selection.md +67 -0
- package/skills/fullstack/shadcn/rules/composition.md +195 -0
- package/skills/fullstack/shadcn/rules/data-table.md +201 -0
- package/skills/fullstack/shadcn/rules/forms.md +255 -0
- package/skills/fullstack/shadcn/rules/icons.md +103 -0
- package/skills/fullstack/shadcn/rules/styling.md +167 -0
- package/skills/fullstack/zod-v4/LICENSE +3 -0
- package/skills/fullstack/zod-v4/SKILL.md +287 -0
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
// workspaces — a tree per unit, and a reap that cannot destroy work (ADR 0001 decisions
|
|
2
|
+
// 3 and 4).
|
|
3
|
+
//
|
|
4
|
+
// A WORKSPACE belongs to a UNIT, from its first producer spawn until the unit is delivered
|
|
5
|
+
// or handed back. Fix rounds reuse it, so a dependency install is paid once per unit rather
|
|
6
|
+
// than once per round, and a spawn that dies leaves a partial its replacement resumes in
|
|
7
|
+
// place.
|
|
8
|
+
//
|
|
9
|
+
// Decision 4 is what most of this file is: **preserved on death, reaped only on success.**
|
|
10
|
+
// Nothing is destroyed while it holds unpublished work. That makes every rule here fail
|
|
11
|
+
// CLOSED — unreadable, unrecognized and still-in-use all preserve — because the two errors
|
|
12
|
+
// are not symmetric. A workspace kept too long is a directory a human deletes; a workspace
|
|
13
|
+
// removed too early is work that existed nowhere else.
|
|
14
|
+
//
|
|
15
|
+
// The defect this addresses is live, not hypothetical: `git worktree list` in this repo on
|
|
16
|
+
// 2026-07-26 showed ten leftovers across four naming schemes, at ONE workspace per run.
|
|
17
|
+
// Per-unit multiplies that by the roster, so the reap lands with the parallelism.
|
|
18
|
+
//
|
|
19
|
+
// node workspaces.mjs path <slug> <unitId> [--repo-dir DIR] # the path a unit's workspace takes
|
|
20
|
+
// node workspaces.mjs reap <slug> [--apply] [--repo-dir DIR] # plan (default) / perform the reap
|
|
21
|
+
// node workspaces.mjs --help
|
|
22
|
+
//
|
|
23
|
+
// EXIT CODES (reap):
|
|
24
|
+
// 0 every recorded workspace is accounted for AND the orphan sweep ran; nothing was
|
|
25
|
+
// preserved for cause
|
|
26
|
+
// 1 something was preserved or refused, or (under --apply) a removal FAILED and the tree
|
|
27
|
+
// is still there — read the rows before touching anything by hand
|
|
28
|
+
// 2 usage error
|
|
29
|
+
// 3 no manifest for that slug (absent is normal — run-state's own convention)
|
|
30
|
+
// 4 the orphan sweep could NOT run (no workspace root recorded, or `git worktree list`
|
|
31
|
+
// could not be read). Nothing is wrong with what was checked — the point is that the
|
|
32
|
+
// disk was not checked, and that used to exit 0 looking like a clean sweep. It
|
|
33
|
+
// OUTRANKS 1: the orchestrator's checkout is refused on every real run, so a code
|
|
34
|
+
// ranked under 1 would never be seen
|
|
35
|
+
|
|
36
|
+
import fs from 'node:fs'
|
|
37
|
+
import path from 'node:path'
|
|
38
|
+
import { execFileSync } from 'node:child_process'
|
|
39
|
+
|
|
40
|
+
import { terminalDisposition } from './terminal-dispositions.mjs'
|
|
41
|
+
// One rule for where a run's manifest lives. A second copy here would be a reap reading a
|
|
42
|
+
// different file from the one the run writes — and reaping against it. The import is
|
|
43
|
+
// one-way on purpose: `run-state.mjs` must not import this module back, because a cycle
|
|
44
|
+
// plus the entrypoint's top-level await deadlocks the CLI (caught by the bundled-script
|
|
45
|
+
// --help screen, which is what that screen is for).
|
|
46
|
+
import { fileFor, readManifest } from './run-state.mjs'
|
|
47
|
+
import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
|
|
48
|
+
|
|
49
|
+
const isObject = (v) => v != null && typeof v === 'object' && !Array.isArray(v)
|
|
50
|
+
const nonEmpty = (v) => typeof v === 'string' && v.trim().length > 0
|
|
51
|
+
const arr = (v) => (Array.isArray(v) ? v : [])
|
|
52
|
+
|
|
53
|
+
// ---- where a unit's workspace lives ----
|
|
54
|
+
|
|
55
|
+
// Beside the repo, never inside it: a workspace inside the checkout lands in `git status`,
|
|
56
|
+
// in every unit's diff, and in the glob every test discovery walks. Namespaced by run AND
|
|
57
|
+
// unit — two units sharing a tree is the corruption this whole change exists to remove, so
|
|
58
|
+
// the path derivation must make it impossible rather than merely unlikely.
|
|
59
|
+
export function unitWorkspacePathFor(toplevel, slug, unitId) {
|
|
60
|
+
const base = String(toplevel || '').replace(/\/+$/, '')
|
|
61
|
+
const safe = (v, fallback) => {
|
|
62
|
+
const s = String(v ?? '').replace(/[^\w.-]+/g, '-').replace(/^-+|-+$/g, '')
|
|
63
|
+
return s.length ? s : fallback
|
|
64
|
+
}
|
|
65
|
+
return `${base}-workspaces/${safe(slug, 'run')}/${safe(unitId, 'unit')}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---- who still holds a workspace ----
|
|
69
|
+
|
|
70
|
+
// The unit owns its tree until it is DELIVERED or HANDED BACK. Everything short of that —
|
|
71
|
+
// queued, in-progress, merge-ready-awaiting-delivery, blocked — still holds it.
|
|
72
|
+
//
|
|
73
|
+
// `blocked` is deliberately holding. A blocked unit is parked on a human decision, and its
|
|
74
|
+
// partial is exactly what that human comes back to; reaping it would delete the evidence
|
|
75
|
+
// the decision is about. `merge-ready` with no terminal outcome is the epic-986 shape: it
|
|
76
|
+
// reads as finished everywhere and is still in flight.
|
|
77
|
+
export function unitHoldsWorkspace(unit) {
|
|
78
|
+
if (!isObject(unit)) return false
|
|
79
|
+
const disposition = terminalDisposition(unit)
|
|
80
|
+
if (disposition && disposition !== 'blocked') return false
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---- observing a workspace ----
|
|
85
|
+
|
|
86
|
+
function git(args, cwd) {
|
|
87
|
+
try {
|
|
88
|
+
return { ok: true, out: execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }) }
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return { ok: false, out: '', error }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Which processes are running INSIDE a tree. `lsof -t +D <path>` lists every PID with a file
|
|
95
|
+
// or cwd under that directory; it is slow on huge trees but this runs once per workspace at
|
|
96
|
+
// reap time, not in a loop.
|
|
97
|
+
//
|
|
98
|
+
// Four answers, and the last two are NOT the same fact:
|
|
99
|
+
// [] nothing holds it (including lsof's exit-1-found-nothing)
|
|
100
|
+
// ['123', ...] these PIDs do — preserve
|
|
101
|
+
// 'unreadable' lsof exists and the call failed — preserve, same reasoning as an
|
|
102
|
+
// unreadable `git status` not being allowed to read as clean
|
|
103
|
+
// 'unavailable' lsof is not installed here — the check cannot run at all; see the catch
|
|
104
|
+
//
|
|
105
|
+
// Deliberately not a process-name allowlist: what matters is that SOMETHING holds the tree,
|
|
106
|
+
// not what it is. A node test runner, a Playwright worker and a shell all lose work the same
|
|
107
|
+
// way. See the preserve branch in reapDecision for the incident.
|
|
108
|
+
export function liveProcessesIn(workspacePath, { run = execFileSync } = {}) {
|
|
109
|
+
const p = String(workspacePath ?? '')
|
|
110
|
+
if (!p) return []
|
|
111
|
+
let out
|
|
112
|
+
try {
|
|
113
|
+
out = run('lsof', ['-t', '+D', p], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// lsof exits 1 when it simply found nothing: a clean negative, not a failure.
|
|
116
|
+
if (error && error.status === 1 && !String(error.stdout ?? '').trim()) return []
|
|
117
|
+
// NOT INSTALLED is a different fact from RAN AND FAILED, and conflating them bricks the
|
|
118
|
+
// reap. `lsof` is absent on a stock GitHub Linux runner, so treating its absence as
|
|
119
|
+
// "unreadable" made every workspace preserve — the reap stopped removing anything and
|
|
120
|
+
// four pre-existing tests went red. Caught by CI on this very change.
|
|
121
|
+
//
|
|
122
|
+
// The liveness check is an ADDITION to a ladder that was already safe on tree contents
|
|
123
|
+
// alone. Where the tool does not exist, degrading to that prior behaviour is right;
|
|
124
|
+
// refusing to reap anything is not. Where the tool DOES exist and failed, something is
|
|
125
|
+
// wrong on this machine and caution is right. Hence two values, not one — and the
|
|
126
|
+
// caller surfaces `unavailable` in the plan so the downgrade is visible rather than a
|
|
127
|
+
// guard that quietly stopped guarding.
|
|
128
|
+
if (error && (error.code === 'ENOENT' || error.status === 127)) return 'unavailable'
|
|
129
|
+
return 'unreadable'
|
|
130
|
+
}
|
|
131
|
+
const pids = String(out ?? '').split('\n').map((l) => l.trim()).filter(Boolean)
|
|
132
|
+
const self = new Set([String(process.pid), String(process.ppid)])
|
|
133
|
+
return [...new Set(pids.filter((pid) => !self.has(pid)))]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Everything the reap decision needs, read from the tree itself. Three separate kinds of
|
|
137
|
+
// unpublished work, because they fail differently:
|
|
138
|
+
//
|
|
139
|
+
// dirty modified tracked files — what `git status` is usually checked for.
|
|
140
|
+
// untracked files nobody has ever committed, let alone pushed. Cheap to overlook and
|
|
141
|
+
// total to lose.
|
|
142
|
+
// unpushed commits contained in no remote-tracking ref. This is the one a status-only
|
|
143
|
+
// check calls CLEAN: the tree is spotless and the entire unit lives in a
|
|
144
|
+
// branch that exists on one machine.
|
|
145
|
+
export function observeWorkspace(workspacePath, { runGit = git, liveProcesses = liveProcessesIn } = {}) {
|
|
146
|
+
const p = String(workspacePath ?? '')
|
|
147
|
+
const empty = { exists: false, readable: false, dirty: [], untracked: [], unpushed: [], branch: null, head: null, gitCommonDir: null, liveProcesses: [] }
|
|
148
|
+
if (!p || !fs.existsSync(p)) return empty
|
|
149
|
+
|
|
150
|
+
const live = liveProcesses(p)
|
|
151
|
+
const head = runGit(['rev-parse', 'HEAD'], p)
|
|
152
|
+
if (!head.ok) return { ...empty, exists: true, liveProcesses: live }
|
|
153
|
+
const status = runGit(['status', '--porcelain'], p)
|
|
154
|
+
// An unreadable status is NOT "clean" — that is the fabricated-evidence direction, and
|
|
155
|
+
// here it costs work rather than a review round.
|
|
156
|
+
if (!status.ok) return { ...empty, exists: true, head: head.out.trim(), liveProcesses: live }
|
|
157
|
+
|
|
158
|
+
const lines = status.out.split('\n').map((l) => l.replace(/\s+$/, '')).filter(Boolean)
|
|
159
|
+
const untracked = lines.filter((l) => l.startsWith('??'))
|
|
160
|
+
const dirty = lines.filter((l) => !l.startsWith('??'))
|
|
161
|
+
|
|
162
|
+
// Contained in NO remote-tracking ref. Worktrees share the object store and the remotes
|
|
163
|
+
// with their parent, so this reads the same refs a push would have updated.
|
|
164
|
+
const revs = runGit(['rev-list', '--max-count=50', 'HEAD', '--not', '--remotes'], p)
|
|
165
|
+
if (!revs.ok) return { exists: true, readable: false, dirty, untracked, unpushed: [], branch: null, head: head.out.trim(), liveProcesses: live }
|
|
166
|
+
const unpushed = revs.out.split('\n').map((l) => l.trim()).filter(Boolean)
|
|
167
|
+
|
|
168
|
+
const branch = runGit(['rev-parse', '--abbrev-ref', 'HEAD'], p)
|
|
169
|
+
const common = runGit(['rev-parse', '--path-format=absolute', '--git-common-dir'], p)
|
|
170
|
+
return {
|
|
171
|
+
exists: true,
|
|
172
|
+
readable: true,
|
|
173
|
+
dirty,
|
|
174
|
+
untracked,
|
|
175
|
+
unpushed,
|
|
176
|
+
branch: branch.ok ? branch.out.trim() : null,
|
|
177
|
+
head: head.out.trim(),
|
|
178
|
+
gitCommonDir: common.ok ? common.out.trim() : null,
|
|
179
|
+
liveProcesses: live,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---- the reap decision ----
|
|
184
|
+
|
|
185
|
+
// Trailing slashes are noise. The filesystem ROOT is not — `/` is a real directory, and
|
|
186
|
+
// stripping it to `''` is what turned a declared scope of `/` into "this run declared no
|
|
187
|
+
// scope" and a protected `/` into "nothing to protect". Both are the set-but-read-as-unset
|
|
188
|
+
// class (#591 F1): a value somebody set, silently read as absent, one layer down from where
|
|
189
|
+
// it was written. `''` stays `''`, because that is what unrecorded genuinely looks like.
|
|
190
|
+
const norm = (p) => {
|
|
191
|
+
const s = String(p ?? '')
|
|
192
|
+
return s.replace(/\/+$/, '') || (s.startsWith('/') ? '/' : '')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// "Nobody recorded a path", as distinct from "somebody recorded a bad one": absent, null,
|
|
196
|
+
// or a string with nothing in it. Exactly `run-state.mjs`'s `unrecordedPath` plus nullish,
|
|
197
|
+
// so the validator and the guard carve out the same set rather than two overlapping ones.
|
|
198
|
+
//
|
|
199
|
+
// This is the predicate the whole class turns on. PRESENCE and USABILITY are two questions,
|
|
200
|
+
// and `nonEmpty(v)` only answers the second — reading its `false` as "unset" is what let a
|
|
201
|
+
// non-string, a whitespace-only string and an all-slash root each arrive as a declaration
|
|
202
|
+
// nobody made, and be replaced by a scope the manifest never named. A value that is present
|
|
203
|
+
// and unusable must reach the guard and be REFUSED there, never routed around.
|
|
204
|
+
const unrecorded = (v) => v === undefined || v === null || (typeof v === 'string' && v.trim().length === 0)
|
|
205
|
+
|
|
206
|
+
// `parent` is, or contains, `child` — both already canonical absolute paths.
|
|
207
|
+
//
|
|
208
|
+
// `${parent}/` is the obvious way to write the prefix test and it is wrong at exactly one
|
|
209
|
+
// value: for `parent === '/'` it is `//`, which prefixes nothing. So the one directory that
|
|
210
|
+
// contains every path there is answered "contains none" — and that is not academic, it is
|
|
211
|
+
// what let a tree resolving to `/` walk past the protected-path guard and come back REAP.
|
|
212
|
+
const contains = (parent, child) => (
|
|
213
|
+
parent === child || (parent === '/' ? child.startsWith('/') : child.startsWith(`${parent}/`))
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
// The filesystem root is never a tree this loop owns, on any side of any decision: as a
|
|
217
|
+
// sweep scope it is every worktree on the machine, as a candidate it is the whole disk.
|
|
218
|
+
// Refused on its own account rather than as a consequence of some protected path happening
|
|
219
|
+
// to be configured, because "it depended on unrelated configuration" is the shape of the
|
|
220
|
+
// last three defects here.
|
|
221
|
+
const isFilesystemRoot = (ident) => isObject(ident) && ident.resolved === true && ident.real === '/'
|
|
222
|
+
|
|
223
|
+
// A path is a directory on disk, not a string. Compared as text, any SECOND SPELLING of one
|
|
224
|
+
// directory walks past every guard below — and the second spelling is not exotic: on macOS
|
|
225
|
+
// `/tmp` IS a symlink to `/private/tmp`, so a workspace created as `/tmp/x` and a guard
|
|
226
|
+
// written as `/private/tmp/x` name one tree and match nothing.
|
|
227
|
+
//
|
|
228
|
+
// There are three such spellings, and CASE is the one a symlink resolver does not close.
|
|
229
|
+
// `fs.realpathSync` — the JS implementation — resolves links but hands the case back
|
|
230
|
+
// untouched: on a case-insensitive filesystem (default APFS, and NTFS) it answers
|
|
231
|
+
// `/PRIVATE/TMP` for `/PRIVATE/TMP` while that IS `/private/tmp`, and the guard then ranks
|
|
232
|
+
// the orchestrator's own checkout REAP under a shouted name. `fs.realpathSync.native` asks
|
|
233
|
+
// the OS instead of walking the links in JS, so it answers with the real on-disk spelling.
|
|
234
|
+
// That is the default here, and it is deliberately not backed by a fallback: falling back to
|
|
235
|
+
// the JS resolver would restore the case-blind compare invisibly, and every OS this runs on
|
|
236
|
+
// has shipped `.native` since Node 9.2. If it were ever absent the call throws inside the
|
|
237
|
+
// loop below, which fails CLOSED — the direction that cannot lose work.
|
|
238
|
+
//
|
|
239
|
+
// What `.native` changes BEYOND case, checked rather than assumed (Node 22, darwin/linux):
|
|
240
|
+
// error codes are identical on every path this walk depends on — ENOENT for a missing entry
|
|
241
|
+
// and for a dangling symlink, ENOTDIR through a non-directory, ELOOP on a symlink cycle — so
|
|
242
|
+
// the ENOENT walk-up below is unaffected and a not-yet-created path still resolves through
|
|
243
|
+
// its nearest real ancestor. The one behavioural difference is at the empty string, where
|
|
244
|
+
// the JS implementation resolves to the process CWD and `.native` throws ENOENT; that input
|
|
245
|
+
// is already refused above, and `.native`'s answer is the safer of the two anyway.
|
|
246
|
+
//
|
|
247
|
+
// Returns { recorded, real, resolved, reason }. `resolved: false` is the answer that
|
|
248
|
+
// matters — it means "this path's identity is unknowable", and every caller here treats
|
|
249
|
+
// that as protected, because the whole point of a guard is that uncertainty must not
|
|
250
|
+
// authorize a deletion.
|
|
251
|
+
//
|
|
252
|
+
// A path that does not exist YET is NOT uncertainty: nothing is there to destroy, and a
|
|
253
|
+
// unit's workspace path is derived before the worktree is added. So resolution walks up to
|
|
254
|
+
// the nearest ancestor that DOES exist and re-appends the rest — `/tmp/run/u1` resolves to
|
|
255
|
+
// `/private/tmp/run/u1` whether or not `u1` was created. Only the ancestor that EXISTS gets
|
|
256
|
+
// canonicalized, because a segment that is not on disk has no on-disk spelling to read — the
|
|
257
|
+
// trailing segments are re-appended exactly as recorded. That is the right answer and not a
|
|
258
|
+
// residual case hole: a path whose parent does not exist cannot be inside anything the guard
|
|
259
|
+
// protects, and there is nothing under it to destroy. Genuine uncertainty is everything
|
|
260
|
+
// else: a relative path (its tree depends on the caller's cwd), a dangling symlink (it
|
|
261
|
+
// exists, and nobody can say what it names), a permission or symlink-loop refusal, an ENOENT
|
|
262
|
+
// with no resolvable ancestor at all.
|
|
263
|
+
// Exported so a test can pin WHICH resolver is the default, which is an implementation
|
|
264
|
+
// assertion on purpose. On a case-SENSITIVE filesystem — ext4, i.e. this repo's Linux CI —
|
|
265
|
+
// no BEHAVIOURAL test can tell the two resolvers apart, because there an uppercase spelling
|
|
266
|
+
// genuinely names a different directory and the case tests skip for cause. Without this pin,
|
|
267
|
+
// the one-identifier revert that reopens the whole case class goes green on the only machine
|
|
268
|
+
// that gates a merge.
|
|
269
|
+
export const RESOLVE_PATH = fs.realpathSync.native
|
|
270
|
+
|
|
271
|
+
export function pathIdentity(input, { realpath = RESOLVE_PATH, lstat = fs.lstatSync } = {}) {
|
|
272
|
+
const recorded = norm(input)
|
|
273
|
+
const unresolved = (reason) => ({ recorded, real: null, resolved: false, reason })
|
|
274
|
+
if (!recorded) return unresolved('an empty path names no tree')
|
|
275
|
+
// A RELATIVE path is the third spelling that evades the guard, after the symlink alias and
|
|
276
|
+
// the trailing slash — and it is the worst of the three, because `realpath` would resolve
|
|
277
|
+
// it against whatever `cwd` the reap happens to run from rather than the tree it was
|
|
278
|
+
// recorded against. `unitWorkspacePathFor` derives from `git rev-parse --show-toplevel`
|
|
279
|
+
// and `git worktree list --porcelain` prints absolute paths, so nothing legitimate is
|
|
280
|
+
// relative; what does arrive this way is a hand-edited manifest (`run-state.mjs validate`
|
|
281
|
+
// says "must be a non-empty absolute path" but only checks non-empty) or a non-string
|
|
282
|
+
// coerced by `norm` — `42` and `[object Object]` are both relative paths under a cwd.
|
|
283
|
+
if (!path.isAbsolute(recorded)) return unresolved(`${recorded} is not an absolute path, so which tree it names depends on the working directory the reap happens to run from`)
|
|
284
|
+
|
|
285
|
+
const trailing = []
|
|
286
|
+
let cur = recorded
|
|
287
|
+
// Bounded: a pathological input must not spin. 64 is far past any real workspace depth.
|
|
288
|
+
for (let depth = 0; depth < 64; depth++) {
|
|
289
|
+
try {
|
|
290
|
+
const real = realpath(cur)
|
|
291
|
+
return { recorded, real: norm(trailing.length ? path.join(real, ...trailing) : real) || '/', resolved: true, reason: null }
|
|
292
|
+
} catch (error) {
|
|
293
|
+
const code = error?.code
|
|
294
|
+
if (code !== 'ENOENT') {
|
|
295
|
+
return unresolved(`realpath refused ${cur} (${code ?? error?.message ?? 'unknown error'})`)
|
|
296
|
+
}
|
|
297
|
+
// ENOENT from realpath but the entry itself is there = a symlink whose target is
|
|
298
|
+
// gone. "Does not exist" and "points somewhere unknown" are different answers.
|
|
299
|
+
let dangling = false
|
|
300
|
+
try { lstat(cur); dangling = true } catch { dangling = false }
|
|
301
|
+
if (dangling) return unresolved(`${cur} is a symlink whose target does not exist, so what it names cannot be established`)
|
|
302
|
+
const parent = path.dirname(cur)
|
|
303
|
+
if (!parent || parent === cur) return unresolved(`no existing ancestor of ${recorded} could be resolved`)
|
|
304
|
+
trailing.unshift(path.basename(cur))
|
|
305
|
+
cur = parent
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return unresolved(`gave up resolving ${recorded} after 64 levels`)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// True when removing `candidate` would take `protectedPath` with it, in either direction:
|
|
312
|
+
// the same tree, a parent of it, or a child of it. A child matters because a unit workspace
|
|
313
|
+
// nested inside the orchestrator's checkout is itself the decision-3 violation — it is not
|
|
314
|
+
// a separate tree at all.
|
|
315
|
+
//
|
|
316
|
+
// Takes IDENTITIES, not strings, and an unresolvable identity on either side collides:
|
|
317
|
+
// a tree nobody can identify cannot be shown to be a different tree from the checkout.
|
|
318
|
+
function collides(candidate, guarded) {
|
|
319
|
+
if (!isObject(candidate) || !isObject(guarded)) return true
|
|
320
|
+
if (!candidate.resolved || !guarded.resolved) return true
|
|
321
|
+
const a = candidate.real
|
|
322
|
+
const b = guarded.real
|
|
323
|
+
if (!a || !b) return true
|
|
324
|
+
return contains(b, a) || contains(a, b)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// The whole safety rule, as one decidable function over an observation.
|
|
328
|
+
//
|
|
329
|
+
// Order matters and is deliberate: identity and the protected-path guard run BEFORE any
|
|
330
|
+
// evidence is weighed, so no amount of cleanliness can talk the reap into removing the
|
|
331
|
+
// orchestrator's checkout. Everything after them preserves on any doubt.
|
|
332
|
+
//
|
|
333
|
+
// `claimants` is every unit the manifest says owns this path. More than one is a corrupt
|
|
334
|
+
// manifest, and it is refused rather than resolved — see the ambiguity guard below.
|
|
335
|
+
export function reapDecision({ path: candidate, unit = null, claimants = null, observation = {}, protectedPaths = [], orchestratorGitCommonDir = null, requireGitOwnership = false, identify = pathIdentity } = {}) {
|
|
336
|
+
const p = norm(candidate)
|
|
337
|
+
const id = isObject(unit) && nonEmpty(unit.id) ? unit.id : null
|
|
338
|
+
// `liveProcesses` rides on every row so the plan header can report a platform that
|
|
339
|
+
// cannot run the check at all — a downgrade the per-row reasons would never show.
|
|
340
|
+
const row = (verdict, reason) => ({ path: p, unit: id, verdict, reap: verdict === 'reap', reason, liveProcesses: (isObject(observation) ? observation : {}).liveProcesses ?? null })
|
|
341
|
+
|
|
342
|
+
// Identity first: a path whose tree on disk cannot be established cannot be shown to be
|
|
343
|
+
// outside the orchestrator's checkout, so the guard below would be deciding on a name
|
|
344
|
+
// rather than on a directory.
|
|
345
|
+
// `resolved` is a claim, and `real` is the thing every guard below actually compares. An
|
|
346
|
+
// identity that claims resolution without naming a tree is not a resolution — `collides`
|
|
347
|
+
// already refuses on an empty `real`, but only once there is a protected path to compare
|
|
348
|
+
// against, so with none configured a lying resolver would fall straight through to REAP.
|
|
349
|
+
// `pathIdentity` cannot produce that shape; an injected `identify` can, and the seam is
|
|
350
|
+
// exported.
|
|
351
|
+
const me = identify(p)
|
|
352
|
+
if (!isObject(me) || !me.resolved || !nonEmpty(me.real)) {
|
|
353
|
+
return row('refuse', `refuses ${p}: its identity on disk cannot be established — ${isObject(me) ? (me.reason ?? 'it resolved to no path at all') : 'no identity was returned'}. A path that will not resolve cannot be shown to be a different tree from the orchestrator's checkout, and uncertainty never authorizes a removal`)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// The filesystem root, before the protected paths, because it is the one candidate whose
|
|
357
|
+
// refusal must not depend on one being configured. `/`, `/.`, `/..` and a symlink to `/`
|
|
358
|
+
// are one directory arriving under four ordinary-looking absolute spellings, and it
|
|
359
|
+
// contains every protected path there could be.
|
|
360
|
+
if (isFilesystemRoot(me)) {
|
|
361
|
+
return row('refuse', `refuses ${p}: it resolves to the filesystem root (/), which is not a unit's workspace — it is every checkout, every worktree and everything else on the machine. No unit owns it and nothing here removes it`)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Decision 3, enforced: the orchestrator's checkout is not a unit's workspace and a
|
|
365
|
+
// unit's workspace is not the orchestrator's checkout. It follows that the reap may
|
|
366
|
+
// never touch it — and this repo's own 2026-07-26 incident is what makes the rule
|
|
367
|
+
// explicit rather than assumed.
|
|
368
|
+
for (const guarded of arr(protectedPaths).filter((g) => !unrecorded(g))) {
|
|
369
|
+
const g = identify(norm(guarded))
|
|
370
|
+
if (!isObject(g) || !g.resolved) {
|
|
371
|
+
return row('refuse', `refuses ${p}: the protected path ${norm(guarded)} will not resolve on disk — ${isObject(g) ? g.reason : 'no identity was returned'}, so nothing can be shown to be a different tree from the orchestrator's checkout`)
|
|
372
|
+
}
|
|
373
|
+
if (collides(me, g)) {
|
|
374
|
+
const via = me.real !== me.recorded || g.real !== norm(guarded) ? ` — ${me.real} and ${g.real} are one tree under two spellings` : ''
|
|
375
|
+
return row('refuse', `refuses ${p}: it is, contains, or sits inside the orchestrator's own checkout (${norm(guarded)})${via}. A workspace belongs to a unit; the orchestrator's tree belongs to no unit, and removing it would take the run with it`)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// A manifest that gives one tree to two units is corrupt — `run-state.mjs validate`
|
|
380
|
+
// already calls it an error (ADR 0001 decision 3). Keying by path and letting the last
|
|
381
|
+
// claimant win drops the other one silently, and the dropped claimant is exactly the unit
|
|
382
|
+
// whose work may be sitting in the tree. Refusing is the only answer that cannot be wrong.
|
|
383
|
+
const owners = arr(claimants).filter((u) => isObject(u) && nonEmpty(u.id)).map((u) => u.id)
|
|
384
|
+
if (owners.length > 1) {
|
|
385
|
+
return row('refuse', `refuses ${p}: units ${owners.join(', ')} all claim this workspace, so the manifest does not say whose work is in it — a workspace belongs to ONE unit, and \`run-state.mjs validate\` refuses this state. Nothing here is removed on an ambiguous claim; fix the manifest, then re-run`)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const o = isObject(observation) ? observation : {}
|
|
389
|
+
if (!o.exists) return row('absent', `nothing at ${p} — recorded but not on disk (already removed, or the run never created it). Reported rather than counted as a reap`)
|
|
390
|
+
|
|
391
|
+
if (unit && unitHoldsWorkspace(unit)) {
|
|
392
|
+
return row('preserve', `${id} still holds this workspace — it has not been delivered or handed back (status ${unit.status ?? 'queued'}${isObject(unit.delivery) && unit.delivery.outcome ? `, delivery ${unit.delivery.outcome}` : ''}). A dead spawn's replacement resumes here`)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// A LIVE PROCESS is the one hazard every other check here is blind to. The rest of this
|
|
396
|
+
// ladder reads the tree's *contents* — clean, published, owned — and a tree can satisfy
|
|
397
|
+
// all of it while an agent is mid-run inside it. Removing it then does not lose committed
|
|
398
|
+
// work; it destroys work in flight, and it does so silently from the reaper's side.
|
|
399
|
+
//
|
|
400
|
+
// fmm-express epic-894: the orchestrator pre-authorized an agent to keep running repeat
|
|
401
|
+
// measurements, read its interim report as terminal, and ran `reap --apply`. The tree was
|
|
402
|
+
// clean and its unit delivered, so the ladder said REAP. Git de-registered the worktree and
|
|
403
|
+
// half-deleted node_modules under a live Playwright worker — one run collapsed with 36
|
|
404
|
+
// `worker process exited unexpectedly`, the next could not start at all, and the crash
|
|
405
|
+
// produced a red mark on a test that read as a real regression until the agent opened the
|
|
406
|
+
// JSON. That near-miss is the cost: a destroyed measurement can also FABRICATE evidence.
|
|
407
|
+
//
|
|
408
|
+
// Preserve rather than warn: this is the same "uncertainty never authorizes cleanup" rule
|
|
409
|
+
// the Git-ownership checks below apply. An unreadable process table preserves too.
|
|
410
|
+
if (arr(o.liveProcesses).length) {
|
|
411
|
+
const n = o.liveProcesses.length
|
|
412
|
+
return row('preserve', `${n} process${n === 1 ? ' is' : 'es are'} still running inside ${p} (${o.liveProcesses.slice(0, 3).join(', ')}${n > 3 ? `, +${n - 3} more` : ''}). A clean, published tree can still have an agent mid-run in it; removing it destroys work in flight rather than work on disk. Wait for the agent to report done AND go idle, then re-run`)
|
|
413
|
+
}
|
|
414
|
+
// `unavailable` (no lsof on this platform) deliberately does NOT preserve — see
|
|
415
|
+
// liveProcessesIn. `unreadable` (lsof present, call failed) does.
|
|
416
|
+
if (o.liveProcesses === 'unreadable') {
|
|
417
|
+
return row('preserve', `could not determine whether any process is running inside ${p}, so it cannot be shown to be idle — the process table is as load-bearing as the Git state here, and uncertainty never authorizes cleanup`)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!o.readable) {
|
|
421
|
+
return row('preserve', `git could not be read at ${p}, so nothing here can be shown to be published — unreadable is not empty`)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// A producer may isolate itself by replacing a linked-worktree pointer with an embedded
|
|
425
|
+
// standalone Git directory (`.git-local`, `.pi-git`, or any equivalent). The tree can be
|
|
426
|
+
// clean and published while no longer belonging to the orchestrator repo's worktree set;
|
|
427
|
+
// sending it to `git worktree remove` then fails late with an opaque validation error.
|
|
428
|
+
// Preserve it at planning time. Cleanup is intentionally reversible and remains human-
|
|
429
|
+
// visible: move the standalone clone aside, prune the stale parent registration, then
|
|
430
|
+
// send the moved clone to Trash after checking this same publication evidence.
|
|
431
|
+
if (requireGitOwnership && (!nonEmpty(orchestratorGitCommonDir) || !nonEmpty(o.gitCommonDir))) {
|
|
432
|
+
return row('preserve', `Git ownership could not be established for ${p} (workspace common dir ${o.gitCommonDir ?? 'unreadable'}, orchestrator common dir ${orchestratorGitCommonDir ?? 'unreadable'}), so the orchestrator cannot prove \`git worktree remove\` owns this path. Resolve both with \`git rev-parse --path-format=absolute --git-common-dir\`; uncertainty never authorizes cleanup`)
|
|
433
|
+
}
|
|
434
|
+
if (nonEmpty(orchestratorGitCommonDir) && nonEmpty(o.gitCommonDir)
|
|
435
|
+
&& path.resolve(o.gitCommonDir) !== path.resolve(orchestratorGitCommonDir)) {
|
|
436
|
+
return row('preserve', `clean published workspace is now a standalone Git checkout in a different Git object store (${o.gitCommonDir}), so the orchestrator repo cannot remove it with \`git worktree remove\`. Move it aside, prune the stale parent worktree registration, then send the moved clone to Trash after rechecking publication; never force-remove or raw-delete it`)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const held = []
|
|
440
|
+
if (arr(o.dirty).length) held.push(`${o.dirty.length} uncommitted change${o.dirty.length === 1 ? '' : 's'}`)
|
|
441
|
+
if (arr(o.untracked).length) held.push(`${o.untracked.length} untracked file${o.untracked.length === 1 ? '' : 's'}`)
|
|
442
|
+
if (arr(o.unpushed).length) held.push(`${o.unpushed.length} commit${o.unpushed.length === 1 ? '' : 's'} on no remote (unpushed)`)
|
|
443
|
+
if (held.length) {
|
|
444
|
+
return row('preserve', `holds unpublished work: ${held.join(', ')}. Push or salvage it first — this is the one rule that never bends`)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return row('reap', `clean and fully published${o.branch ? ` (branch ${o.branch})` : ''}, and its unit is ${id ? 'delivered or handed back' : 'no longer in the roster'} — safe to remove`)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ---- the plan ----
|
|
451
|
+
|
|
452
|
+
// Reads the manifest-recorded paths AND the worktrees actually on disk, because those two
|
|
453
|
+
// sets differ in exactly the case that matters: a session that died before recording the
|
|
454
|
+
// path it had already created. A manifest walk alone leaves those orphans forever; a disk
|
|
455
|
+
// walk alone loses the unit each path belongs to.
|
|
456
|
+
// `knownWorktrees` distinguishes three states on purpose. An array is a listing that was
|
|
457
|
+
// taken (empty = looked, found none); `null`/omitted means NO listing was taken or it could
|
|
458
|
+
// not be read — which is reported as such, never as an empty disk.
|
|
459
|
+
export function planReap(manifest = {}, { orchestratorCheckout = null, orchestratorGitCommonDir = null, requireGitOwnership = false, knownWorktrees = null, observe = observeWorkspace, identify = pathIdentity } = {}) {
|
|
460
|
+
const m = isObject(manifest) ? manifest : {}
|
|
461
|
+
const units = arr(m.units).filter((u) => isObject(u) && nonEmpty(u.id))
|
|
462
|
+
|
|
463
|
+
// One identity per distinct spelling, computed once: every guard below decides on the
|
|
464
|
+
// tree a path names rather than on the text of the path.
|
|
465
|
+
const cache = new Map()
|
|
466
|
+
const idOf = (p) => {
|
|
467
|
+
const k = norm(p)
|
|
468
|
+
if (!cache.has(k)) cache.set(k, identify(k))
|
|
469
|
+
return cache.get(k)
|
|
470
|
+
}
|
|
471
|
+
// A tree nobody can identify still needs a stable key, and it must never collide with
|
|
472
|
+
// another unidentifiable one — two unresolvable paths are not known to be the same tree.
|
|
473
|
+
const keyOf = (p) => {
|
|
474
|
+
const ident = idOf(p)
|
|
475
|
+
return isObject(ident) && ident.resolved ? ident.real : `unresolved:${norm(p)}`
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// The run-level checkout is the orchestrator's own tree under `workspaceMode: worktree`.
|
|
479
|
+
// It is protected here for the same reason the primary checkout is: it belongs to no
|
|
480
|
+
// unit, and the handoff removes it separately, last, after every branch is pushed.
|
|
481
|
+
// Filtered on `unrecorded`, not on `nonEmpty`: a run-level `workspacePath` of `42` was
|
|
482
|
+
// dropped here outright, and one of `/` survived the filter only to be `norm`ed to `''`
|
|
483
|
+
// and dropped by the `nonEmpty` filters downstream. Either way the run's own checkout
|
|
484
|
+
// stopped being protected while the manifest still named it. A guard nobody can read now
|
|
485
|
+
// reaches `reapDecision` and refuses every candidate, which is the fail-closed answer.
|
|
486
|
+
const protectedPaths = [orchestratorCheckout, m.workspacePath].filter((p) => !unrecorded(p)).map(norm)
|
|
487
|
+
|
|
488
|
+
// Claims, keyed by identity rather than by spelling — so two spellings of one tree are
|
|
489
|
+
// one row with two claimants (ambiguous, refused), not two rows that each half-decide.
|
|
490
|
+
const claims = new Map()
|
|
491
|
+
const claim = (p, unit) => {
|
|
492
|
+
const key = keyOf(p)
|
|
493
|
+
if (!claims.has(key)) claims.set(key, { path: norm(p), units: [] })
|
|
494
|
+
if (unit) claims.get(key).units.push(unit)
|
|
495
|
+
return claims.get(key)
|
|
496
|
+
}
|
|
497
|
+
for (const u of units) {
|
|
498
|
+
// Same rule as everywhere else: a unit that recorded NOTHING has no tree to claim; a
|
|
499
|
+
// unit that recorded something unreadable does, and dropping its claim silently would
|
|
500
|
+
// leave that tree unowned — collectable as an orphan by a sweep that never heard of it.
|
|
501
|
+
if (unrecorded(u.workspacePath)) continue
|
|
502
|
+
claim(u.workspacePath, u)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Orphans: a worktree on disk under this run's workspace root that no unit claims.
|
|
506
|
+
const scope = sweepScope(m, { identify: idOf, protectedPaths })
|
|
507
|
+
const listed = Array.isArray(knownWorktrees)
|
|
508
|
+
const onDisk = arr(knownWorktrees).filter((p) => !unrecorded(p)).map(norm)
|
|
509
|
+
let collected = 0
|
|
510
|
+
let unexamined = 0
|
|
511
|
+
let unidentified = 0
|
|
512
|
+
for (const wt of onDisk) {
|
|
513
|
+
if (claims.has(keyOf(wt))) continue
|
|
514
|
+
const ident = idOf(wt)
|
|
515
|
+
// A tree whose identity will not resolve can be placed neither INSIDE the scope nor
|
|
516
|
+
// outside it. Skipping it answered "outside" and dropped the row, so with no protected
|
|
517
|
+
// path configured the plan reported a sweep that ran and said nothing whatever about a
|
|
518
|
+
// tree it could not look at — #586 NOTE-1 one row down. Worse, the silence was
|
|
519
|
+
// conditional: with a protected path, `collides` fails closed on an unresolved identity
|
|
520
|
+
// and the same input DID produce a row. It gets one either way now, and `reapDecision`
|
|
521
|
+
// refuses it on the same rule every other unidentifiable path meets.
|
|
522
|
+
if (!isObject(ident) || !ident.resolved) {
|
|
523
|
+
claim(wt, null)
|
|
524
|
+
unidentified++
|
|
525
|
+
continue
|
|
526
|
+
}
|
|
527
|
+
const isProtected = protectedPaths.some((g) => collides(ident, idOf(g)))
|
|
528
|
+
const underRoot = Boolean(scope.ok && contains(scope.id.real, ident.real))
|
|
529
|
+
if (!isProtected && !underRoot) {
|
|
530
|
+
// Skipped because it is out of scope (a scope IS established and this sits outside
|
|
531
|
+
// it), or because there is no scope to compare it to. Only the second is a failure
|
|
532
|
+
// to look.
|
|
533
|
+
if (!scope.ok) unexamined++
|
|
534
|
+
continue
|
|
535
|
+
}
|
|
536
|
+
claim(wt, null)
|
|
537
|
+
if (!isProtected) collected++
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// "Nothing to collect" and "I could not look" are different answers, and reporting them
|
|
541
|
+
// identically is what keeps orphans invisible (#586 NOTE-1). Same shape as
|
|
542
|
+
// `record-gate-result`: a sweep that did not run owes a REASON.
|
|
543
|
+
const collection = !listed
|
|
544
|
+
? {
|
|
545
|
+
ran: false,
|
|
546
|
+
scanned: 0,
|
|
547
|
+
collected: 0,
|
|
548
|
+
unexamined: 0,
|
|
549
|
+
unidentified: 0,
|
|
550
|
+
reason: 'no `git worktree list` result was available, so nothing outside the manifest was examined — the trees a dead session left behind are exactly the ones the manifest does not name',
|
|
551
|
+
}
|
|
552
|
+
: !scope.ok
|
|
553
|
+
? {
|
|
554
|
+
ran: false,
|
|
555
|
+
scanned: onDisk.length,
|
|
556
|
+
collected: 0,
|
|
557
|
+
unexamined,
|
|
558
|
+
unidentified,
|
|
559
|
+
reason: `this run's workspace root could not be established — ${scope.reason}. ${unexamined} worktree${unexamined === 1 ? '' : 's'} on disk ${unexamined === 1 ? 'was' : 'were'} never examined for orphans. This is "could not look", not "all clear": declare the scope with \`run-state.mjs set <slug> workspaceRoot <abs-path>\` (or record units[].workspacePath as workspaces are handed out), and read \`git worktree list\` by hand meanwhile`,
|
|
560
|
+
}
|
|
561
|
+
: { ran: true, scanned: onDisk.length, collected, unexamined: 0, unidentified, reason: null }
|
|
562
|
+
|
|
563
|
+
const decisions = [...claims.values()].map(({ path: p, units: owners }) => reapDecision({
|
|
564
|
+
path: p,
|
|
565
|
+
unit: owners[0] ?? null,
|
|
566
|
+
claimants: owners,
|
|
567
|
+
observation: observe(p),
|
|
568
|
+
protectedPaths,
|
|
569
|
+
orchestratorGitCommonDir,
|
|
570
|
+
requireGitOwnership,
|
|
571
|
+
identify: idOf,
|
|
572
|
+
}))
|
|
573
|
+
|
|
574
|
+
return {
|
|
575
|
+
root: scope.root,
|
|
576
|
+
protectedPaths,
|
|
577
|
+
collection,
|
|
578
|
+
decisions,
|
|
579
|
+
reapable: decisions.filter((d) => d.verdict === 'reap'),
|
|
580
|
+
preserved: decisions.filter((d) => d.verdict === 'preserve'),
|
|
581
|
+
refused: decisions.filter((d) => d.verdict === 'refuse'),
|
|
582
|
+
absent: decisions.filter((d) => d.verdict === 'absent'),
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// The orphan sweep's SCOPE — the directory every one of this run's unit workspaces sits
|
|
587
|
+
// under — decided rather than assumed. Returns `{ root, id, ok, reason }`; `ok: false` is a
|
|
588
|
+
// "could not look", never a clean sweep, and it always names a reason.
|
|
589
|
+
//
|
|
590
|
+
// It used to be `dirname` of the FIRST recorded workspace path, which is arbitrary in one
|
|
591
|
+
// direction and dangerous in the other. Arbitrary: with several recorded paths, the first
|
|
592
|
+
// entry decides the scope for all of them and nothing checks that the rest agree.
|
|
593
|
+
// Dangerous: `dirname` of a unit mis-recorded as the checkout itself is the repo's PARENT —
|
|
594
|
+
// a directory holding every OTHER checkout on the machine, whose worktrees then rank as
|
|
595
|
+
// this run's orphans. Only clean, fully-pushed trees could ever have been removed, so the
|
|
596
|
+
// evidence rules bounded the blast radius; that is not the same as the scope being right.
|
|
597
|
+
//
|
|
598
|
+
// So, in order:
|
|
599
|
+
//
|
|
600
|
+
// declared `workspaceRoot` on the manifest is the run SAYING where its trees live.
|
|
601
|
+
// It wins outright — the recorded paths do not get a vote.
|
|
602
|
+
// inferred the fallback, kept because a run whose paths were chosen by an older
|
|
603
|
+
// scheme still deserves its orphans collected. It now requires AGREEMENT:
|
|
604
|
+
// every recorded path must sit under one parent TREE (two spellings of one
|
|
605
|
+
// directory agree; two directories do not). Disagreement yields no scope
|
|
606
|
+
// rather than the first entry's, which reads downstream as "could not look".
|
|
607
|
+
// bounded whichever way it arrived, a root that is, contains, or sits inside a
|
|
608
|
+
// protected tree is refused. Declared does not mean unchecked: a run may no
|
|
609
|
+
// more declare `/` than infer the repo's parent.
|
|
610
|
+
export function sweepScope(manifest, { identify = pathIdentity, protectedPaths = [] } = {}) {
|
|
611
|
+
const m = isObject(manifest) ? manifest : {}
|
|
612
|
+
const no = (root, reason) => ({ root, id: null, ok: false, reason })
|
|
613
|
+
|
|
614
|
+
// `unrecorded`, not `nonEmpty`, and `=== null`, not falsy — both halves matter, and both
|
|
615
|
+
// were the same mistake. DECLARED is a question about presence; whether the declaration is
|
|
616
|
+
// usable is a separate question, answered below by `identify` and the bounds. Deciding
|
|
617
|
+
// presence off "did this normalize to something truthy" is what discarded a declared `42`,
|
|
618
|
+
// a declared `' '` and a declared `/` alike and swept an INFERRED scope instead — the
|
|
619
|
+
// manifest's own words overruled by a guess, in the one direction that removes things.
|
|
620
|
+
// A declared root that will not resolve, or that IS the filesystem root, now falls out of
|
|
621
|
+
// the checks below as a refusal, which is where it belongs.
|
|
622
|
+
let root = unrecorded(m.workspaceRoot) ? null : norm(m.workspaceRoot)
|
|
623
|
+
if (root === null) {
|
|
624
|
+
const recorded = arr(m.units).map((u) => (isObject(u) ? u.workspacePath : null)).filter((p) => !unrecorded(p))
|
|
625
|
+
if (!recorded.length) {
|
|
626
|
+
return no(null, 'no unit on this manifest records a workspacePath and the run declares no workspaceRoot')
|
|
627
|
+
}
|
|
628
|
+
const parents = recorded.map((p) => norm(path.dirname(norm(p))))
|
|
629
|
+
// Keyed by identity, like every other compare in this file: `/tmp/run` and
|
|
630
|
+
// `/private/tmp/run` are one parent, and two unresolvable parents are never known to be.
|
|
631
|
+
const keys = new Set(parents.map((p) => {
|
|
632
|
+
const ident = identify(p)
|
|
633
|
+
return isObject(ident) && ident.resolved ? ident.real : `unresolved:${p}`
|
|
634
|
+
}))
|
|
635
|
+
if (keys.size !== 1) {
|
|
636
|
+
return no(null, `this run's ${recorded.length} recorded workspaces sit under ${keys.size} different parents (${[...new Set(parents)].join(', ')}), so none of them is the run's root — picking the first is picking one at random`)
|
|
637
|
+
}
|
|
638
|
+
root = parents[0]
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const id = identify(root)
|
|
642
|
+
if (!isObject(id) || !id.resolved) {
|
|
643
|
+
return no(root, `${root} will not resolve on disk (${isObject(id) ? (id.reason ?? 'it resolved to no path at all') : 'no identity was returned'})`)
|
|
644
|
+
}
|
|
645
|
+
// Bounded on its own account first, because `/` is the one scope whose refusal must not
|
|
646
|
+
// depend on a protected path being configured — and because `/`, `//`, `/.`, `/..` and a
|
|
647
|
+
// symlink to `/` are five spellings of it, only the first two of which look alarming.
|
|
648
|
+
if (isFilesystemRoot(id)) {
|
|
649
|
+
return no(root, `${root} resolves to the filesystem root (/), which holds every checkout and every worktree on the machine — a scope of / proposes all of them as this run's orphans. A run may no more declare / than infer the repo's parent`)
|
|
650
|
+
}
|
|
651
|
+
for (const guarded of arr(protectedPaths).filter((g) => !unrecorded(g))) {
|
|
652
|
+
if (collides(id, identify(norm(guarded)))) {
|
|
653
|
+
return no(root, `${root} is, contains, or sits inside the orchestrator's checkout (${norm(guarded)}) — a scope holding the checkout holds every other checkout beside it too, and their worktrees are not this run's orphans`)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return { root, id, ok: true, reason: null }
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// What root a manifest names, with no judgement about whether it is usable. `sweepScope` is
|
|
660
|
+
// what the reap decides on; this stays for callers that only want the path.
|
|
661
|
+
export function runWorkspaceRoot(manifest, opts = {}) {
|
|
662
|
+
return sweepScope(manifest, opts).root
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ---- CLI ----
|
|
666
|
+
|
|
667
|
+
// `null`, not `[]`, when git could not answer: an empty array is the claim that the disk
|
|
668
|
+
// was read and held nothing, which is the conflation this whole path exists to remove.
|
|
669
|
+
export function listWorktrees(repoDir) {
|
|
670
|
+
const r = git(['worktree', 'list', '--porcelain'], repoDir)
|
|
671
|
+
if (!r.ok) return null
|
|
672
|
+
return r.out.split('\n').filter((l) => l.startsWith('worktree ')).map((l) => l.slice('worktree '.length).trim())
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export function renderPlan(plan) {
|
|
676
|
+
const c = plan.collection ?? { ran: false, reason: 'orphan collection reported nothing about itself' }
|
|
677
|
+
const out = [
|
|
678
|
+
'REAP PLAN',
|
|
679
|
+
// No reason of its own: `root` is null for several different causes — nothing recorded,
|
|
680
|
+
// recorded paths that disagree about their parent, a declaration that cannot be read —
|
|
681
|
+
// and this line used to assert the first one for all of them. A wrong reason on a
|
|
682
|
+
// correct refusal sends a human looking for a field that is not missing (#591 F3). The
|
|
683
|
+
// one true reason is already one line down, on the collection row.
|
|
684
|
+
` workspace root: ${plan.root ?? '— not established; see orphan collection below'}`,
|
|
685
|
+
c.ran
|
|
686
|
+
? ` orphan collection: ran over ${c.scanned} worktree${c.scanned === 1 ? '' : 's'} on disk; ${c.collected} orphan${c.collected === 1 ? '' : 's'} collected${c.unidentified ? `; ${c.unidentified} could not be identified and ${c.unidentified === 1 ? 'is' : 'are'} refused below` : ''}`
|
|
687
|
+
: ` orphan collection: NOT RUN — ${c.reason}`,
|
|
688
|
+
]
|
|
689
|
+
// A guard that silently stopped guarding is worse than no guard: the plan would read
|
|
690
|
+
// exactly as it did before the liveness check existed. Say so once, at the top, where the
|
|
691
|
+
// reader is already deciding whether to trust the REAP rows.
|
|
692
|
+
if (plan.decisions.some((d) => d.liveProcesses === 'unavailable')) {
|
|
693
|
+
out.push(' live-process check: NOT AVAILABLE — `lsof` is not installed here, so a tree'
|
|
694
|
+
+ ' holding a running agent cannot be told from an idle one. Rows below are decided on'
|
|
695
|
+
+ ' tree contents alone. Confirm your agents are done before --apply.')
|
|
696
|
+
}
|
|
697
|
+
out.push('')
|
|
698
|
+
for (const d of plan.decisions) {
|
|
699
|
+
const mark = { reap: 'REAP ', preserve: 'PRESERVE', refuse: 'REFUSE ', absent: 'absent ' }[d.verdict]
|
|
700
|
+
out.push(` ${mark} ${d.path}${d.unit ? ` (${d.unit})` : ' (orphan — no unit claims it)'}`)
|
|
701
|
+
out.push(` ${d.reason}`)
|
|
702
|
+
}
|
|
703
|
+
if (!plan.decisions.length) out.push(' nothing recorded and nothing on disk under this run — no workspaces to reap')
|
|
704
|
+
return out.join('\n')
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// `listWorktrees` is a seam, and a narrow one on purpose. `--apply`'s exit fold —
|
|
708
|
+
// `notRemoved ||` below — is unreachable through the real CLI: `git worktree list` always
|
|
709
|
+
// includes the orchestrator's own checkout, which is always REFUSED, so `plan.refused`
|
|
710
|
+
// carries exit 1 on every real invocation and a revert of the fold is invisible. Injecting
|
|
711
|
+
// the listing is the smallest thing that lets a test reach the fold on the fold alone; the
|
|
712
|
+
// worktree, git's refusal and `applyReap` all stay real (see `workspaces.test.mjs`).
|
|
713
|
+
export function runCli(argv, { readManifestFor, cwd = process.cwd(), listWorktrees: listOnDisk = listWorktrees } = {}) {
|
|
714
|
+
const positional = []
|
|
715
|
+
const opts = {}
|
|
716
|
+
for (let i = 0; i < argv.length; i++) {
|
|
717
|
+
const a = argv[i]
|
|
718
|
+
if (a === '--apply') opts.apply = true
|
|
719
|
+
else if (a === '--json') opts.json = true
|
|
720
|
+
else if (a === '--repo-dir') opts.repoDir = argv[++i]
|
|
721
|
+
else if (a === '--file') opts.file = argv[++i]
|
|
722
|
+
else if (a === '--help' || a === '-h') opts.help = true
|
|
723
|
+
else if (a.startsWith('--')) { console.error(`workspaces: unrecognized flag ${a}`); return 2 }
|
|
724
|
+
else positional.push(a)
|
|
725
|
+
}
|
|
726
|
+
if (opts.help || !positional.length) {
|
|
727
|
+
console.log(`workspaces path <slug> <unitId> [--repo-dir DIR]
|
|
728
|
+
workspaces reap <slug> [--apply] [--json] [--repo-dir DIR] [--file PATH]
|
|
729
|
+
|
|
730
|
+
A workspace belongs to a unit, from its first producer spawn until the unit is
|
|
731
|
+
delivered or handed back. It is PRESERVED ON DEATH and reaped only on success —
|
|
732
|
+
branch pushed, nothing unpublished. \`reap\` plans by default and removes only with
|
|
733
|
+
--apply; it refuses the orchestrator's own checkout outright.
|
|
734
|
+
|
|
735
|
+
exit 0 = all clear · 1 = something preserved/refused, or --apply could not remove it
|
|
736
|
+
2 = usage · 3 = no manifest
|
|
737
|
+
4 = the orphan sweep could not run (nothing recorded to scope it, or no
|
|
738
|
+
\`git worktree list\`) — "could not look", never reported as "all clear"`)
|
|
739
|
+
return opts.help ? 0 : 2
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const repoDir = opts.repoDir || cwd
|
|
743
|
+
const [cmd, slug, unitId] = positional
|
|
744
|
+
|
|
745
|
+
if (cmd === 'path') {
|
|
746
|
+
const top = git(['rev-parse', '--show-toplevel'], repoDir)
|
|
747
|
+
if (!top.ok) { console.error(`workspaces path: ${repoDir} is not a git repository`); return 2 }
|
|
748
|
+
if (!nonEmpty(slug) || !nonEmpty(unitId)) { console.error('workspaces path: <slug> and <unitId> are both required'); return 2 }
|
|
749
|
+
console.log(unitWorkspacePathFor(top.out.trim(), slug, unitId))
|
|
750
|
+
return 0
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (cmd !== 'reap') { console.error(`workspaces: unknown command ${cmd}`); return 2 }
|
|
754
|
+
|
|
755
|
+
const manifest = readManifestFor ? readManifestFor(slug, opts) : null
|
|
756
|
+
if (manifest == null) {
|
|
757
|
+
console.error(`workspaces reap: no manifest for ${slug} (absent is normal — the run dir is temp). Nothing recorded means nothing to reap BY PATH; check \`git worktree list\` by hand.`)
|
|
758
|
+
return 3
|
|
759
|
+
}
|
|
760
|
+
const top = git(['rev-parse', '--show-toplevel'], repoDir)
|
|
761
|
+
const common = git(['rev-parse', '--path-format=absolute', '--git-common-dir'], repoDir)
|
|
762
|
+
const plan = planReap(manifest, {
|
|
763
|
+
orchestratorCheckout: top.ok ? top.out.trim() : repoDir,
|
|
764
|
+
orchestratorGitCommonDir: common.ok ? common.out.trim() : null,
|
|
765
|
+
requireGitOwnership: true,
|
|
766
|
+
knownWorktrees: listOnDisk(repoDir),
|
|
767
|
+
})
|
|
768
|
+
if (!opts.json) console.log(renderPlan(plan))
|
|
769
|
+
|
|
770
|
+
let notRemoved = 0
|
|
771
|
+
let applied = null
|
|
772
|
+
if (opts.apply) {
|
|
773
|
+
applied = applyReapResult(plan, repoDir, { log: opts.json ? () => {} : console.log })
|
|
774
|
+
notRemoved = applied.removalFailures.length
|
|
775
|
+
} else if (plan.reapable.length && !opts.json) {
|
|
776
|
+
console.log(`\n ${plan.reapable.length} removable — re-run with --apply to remove them.`)
|
|
777
|
+
}
|
|
778
|
+
if (opts.json) console.log(JSON.stringify(applied ?? { collection: plan.collection, decisions: plan.decisions, removed: [], remaining: [...plan.preserved, ...plan.refused].map((row) => row.path), removalFailures: [] }, null, 2))
|
|
779
|
+
// 4 outranks 1 — deliberately, and this is the whole reason it is a code and not just a
|
|
780
|
+
// line of output. The orchestrator's own checkout is in `git worktree list` on every real
|
|
781
|
+
// invocation and is always REFUSED, so 1 fires almost always; a "could not look" ranked
|
|
782
|
+
// under it would never once be visible in the exit status, and the fix would live only in
|
|
783
|
+
// prose nobody reads. Both codes mean "read the rows"; 4 additionally means the disk was
|
|
784
|
+
// never swept, which is the one thing the rows themselves cannot tell you.
|
|
785
|
+
if (!plan.collection.ran) return 4
|
|
786
|
+
if (notRemoved || plan.preserved.length || plan.refused.length) return 1
|
|
787
|
+
return 0
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// `--apply` is the only step in this loop that touches the disk, and a removal git REFUSED
|
|
791
|
+
// is not a removal. It used to be reported on stdout and nowhere else, so a caller scripting
|
|
792
|
+
// `reap --apply` read exit 0 over a printed FAILED — the same defect shape as a status-only
|
|
793
|
+
// reap calling an unpushed tree clean: a destructive operation reporting success it did not
|
|
794
|
+
// have. The count folds into exit 1 rather than taking a new code, because 1 already means
|
|
795
|
+
// "something was left in place, read the rows before touching anything by hand", and that is
|
|
796
|
+
// precisely what a refused removal is.
|
|
797
|
+
//
|
|
798
|
+
// Never `--force`, and never fall back to `fs.rm`: `git worktree remove` refusing is the
|
|
799
|
+
// last guard between this loop and work that exists nowhere else, and the caller who reads
|
|
800
|
+
// the row is better placed than this script to decide what the refusal meant.
|
|
801
|
+
export function applyReapResult(plan, repoDir, { remove = (p, cwd) => git(['worktree', 'remove', p], cwd), log = console.log } = {}) {
|
|
802
|
+
const removed = []
|
|
803
|
+
const removalFailures = []
|
|
804
|
+
for (const d of arr(isObject(plan) ? plan.reapable : null)) {
|
|
805
|
+
const r = remove(d.path, repoDir)
|
|
806
|
+
if (isObject(r) && r.ok) { removed.push(d.path); log(`removed ${d.path}`); continue }
|
|
807
|
+
const error = (isObject(r) && r.error?.message) || 'git refused'
|
|
808
|
+
removalFailures.push({ path: d.path, error })
|
|
809
|
+
log(`FAILED to remove ${d.path} — ${error}; left in place`)
|
|
810
|
+
}
|
|
811
|
+
const remaining = [
|
|
812
|
+
...arr(plan?.preserved).map((row) => row.path),
|
|
813
|
+
...arr(plan?.refused).map((row) => row.path),
|
|
814
|
+
...removalFailures.map((row) => row.path),
|
|
815
|
+
]
|
|
816
|
+
return { collection: structuredClone(plan?.collection ?? null), decisions: structuredClone(arr(plan?.decisions)), removed, remaining, removalFailures }
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export function applyReap(plan, repoDir, options = {}) {
|
|
820
|
+
return applyReapResult(plan, repoDir, options).removalFailures.length
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (isMainModule(import.meta.url)) {
|
|
824
|
+
process.exit(runCli(process.argv.slice(2), {
|
|
825
|
+
readManifestFor: (slug, opts) => {
|
|
826
|
+
try { return readManifest(fileFor(slug, opts)) } catch { return null }
|
|
827
|
+
},
|
|
828
|
+
}))
|
|
829
|
+
}
|