dorfl 0.0.0 → 0.1.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/dist/advance-ci-template.d.ts +73 -0
- package/dist/advance-ci-template.d.ts.map +1 -0
- package/dist/advance-ci-template.js +104 -0
- package/dist/advance-ci-template.js.map +1 -0
- package/dist/advance-classify.d.ts +132 -0
- package/dist/advance-classify.d.ts.map +1 -0
- package/dist/advance-classify.js +120 -0
- package/dist/advance-classify.js.map +1 -0
- package/dist/advance-drivers.d.ts +182 -0
- package/dist/advance-drivers.d.ts.map +1 -0
- package/dist/advance-drivers.js +231 -0
- package/dist/advance-drivers.js.map +1 -0
- package/dist/advance-isolated.d.ts +156 -0
- package/dist/advance-isolated.d.ts.map +1 -0
- package/dist/advance-isolated.js +256 -0
- package/dist/advance-isolated.js.map +1 -0
- package/dist/advance-lifecycle-template.d.ts +107 -0
- package/dist/advance-lifecycle-template.d.ts.map +1 -0
- package/dist/advance-lifecycle-template.js +668 -0
- package/dist/advance-lifecycle-template.js.map +1 -0
- package/dist/advance-loop-driver.d.ts +325 -0
- package/dist/advance-loop-driver.d.ts.map +1 -0
- package/dist/advance-loop-driver.js +437 -0
- package/dist/advance-loop-driver.js.map +1 -0
- package/dist/advance-treeless-publish.d.ts +108 -0
- package/dist/advance-treeless-publish.d.ts.map +1 -0
- package/dist/advance-treeless-publish.js +71 -0
- package/dist/advance-treeless-publish.js.map +1 -0
- package/dist/advance.d.ts +340 -0
- package/dist/advance.d.ts.map +1 -0
- package/dist/advance.js +1122 -0
- package/dist/advance.js.map +1 -0
- package/dist/advancing-lock.d.ts +294 -0
- package/dist/advancing-lock.d.ts.map +1 -0
- package/dist/advancing-lock.js +594 -0
- package/dist/advancing-lock.js.map +1 -0
- package/dist/agent-launch.d.ts +79 -0
- package/dist/agent-launch.d.ts.map +1 -0
- package/dist/agent-launch.js +61 -0
- package/dist/agent-launch.js.map +1 -0
- package/dist/agent-stop.d.ts +149 -0
- package/dist/agent-stop.d.ts.map +1 -0
- package/dist/agent-stop.js +307 -0
- package/dist/agent-stop.js.map +1 -0
- package/dist/apply-decide.d.ts +127 -0
- package/dist/apply-decide.d.ts.map +1 -0
- package/dist/apply-decide.js +176 -0
- package/dist/apply-decide.js.map +1 -0
- package/dist/apply-merge-action.d.ts +206 -0
- package/dist/apply-merge-action.d.ts.map +1 -0
- package/dist/apply-merge-action.js +307 -0
- package/dist/apply-merge-action.js.map +1 -0
- package/dist/apply-persist.d.ts +174 -0
- package/dist/apply-persist.d.ts.map +1 -0
- package/dist/apply-persist.js +359 -0
- package/dist/apply-persist.js.map +1 -0
- package/dist/arbiter.d.ts +120 -0
- package/dist/arbiter.d.ts.map +1 -0
- package/dist/arbiter.js +255 -0
- package/dist/arbiter.js.map +1 -0
- package/dist/brand.d.ts +70 -0
- package/dist/brand.d.ts.map +1 -0
- package/dist/brand.js +84 -0
- package/dist/brand.js.map +1 -0
- package/dist/buildable-body.d.ts +132 -0
- package/dist/buildable-body.d.ts.map +1 -0
- package/dist/buildable-body.js +131 -0
- package/dist/buildable-body.js.map +1 -0
- package/dist/categorise.d.ts +66 -0
- package/dist/categorise.d.ts.map +1 -0
- package/dist/categorise.js +106 -0
- package/dist/categorise.js.map +1 -0
- package/dist/claim-cas.d.ts +117 -0
- package/dist/claim-cas.d.ts.map +1 -0
- package/dist/claim-cas.js +312 -0
- package/dist/claim-cas.js.map +1 -0
- package/dist/cli-spinner.d.ts +112 -0
- package/dist/cli-spinner.d.ts.map +1 -0
- package/dist/cli-spinner.js +157 -0
- package/dist/cli-spinner.js.map +1 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +3094 -0
- package/dist/cli.js.map +1 -0
- package/dist/close-job-template.d.ts +70 -0
- package/dist/close-job-template.d.ts.map +1 -0
- package/dist/close-job-template.js +180 -0
- package/dist/close-job-template.js.map +1 -0
- package/dist/close-job.d.ts +95 -0
- package/dist/close-job.d.ts.map +1 -0
- package/dist/close-job.js +226 -0
- package/dist/close-job.js.map +1 -0
- package/dist/complete.d.ts +361 -0
- package/dist/complete.d.ts.map +1 -0
- package/dist/complete.js +885 -0
- package/dist/complete.js.map +1 -0
- package/dist/concurrency.d.ts +68 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +112 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/config-override.d.ts +76 -0
- package/dist/config-override.d.ts.map +1 -0
- package/dist/config-override.js +50 -0
- package/dist/config-override.js.map +1 -0
- package/dist/config.d.ts +668 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +241 -0
- package/dist/config.js.map +1 -0
- package/dist/continue-branch.d.ts +249 -0
- package/dist/continue-branch.d.ts.map +1 -0
- package/dist/continue-branch.js +389 -0
- package/dist/continue-branch.js.map +1 -0
- package/dist/cwd-section.d.ts +186 -0
- package/dist/cwd-section.d.ts.map +1 -0
- package/dist/cwd-section.js +209 -0
- package/dist/cwd-section.js.map +1 -0
- package/dist/decision-engine.d.ts +170 -0
- package/dist/decision-engine.d.ts.map +1 -0
- package/dist/decision-engine.js +136 -0
- package/dist/decision-engine.js.map +1 -0
- package/dist/detect.d.ts +17 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +118 -0
- package/dist/detect.js.map +1 -0
- package/dist/do-autopick.d.ts +85 -0
- package/dist/do-autopick.d.ts.map +1 -0
- package/dist/do-autopick.js +112 -0
- package/dist/do-autopick.js.map +1 -0
- package/dist/do-config.d.ts +312 -0
- package/dist/do-config.d.ts.map +1 -0
- package/dist/do-config.js +358 -0
- package/dist/do-config.js.map +1 -0
- package/dist/do-remote-auto.d.ts +75 -0
- package/dist/do-remote-auto.d.ts.map +1 -0
- package/dist/do-remote-auto.js +111 -0
- package/dist/do-remote-auto.js.map +1 -0
- package/dist/do.d.ts +621 -0
- package/dist/do.d.ts.map +1 -0
- package/dist/do.js +1882 -0
- package/dist/do.js.map +1 -0
- package/dist/drop-source.d.ts +96 -0
- package/dist/drop-source.d.ts.map +1 -0
- package/dist/drop-source.js +91 -0
- package/dist/drop-source.js.map +1 -0
- package/dist/eligibility.d.ts +46 -0
- package/dist/eligibility.d.ts.map +1 -0
- package/dist/eligibility.js +34 -0
- package/dist/eligibility.js.map +1 -0
- package/dist/env-config.d.ts +51 -0
- package/dist/env-config.d.ts.map +1 -0
- package/dist/env-config.js +272 -0
- package/dist/env-config.js.map +1 -0
- package/dist/failure-cause.d.ts +70 -0
- package/dist/failure-cause.d.ts.map +1 -0
- package/dist/failure-cause.js +126 -0
- package/dist/failure-cause.js.map +1 -0
- package/dist/format.d.ts +43 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +256 -0
- package/dist/format.js.map +1 -0
- package/dist/frontmatter.d.ts +215 -0
- package/dist/frontmatter.d.ts.map +1 -0
- package/dist/frontmatter.js +345 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/gate-readiness.d.ts +84 -0
- package/dist/gate-readiness.d.ts.map +1 -0
- package/dist/gate-readiness.js +103 -0
- package/dist/gate-readiness.js.map +1 -0
- package/dist/gc.d.ts +165 -0
- package/dist/gc.d.ts.map +1 -0
- package/dist/gc.js +313 -0
- package/dist/gc.js.map +1 -0
- package/dist/gh-failure.d.ts +42 -0
- package/dist/gh-failure.d.ts.map +1 -0
- package/dist/gh-failure.js +49 -0
- package/dist/gh-failure.js.map +1 -0
- package/dist/git.d.ts +75 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +130 -0
- package/dist/git.js.map +1 -0
- package/dist/github.d.ts +187 -0
- package/dist/github.d.ts.map +1 -0
- package/dist/github.js +343 -0
- package/dist/github.js.map +1 -0
- package/dist/harness.d.ts +242 -0
- package/dist/harness.d.ts.map +1 -0
- package/dist/harness.js +157 -0
- package/dist/harness.js.map +1 -0
- package/dist/identity.d.ts +167 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +231 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +147 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/install-ci-branch-protection.d.ts +147 -0
- package/dist/install-ci-branch-protection.d.ts.map +1 -0
- package/dist/install-ci-branch-protection.js +166 -0
- package/dist/install-ci-branch-protection.js.map +1 -0
- package/dist/install-ci-capabilities/advance-lifecycle.d.ts +15 -0
- package/dist/install-ci-capabilities/advance-lifecycle.d.ts.map +1 -0
- package/dist/install-ci-capabilities/advance-lifecycle.js +28 -0
- package/dist/install-ci-capabilities/advance-lifecycle.js.map +1 -0
- package/dist/install-ci-capabilities/close-job.d.ts +13 -0
- package/dist/install-ci-capabilities/close-job.d.ts.map +1 -0
- package/dist/install-ci-capabilities/close-job.js +26 -0
- package/dist/install-ci-capabilities/close-job.js.map +1 -0
- package/dist/install-ci-capabilities/example-noop.d.ts +16 -0
- package/dist/install-ci-capabilities/example-noop.d.ts.map +1 -0
- package/dist/install-ci-capabilities/example-noop.js +23 -0
- package/dist/install-ci-capabilities/example-noop.js.map +1 -0
- package/dist/install-ci-capabilities/intake.d.ts +15 -0
- package/dist/install-ci-capabilities/intake.d.ts.map +1 -0
- package/dist/install-ci-capabilities/intake.js +28 -0
- package/dist/install-ci-capabilities/intake.js.map +1 -0
- package/dist/install-ci-capabilities/verify.d.ts +14 -0
- package/dist/install-ci-capabilities/verify.d.ts.map +1 -0
- package/dist/install-ci-capabilities/verify.js +27 -0
- package/dist/install-ci-capabilities/verify.js.map +1 -0
- package/dist/install-ci-core.d.ts +446 -0
- package/dist/install-ci-core.d.ts.map +1 -0
- package/dist/install-ci-core.js +760 -0
- package/dist/install-ci-core.js.map +1 -0
- package/dist/install-ci-github.d.ts +167 -0
- package/dist/install-ci-github.d.ts.map +1 -0
- package/dist/install-ci-github.js +315 -0
- package/dist/install-ci-github.js.map +1 -0
- package/dist/install-ci.d.ts +105 -0
- package/dist/install-ci.d.ts.map +1 -0
- package/dist/install-ci.js +363 -0
- package/dist/install-ci.js.map +1 -0
- package/dist/intake-event.d.ts +88 -0
- package/dist/intake-event.d.ts.map +1 -0
- package/dist/intake-event.js +66 -0
- package/dist/intake-event.js.map +1 -0
- package/dist/intake-marker.d.ts +95 -0
- package/dist/intake-marker.d.ts.map +1 -0
- package/dist/intake-marker.js +127 -0
- package/dist/intake-marker.js.map +1 -0
- package/dist/intake-triage.d.ts +48 -0
- package/dist/intake-triage.d.ts.map +1 -0
- package/dist/intake-triage.js +95 -0
- package/dist/intake-triage.js.map +1 -0
- package/dist/intake-trigger-template.d.ts +185 -0
- package/dist/intake-trigger-template.d.ts.map +1 -0
- package/dist/intake-trigger-template.js +449 -0
- package/dist/intake-trigger-template.js.map +1 -0
- package/dist/intake.d.ts +569 -0
- package/dist/intake.d.ts.map +1 -0
- package/dist/intake.js +1628 -0
- package/dist/intake.js.map +1 -0
- package/dist/integration-core.d.ts +539 -0
- package/dist/integration-core.d.ts.map +1 -0
- package/dist/integration-core.js +2195 -0
- package/dist/integration-core.js.map +1 -0
- package/dist/integrator.d.ts +343 -0
- package/dist/integrator.d.ts.map +1 -0
- package/dist/integrator.js +400 -0
- package/dist/integrator.js.map +1 -0
- package/dist/isolation.d.ts +219 -0
- package/dist/isolation.d.ts.map +1 -0
- package/dist/isolation.js +261 -0
- package/dist/isolation.js.map +1 -0
- package/dist/issue-provider.d.ts +349 -0
- package/dist/issue-provider.d.ts.map +1 -0
- package/dist/issue-provider.js +360 -0
- package/dist/issue-provider.js.map +1 -0
- package/dist/item-lock.d.ts +626 -0
- package/dist/item-lock.d.ts.map +1 -0
- package/dist/item-lock.js +1381 -0
- package/dist/item-lock.js.map +1 -0
- package/dist/item-path.d.ts +49 -0
- package/dist/item-path.d.ts.map +1 -0
- package/dist/item-path.js +66 -0
- package/dist/item-path.js.map +1 -0
- package/dist/ledger-lint.d.ts +129 -0
- package/dist/ledger-lint.d.ts.map +1 -0
- package/dist/ledger-lint.js +249 -0
- package/dist/ledger-lint.js.map +1 -0
- package/dist/ledger-read.d.ts +357 -0
- package/dist/ledger-read.d.ts.map +1 -0
- package/dist/ledger-read.js +442 -0
- package/dist/ledger-read.js.map +1 -0
- package/dist/ledger-write.d.ts +330 -0
- package/dist/ledger-write.d.ts.map +1 -0
- package/dist/ledger-write.js +411 -0
- package/dist/ledger-write.js.map +1 -0
- package/dist/lifecycle-gather.d.ts +30 -0
- package/dist/lifecycle-gather.d.ts.map +1 -0
- package/dist/lifecycle-gather.js +205 -0
- package/dist/lifecycle-gather.js.map +1 -0
- package/dist/lifecycle-pools.d.ts +180 -0
- package/dist/lifecycle-pools.d.ts.map +1 -0
- package/dist/lifecycle-pools.js +78 -0
- package/dist/lifecycle-pools.js.map +1 -0
- package/dist/merge-question-surfacer.d.ts +166 -0
- package/dist/merge-question-surfacer.d.ts.map +1 -0
- package/dist/merge-question-surfacer.js +297 -0
- package/dist/merge-question-surfacer.js.map +1 -0
- package/dist/mint-adr.d.ts +126 -0
- package/dist/mint-adr.d.ts.map +1 -0
- package/dist/mint-adr.js +257 -0
- package/dist/mint-adr.js.map +1 -0
- package/dist/mirror-pool-scan.d.ts +125 -0
- package/dist/mirror-pool-scan.d.ts.map +1 -0
- package/dist/mirror-pool-scan.js +104 -0
- package/dist/mirror-pool-scan.js.map +1 -0
- package/dist/needs-attention.d.ts +341 -0
- package/dist/needs-attention.d.ts.map +1 -0
- package/dist/needs-attention.js +900 -0
- package/dist/needs-attention.js.map +1 -0
- package/dist/orphan-sidecar.d.ts +79 -0
- package/dist/orphan-sidecar.d.ts.map +1 -0
- package/dist/orphan-sidecar.js +71 -0
- package/dist/orphan-sidecar.js.map +1 -0
- package/dist/output.d.ts +48 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +66 -0
- package/dist/output.js.map +1 -0
- package/dist/pi-harness.d.ts +179 -0
- package/dist/pi-harness.d.ts.map +1 -0
- package/dist/pi-harness.js +342 -0
- package/dist/pi-harness.js.map +1 -0
- package/dist/placement.d.ts +99 -0
- package/dist/placement.d.ts.map +1 -0
- package/dist/placement.js +67 -0
- package/dist/placement.js.map +1 -0
- package/dist/prd-to-spec.d.ts +315 -0
- package/dist/prd-to-spec.d.ts.map +1 -0
- package/dist/prd-to-spec.js +684 -0
- package/dist/prd-to-spec.js.map +1 -0
- package/dist/prepare.d.ts +121 -0
- package/dist/prepare.d.ts.map +1 -0
- package/dist/prepare.js +140 -0
- package/dist/prepare.js.map +1 -0
- package/dist/prompt.d.ts +360 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +499 -0
- package/dist/prompt.js.map +1 -0
- package/dist/protocol/ADR-FORMAT.md +47 -0
- package/dist/protocol/CLAIM-PROTOCOL.md +217 -0
- package/dist/protocol/REVIEW-PROTOCOL.md +119 -0
- package/dist/protocol/SURFACE-PROTOCOL.md +121 -0
- package/dist/protocol/TASKING-PROTOCOL.md +122 -0
- package/dist/protocol/WORK-CONTRACT.md +276 -0
- package/dist/protocol/spec-template.md +71 -0
- package/dist/protocol/task-template.md +65 -0
- package/dist/readiness.d.ts +66 -0
- package/dist/readiness.d.ts.map +1 -0
- package/dist/readiness.js +36 -0
- package/dist/readiness.js.map +1 -0
- package/dist/reap-branches.d.ts +102 -0
- package/dist/reap-branches.d.ts.map +1 -0
- package/dist/reap-branches.js +149 -0
- package/dist/reap-branches.js.map +1 -0
- package/dist/recover-isolated.d.ts +72 -0
- package/dist/recover-isolated.d.ts.map +1 -0
- package/dist/recover-isolated.js +188 -0
- package/dist/recover-isolated.js.map +1 -0
- package/dist/registry.d.ts +172 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +296 -0
- package/dist/registry.js.map +1 -0
- package/dist/repo-config.d.ts +201 -0
- package/dist/repo-config.d.ts.map +1 -0
- package/dist/repo-config.js +414 -0
- package/dist/repo-config.js.map +1 -0
- package/dist/repo-key.d.ts +20 -0
- package/dist/repo-key.d.ts.map +1 -0
- package/dist/repo-key.js +68 -0
- package/dist/repo-key.js.map +1 -0
- package/dist/repo-mirror.d.ts +177 -0
- package/dist/repo-mirror.d.ts.map +1 -0
- package/dist/repo-mirror.js +271 -0
- package/dist/repo-mirror.js.map +1 -0
- package/dist/retry-backoff.d.ts +90 -0
- package/dist/retry-backoff.d.ts.map +1 -0
- package/dist/retry-backoff.js +98 -0
- package/dist/retry-backoff.js.map +1 -0
- package/dist/review-gate.d.ts +173 -0
- package/dist/review-gate.d.ts.map +1 -0
- package/dist/review-gate.js +261 -0
- package/dist/review-gate.js.map +1 -0
- package/dist/review-verdict.d.ts +149 -0
- package/dist/review-verdict.d.ts.map +1 -0
- package/dist/review-verdict.js +332 -0
- package/dist/review-verdict.js.map +1 -0
- package/dist/run.d.ts +221 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +963 -0
- package/dist/run.js.map +1 -0
- package/dist/scan.d.ts +308 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +374 -0
- package/dist/scan.js.map +1 -0
- package/dist/select-order.d.ts +75 -0
- package/dist/select-order.d.ts.map +1 -0
- package/dist/select-order.js +108 -0
- package/dist/select-order.js.map +1 -0
- package/dist/select-priority.d.ts +188 -0
- package/dist/select-priority.d.ts.map +1 -0
- package/dist/select-priority.js +80 -0
- package/dist/select-priority.js.map +1 -0
- package/dist/select.d.ts +25 -0
- package/dist/select.d.ts.map +1 -0
- package/dist/select.js +43 -0
- package/dist/select.js.map +1 -0
- package/dist/session-path.d.ts +36 -0
- package/dist/session-path.d.ts.map +1 -0
- package/dist/session-path.js +129 -0
- package/dist/session-path.js.map +1 -0
- package/dist/sidecar-apply.d.ts +83 -0
- package/dist/sidecar-apply.d.ts.map +1 -0
- package/dist/sidecar-apply.js +111 -0
- package/dist/sidecar-apply.js.map +1 -0
- package/dist/sidecar.d.ts +245 -0
- package/dist/sidecar.d.ts.map +1 -0
- package/dist/sidecar.js +481 -0
- package/dist/sidecar.js.map +1 -0
- package/dist/slug-namespace.d.ts +204 -0
- package/dist/slug-namespace.d.ts.map +1 -0
- package/dist/slug-namespace.js +229 -0
- package/dist/slug-namespace.js.map +1 -0
- package/dist/spec-complete.d.ts +44 -0
- package/dist/spec-complete.d.ts.map +1 -0
- package/dist/spec-complete.js +69 -0
- package/dist/spec-complete.js.map +1 -0
- package/dist/start.d.ts +97 -0
- package/dist/start.d.ts.map +1 -0
- package/dist/start.js +633 -0
- package/dist/start.js.map +1 -0
- package/dist/status.d.ts +199 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +228 -0
- package/dist/status.js.map +1 -0
- package/dist/surface-gate.d.ts +162 -0
- package/dist/surface-gate.d.ts.map +1 -0
- package/dist/surface-gate.js +206 -0
- package/dist/surface-gate.js.map +1 -0
- package/dist/surface-persist.d.ts +86 -0
- package/dist/surface-persist.d.ts.map +1 -0
- package/dist/surface-persist.js +129 -0
- package/dist/surface-persist.js.map +1 -0
- package/dist/tasker-review-loop.d.ts +249 -0
- package/dist/tasker-review-loop.d.ts.map +1 -0
- package/dist/tasker-review-loop.js +369 -0
- package/dist/tasker-review-loop.js.map +1 -0
- package/dist/tasking-eligibility.d.ts +74 -0
- package/dist/tasking-eligibility.d.ts.map +1 -0
- package/dist/tasking-eligibility.js +52 -0
- package/dist/tasking-eligibility.js.map +1 -0
- package/dist/tasking-lock.d.ts +111 -0
- package/dist/tasking-lock.d.ts.map +1 -0
- package/dist/tasking-lock.js +256 -0
- package/dist/tasking-lock.js.map +1 -0
- package/dist/tasking.d.ts +275 -0
- package/dist/tasking.d.ts.map +1 -0
- package/dist/tasking.js +952 -0
- package/dist/tasking.js.map +1 -0
- package/dist/triage-gate.d.ts +127 -0
- package/dist/triage-gate.d.ts.map +1 -0
- package/dist/triage-gate.js +139 -0
- package/dist/triage-gate.js.map +1 -0
- package/dist/triage-persist.d.ts +163 -0
- package/dist/triage-persist.d.ts.map +1 -0
- package/dist/triage-persist.js +387 -0
- package/dist/triage-persist.js.map +1 -0
- package/dist/verdict-json.d.ts +32 -0
- package/dist/verdict-json.d.ts.map +1 -0
- package/dist/verdict-json.js +74 -0
- package/dist/verdict-json.js.map +1 -0
- package/dist/verify-workflow-template.d.ts +60 -0
- package/dist/verify-workflow-template.d.ts.map +1 -0
- package/dist/verify-workflow-template.js +126 -0
- package/dist/verify-workflow-template.js.map +1 -0
- package/dist/verify.d.ts +60 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +62 -0
- package/dist/verify.js.map +1 -0
- package/dist/watch-session.d.ts +112 -0
- package/dist/watch-session.d.ts.map +1 -0
- package/dist/watch-session.js +347 -0
- package/dist/watch-session.js.map +1 -0
- package/dist/work-layout.d.ts +198 -0
- package/dist/work-layout.d.ts.map +1 -0
- package/dist/work-layout.js +217 -0
- package/dist/work-layout.js.map +1 -0
- package/dist/work-on.d.ts +154 -0
- package/dist/work-on.d.ts.map +1 -0
- package/dist/work-on.js +387 -0
- package/dist/work-on.js.map +1 -0
- package/dist/workspace.d.ts +224 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +325 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +46 -2
- package/src/advance-ci-template.ts +203 -0
- package/src/advance-classify.ts +197 -0
- package/src/advance-drivers.ts +414 -0
- package/src/advance-isolated.ts +432 -0
- package/src/advance-lifecycle-template.ts +791 -0
- package/src/advance-loop-driver.ts +745 -0
- package/src/advance-treeless-publish.ts +177 -0
- package/src/advance.ts +1564 -0
- package/src/advancing-lock.ts +988 -0
- package/src/agent-launch.ts +137 -0
- package/src/agent-stop.ts +361 -0
- package/src/apply-decide.ts +242 -0
- package/src/apply-merge-action.ts +502 -0
- package/src/apply-persist.ts +518 -0
- package/src/arbiter.ts +372 -0
- package/src/brand.ts +111 -0
- package/src/buildable-body.ts +196 -0
- package/src/categorise.ts +158 -0
- package/src/claim-cas.ts +513 -0
- package/src/cli-spinner.ts +225 -0
- package/src/cli.ts +4369 -0
- package/src/close-job-template.ts +236 -0
- package/src/close-job.ts +319 -0
- package/src/complete.ts +1379 -0
- package/src/concurrency.ts +151 -0
- package/src/config-override.ts +116 -0
- package/src/config.ts +883 -0
- package/src/continue-branch.ts +542 -0
- package/src/cwd-section.ts +392 -0
- package/src/decision-engine.ts +272 -0
- package/src/detect.ts +124 -0
- package/src/do-autopick.ts +223 -0
- package/src/do-config.ts +589 -0
- package/src/do-remote-auto.ts +197 -0
- package/src/do.ts +2623 -0
- package/src/drop-source.ts +194 -0
- package/src/eligibility.ts +79 -0
- package/src/env-config.ts +305 -0
- package/src/failure-cause.ts +142 -0
- package/src/format.ts +313 -0
- package/src/frontmatter.ts +485 -0
- package/src/gate-readiness.ts +147 -0
- package/src/gc.ts +510 -0
- package/src/gh-failure.ts +53 -0
- package/src/git.ts +186 -0
- package/src/github.ts +468 -0
- package/src/harness.ts +355 -0
- package/src/identity.ts +322 -0
- package/src/index.ts +785 -0
- package/src/install-ci-branch-protection.ts +255 -0
- package/src/install-ci-capabilities/advance-lifecycle.ts +34 -0
- package/src/install-ci-capabilities/close-job.ts +32 -0
- package/src/install-ci-capabilities/example-noop.ts +24 -0
- package/src/install-ci-capabilities/intake.ts +34 -0
- package/src/install-ci-capabilities/verify.ts +33 -0
- package/src/install-ci-core.ts +1088 -0
- package/src/install-ci-github.ts +376 -0
- package/src/install-ci.ts +552 -0
- package/src/intake-event.ts +102 -0
- package/src/intake-marker.ts +195 -0
- package/src/intake-triage.ts +138 -0
- package/src/intake-trigger-template.ts +591 -0
- package/src/intake.ts +2445 -0
- package/src/integration-core.ts +3065 -0
- package/src/integrator.ts +771 -0
- package/src/isolation.ts +484 -0
- package/src/issue-provider.ts +733 -0
- package/src/item-lock.ts +1858 -0
- package/src/item-path.ts +75 -0
- package/src/ledger-lint.ts +332 -0
- package/src/ledger-read.ts +924 -0
- package/src/ledger-write.ts +865 -0
- package/src/lifecycle-gather.ts +298 -0
- package/src/lifecycle-pools.ts +250 -0
- package/src/merge-question-surfacer.ts +496 -0
- package/src/mint-adr.ts +362 -0
- package/src/mirror-pool-scan.ts +240 -0
- package/src/needs-attention.ts +1506 -0
- package/src/orphan-sidecar.ts +150 -0
- package/src/output.ts +89 -0
- package/src/pi-harness.ts +403 -0
- package/src/placement.ts +131 -0
- package/src/prd-to-spec.ts +1023 -0
- package/src/prepare.ts +230 -0
- package/src/prompt.ts +760 -0
- package/src/readiness.ts +98 -0
- package/src/reap-branches.ts +278 -0
- package/src/recover-isolated.ts +276 -0
- package/src/registry.ts +475 -0
- package/src/repo-config.ts +550 -0
- package/src/repo-key.ts +74 -0
- package/src/repo-mirror.ts +367 -0
- package/src/retry-backoff.ts +130 -0
- package/src/review-gate.ts +389 -0
- package/src/review-verdict.ts +422 -0
- package/src/run.ts +1430 -0
- package/src/scan.ts +611 -0
- package/src/select-order.ts +143 -0
- package/src/select-priority.ts +266 -0
- package/src/select.ts +62 -0
- package/src/session-path.ts +153 -0
- package/src/sidecar-apply.ts +216 -0
- package/src/sidecar.ts +700 -0
- package/src/slug-namespace.ts +367 -0
- package/src/spec-complete.ts +118 -0
- package/src/start.ts +974 -0
- package/src/status.ts +441 -0
- package/src/surface-gate.ts +337 -0
- package/src/surface-persist.ts +241 -0
- package/src/tasker-review-loop.ts +671 -0
- package/src/tasking-eligibility.ts +114 -0
- package/src/tasking-lock.ts +416 -0
- package/src/tasking.ts +1438 -0
- package/src/triage-gate.ts +248 -0
- package/src/triage-persist.ts +570 -0
- package/src/verdict-json.ts +73 -0
- package/src/verify-workflow-template.ts +159 -0
- package/src/verify.ts +123 -0
- package/src/watch-session.ts +397 -0
- package/src/work-layout.ts +262 -0
- package/src/work-on.ts +660 -0
- package/src/workspace.ts +502 -0
|
@@ -0,0 +1,988 @@
|
|
|
1
|
+
import {mkdirSync, writeFileSync, rmSync} from 'node:fs';
|
|
2
|
+
import {dirname, join} from 'node:path';
|
|
3
|
+
import {runAsync, type RunResult} from './git.js';
|
|
4
|
+
import {ledgerWrite} from './ledger-write.js';
|
|
5
|
+
import {resolveSidecarIdentity} from './sidecar.js';
|
|
6
|
+
import {acquireItemLock, releaseItemLock} from './item-lock.js';
|
|
7
|
+
import {realSleep, type Sleep} from './retry-backoff.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The **advancing-lock BORROW** (prd `advance-loop`, task
|
|
11
|
+
* `advancing-lock-borrow`, US #19–24).
|
|
12
|
+
*
|
|
13
|
+
* The surface/apply/triage phase's SHORT borrow. As of the capstone cut-over
|
|
14
|
+
* (task `cutover-retire-slicing-advancing-markers-and-trim-folder-sets`, prd
|
|
15
|
+
* `ledger-status-per-item-lock-refs`; ADR `ledger-status-on-per-item-lock-refs`)
|
|
16
|
+
* the legacy `work/advancing/<entry>.md` presence-MARKER on `main` is GONE. The
|
|
17
|
+
* advancing borrow now rides ONLY the UNIFIED per-item lock
|
|
18
|
+
* (`refs/dorfl/lock/<entry>`, `action: advance`) — there is no transient
|
|
19
|
+
* status in `main`'s tree anymore, so a work branch cut from `main` inherits no
|
|
20
|
+
* stale advancing marker.
|
|
21
|
+
*
|
|
22
|
+
* The borrow is keyed by item IDENTITY (`<type>-<slug>`, via
|
|
23
|
+
* {@link resolveSidecarIdentity} — the single source of truth the unified lock,
|
|
24
|
+
* the sidecar, and the work branch all share), so a task, a prd, and an
|
|
25
|
+
* observation that share a slug NEVER collide, and the SAME `<entry>` ref means an
|
|
26
|
+
* `advance` hold is MUTUALLY EXCLUSIVE with a claim/task hold of the SAME item BY
|
|
27
|
+
* CONSTRUCTION (the second acquirer loses the SAME create-only ref CAS).
|
|
28
|
+
*
|
|
29
|
+
* It is file-ORTHOGONAL to the item it locks: the item's own lifecycle file NEVER
|
|
30
|
+
* moves (the borrow is a LOCK, not a lifecycle transition), which is exactly why
|
|
31
|
+
* it can lock items resting in DIFFERENT source folders (a backlog task, a `prds/`
|
|
32
|
+
* prd, an `observations/` note) with one uniform mechanism.
|
|
33
|
+
*
|
|
34
|
+
* **TREE-LESS vs BUILD/TASK RUNGS (`acquireUnified`).** The advance tick sets
|
|
35
|
+
* `acquireUnified` PER RUNG (the policy lives in `advance.ts`, where the rung is
|
|
36
|
+
* known):
|
|
37
|
+
*
|
|
38
|
+
* - **TREE-LESS rungs** (`surface`/`apply`/`triage`) have NO inner `do`, so the
|
|
39
|
+
* advancing acquire takes the item's unified `action: advance` lock — that hold
|
|
40
|
+
* IS the advance∥claim / advance∥task exclusion. Acquire/release delegate to
|
|
41
|
+
* {@link acquireItemLock} / {@link releaseItemLock} (a parentless ref CAS, no
|
|
42
|
+
* working-tree write, no retry budget).
|
|
43
|
+
* - **BUILD-TASK / TASK-PRD rungs** never take the unified lock at the advance
|
|
44
|
+
* layer (`acquireUnified` false): `performAdvance` orchestrates an inner
|
|
45
|
+
* `performDo` that ITSELF acquires the SAME `task-<slug>`/`prd-<slug>` ref
|
|
46
|
+
* (the create-only CAS with NO re-entrancy/auto-steal), so taking it again here
|
|
47
|
+
* would DEADLOCK the tick against itself. For these rungs the acquire/release
|
|
48
|
+
* are a NO-OP (`acquired`/`released`); the inner `do`'s lock is the sole
|
|
49
|
+
* exclusion point (the POST-#9 EXCLUSION PROOF, owned by this task).
|
|
50
|
+
*
|
|
51
|
+
* A dry-run never takes the lock (it mutates nothing). This module stays
|
|
52
|
+
* rung-agnostic — it only knows "unified or not", never the rung kind.
|
|
53
|
+
*
|
|
54
|
+
* This module also delivers {@link createItemThroughCas}: new-item creation routed
|
|
55
|
+
* THROUGH the SAME write-seam CAS, keyed on the NEW item's identity (its target
|
|
56
|
+
* path), so the (unlikely) same-slug new-item race needs NO special case — the
|
|
57
|
+
* loser simply fails the CAS and backs off. The triage rung (observation→promote
|
|
58
|
+
* drafting a new `work/backlog/<new-slug>.md`) consumes it. It is NOT a lock and
|
|
59
|
+
* was never tied to the marker.
|
|
60
|
+
*
|
|
61
|
+
* Lock discipline: MANDATORY for the autonomous driver (a contender may be active),
|
|
62
|
+
* a no-op formality for a solo human (no contender). Every lock acquire/release is
|
|
63
|
+
* RUNNER-mediated (the agent never touches the lock ref). Recovery is the unified
|
|
64
|
+
* lock's: `release-lock <item>` + the `gc --ledger` stuck-lock report (no liveness
|
|
65
|
+
* heartbeat, no auto-sweep; a human asserts a lock is dead).
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
const DEFAULT_ARBITER = 'origin';
|
|
69
|
+
|
|
70
|
+
// --- Acquire --------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/** A semantic label for the lock-acquire outcome (never the verdict itself). */
|
|
73
|
+
export type AcquireAdvancingLockOutcome =
|
|
74
|
+
| 'acquired'
|
|
75
|
+
| 'usage-error'
|
|
76
|
+
| 'lost'
|
|
77
|
+
| 'contended';
|
|
78
|
+
|
|
79
|
+
/** Maps onto the claim-CAS exit codes (identical semantics). */
|
|
80
|
+
export type AcquireAdvancingLockExitCode = 0 | 1 | 2 | 3;
|
|
81
|
+
|
|
82
|
+
export interface AcquireAdvancingLockOptions {
|
|
83
|
+
/**
|
|
84
|
+
* The NAMESPACED item identity to lock (`task:<slug>` / `prd:<slug>` /
|
|
85
|
+
* `obs:<slug>` / `observation:<slug>`, or a bare `<slug>` = task). The
|
|
86
|
+
* resolver derives the type-encoded entry `<type>-<slug>` from it.
|
|
87
|
+
*/
|
|
88
|
+
item: string;
|
|
89
|
+
/** Working clone/worktree the lock acquire runs in. */
|
|
90
|
+
cwd: string;
|
|
91
|
+
/** Name of the arbiter remote (`--arbiter`). Defaults to `origin`. */
|
|
92
|
+
arbiter?: string;
|
|
93
|
+
/** Advisory locker id. Defaults to git user.name, then $USER. */
|
|
94
|
+
by?: string;
|
|
95
|
+
/** Cap on push retries when main merely advanced. Default 3. (Unified lock is retry-free; retained for API parity.) */
|
|
96
|
+
retries?: number;
|
|
97
|
+
/** Show the intended push without mutating the arbiter (`--dry-run`). */
|
|
98
|
+
dryRun?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Acquire the item's UNIFIED per-item lock (`action: advance`). The advance tick
|
|
101
|
+
* sets this PER RUNG (the policy lives where the rung is known — `advance.ts`):
|
|
102
|
+
* `true` for the TREE-LESS rungs (`surface`/`apply`/`triage`), which have no
|
|
103
|
+
* inner `do` and so genuinely need the unified hold to realise advance∥claim /
|
|
104
|
+
* advance∥task exclusion; `false` (the default) for the build-task / task-spec
|
|
105
|
+
* rungs, whose inner `performDo` ALREADY takes the SAME `task-<slug>`/`prd-<slug>`
|
|
106
|
+
* ref — taking it again here would DEADLOCK the tick against itself, so for those
|
|
107
|
+
* the acquire is a NO-OP `acquired`. When `true`, a lock `lost` makes the acquire
|
|
108
|
+
* lose DEFINITIVELY (no retry budget). A dry-run never takes the lock (it mutates
|
|
109
|
+
* nothing). This module stays rung-agnostic — it only knows "unified or not".
|
|
110
|
+
*/
|
|
111
|
+
acquireUnified?: boolean;
|
|
112
|
+
/** Environment for child git processes. */
|
|
113
|
+
env?: NodeJS.ProcessEnv;
|
|
114
|
+
/** Sink for human-readable progress notes. */
|
|
115
|
+
note?: (message: string) => void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface AcquireAdvancingLockResult {
|
|
119
|
+
exitCode: AcquireAdvancingLockExitCode;
|
|
120
|
+
outcome: AcquireAdvancingLockOutcome;
|
|
121
|
+
/** Human-readable summary of the terminal condition. */
|
|
122
|
+
message: string;
|
|
123
|
+
/**
|
|
124
|
+
* The type-encoded lock entry name (`<type>-<slug>`) the borrow keyed onto —
|
|
125
|
+
* the unified lock ref is `refs/dorfl/lock/<entry>`. Surfaced so the
|
|
126
|
+
* caller hands the SAME entry back (and so tests can assert the type-encoding).
|
|
127
|
+
*/
|
|
128
|
+
entry?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Raised for usage/environment errors (exit 1). */
|
|
132
|
+
class AdvancingLockUsageError extends Error {}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Acquire the advancing borrow for `item`. For a TREE-LESS rung
|
|
136
|
+
* (`acquireUnified: true`) this is the item's unified `action: advance` lock (a
|
|
137
|
+
* parentless ref CAS); for a build/task rung (the default) it is a NO-OP
|
|
138
|
+
* `acquired` (the inner `do`'s claim/task lock is the exclusion). Never throws
|
|
139
|
+
* for the expected "lost the race" (exit 2) case — it is returned.
|
|
140
|
+
*/
|
|
141
|
+
export async function acquireAdvancingLock(
|
|
142
|
+
options: AcquireAdvancingLockOptions,
|
|
143
|
+
): Promise<AcquireAdvancingLockResult> {
|
|
144
|
+
const note = options.note ?? (() => {});
|
|
145
|
+
try {
|
|
146
|
+
return await runAcquire(options, note);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
if (err instanceof AdvancingLockUsageError) {
|
|
149
|
+
return {exitCode: 1, outcome: 'usage-error', message: err.message};
|
|
150
|
+
}
|
|
151
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
152
|
+
return {exitCode: 1, outcome: 'usage-error', message};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function runAcquire(
|
|
157
|
+
options: AcquireAdvancingLockOptions,
|
|
158
|
+
note: (m: string) => void,
|
|
159
|
+
): Promise<AcquireAdvancingLockResult> {
|
|
160
|
+
const arbiter = options.arbiter ?? DEFAULT_ARBITER;
|
|
161
|
+
const dryRun = options.dryRun ?? false;
|
|
162
|
+
const cwd = options.cwd;
|
|
163
|
+
const env = options.env;
|
|
164
|
+
|
|
165
|
+
if (!options.item) {
|
|
166
|
+
throw new AdvancingLockUsageError(
|
|
167
|
+
'missing <item>. usage: acquireAdvancingLock({item, cwd, arbiter})',
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
// The type-encoded entry `<type>-<slug>` — the SAME identity scheme the sidecar
|
|
171
|
+
// and the unified lock use, derived via the shared resolver (the source of truth).
|
|
172
|
+
const {type, slug} = resolveSidecarIdentity(options.item);
|
|
173
|
+
const entry = `${type}-${slug}`;
|
|
174
|
+
|
|
175
|
+
if ((await gitSoft(['rev-parse', '--git-dir'], cwd, env)).status !== 0) {
|
|
176
|
+
throw new AdvancingLockUsageError('not inside a git repository');
|
|
177
|
+
}
|
|
178
|
+
if ((await gitSoft(['remote', 'get-url', arbiter], cwd, env)).status !== 0) {
|
|
179
|
+
throw new AdvancingLockUsageError(
|
|
180
|
+
`no git remote named '${arbiter}' (set one, or pass --arbiter)`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
const by = options.by || (await resolveBy(cwd, env));
|
|
184
|
+
|
|
185
|
+
// BUILD/TASK rung (or a dry-run): NO advance-layer hold at all. The inner `do`'s
|
|
186
|
+
// claim/task unified lock is the sole exclusion point — taking it again here
|
|
187
|
+
// would deadlock the tick against itself. A NO-OP `acquired`.
|
|
188
|
+
const acquireUnified = (options.acquireUnified ?? false) && !dryRun;
|
|
189
|
+
if (!acquireUnified) {
|
|
190
|
+
const message = dryRun
|
|
191
|
+
? `[dry-run] advancing borrow for '${entry}' (no lock taken).`
|
|
192
|
+
: `advancing borrow for '${entry}' rides the inner do's lock (no advance-layer hold).`;
|
|
193
|
+
note(message);
|
|
194
|
+
return {exitCode: 0, outcome: 'acquired', message, entry};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// TREE-LESS rung: take the item's UNIFIED per-item lock (`action: advance`,
|
|
198
|
+
// keyed `item` so it shares the ONE `<type>-<slug>` ref with claim/task/advance
|
|
199
|
+
// of the SAME item). A create-only ref CAS: the winner holds it, the loser is
|
|
200
|
+
// DEFINITIVELY `lost` (exit 2, no retry budget — a per-item conflict the loser
|
|
201
|
+
// should lose). No auto-steal of an orphaned lock, consistent with claim/task
|
|
202
|
+
// and the ADR's recovery model (no liveness heartbeat / auto-sweep; a human
|
|
203
|
+
// asserts a lock is dead via `release-lock` + `gc --ledger`).
|
|
204
|
+
const lock = await acquireItemLock({
|
|
205
|
+
item: options.item,
|
|
206
|
+
action: 'advance',
|
|
207
|
+
cwd,
|
|
208
|
+
arbiter,
|
|
209
|
+
holder: by,
|
|
210
|
+
env,
|
|
211
|
+
});
|
|
212
|
+
if (lock.outcome === 'error') {
|
|
213
|
+
throw new AdvancingLockUsageError(
|
|
214
|
+
`failed to acquire the item lock for '${entry}': ${lock.message}`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (lock.outcome === 'lost') {
|
|
218
|
+
note(lock.message);
|
|
219
|
+
return {exitCode: 2, outcome: 'lost', message: lock.message, entry};
|
|
220
|
+
}
|
|
221
|
+
const message = `LOCKED '${entry}' for advancing on ${arbiter} (unified lock).`;
|
|
222
|
+
note(message);
|
|
223
|
+
return {exitCode: 0, outcome: 'acquired', message, entry};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// --- Release --------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
/** A semantic label for the lock-release outcome. */
|
|
229
|
+
export type ReleaseAdvancingLockOutcome =
|
|
230
|
+
| 'released'
|
|
231
|
+
| 'usage-error'
|
|
232
|
+
| 'lost'
|
|
233
|
+
| 'contended';
|
|
234
|
+
|
|
235
|
+
/** Maps onto the claim-CAS exit codes (identical semantics). */
|
|
236
|
+
export type ReleaseAdvancingLockExitCode = 0 | 1 | 2 | 3;
|
|
237
|
+
|
|
238
|
+
export interface ReleaseAdvancingLockOptions {
|
|
239
|
+
/** The NAMESPACED item identity whose borrow to release (same forms as acquire). */
|
|
240
|
+
item: string;
|
|
241
|
+
/** Working clone/worktree the release runs in. */
|
|
242
|
+
cwd: string;
|
|
243
|
+
/** Name of the arbiter remote (`--arbiter`). Defaults to `origin`. */
|
|
244
|
+
arbiter?: string;
|
|
245
|
+
/** Advisory releaser id. Defaults to git user.name, then $USER. */
|
|
246
|
+
by?: string;
|
|
247
|
+
/** Cap on push retries when main merely advanced. Default 3. (Retained for API parity.) */
|
|
248
|
+
retries?: number;
|
|
249
|
+
/**
|
|
250
|
+
* Release the item's UNIFIED per-item lock (the complement of
|
|
251
|
+
* {@link AcquireAdvancingLockOptions.acquireUnified}). The advance tick sets this
|
|
252
|
+
* for a TREE-LESS rung (`surface`/`apply`/`triage`), where the acquire took the
|
|
253
|
+
* unified lock; `false` (the default) for the build-task / task-spec rungs,
|
|
254
|
+
* which never took it at the advance layer (the inner `performDo`'s claim/task
|
|
255
|
+
* lock is the exclusion point and is released by the inner `do`) — for those the
|
|
256
|
+
* release is a NO-OP `released`.
|
|
257
|
+
*/
|
|
258
|
+
releaseUnified?: boolean;
|
|
259
|
+
/** Environment for child git processes. */
|
|
260
|
+
env?: NodeJS.ProcessEnv;
|
|
261
|
+
/** Sink for human-readable progress notes. */
|
|
262
|
+
note?: (message: string) => void;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface ReleaseAdvancingLockResult {
|
|
266
|
+
exitCode: ReleaseAdvancingLockExitCode;
|
|
267
|
+
outcome: ReleaseAdvancingLockOutcome;
|
|
268
|
+
/** Human-readable summary of the terminal condition. */
|
|
269
|
+
message: string;
|
|
270
|
+
/** The type-encoded lock entry name (`<type>-<slug>`) that was released. */
|
|
271
|
+
entry?: string;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Release the advancing borrow for `item`. For a TREE-LESS rung
|
|
276
|
+
* (`releaseUnified: true`) this DELETES the item's unified `action: advance` lock
|
|
277
|
+
* ref; for a build/task rung (the default) it is a NO-OP `released` (that rung
|
|
278
|
+
* never took an advance-layer hold — the inner `do` released its own lock). It
|
|
279
|
+
* moves NO lifecycle file: the borrow is a LOCK, not a lifecycle transition, so
|
|
280
|
+
* the item is returned exactly where it rested.
|
|
281
|
+
*
|
|
282
|
+
* Idempotent: a unified release of an already-absent ref reports `released` (the
|
|
283
|
+
* item is returned to rest with no held lock). Best-effort + never throws for the
|
|
284
|
+
* expected cases.
|
|
285
|
+
*/
|
|
286
|
+
export async function releaseAdvancingLock(
|
|
287
|
+
options: ReleaseAdvancingLockOptions,
|
|
288
|
+
): Promise<ReleaseAdvancingLockResult> {
|
|
289
|
+
const note = options.note ?? (() => {});
|
|
290
|
+
try {
|
|
291
|
+
return await runRelease(options, note);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
if (err instanceof AdvancingLockUsageError) {
|
|
294
|
+
return {exitCode: 1, outcome: 'usage-error', message: err.message};
|
|
295
|
+
}
|
|
296
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
297
|
+
return {exitCode: 1, outcome: 'usage-error', message};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function runRelease(
|
|
302
|
+
options: ReleaseAdvancingLockOptions,
|
|
303
|
+
note: (m: string) => void,
|
|
304
|
+
): Promise<ReleaseAdvancingLockResult> {
|
|
305
|
+
const arbiter = options.arbiter ?? DEFAULT_ARBITER;
|
|
306
|
+
const cwd = options.cwd;
|
|
307
|
+
const env = options.env;
|
|
308
|
+
|
|
309
|
+
if (!options.item) {
|
|
310
|
+
throw new AdvancingLockUsageError(
|
|
311
|
+
'missing <item>. usage: releaseAdvancingLock({item, cwd, arbiter})',
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
const {type, slug} = resolveSidecarIdentity(options.item);
|
|
315
|
+
const entry = `${type}-${slug}`;
|
|
316
|
+
|
|
317
|
+
if ((await gitSoft(['rev-parse', '--git-dir'], cwd, env)).status !== 0) {
|
|
318
|
+
throw new AdvancingLockUsageError('not inside a git repository');
|
|
319
|
+
}
|
|
320
|
+
if ((await gitSoft(['remote', 'get-url', arbiter], cwd, env)).status !== 0) {
|
|
321
|
+
throw new AdvancingLockUsageError(
|
|
322
|
+
`no git remote named '${arbiter}' (set one, or pass --arbiter)`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// BUILD/TASK rung: nothing was held at the advance layer — a NO-OP `released`.
|
|
327
|
+
const releaseUnified = options.releaseUnified ?? false;
|
|
328
|
+
if (!releaseUnified) {
|
|
329
|
+
const message = `advancing borrow for '${entry}' released (no advance-layer hold to drop).`;
|
|
330
|
+
note(message);
|
|
331
|
+
return {exitCode: 0, outcome: 'released', message, entry};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// TREE-LESS rung: delete the unified `action: advance` lock ref (idempotent —
|
|
335
|
+
// an already-absent ref is `not-held`, still mapped to `released`: the item is
|
|
336
|
+
// returned to rest with no held lock).
|
|
337
|
+
const released = await releaseItemLock({
|
|
338
|
+
item: options.item,
|
|
339
|
+
cwd,
|
|
340
|
+
arbiter,
|
|
341
|
+
env,
|
|
342
|
+
});
|
|
343
|
+
if (released.outcome === 'error') {
|
|
344
|
+
throw new AdvancingLockUsageError(
|
|
345
|
+
`failed to release the item lock for '${entry}': ${released.message}`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
const message =
|
|
349
|
+
released.outcome === 'not-held'
|
|
350
|
+
? `advancing borrow for '${entry}' was already released (no lock held).`
|
|
351
|
+
: `RELEASED '${entry}' advancing borrow on ${arbiter} (item untouched).`;
|
|
352
|
+
note(message);
|
|
353
|
+
return {exitCode: 0, outcome: 'released', message, entry};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// --- New-item creation through the CAS ------------------------------------
|
|
357
|
+
|
|
358
|
+
// --- Contention retry: jittered delay + widened budget (lifecycle fan-out) ---
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* The CONTENTION-RETRY BUDGET for the create/publish CAS in
|
|
362
|
+
* {@link createItemThroughCas} — the tuple of bounds that turns the loop from
|
|
363
|
+
* "instantly retry, tiny fixed cap" into a bounded DECORRELATED-JITTER retry with
|
|
364
|
+
* a wider attempt / wall-clock envelope. Deliberately DISTINCT from the OUTAGE
|
|
365
|
+
* regime in `retry-backoff.ts` (exponential backoff, modelling an unreachable
|
|
366
|
+
* remote): a rejected push against a MOVED ref just wants a small RANDOM delay to
|
|
367
|
+
* BREAK LOCKSTEP among parallel legs, not an exponential outage ramp. See the
|
|
368
|
+
* decision block at the bottom of this file for the rationale.
|
|
369
|
+
*
|
|
370
|
+
* All fields optional; the effective values fall back to the interactive default
|
|
371
|
+
* ({@link INTERACTIVE_CAS_CONTENTION}) — a small fixed cap with NO delay, so a
|
|
372
|
+
* lone caller stays prompt. The lifecycle driver passes
|
|
373
|
+
* {@link LIFECYCLE_CAS_CONTENTION} to widen a fan-out.
|
|
374
|
+
*/
|
|
375
|
+
export interface CasContentionBudget {
|
|
376
|
+
/** Cap on push retries when main merely advanced. */
|
|
377
|
+
retries?: number;
|
|
378
|
+
/** Base delay (ms) for the decorrelated-jitter schedule. `0` disables the delay. */
|
|
379
|
+
initialDelayMs?: number;
|
|
380
|
+
/** Cap (ms) the growing jittered delay never exceeds. */
|
|
381
|
+
maxDelayMs?: number;
|
|
382
|
+
/** Wall-clock budget (ms) for cumulative retry delays. `0` disables the wall-clock cap. */
|
|
383
|
+
maxTotalMs?: number;
|
|
384
|
+
/** Injected sleep seam (tests). Defaults to {@link realSleep}. */
|
|
385
|
+
sleep?: Sleep;
|
|
386
|
+
/** Injected `[0,1)` RNG (tests). Defaults to `Math.random`. */
|
|
387
|
+
rng?: () => number;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The INTERACTIVE default: today's shape (3 retries, NO delay). A human-typed
|
|
392
|
+
* single-item caller keeps the prompt bounded give-up it always had. Preserved
|
|
393
|
+
* so the API change is backwards-compatible for direct callers of
|
|
394
|
+
* {@link createItemThroughCas} that pass nothing.
|
|
395
|
+
*/
|
|
396
|
+
export const INTERACTIVE_CAS_CONTENTION: Required<
|
|
397
|
+
Omit<CasContentionBudget, 'sleep' | 'rng'>
|
|
398
|
+
> = {
|
|
399
|
+
retries: 3,
|
|
400
|
+
initialDelayMs: 0,
|
|
401
|
+
maxDelayMs: 0,
|
|
402
|
+
maxTotalMs: 0,
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* The LIFECYCLE default: the wider envelope for the parallel-legs fan-out (the
|
|
407
|
+
* lifecycle propose/triage tick's promote-CAS + `mint-adr` legs). A modest base
|
|
408
|
+
* delay grows under an AWS-style DECORRELATED-JITTER schedule up to a cap, with a
|
|
409
|
+
* generous wall-clock envelope so a herd of valid appends DRAINS instead of
|
|
410
|
+
* thrashing to exhaustion. Sized so a lone leg is barely slower than today
|
|
411
|
+
* (jitter is small on the first retry) while a fan-out of N legs desynchronises
|
|
412
|
+
* within a few retries. See the decision block at the bottom of this file.
|
|
413
|
+
*/
|
|
414
|
+
export const LIFECYCLE_CAS_CONTENTION: Required<
|
|
415
|
+
Omit<CasContentionBudget, 'sleep' | 'rng'>
|
|
416
|
+
> = {
|
|
417
|
+
retries: 32,
|
|
418
|
+
initialDelayMs: 25,
|
|
419
|
+
maxDelayMs: 2_000,
|
|
420
|
+
maxTotalMs: 30_000,
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* The outcome of a single CAS attempt inside {@link runCasContentionLoop}
|
|
425
|
+
* (mirrors {@link CreateAttemptResult}, exported for the retry-loop seam).
|
|
426
|
+
*/
|
|
427
|
+
export type CasAttemptResult =
|
|
428
|
+
| {kind: 'created'; message: string}
|
|
429
|
+
| {kind: 'lost'; message: string}
|
|
430
|
+
| {kind: 'rejected'; message: string};
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* The retry-loop terminal outcome ({@link runCasContentionLoop}). Adds
|
|
434
|
+
* observation counters (`attempts`, the sequence of injected `sleep(ms)` calls)
|
|
435
|
+
* so tests can assert the RETRY TIMELINE + the JITTER SHAPE deterministically.
|
|
436
|
+
*/
|
|
437
|
+
export interface CasContentionLoopResult {
|
|
438
|
+
/** Terminal kind: `created` / `lost` / `contended` (bounded give-up). */
|
|
439
|
+
kind: 'created' | 'lost' | 'contended';
|
|
440
|
+
/** Human-readable summary. */
|
|
441
|
+
message: string;
|
|
442
|
+
/** Total attempts made (1 + retries taken). */
|
|
443
|
+
attempts: number;
|
|
444
|
+
/** The sequence of sleep durations (ms) between retries — the jitter timeline. */
|
|
445
|
+
sleeps: number[];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Run a CAS create/publish attempt inside the CONTENTION-RETRY loop. Reusable
|
|
450
|
+
* primitive shared between {@link createItemThroughCas} (the production path) and
|
|
451
|
+
* the unit tests that drive the loop with an injected fake attempt + injected
|
|
452
|
+
* `Sleep` + injected RNG so the retry timeline + jitter are fully deterministic
|
|
453
|
+
* with no real wall-clock waits.
|
|
454
|
+
*
|
|
455
|
+
* The `attempt` callback runs ONE CAS attempt and reports `created` / `lost` /
|
|
456
|
+
* `rejected`. On `rejected` the loop consults the {@link CasContentionBudget}:
|
|
457
|
+
* 1. if the attempts cap is reached → clean bounded give-up (`contended`);
|
|
458
|
+
* 2. else compute a DECORRELATED-JITTER delay ({@link nextCasContentionDelayMs});
|
|
459
|
+
* 3. if the delay would push cumulative sleep past `maxTotalMs` → clean bounded
|
|
460
|
+
* give-up (`contended` — the wall-clock terminator);
|
|
461
|
+
* 4. else emit the retry note, sleep, and loop.
|
|
462
|
+
*/
|
|
463
|
+
export async function runCasContentionLoop(input: {
|
|
464
|
+
attempt: () => Promise<CasAttemptResult>;
|
|
465
|
+
budget: CasContentionBudget;
|
|
466
|
+
note?: (m: string) => void;
|
|
467
|
+
}): Promise<CasContentionLoopResult> {
|
|
468
|
+
const note = input.note ?? (() => {});
|
|
469
|
+
const budget = input.budget;
|
|
470
|
+
const retries = budget.retries ?? INTERACTIVE_CAS_CONTENTION.retries;
|
|
471
|
+
const initialDelayMs =
|
|
472
|
+
budget.initialDelayMs ?? INTERACTIVE_CAS_CONTENTION.initialDelayMs;
|
|
473
|
+
const maxDelayMs = budget.maxDelayMs ?? INTERACTIVE_CAS_CONTENTION.maxDelayMs;
|
|
474
|
+
const maxTotalMs = budget.maxTotalMs ?? INTERACTIVE_CAS_CONTENTION.maxTotalMs;
|
|
475
|
+
const sleep = budget.sleep ?? realSleep;
|
|
476
|
+
const rng = budget.rng ?? Math.random;
|
|
477
|
+
|
|
478
|
+
let attempts = 0;
|
|
479
|
+
let elapsed = 0;
|
|
480
|
+
let previousDelayMs = 0;
|
|
481
|
+
const sleeps: number[] = [];
|
|
482
|
+
// eslint-disable-next-line no-constant-condition
|
|
483
|
+
while (true) {
|
|
484
|
+
attempts += 1;
|
|
485
|
+
const result = await input.attempt();
|
|
486
|
+
if (result.kind === 'created') {
|
|
487
|
+
return {kind: 'created', message: result.message, attempts, sleeps};
|
|
488
|
+
}
|
|
489
|
+
if (result.kind === 'lost') {
|
|
490
|
+
return {kind: 'lost', message: result.message, attempts, sleeps};
|
|
491
|
+
}
|
|
492
|
+
const rejectedCount = attempts; // attempts so far == rejections so far
|
|
493
|
+
if (rejectedCount > retries) {
|
|
494
|
+
const message = `push rejected ${rejectedCount} times (main is contended). Try again shortly.`;
|
|
495
|
+
note(message);
|
|
496
|
+
return {kind: 'contended', message, attempts, sleeps};
|
|
497
|
+
}
|
|
498
|
+
const delay = nextCasContentionDelayMs({
|
|
499
|
+
previousDelayMs,
|
|
500
|
+
initialDelayMs,
|
|
501
|
+
maxDelayMs,
|
|
502
|
+
rng,
|
|
503
|
+
});
|
|
504
|
+
if (maxTotalMs > 0 && elapsed + delay > maxTotalMs) {
|
|
505
|
+
const message = `push rejected ${rejectedCount} times (main is contended; wall-clock budget ${maxTotalMs}ms exhausted). Try again shortly.`;
|
|
506
|
+
note(message);
|
|
507
|
+
return {kind: 'contended', message, attempts, sleeps};
|
|
508
|
+
}
|
|
509
|
+
note(
|
|
510
|
+
`main advanced under us — refetch and retry (${rejectedCount}/${retries})...`,
|
|
511
|
+
);
|
|
512
|
+
if (delay > 0) {
|
|
513
|
+
await sleep(delay);
|
|
514
|
+
elapsed += delay;
|
|
515
|
+
previousDelayMs = delay;
|
|
516
|
+
sleeps.push(delay);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Compute the NEXT contention-retry delay under AWS-style DECORRELATED JITTER:
|
|
523
|
+
* `delay = min(cap, floor(base + rng() * (min(cap, prev*3) - base)))`, clamped to
|
|
524
|
+
* `[base, cap]`. `previousDelayMs = 0` (the first retry) uses `base` as the
|
|
525
|
+
* seed. Returns `0` iff the delay is disabled (either bound is `0` or `NaN`),
|
|
526
|
+
* which preserves the interactive "instant retry" shape.
|
|
527
|
+
*
|
|
528
|
+
* Exported for the pure-unit shape test — the property is BOUNDED randomness
|
|
529
|
+
* that grows toward the cap.
|
|
530
|
+
*/
|
|
531
|
+
export function nextCasContentionDelayMs(input: {
|
|
532
|
+
previousDelayMs: number;
|
|
533
|
+
initialDelayMs: number;
|
|
534
|
+
maxDelayMs: number;
|
|
535
|
+
rng: () => number;
|
|
536
|
+
}): number {
|
|
537
|
+
const base = input.initialDelayMs;
|
|
538
|
+
const cap = input.maxDelayMs;
|
|
539
|
+
if (!(base > 0) || !(cap > 0)) {
|
|
540
|
+
return 0;
|
|
541
|
+
}
|
|
542
|
+
const prev = input.previousDelayMs > 0 ? input.previousDelayMs : base;
|
|
543
|
+
const upper = Math.min(cap, prev * 3);
|
|
544
|
+
const r = Math.max(0, Math.min(1, input.rng()));
|
|
545
|
+
return Math.max(base, Math.min(cap, Math.floor(base + r * (upper - base))));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** A semantic label for the new-item creation outcome. */
|
|
549
|
+
export type CreateItemOutcome =
|
|
550
|
+
| 'created'
|
|
551
|
+
| 'usage-error'
|
|
552
|
+
| 'lost'
|
|
553
|
+
| 'contended';
|
|
554
|
+
|
|
555
|
+
/** Maps onto the claim-CAS exit codes (identical semantics). */
|
|
556
|
+
export type CreateItemExitCode = 0 | 1 | 2 | 3;
|
|
557
|
+
|
|
558
|
+
export interface CreateItemThroughCasOptions {
|
|
559
|
+
/**
|
|
560
|
+
* The NEW item's path RELATIVE to the repo root (e.g.
|
|
561
|
+
* `work/backlog/<new-slug>.md`). The CAS is keyed on THIS path (the new item's
|
|
562
|
+
* identity): two concurrent creators of the same path race, the loser fails the
|
|
563
|
+
* CAS — no special case for the (unlikely) same-slug new-item collision.
|
|
564
|
+
*/
|
|
565
|
+
path: string;
|
|
566
|
+
/** The new item's file content. */
|
|
567
|
+
content: string;
|
|
568
|
+
/**
|
|
569
|
+
* Extra repo-relative paths to `git rm` IN THE SAME create commit (e.g. the
|
|
570
|
+
* promoted observation + its answered sidecar). They ride the winning creator's
|
|
571
|
+
* ATOMIC commit, so a crash never leaves the source deleted without its
|
|
572
|
+
* successor (or the successor created with the source still live), and a CAS
|
|
573
|
+
* LOSER — which never reaches the commit — leaves them INTACT for a retry.
|
|
574
|
+
* A listed path that is absent on the create branch is skipped (best-effort rm).
|
|
575
|
+
*/
|
|
576
|
+
deletePaths?: string[];
|
|
577
|
+
/** Working clone/worktree the creation runs in. */
|
|
578
|
+
cwd: string;
|
|
579
|
+
/** Name of the arbiter remote (`--arbiter`). Defaults to `origin`. */
|
|
580
|
+
arbiter?: string;
|
|
581
|
+
/** Advisory creator id. Defaults to git user.name, then $USER. */
|
|
582
|
+
by?: string;
|
|
583
|
+
/**
|
|
584
|
+
* Cap on push retries when main merely advanced. Default 3 (interactive shape).
|
|
585
|
+
* The lifecycle driver widens this via {@link contention} /
|
|
586
|
+
* {@link LIFECYCLE_CAS_CONTENTION} so a parallel fan-out DRAINS.
|
|
587
|
+
*/
|
|
588
|
+
retries?: number;
|
|
589
|
+
/**
|
|
590
|
+
* The CONTENTION-RETRY BUDGET (jittered delay + wider attempt / wall-clock
|
|
591
|
+
* envelope). Optional: absent ⇒ {@link INTERACTIVE_CAS_CONTENTION}
|
|
592
|
+
* (today's instant-retry shape, so direct callers are unchanged); the
|
|
593
|
+
* lifecycle driver in `advance.ts` passes {@link LIFECYCLE_CAS_CONTENTION} to
|
|
594
|
+
* WIDEN a fan-out. Individual `contention` fields override {@link retries}.
|
|
595
|
+
*/
|
|
596
|
+
contention?: CasContentionBudget;
|
|
597
|
+
/** Show the intended push without mutating the arbiter (`--dry-run`). */
|
|
598
|
+
dryRun?: boolean;
|
|
599
|
+
/** Environment for child git processes. */
|
|
600
|
+
env?: NodeJS.ProcessEnv;
|
|
601
|
+
/** Sink for human-readable progress notes. */
|
|
602
|
+
note?: (message: string) => void;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export interface CreateItemThroughCasResult {
|
|
606
|
+
exitCode: CreateItemExitCode;
|
|
607
|
+
outcome: CreateItemOutcome;
|
|
608
|
+
/** Human-readable summary of the terminal condition. */
|
|
609
|
+
message: string;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** Internal: the result of a single create attempt. */
|
|
613
|
+
type CreateAttemptResult =
|
|
614
|
+
| {kind: 'created'; message: string}
|
|
615
|
+
| {kind: 'lost'; message: string}
|
|
616
|
+
| {kind: 'rejected'; message: string};
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Create a NEW `work/` item THROUGH the CAS, keyed on the new item's identity (its
|
|
620
|
+
* `path`). The reusable helper the triage rung consumes when an observation→promote
|
|
621
|
+
* drafts a new `work/backlog/<new-slug>.md`: the (unlikely) same-slug new-item race
|
|
622
|
+
* needs NO special case — exactly one creator lands the file, the loser fails the
|
|
623
|
+
* CAS (exit 2) and backs off. Same CAS-micro-commit / force-with-lease shape as the
|
|
624
|
+
* unified lock; it is NOT a lock (it does not hold a borrow), it ATOMICALLY publishes
|
|
625
|
+
* a new file iff that path is still absent on the arbiter.
|
|
626
|
+
*
|
|
627
|
+
* An optional {@link CreateItemThroughCasOptions.deletePaths} rides the SAME create
|
|
628
|
+
* commit (the promote route's note + sidecar `git rm`), so create + delete are ONE
|
|
629
|
+
* atomic commit: a crash never strands the source without its successor, and a CAS
|
|
630
|
+
* LOSER — which never reaches the commit — leaves the source intact for a retry.
|
|
631
|
+
*/
|
|
632
|
+
export async function createItemThroughCas(
|
|
633
|
+
options: CreateItemThroughCasOptions,
|
|
634
|
+
): Promise<CreateItemThroughCasResult> {
|
|
635
|
+
const note = options.note ?? (() => {});
|
|
636
|
+
try {
|
|
637
|
+
return await runCreate(options, note);
|
|
638
|
+
} catch (err) {
|
|
639
|
+
if (err instanceof AdvancingLockUsageError) {
|
|
640
|
+
return {exitCode: 1, outcome: 'usage-error', message: err.message};
|
|
641
|
+
}
|
|
642
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
643
|
+
return {exitCode: 1, outcome: 'usage-error', message};
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async function runCreate(
|
|
648
|
+
options: CreateItemThroughCasOptions,
|
|
649
|
+
note: (m: string) => void,
|
|
650
|
+
): Promise<CreateItemThroughCasResult> {
|
|
651
|
+
const arbiter = options.arbiter ?? DEFAULT_ARBITER;
|
|
652
|
+
const budget = options.contention ?? {};
|
|
653
|
+
const retries =
|
|
654
|
+
budget.retries ?? options.retries ?? INTERACTIVE_CAS_CONTENTION.retries;
|
|
655
|
+
const initialDelayMs =
|
|
656
|
+
budget.initialDelayMs ?? INTERACTIVE_CAS_CONTENTION.initialDelayMs;
|
|
657
|
+
const maxDelayMs = budget.maxDelayMs ?? INTERACTIVE_CAS_CONTENTION.maxDelayMs;
|
|
658
|
+
const maxTotalMs = budget.maxTotalMs ?? INTERACTIVE_CAS_CONTENTION.maxTotalMs;
|
|
659
|
+
const sleep = budget.sleep ?? realSleep;
|
|
660
|
+
const rng = budget.rng ?? Math.random;
|
|
661
|
+
const dryRun = options.dryRun ?? false;
|
|
662
|
+
const cwd = options.cwd;
|
|
663
|
+
const env = options.env;
|
|
664
|
+
const path = options.path;
|
|
665
|
+
|
|
666
|
+
if (!path) {
|
|
667
|
+
throw new AdvancingLockUsageError(
|
|
668
|
+
'missing <path>. usage: createItemThroughCas({path, content, cwd, arbiter})',
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
if ((await gitSoft(['rev-parse', '--git-dir'], cwd, env)).status !== 0) {
|
|
672
|
+
throw new AdvancingLockUsageError('not inside a git repository');
|
|
673
|
+
}
|
|
674
|
+
if ((await gitSoft(['remote', 'get-url', arbiter], cwd, env)).status !== 0) {
|
|
675
|
+
throw new AdvancingLockUsageError(
|
|
676
|
+
`no git remote named '${arbiter}' (set one, or pass --arbiter)`,
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
const by = options.by || (await resolveBy(cwd, env));
|
|
680
|
+
|
|
681
|
+
const dirtyWorktree =
|
|
682
|
+
(await gitSoft(['diff', '--quiet'], cwd, env)).status !== 0;
|
|
683
|
+
const dirtyIndex =
|
|
684
|
+
(await gitSoft(['diff', '--cached', '--quiet'], cwd, env)).status !== 0;
|
|
685
|
+
if (dirtyWorktree || dirtyIndex) {
|
|
686
|
+
throw new AdvancingLockUsageError(
|
|
687
|
+
'working tree has uncommitted changes; commit/stash them before creating',
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// A throwaway create branch, slugified off the new item's path (so concurrent
|
|
692
|
+
// creators of DIFFERENT paths don't collide on the branch name either).
|
|
693
|
+
const createBranch = `create/${path.replace(/[^A-Za-z0-9._-]+/g, '-')}`;
|
|
694
|
+
|
|
695
|
+
const origRef = await originalRef(cwd, env);
|
|
696
|
+
try {
|
|
697
|
+
// The bounded DECORRELATED-JITTER contention-retry lives in
|
|
698
|
+
// {@link runCasContentionLoop} — the shared primitive tests drive directly
|
|
699
|
+
// with an injected fake attempt + `Sleep` + RNG (no real waits, no flakes).
|
|
700
|
+
const loop = await runCasContentionLoop({
|
|
701
|
+
attempt: () =>
|
|
702
|
+
createAttempt({
|
|
703
|
+
path,
|
|
704
|
+
content: options.content,
|
|
705
|
+
deletePaths: options.deletePaths ?? [],
|
|
706
|
+
by,
|
|
707
|
+
arbiter,
|
|
708
|
+
dryRun,
|
|
709
|
+
cwd,
|
|
710
|
+
env,
|
|
711
|
+
createBranch,
|
|
712
|
+
note,
|
|
713
|
+
}),
|
|
714
|
+
budget: {
|
|
715
|
+
retries,
|
|
716
|
+
initialDelayMs,
|
|
717
|
+
maxDelayMs,
|
|
718
|
+
maxTotalMs,
|
|
719
|
+
sleep,
|
|
720
|
+
rng,
|
|
721
|
+
},
|
|
722
|
+
note,
|
|
723
|
+
});
|
|
724
|
+
if (loop.kind === 'created') {
|
|
725
|
+
return {exitCode: 0, outcome: 'created', message: loop.message};
|
|
726
|
+
}
|
|
727
|
+
if (loop.kind === 'lost') {
|
|
728
|
+
return {exitCode: 2, outcome: 'lost', message: loop.message};
|
|
729
|
+
}
|
|
730
|
+
return {exitCode: 3, outcome: 'contended', message: loop.message};
|
|
731
|
+
} finally {
|
|
732
|
+
await cleanup(cwd, origRef, createBranch, env);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
interface CreateAttemptContext {
|
|
737
|
+
path: string;
|
|
738
|
+
content: string;
|
|
739
|
+
deletePaths: string[];
|
|
740
|
+
by: string;
|
|
741
|
+
arbiter: string;
|
|
742
|
+
dryRun: boolean;
|
|
743
|
+
cwd: string;
|
|
744
|
+
env: NodeJS.ProcessEnv | undefined;
|
|
745
|
+
createBranch: string;
|
|
746
|
+
note: (m: string) => void;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** One create attempt: branch off arbiter/main, add the new file, CAS-push. */
|
|
750
|
+
async function createAttempt(
|
|
751
|
+
ctx: CreateAttemptContext,
|
|
752
|
+
): Promise<CreateAttemptResult> {
|
|
753
|
+
const {arbiter, path, content, deletePaths, createBranch, cwd, env, note} =
|
|
754
|
+
ctx;
|
|
755
|
+
|
|
756
|
+
await gitHard(['fetch', '--quiet', arbiter], cwd, env);
|
|
757
|
+
|
|
758
|
+
// Is the path still free on the arbiter's main? (the new item absent.)
|
|
759
|
+
if (await catFileExists(`${arbiter}/main:${path}`, cwd, env)) {
|
|
760
|
+
const message = `'${path}' already exists on ${arbiter}/main — the new item lost the create race (or the slug is taken). Back off.`;
|
|
761
|
+
note(message);
|
|
762
|
+
return {kind: 'lost', message};
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
await gitHard(
|
|
766
|
+
['checkout', '--quiet', '--detach', `${arbiter}/main`],
|
|
767
|
+
cwd,
|
|
768
|
+
env,
|
|
769
|
+
);
|
|
770
|
+
await gitSoft(['branch', '-D', createBranch], cwd, env);
|
|
771
|
+
await gitHard(
|
|
772
|
+
['checkout', '--quiet', '-b', createBranch, `${arbiter}/main`],
|
|
773
|
+
cwd,
|
|
774
|
+
env,
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
const fileAbs = join(cwd, path);
|
|
778
|
+
mkdirSync(dirname(fileAbs), {recursive: true});
|
|
779
|
+
writeFileSync(fileAbs, content);
|
|
780
|
+
const add = await gitSoft(['add', '--', path], cwd, env);
|
|
781
|
+
if (add.status !== 0) {
|
|
782
|
+
throw new AdvancingLockUsageError(
|
|
783
|
+
`git add failed for '${path}' (unexpected — aborting create)`,
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// `git rm` the extra paths (the promoted source note + its sidecar) INTO this
|
|
788
|
+
// SAME create commit, so the create + delete are ONE atomic commit. Best-effort
|
|
789
|
+
// per path: a path absent on the create branch (e.g. an uncommitted local note,
|
|
790
|
+
// or an already-gone sidecar) is staged as a working-tree removal instead so
|
|
791
|
+
// the commit still carries the intended deletion without aborting.
|
|
792
|
+
for (const del of deletePaths) {
|
|
793
|
+
if (del === '' || del === path) {
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
const rm = await gitSoft(['rm', '--quiet', '--', del], cwd, env);
|
|
797
|
+
if (rm.status !== 0) {
|
|
798
|
+
// Not tracked on this branch — remove from the working tree (if present)
|
|
799
|
+
// and stage that removal; a never-existed path is simply skipped.
|
|
800
|
+
rmSync(join(cwd, del), {force: true});
|
|
801
|
+
await gitSoft(['add', '--', del], cwd, env);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
await gitHard(
|
|
806
|
+
['commit', '--quiet', '-m', `advance: create ${path} (by ${ctx.by})`],
|
|
807
|
+
cwd,
|
|
808
|
+
env,
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
const base = (
|
|
812
|
+
await gitHard(['rev-parse', `${arbiter}/main`], cwd, env)
|
|
813
|
+
).stdout.trim();
|
|
814
|
+
const head = (await gitHard(['rev-parse', 'HEAD'], cwd, env)).stdout.trim();
|
|
815
|
+
if (head === base) {
|
|
816
|
+
throw new AdvancingLockUsageError(
|
|
817
|
+
'advance-create commit is a no-op (nothing added) — aborting',
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Publish through the SAME seam (kind `advancing`) — keyed on the new item's
|
|
822
|
+
// identity (its path). The lease guards the CAS: a concurrent creator that
|
|
823
|
+
// landed first advanced main past our base, so our lease fails → rejected; the
|
|
824
|
+
// outer loop refetches and the next attempt finds the path TAKEN → lost (no
|
|
825
|
+
// special case). This holds EVEN for a same-identity racer who built an
|
|
826
|
+
// identical create commit within git's 1-second timestamp resolution: the seam
|
|
827
|
+
// stamps each attempt with a per-attempt `CAS-Nonce`, so the two racers'
|
|
828
|
+
// commits have DISTINCT shas — the loser's push is a genuine lease rejection,
|
|
829
|
+
// NOT an "Everything up-to-date" no-op that would spuriously verify as won.
|
|
830
|
+
if (ctx.dryRun) {
|
|
831
|
+
const result = await ledgerWrite.applyTransition({
|
|
832
|
+
kind: 'advancing',
|
|
833
|
+
arbiter,
|
|
834
|
+
localBranch: createBranch,
|
|
835
|
+
expectedBase: base,
|
|
836
|
+
head,
|
|
837
|
+
cwd,
|
|
838
|
+
dryRun: true,
|
|
839
|
+
env,
|
|
840
|
+
note,
|
|
841
|
+
});
|
|
842
|
+
return {kind: 'created', message: result.message};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const result = await ledgerWrite.applyTransition({
|
|
846
|
+
kind: 'advancing',
|
|
847
|
+
arbiter,
|
|
848
|
+
localBranch: createBranch,
|
|
849
|
+
expectedBase: base,
|
|
850
|
+
head,
|
|
851
|
+
cwd,
|
|
852
|
+
env,
|
|
853
|
+
note,
|
|
854
|
+
});
|
|
855
|
+
if (result.kind === 'published') {
|
|
856
|
+
const message = `CREATED '${path}' on ${arbiter}/main.`;
|
|
857
|
+
note(message);
|
|
858
|
+
return {kind: 'created', message};
|
|
859
|
+
}
|
|
860
|
+
return {kind: 'rejected', message: result.message};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// --- Shared helpers (same shape as claim-cas.ts) --------------------------
|
|
864
|
+
|
|
865
|
+
/** The branch (or detached HEAD sha) we should return to afterward. */
|
|
866
|
+
async function originalRef(
|
|
867
|
+
cwd: string,
|
|
868
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
869
|
+
): Promise<string> {
|
|
870
|
+
const sym = await gitSoft(
|
|
871
|
+
['symbolic-ref', '--quiet', '--short', 'HEAD'],
|
|
872
|
+
cwd,
|
|
873
|
+
env,
|
|
874
|
+
);
|
|
875
|
+
if (sym.status === 0 && sym.stdout.trim() !== '') {
|
|
876
|
+
return sym.stdout.trim();
|
|
877
|
+
}
|
|
878
|
+
return (await gitHard(['rev-parse', 'HEAD'], cwd, env)).stdout.trim();
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/** Best-effort: return to where we were and drop the throwaway branch. */
|
|
882
|
+
async function cleanup(
|
|
883
|
+
cwd: string,
|
|
884
|
+
origRef: string,
|
|
885
|
+
branch: string,
|
|
886
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
887
|
+
): Promise<void> {
|
|
888
|
+
await gitSoft(['checkout', '--quiet', origRef], cwd, env);
|
|
889
|
+
await gitSoft(['branch', '-D', branch], cwd, env);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/** Resolve the advisory locker: git user.name, else $USER/$USERNAME, else ''. */
|
|
893
|
+
async function resolveBy(
|
|
894
|
+
cwd: string,
|
|
895
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
896
|
+
): Promise<string> {
|
|
897
|
+
const name = await gitSoft(['config', 'user.name'], cwd, env);
|
|
898
|
+
if (name.status === 0 && name.stdout.trim() !== '') {
|
|
899
|
+
return name.stdout.trim();
|
|
900
|
+
}
|
|
901
|
+
const e = env ?? process.env;
|
|
902
|
+
return e.USER ?? e.USERNAME ?? '';
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/** `git cat-file -e <object>` — true iff the object exists. */
|
|
906
|
+
async function catFileExists(
|
|
907
|
+
object: string,
|
|
908
|
+
cwd: string,
|
|
909
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
910
|
+
): Promise<boolean> {
|
|
911
|
+
return (await gitSoft(['cat-file', '-e', object], cwd, env)).status === 0;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** Run git, returning the raw result (no throw) — for soft checks. */
|
|
915
|
+
function gitSoft(
|
|
916
|
+
args: string[],
|
|
917
|
+
cwd: string,
|
|
918
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
919
|
+
): Promise<RunResult> {
|
|
920
|
+
return runAsync('git', args, cwd, {env});
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** Run git; throw on non-zero (genuinely unexpected plumbing failures). */
|
|
924
|
+
async function gitHard(
|
|
925
|
+
args: string[],
|
|
926
|
+
cwd: string,
|
|
927
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
928
|
+
): Promise<RunResult> {
|
|
929
|
+
const result = await runAsync('git', args, cwd, {env});
|
|
930
|
+
if (result.status !== 0) {
|
|
931
|
+
throw new Error(
|
|
932
|
+
`git ${args.join(' ')} failed (exit ${result.status}): ${result.stderr.trim()}`,
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
return result;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// --- Decisions ------------------------------------------------------------
|
|
939
|
+
//
|
|
940
|
+
// **CAS contention retry: from instant-fixed-cap to bounded-JITTER + widened
|
|
941
|
+
// budget (fan-out drain).** The earlier recorded model for `createItemThroughCas`
|
|
942
|
+
// was "contention retries INSTANTLY, no delay, tiny fixed cap (3)". That is
|
|
943
|
+
// correct for a SINGLE contender (grab the new base and go) but PATHOLOGICAL for
|
|
944
|
+
// a lifecycle propose fan-out: N parallel legs (one per answered/triaged item)
|
|
945
|
+
// all CAS-append against the SAME `main` ref, refetch and re-push in lockstep,
|
|
946
|
+
// exhaust the tiny cap, and a large fraction exit `contended` (exit 3) EVEN
|
|
947
|
+
// THOUGH every leg's write is valid and would land if they took turns. A real
|
|
948
|
+
// 66-leg propose tick left ~33 legs at exit 3 \u2014 a thundering herd on one ref.
|
|
949
|
+
//
|
|
950
|
+
// The revised model, encoded here:
|
|
951
|
+
// 1. Add a bounded DECORRELATED-JITTER inter-retry delay
|
|
952
|
+
// ({@link nextCasContentionDelayMs}). Parallel legs desynchronise; the herd
|
|
953
|
+
// breaks; the fan-out DRAINS. Small enough that a lone leg is barely
|
|
954
|
+
// slower than today (first-retry delay is O(base)).
|
|
955
|
+
// 2. WIDEN the budget on the lifecycle path ({@link LIFECYCLE_CAS_CONTENTION}):
|
|
956
|
+
// more retries + a wall-clock envelope, since a lifecycle leg has no human
|
|
957
|
+
// waiting. The regime stays BOUNDED (attempts cap AND wall-clock cap) so
|
|
958
|
+
// genuine exhaustion still yields a clean `contended`, never a hang.
|
|
959
|
+
// 3. Scope the widened budget to the LIFECYCLE path via an explicit
|
|
960
|
+
// `contention` option threaded from the driver (`advance.ts` \u2192
|
|
961
|
+
// `promoteObservation` / `mintAdr` \u2192 `createItemThroughCas`). Direct
|
|
962
|
+
// callers that pass nothing keep the INTERACTIVE shape
|
|
963
|
+
// ({@link INTERACTIVE_CAS_CONTENTION}): 3 retries, NO delay \u2014 backwards
|
|
964
|
+
// compatible, so a human-typed single-item caller stays prompt.
|
|
965
|
+
//
|
|
966
|
+
// **Deliberately DISTINCT from the OUTAGE regime in `retry-backoff.ts`.** That
|
|
967
|
+
// helper models an unreachable remote with EXPONENTIAL backoff (the remote may
|
|
968
|
+
// come back). Contention is a REJECTED push against a MOVED ref \u2014 the remote is
|
|
969
|
+
// fine, the base just advanced. It wants a small RANDOM delay to BREAK LOCKSTEP,
|
|
970
|
+
// not an exponential outage ramp. Do NOT reuse `retryWithBackoff` here.
|
|
971
|
+
//
|
|
972
|
+
// **Jitter shape: AWS-style DECORRELATED JITTER**
|
|
973
|
+
// (`delay = uniform(base, min(cap, prev*3))`). Grows toward the cap when the
|
|
974
|
+
// contention persists (more spread across the herd as the tail hangs on), but
|
|
975
|
+
// bounded above by `cap`. The alternative \u2014 flat `uniform(0, base)` \u2014 was
|
|
976
|
+
// considered and rejected: it does not adapt to a long tail, so the last N legs
|
|
977
|
+
// still collide at similar rates once the herd shrinks; the decorrelated growth
|
|
978
|
+
// is what makes the tail DRAIN under sustained parallel load. Same shape as the
|
|
979
|
+
// recovery-rebase jitter loop in `integration-core.ts` uses (`Math.floor(random
|
|
980
|
+
// * (jitterMs + 1))` on a fixed cap) but with the AWS growth term so a lifecycle
|
|
981
|
+
// tail is not stuck on a tiny fixed window.
|
|
982
|
+
//
|
|
983
|
+
// **Composes with, does NOT duplicate, the held-lock-subtraction sibling**
|
|
984
|
+
// (`advance-matrix-enumerates-held-locked-items-so-legs-fail-every-tick`): that
|
|
985
|
+
// task reduces the NUMBER of legs entering the matrix (never enumerate a held
|
|
986
|
+
// item); this task handles the RESIDUAL legitimate contention among the valid
|
|
987
|
+
// legs that DO get scheduled. The two layers together should quiet the propose-
|
|
988
|
+
// matrix CI noise.
|