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,1506 @@
|
|
|
1
|
+
import {rmSync} from 'node:fs';
|
|
2
|
+
import {tmpdir} from 'node:os';
|
|
3
|
+
import {join} from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
type WorkFolderKey,
|
|
6
|
+
workFolderPrefix,
|
|
7
|
+
workFolderRel,
|
|
8
|
+
workItemRel,
|
|
9
|
+
isWorkItemFile,
|
|
10
|
+
} from './work-layout.js';
|
|
11
|
+
import {run, runAsync, type RunResult} from './git.js';
|
|
12
|
+
import {branchAheadOf} from './continue-branch.js';
|
|
13
|
+
import {
|
|
14
|
+
acquireItemLock,
|
|
15
|
+
releaseItemLock,
|
|
16
|
+
readItemLock,
|
|
17
|
+
itemLockRef,
|
|
18
|
+
lockEntryFor,
|
|
19
|
+
parseLockEntry,
|
|
20
|
+
type LockEntry,
|
|
21
|
+
} from './item-lock.js';
|
|
22
|
+
import {ledgerWrite, type LedgerTransitionKind} from './ledger-write.js';
|
|
23
|
+
import {workBranchRef} from './slug-namespace.js';
|
|
24
|
+
import {
|
|
25
|
+
retryWithBackoff,
|
|
26
|
+
realSleep,
|
|
27
|
+
type BackoffOptions,
|
|
28
|
+
type Sleep,
|
|
29
|
+
} from './retry-backoff.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The **needs-attention mechanism** (ADR `ledger-status-on-per-item-lock-refs`;
|
|
33
|
+
* prd `ledger-status-per-item-lock-refs`; ADR §12 for the original folder model).
|
|
34
|
+
* Every "couldn't finish, a human must look" outcome (a failed acceptance gate
|
|
35
|
+
* (red `verify`), a rebase/merge conflict (ADR §10), a task the agent reported
|
|
36
|
+
* too ambiguous to build, a timeout, or a rejected review) resolves to ONE
|
|
37
|
+
* observable move: the RUNNER AMENDS the claimed item's HELD per-item lock
|
|
38
|
+
* `active → stuck` (`refs/dorfl/lock/<entry>`), writing the reason (+ any
|
|
39
|
+
* agent-surfaced questions) into the lock-entry BODY. There is NO `git mv` to a
|
|
40
|
+
* `work/needs-attention/` folder and NO on-`main` surface (the lock cut-over,
|
|
41
|
+
* task `cutover-needs-attention-becomes-lock-stuck-recovery-surface`): so a
|
|
42
|
+
* protected-`main` bounce succeeds, and a work branch cut from `main` inherits no
|
|
43
|
+
* stuck record. The RECOVERABLE half is the kept `work/<slug>` branch.
|
|
44
|
+
*
|
|
45
|
+
* This is the conflict-safe form of "surfacing": the surface is the lock
|
|
46
|
+
* `state: stuck`, read by `scan`/`status`/`gc --ledger` reading the lock refs (a
|
|
47
|
+
* COMMAND a human runs, not an `ls` of a folder). There is **no status/label
|
|
48
|
+
* field** on `main` (honours WORK-CONTRACT rule 3). The reason is prose in the
|
|
49
|
+
* lock-entry body, never a source-of-truth frontmatter field.
|
|
50
|
+
*
|
|
51
|
+
* Ownership: this module OWNS the mechanism (the move helper + the surface
|
|
52
|
+
* reader + the return path). Consumers (`complete.ts`'s gate-failed/rebase-
|
|
53
|
+
* conflict abort paths, the runner's stuck routing in `run.ts`, the human
|
|
54
|
+
* `return` command) drive these through the ledger write seam's NEEDS-ATTENTION
|
|
55
|
+
* transition (`ledgerWrite.applyNeedsAttentionTransition` /
|
|
56
|
+
* `applyReturnToBacklogTransition` in `ledger-write.ts`), whose sole strategy
|
|
57
|
+
* delegates to `routeToNeedsAttention` / `returnToBacklog` here UNCHANGED — so
|
|
58
|
+
* the later cherry-pick-to-`main` surfacing is built AGAINST the seam, not
|
|
59
|
+
* bolted onto this move code. The build agent NEVER does this — agents do no git
|
|
60
|
+
* (ADR §12).
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/** Marker that opens the appended reason block in a needs-attention item body. */
|
|
64
|
+
const REASON_HEADING = '## Needs attention';
|
|
65
|
+
|
|
66
|
+
export interface RouteToNeedsAttentionOptions {
|
|
67
|
+
/** The working clone / job worktree the `work/<slug>` branch lives in. */
|
|
68
|
+
cwd: string;
|
|
69
|
+
/** The slug of the in-progress item to bounce. */
|
|
70
|
+
slug: string;
|
|
71
|
+
/** Why the item is stuck (red gate, rebase conflict, ambiguity, timeout, …). */
|
|
72
|
+
reason: string;
|
|
73
|
+
/** Any questions the agent surfaced for the human, recorded under the reason. */
|
|
74
|
+
questions?: string[];
|
|
75
|
+
/**
|
|
76
|
+
* The arbiter remote to push the transition to (like the done-move). When
|
|
77
|
+
* omitted, the move is committed locally only (the caller pushes the branch as
|
|
78
|
+
* part of its own flow, e.g. the runner's integration step).
|
|
79
|
+
*/
|
|
80
|
+
arbiter?: string;
|
|
81
|
+
/**
|
|
82
|
+
* The work branch to push to the arbiter (the RECOVERABLE half — see the seam
|
|
83
|
+
* docstring). DEFAULT `work/<slug>`: the build-bounce branch the wip/move
|
|
84
|
+
* commits landed on. A tasking bounce passes its own branch (`work/prds/
|
|
85
|
+
* ready-<slug>`). The supplied branch MUST be the one HEAD is on (the branch the
|
|
86
|
+
* wip/move commits landed on) — NEVER a default that differs from HEAD; a
|
|
87
|
+
* caller NOT checked out on the work branch (e.g. a temp branch off main) must
|
|
88
|
+
* be SURFACE-ONLY ({@link pushBranch} `false`) so no wrong-branch ref is
|
|
89
|
+
* pushed. Only consulted when {@link arbiter} is given and {@link pushBranch}
|
|
90
|
+
* is not `false`.
|
|
91
|
+
*/
|
|
92
|
+
branch?: string;
|
|
93
|
+
/**
|
|
94
|
+
* SURFACE-ONLY when `false`: publish the ledger surface (when an `arbiter` is
|
|
95
|
+
* given) but push NO work branch. For a caller that is NOT checked out on the
|
|
96
|
+
* work branch (a throwaway temp branch off main — e.g. `start.ts`'s
|
|
97
|
+
* `routeContinueConflict`, whose real `work/<slug>` is already on the arbiter
|
|
98
|
+
* from the prior requeue). Defaults to pushing (the build-bounce common case).
|
|
99
|
+
*/
|
|
100
|
+
pushBranch?: boolean;
|
|
101
|
+
/** Environment for child git processes (identity etc.). */
|
|
102
|
+
env?: NodeJS.ProcessEnv;
|
|
103
|
+
/** Sink for human-readable progress notes. */
|
|
104
|
+
note?: (message: string) => void;
|
|
105
|
+
/**
|
|
106
|
+
* Bounded-backoff bounds for the RECOVERABLE branch push (the OUTAGE-retry,
|
|
107
|
+
* NOT the instant-contention loop). Defaults to {@link DEFAULT_BACKOFF}.
|
|
108
|
+
*/
|
|
109
|
+
backoff?: BackoffOptions;
|
|
110
|
+
/**
|
|
111
|
+
* Injectable sleep for the backoff (tests drive the retry timeline with NO
|
|
112
|
+
* real waits — the `run.ts` `sleep`/`realSleep` seam). Defaults to a real
|
|
113
|
+
* `setTimeout`. Threaded into {@link backoff} when the latter omits its own.
|
|
114
|
+
*/
|
|
115
|
+
sleep?: Sleep;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The outcome of the RECOVERABLE branch push (the per-op honest report). */
|
|
119
|
+
export type BranchPushOutcome =
|
|
120
|
+
/** Pushed to the arbiter (cross-machine recoverable). */
|
|
121
|
+
| 'pushed'
|
|
122
|
+
/** Skipped by the emptiness guard — nothing beyond main to recover YET. */
|
|
123
|
+
| 'skipped-empty'
|
|
124
|
+
/** Retried with backoff, then gave up — saved LOCALLY only. */
|
|
125
|
+
| 'failed'
|
|
126
|
+
/** No arbiter / surface-only — no push was attempted. */
|
|
127
|
+
| 'not-attempted';
|
|
128
|
+
|
|
129
|
+
export interface RouteToNeedsAttentionResult {
|
|
130
|
+
/** True iff the item was moved + committed. */
|
|
131
|
+
moved: boolean;
|
|
132
|
+
/** When `moved`, the committed transition message (of the MOVE-ONLY commit). */
|
|
133
|
+
commitMessage?: string;
|
|
134
|
+
/**
|
|
135
|
+
* The per-op outcome of the RECOVERABLE branch push (honest reporting — the
|
|
136
|
+
* message reads THIS, never assumes "pushed" off the local move). Absent when
|
|
137
|
+
* the item did not move.
|
|
138
|
+
*/
|
|
139
|
+
branchPush?: BranchPushOutcome;
|
|
140
|
+
/** When the branch push FAILED after retries, the last git error (for the report). */
|
|
141
|
+
pushError?: string;
|
|
142
|
+
/**
|
|
143
|
+
* When `moved`, the sha of the **move-only** commit — the tip of `work/<slug>`
|
|
144
|
+
* that carries PURELY the `git mv → needs-attention/` + the reason (the wip
|
|
145
|
+
* commit holding the aborted agent work sits BELOW it). A surfacing strategy
|
|
146
|
+
* cherry-picks THIS commit to make the stuck state observable, so the wip never
|
|
147
|
+
* reaches the ledger.
|
|
148
|
+
*/
|
|
149
|
+
moveCommit?: string;
|
|
150
|
+
/** When NOT moved, why (e.g. the slug was not in-progress). */
|
|
151
|
+
reasonNotMoved?: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface ReturnToBacklogOptions {
|
|
155
|
+
/** The working clone the `work/` tree lives in. */
|
|
156
|
+
cwd: string;
|
|
157
|
+
/**
|
|
158
|
+
* The slug of the stuck item to re-queue — recovered from `needs-attention/`
|
|
159
|
+
* OR `in-progress/` (the actual current folder is resolved on the arbiter).
|
|
160
|
+
*/
|
|
161
|
+
slug: string;
|
|
162
|
+
/** The arbiter remote to push the transition to. Optional (see above). */
|
|
163
|
+
arbiter?: string;
|
|
164
|
+
/**
|
|
165
|
+
* `requeue --reset` (the destructive opt-out): DISCARD the kept work, so the
|
|
166
|
+
* NEXT claim starts FRESH. At requeue-time — BEFORE the backlog move — delete
|
|
167
|
+
* the remote `work/<slug>` branch on `arbiter`
|
|
168
|
+
* (`git push <arbiter> --delete work/<slug>`, plain provider-agnostic git that
|
|
169
|
+
* works against a local `--bare` arbiter) and drop any stale LOCAL `work/<slug>`.
|
|
170
|
+
* Delete-before-move closes the claim-race window (no backlog item exists while
|
|
171
|
+
* the to-be-discarded branch still does). A FAILED delete ABORTS the requeue
|
|
172
|
+
* (no backlog move) — the item stays in needs-attention rather than become
|
|
173
|
+
* claimable while continuing from a branch you meant to throw away. Requires
|
|
174
|
+
* `arbiter`. Explicit/guarded — a deliberate departure from the loud "never
|
|
175
|
+
* delete the remote branch" invariant; never on the default (keep+continue)
|
|
176
|
+
* path.
|
|
177
|
+
*/
|
|
178
|
+
reset?: boolean;
|
|
179
|
+
/**
|
|
180
|
+
* `requeue -m "<note>"` (the handoff note): an optional human steer for the
|
|
181
|
+
* NEXT agent. APPENDED (never overwritten) as a dated `## Requeue YYYY-MM-DD`
|
|
182
|
+
* section to the item BODY before the move — the ledger file is the durable,
|
|
183
|
+
* conflict-safe, cross-machine home (same place the needs-attention reason
|
|
184
|
+
* lives). Repeated requeues ACCUMULATE a handoff log. Applies to BOTH modes
|
|
185
|
+
* (a steer is relevant even on `--reset`).
|
|
186
|
+
*/
|
|
187
|
+
message?: string;
|
|
188
|
+
/** Environment for child git processes. */
|
|
189
|
+
env?: NodeJS.ProcessEnv;
|
|
190
|
+
/** Sink for human-readable progress notes. */
|
|
191
|
+
note?: (message: string) => void;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface ReturnToBacklogResult {
|
|
195
|
+
/** True iff the item was moved back + committed. */
|
|
196
|
+
moved: boolean;
|
|
197
|
+
/** When `moved`, the committed transition message. */
|
|
198
|
+
commitMessage?: string;
|
|
199
|
+
/** True iff `--reset` deleted the remote `work/<slug>` branch on the arbiter. */
|
|
200
|
+
deletedRemoteBranch?: boolean;
|
|
201
|
+
/** When NOT moved, why (e.g. the slug was in neither needs-attention/ nor in-progress/, or a failed --reset delete). */
|
|
202
|
+
reasonNotMoved?: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface SurfaceToNeedsAttentionOptions {
|
|
206
|
+
/**
|
|
207
|
+
* The working clone the move is ORIGINATED from — purely the ORIGIN SOURCE
|
|
208
|
+
* (it resolves the arbiter remote + holds the object store the plumbing writes
|
|
209
|
+
* into), NEVER a write TARGET. Tree-less: the cwd index/HEAD/working tree are
|
|
210
|
+
* never touched (parity with {@link returnToBacklog}).
|
|
211
|
+
*/
|
|
212
|
+
cwd: string;
|
|
213
|
+
/** The slug of the in-progress item to surface to needs-attention. */
|
|
214
|
+
slug: string;
|
|
215
|
+
/** Why the item is stuck (terminal continue-push failure, rebase conflict, …). */
|
|
216
|
+
reason: string;
|
|
217
|
+
/** Any questions the agent surfaced for the human, recorded under the reason. */
|
|
218
|
+
questions?: string[];
|
|
219
|
+
/**
|
|
220
|
+
* The arbiter remote the surface move is CAS-published to. REQUIRED — like
|
|
221
|
+
* {@link returnToBacklog}, the move is a tree-less compare-and-swap to the
|
|
222
|
+
* arbiter ref, so there is no local-only mode.
|
|
223
|
+
*/
|
|
224
|
+
arbiter: string;
|
|
225
|
+
/** Environment for child git processes (identity etc.). */
|
|
226
|
+
env?: NodeJS.ProcessEnv;
|
|
227
|
+
/** Sink for human-readable progress notes. */
|
|
228
|
+
note?: (message: string) => void;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface SurfaceToNeedsAttentionResult {
|
|
232
|
+
/** True iff the item was surfaced (moved + CAS-published) on the arbiter. */
|
|
233
|
+
moved: boolean;
|
|
234
|
+
/** When `moved`, the committed transition message. */
|
|
235
|
+
commitMessage?: string;
|
|
236
|
+
/** When NOT moved, why (no arbiter, item not on the arbiter, contention exhausted). */
|
|
237
|
+
reasonNotMoved?: string;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Save the RECOVERABLE half of a stuck-item bounce (ADR §12). The RUNNER calls
|
|
242
|
+
* this; the build agent never does.
|
|
243
|
+
*
|
|
244
|
+
* **Post `ledger-status-per-item-lock-refs` cut-over (tasks 9a–9d, decision i+):**
|
|
245
|
+
* a bounce is now a PURE LOCK AMEND — the seam (`bounceToStuckLock` →
|
|
246
|
+
* `markStuckItemLock`) marks the per-item lock `state: stuck` and records the
|
|
247
|
+
* reason/questions ON THE LOCK ENTRY. There is NO `git mv` to a
|
|
248
|
+
* `needs-attention/` folder and NO on-`main` surface commit. The item body never
|
|
249
|
+
* moves (it rests in `backlog/` since claim stopped moving it, task 9a). So what
|
|
250
|
+
* REMAINS here is purely the never-lose-work half:
|
|
251
|
+
*
|
|
252
|
+
* 1. A **wip** commit on the `work/<slug>` branch tip holding whatever the agent
|
|
253
|
+
* left uncommitted (`git add -A`). Skipped when the tree is already clean
|
|
254
|
+
* (no aborted work to save). No bookkeeping trailer — after the cut-over NO
|
|
255
|
+
* transient-status move-only commit lands on a branch, so a branch rebases
|
|
256
|
+
* PLAINLY with nothing to drop (`drop-bookkeeping-rebase` is deleted, 9d).
|
|
257
|
+
* 2. Optionally PUSH the work branch to the arbiter so the saved wip travels
|
|
258
|
+
* cross-machine and a `requeue` continues from the branch tip. BEST-EFFORT
|
|
259
|
+
* (an unreachable arbiter leaves the local branch standing — recovery
|
|
260
|
+
* degrades, never crashes the bounce; retried with bounded backoff on an
|
|
261
|
+
* outage), BRANCH-PARAMETERISED (default `work/<slug>`; an explicit `branch`
|
|
262
|
+
* overrides — the tasking bounce passes `work/prds/ready-<slug>`; `pushBranch: false`
|
|
263
|
+
* ⇒ push NOTHING), and EMPTINESS-GUARDED (a branch with no commits beyond
|
|
264
|
+
* main, or an absent branch, is skipped). The branch MUST be the one HEAD is
|
|
265
|
+
* on. The work-branch push is NOT a `main` write.
|
|
266
|
+
*
|
|
267
|
+
* The stuck STATE itself (the `state: stuck` + reason/questions) is owned by the
|
|
268
|
+
* lock amend in the seam, not by this function. NEVER throws for the expected
|
|
269
|
+
* case — returns `{moved, ...}` so consumers can branch cleanly; genuine git
|
|
270
|
+
* plumbing failures still throw (they are unexpected).
|
|
271
|
+
*/
|
|
272
|
+
export async function routeToNeedsAttention(
|
|
273
|
+
options: RouteToNeedsAttentionOptions,
|
|
274
|
+
): Promise<RouteToNeedsAttentionResult> {
|
|
275
|
+
const note = options.note ?? (() => {});
|
|
276
|
+
const {cwd, slug, env} = options;
|
|
277
|
+
|
|
278
|
+
// TASK `cutover-needs-attention-becomes-lock-stuck-recovery-surface`
|
|
279
|
+
// (decision i+): the bounce is now a PURE lock amend (done by the seam) — there
|
|
280
|
+
// is NO `git mv` to `needs-attention/` and NO on-`main` surface. What REMAINS
|
|
281
|
+
// here is the RECOVERABLE half: SAVE the agent's uncommitted work as a wip
|
|
282
|
+
// commit on the `work/<slug>` branch tip and PUSH the branch to the arbiter, so
|
|
283
|
+
// the partial work travels cross-machine and a `requeue` continues from the
|
|
284
|
+
// branch tip. The reason/questions ride on the lock entry (the seam amends it),
|
|
285
|
+
// NOT a moved `.md`. The work branch push is NOT a `main` write.
|
|
286
|
+
|
|
287
|
+
// 1. WIP commit: save whatever the agent left uncommitted to the work branch
|
|
288
|
+
// tip. Skip when the tree is clean (no aborted work to save). NOTE this no
|
|
289
|
+
// longer needs a folder source — the body rests in `backlog/` (task 9a) and
|
|
290
|
+
// never moves on a bounce.
|
|
291
|
+
gitHard(['add', '-A'], cwd, env);
|
|
292
|
+
const hadWip = !nothingStaged(cwd, env);
|
|
293
|
+
if (hadWip) {
|
|
294
|
+
// Save the agent's uncommitted work as a plain wip commit on the work-branch
|
|
295
|
+
// tip (the RECOVERABLE half — a `requeue` continues from it). No bookkeeping
|
|
296
|
+
// trailer: after the per-item-lock cut-over (tasks 9a–9d) NO transient status
|
|
297
|
+
// (no `needs-attention/` move-only commit) lands on a branch, so a branch cut
|
|
298
|
+
// from `main` rebases PLAINLY with nothing to drop — the `drop-bookkeeping-rebase`
|
|
299
|
+
// machinery and its `Dorfl-Bookkeeping` trailer are gone (9d).
|
|
300
|
+
const wipBody = `chore(${slug}): save aborted work (wip)`;
|
|
301
|
+
gitHard(['commit', '-q', '-m', wipBody], cwd, env);
|
|
302
|
+
}
|
|
303
|
+
const commitMessage = `chore(${slug}): bounce to stuck; ${options.reason}`;
|
|
304
|
+
const moveCommit = hadWip ? revParseHead(cwd, env) : undefined;
|
|
305
|
+
note(`Bounced '${slug}' to stuck (lock): ${options.reason}`);
|
|
306
|
+
|
|
307
|
+
// 2. Push the work branch to the arbiter — the RECOVERABLE half of the bounce
|
|
308
|
+
// (so the saved wip travels cross-machine and a requeue continues from the
|
|
309
|
+
// branch tip). Three behaviours: SURFACE-ONLY (no push) when
|
|
310
|
+
// `pushBranch === false`; an explicit `branch` target; else the default
|
|
311
|
+
// `work/<slug>`. BEST-EFFORT (no throw on a failed/unreachable push), RETRIED
|
|
312
|
+
// with bounded backoff on an OUTAGE, and EMPTINESS-GUARDED (a branch with no
|
|
313
|
+
// work beyond main / an absent branch is skipped). The OUTCOME is CAPTURED +
|
|
314
|
+
// RETURNED (`branchPush`) so the caller reports what ACTUALLY landed.
|
|
315
|
+
let branchPush: BranchPushOutcome = 'not-attempted';
|
|
316
|
+
let pushError: string | undefined;
|
|
317
|
+
if (options.arbiter && options.pushBranch !== false) {
|
|
318
|
+
// DEFAULT to the task-namespaced build-bounce branch; a non-task caller
|
|
319
|
+
// (the tasking bounce) passes its own `work/prds/ready-<slug>` via `branch`.
|
|
320
|
+
const branch = options.branch ?? workBranchRef('task', slug);
|
|
321
|
+
if (branchAheadOf(cwd, branch, 'main', env)) {
|
|
322
|
+
const arbiter = options.arbiter;
|
|
323
|
+
const result = await retryWithBackoff(
|
|
324
|
+
async () => {
|
|
325
|
+
const r = gitSoftRun(
|
|
326
|
+
['push', arbiter, `${branch}:${branch}`],
|
|
327
|
+
cwd,
|
|
328
|
+
env,
|
|
329
|
+
);
|
|
330
|
+
return r.status === 0
|
|
331
|
+
? {ok: true as const, value: undefined}
|
|
332
|
+
: {ok: false as const, error: r.stderr.trim()};
|
|
333
|
+
},
|
|
334
|
+
{sleep: options.sleep ?? realSleep, ...options.backoff},
|
|
335
|
+
);
|
|
336
|
+
if (result.ok) {
|
|
337
|
+
branchPush = 'pushed';
|
|
338
|
+
} else {
|
|
339
|
+
branchPush = 'failed';
|
|
340
|
+
pushError = result.lastError;
|
|
341
|
+
note(
|
|
342
|
+
`Could not push ${branch} to ${arbiter} after ${result.attempts} ` +
|
|
343
|
+
`attempt(s) (${pushError ?? 'unknown error'}) — the work is saved ` +
|
|
344
|
+
'LOCALLY only; push the branch when online, then `requeue`.',
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
branchPush = 'skipped-empty';
|
|
349
|
+
note(
|
|
350
|
+
`Skipped pushing ${branch} (no work beyond main / branch absent) — ` +
|
|
351
|
+
'nothing to recover.',
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return {moved: true, commitMessage, moveCommit, branchPush, pushError};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* The clean re-queue (ADR §12 / WORK-CONTRACT return path): once the human has
|
|
361
|
+
* resolved the cause, move the stuck item back to `work/backlog/<slug>.md` and
|
|
362
|
+
* commit it so the item can be re-claimed (it must not rot stuck). It recovers a
|
|
363
|
+
* task stuck in EITHER `work/needs-attention/<slug>.md` (the resolved-surface
|
|
364
|
+
* path) OR `work/in-progress/<slug>.md` (a claim that never surfaced — an
|
|
365
|
+
* un-surfaced abort, a killed run, or an in-place requeue note; defect 2, story
|
|
366
|
+
* 4): the slug's ACTUAL current folder is resolved on the arbiter and moved to
|
|
367
|
+
* `backlog/` via the SAME tree-less CAS. Any recorded reason/handoff block stays
|
|
368
|
+
* in the body as a durable note of what happened; the resolution itself is the
|
|
369
|
+
* human's.
|
|
370
|
+
*
|
|
371
|
+
* The `requeue` verb's THREE behaviours (ADR §14 / task
|
|
372
|
+
* `requeue-continue-and-reset`) are realised here:
|
|
373
|
+
* - **default = KEEP + CONTINUE.** The `work/<slug>` branch is left UNTOUCHED;
|
|
374
|
+
* it is the durable artifact the next claim CONTINUES from (the continue-
|
|
375
|
+
* detection in `continue-branch.ts` feeds both onboarding paths). This
|
|
376
|
+
* function only does the ledger move.
|
|
377
|
+
* - **`--reset` = DISCARD + FRESH.** When `reset` is set, DELETE the remote
|
|
378
|
+
* `work/<slug>` branch on `arbiter` FIRST (+ drop any stale local branch),
|
|
379
|
+
* THEN the backlog move. Delete-before-move closes the claim-race window; a
|
|
380
|
+
* FAILED delete ABORTS (no backlog move) so the item stays in
|
|
381
|
+
* needs-attention. The next claim then finds NO arbiter branch and cuts
|
|
382
|
+
* fresh — no special claim-time logic.
|
|
383
|
+
* - **`-m "<note>"` = HANDOFF NOTE.** When `message` is set, APPEND a dated
|
|
384
|
+
* `## Requeue YYYY-MM-DD` section to the item BODY (append-only; accumulates
|
|
385
|
+
* over repeated requeues) for the next agent. Applies to BOTH modes.
|
|
386
|
+
*
|
|
387
|
+
* Like the move, NEVER throws for the expected "not in needs-attention" case.
|
|
388
|
+
*/
|
|
389
|
+
/**
|
|
390
|
+
* Read the item's lock entry from the LOCAL lock ref (no fetch) — the resilient
|
|
391
|
+
* fall-back for {@link returnToBacklog} when an arbiter fetch fails (e.g. a
|
|
392
|
+
* `--reset` against a moved-away arbiter). The up-front soft fetch already
|
|
393
|
+
* refreshed the local refs, so reading them locally is the best-effort truth
|
|
394
|
+
* without throwing.
|
|
395
|
+
*/
|
|
396
|
+
async function readLocalItemLock(
|
|
397
|
+
slug: string,
|
|
398
|
+
cwd: string,
|
|
399
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
400
|
+
): Promise<LockEntry | undefined> {
|
|
401
|
+
const ref = itemLockRef(lockEntryFor(`task:${slug}`));
|
|
402
|
+
const show = await gitSoftAsync(['show', `${ref}:lock.md`], cwd, env);
|
|
403
|
+
if (show.status !== 0) {
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
406
|
+
return parseLockEntry(show.stdout);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export async function returnToBacklog(
|
|
410
|
+
options: ReturnToBacklogOptions,
|
|
411
|
+
): Promise<ReturnToBacklogResult> {
|
|
412
|
+
const note = options.note ?? (() => {});
|
|
413
|
+
const {cwd, slug, env} = options;
|
|
414
|
+
|
|
415
|
+
// Tree-less CAS, EXACTLY like `claim` (`performClaim`): the move is published
|
|
416
|
+
// to the arbiter ref via the shared `ledger-write` write seam — it NEVER stages
|
|
417
|
+
// or commits in the cwd working tree (so a `requeue` in a shared checkout can no
|
|
418
|
+
// longer sweep up a concurrent writer's uncommitted files — the `8c92f63`
|
|
419
|
+
// incident, see
|
|
420
|
+
// `work/notes/observations/drive-backlog-skill-assumes-in-place-do-not-remote.md`).
|
|
421
|
+
// `--cwd` is purely the ORIGIN SOURCE (it resolves the arbiter remote + holds
|
|
422
|
+
// the object store the plumbing writes into), never a write TARGET. A tree-less
|
|
423
|
+
// CAS needs a ref to push to, so an `arbiter` is REQUIRED (parity with `claim`).
|
|
424
|
+
if (!options.arbiter) {
|
|
425
|
+
return {
|
|
426
|
+
moved: false,
|
|
427
|
+
reasonNotMoved:
|
|
428
|
+
`requeue for '${slug}' needs an --arbiter: the move is published as a ` +
|
|
429
|
+
'tree-less compare-and-swap to the arbiter ref (like claim), so there ' +
|
|
430
|
+
'is no local-only mode — pass --arbiter.',
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const arbiter = options.arbiter;
|
|
434
|
+
|
|
435
|
+
if (
|
|
436
|
+
(await gitSoftAsync(['remote', 'get-url', arbiter], cwd, env)).status !== 0
|
|
437
|
+
) {
|
|
438
|
+
return {
|
|
439
|
+
moved: false,
|
|
440
|
+
reasonNotMoved: `no git remote named '${arbiter}' (set one, or pass --arbiter).`,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Refresh the remote-tracking refs so every check below (the item's residence,
|
|
445
|
+
// the continue-branch guard, the CAS base) sees the arbiter's TRUTH, not a stale
|
|
446
|
+
// local copy. This is a fetch, not a checkout — the working tree is untouched.
|
|
447
|
+
await gitSoftAsync(['fetch', '--quiet', arbiter], cwd, env);
|
|
448
|
+
|
|
449
|
+
// Is the item LOCK-HELD on the arbiter? (task
|
|
450
|
+
// `cutover-needs-attention-becomes-lock-stuck-recovery-surface`, decision i+:
|
|
451
|
+
// stuck-state is the per-item lock `state: stuck`, NOT a `needs-attention/`
|
|
452
|
+
// folder file). `requeue` recovers a STUCK hold (the resolved-recovery path) and
|
|
453
|
+
// tolerates an ACTIVE hold (a killed run that never surfaced) — both legitimate
|
|
454
|
+
// in-flight states the human's recovery verb returns to the pool. We read the
|
|
455
|
+
// LOCK ref (arbiter-is-truth), NOT a folder. No held lock ⇒ nothing to requeue
|
|
456
|
+
// (the item is already at rest — unclaimed in `backlog/`, or terminal).
|
|
457
|
+
// Read the held lock TOLERANTLY: a broken/unreachable arbiter (e.g. a
|
|
458
|
+
// `--reset` against a moved-away arbiter) must NOT throw out of requeue — the
|
|
459
|
+
// up-front soft fetch above already refreshed the local lock refs, so on a
|
|
460
|
+
// fetch fault we fall back to the local lock ref (best-effort) rather than
|
|
461
|
+
// crashing. A genuinely absent lock still refuses below.
|
|
462
|
+
let held: Awaited<ReturnType<typeof readItemLock>>;
|
|
463
|
+
try {
|
|
464
|
+
held = await readItemLock({item: `task:${slug}`, cwd, arbiter, env});
|
|
465
|
+
} catch {
|
|
466
|
+
held = await readLocalItemLock(slug, cwd, env);
|
|
467
|
+
}
|
|
468
|
+
if (!held) {
|
|
469
|
+
return {
|
|
470
|
+
moved: false,
|
|
471
|
+
reasonNotMoved:
|
|
472
|
+
`'${slug}' has no held per-item lock on ${arbiter} — nothing to requeue ` +
|
|
473
|
+
'(wrong slug, or already at rest in backlog/done?). requeue recovers a ' +
|
|
474
|
+
'task whose lock is held stuck (needs-attention) or active (a killed ' +
|
|
475
|
+
'in-progress run).',
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// `--reset`: DELETE the remote work branch (before the backlog move). The
|
|
480
|
+
// deletion is WRITE-THROUGH: the LOCAL refs that drive continue-detection
|
|
481
|
+
// (`refs/remotes/<arbiter>/work/<slug>` AND any local head `work/<slug>`) are
|
|
482
|
+
// deleted FIRST, THEN the arbiter `git push --delete`. The asymmetry is the
|
|
483
|
+
// point: the arbiter is the source of truth and the local ref is derived, so
|
|
484
|
+
// inverting today's arbiter-first ordering converts a dangerous failure mode
|
|
485
|
+
// (local AHEAD of arbiter — a permanent stale-continue) into a self-healing
|
|
486
|
+
// one (local BEHIND — the next fetch restores it from the arbiter). A FAILED
|
|
487
|
+
// arbiter delete still ABORTS the requeue (no backlog move); the local
|
|
488
|
+
// behind-state is recoverable by a subsequent fetch and CANNOT drive a wrong
|
|
489
|
+
// continue. Delete-before-move also closes the claim-race window.
|
|
490
|
+
let deletedRemoteBranch = false;
|
|
491
|
+
if (options.reset) {
|
|
492
|
+
const branch = workBranchRef('task', slug);
|
|
493
|
+
// LOCAL-FIRST: the tracking ref `branchAheadOf` reads (the one whose
|
|
494
|
+
// staleness today silently turns `--reset` into a no-op — verified live in
|
|
495
|
+
// `work/notes/observations/requeue-reset-does-not-prune-hub-mirror-stale-branch-ref.md`,
|
|
496
|
+
// where `--reset` deleted the arbiter branch but the local tracking ref
|
|
497
|
+
// survived and resurrected a "continue" on the next `do`). Both deletes
|
|
498
|
+
// are best-effort — their absence is fine, what matters is they are not
|
|
499
|
+
// LEFT BEHIND when the arbiter delete succeeds.
|
|
500
|
+
await gitSoftAsync(
|
|
501
|
+
['update-ref', '-d', `refs/remotes/${arbiter}/${branch}`],
|
|
502
|
+
cwd,
|
|
503
|
+
env,
|
|
504
|
+
);
|
|
505
|
+
await gitSoftAsync(['branch', '-D', branch], cwd, env);
|
|
506
|
+
// THEN the arbiter delete (explicit/guarded departure from the "never delete
|
|
507
|
+
// the remote branch" invariant; only on the `--reset` path, never the
|
|
508
|
+
// default).
|
|
509
|
+
const del = await gitSoftAsync(
|
|
510
|
+
['push', arbiter, '--delete', branch],
|
|
511
|
+
cwd,
|
|
512
|
+
env,
|
|
513
|
+
);
|
|
514
|
+
if (del.status !== 0) {
|
|
515
|
+
const stderr = del.stderr.trim();
|
|
516
|
+
// Tolerate "remote ref does not exist" (already gone): treat as deleted.
|
|
517
|
+
const alreadyGone = /remote ref does not exist|unable to delete/i.test(
|
|
518
|
+
stderr,
|
|
519
|
+
);
|
|
520
|
+
if (!alreadyGone) {
|
|
521
|
+
const message =
|
|
522
|
+
`requeue --reset for '${slug}': failed to delete the remote branch ` +
|
|
523
|
+
`${branch} on ${arbiter} (${stderr || 'unknown error'}); ` +
|
|
524
|
+
'aborting the requeue — item left in needs-attention (no backlog move). ' +
|
|
525
|
+
'The local tracking ref was already cleared (write-through ordering); ' +
|
|
526
|
+
'a subsequent fetch will restore it from the arbiter — the local store ' +
|
|
527
|
+
'is BEHIND the arbiter (self-healing), never AHEAD (which would drive a ' +
|
|
528
|
+
'stale continue).';
|
|
529
|
+
note(message);
|
|
530
|
+
return {moved: false, reasonNotMoved: message};
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
deletedRemoteBranch = true;
|
|
534
|
+
note(`Deleted the remote branch ${branch} on ${arbiter} (--reset).`);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// DEFAULT (keep+continue) REQUEUE-SAFETY GUARD: a claimable item's continue-
|
|
538
|
+
// branch MUST be reachable by ANY worker, so before releasing the lock verify
|
|
539
|
+
// the ARBITER branch `<arbiter>/work/<slug>` exists + is ahead of main — the
|
|
540
|
+
// EXACT "is the continue-branch on the arbiter?" question the continue-path asks
|
|
541
|
+
// in `isolation.ts`. We check the ARBITER ref (already fetched above), NOT the
|
|
542
|
+
// local `work/<slug>` (which SURVIVES a failed push). NOT on `--reset` (which
|
|
543
|
+
// discards the branch by design).
|
|
544
|
+
if (!options.reset) {
|
|
545
|
+
const branch = workBranchRef('task', slug);
|
|
546
|
+
const onArbiter = branchAheadOf(
|
|
547
|
+
cwd,
|
|
548
|
+
`${arbiter}/${branch}`,
|
|
549
|
+
`${arbiter}/main`,
|
|
550
|
+
env,
|
|
551
|
+
);
|
|
552
|
+
if (!onArbiter) {
|
|
553
|
+
const message =
|
|
554
|
+
`the work branch ${branch} isn't on ${arbiter} (the continue ` +
|
|
555
|
+
`branch a cross-machine worker would resume from) — push it first, or ` +
|
|
556
|
+
'`requeue --reset` to discard and start fresh. Item left stuck (lock not ' +
|
|
557
|
+
'released).';
|
|
558
|
+
note(message);
|
|
559
|
+
return {moved: false, reasonNotMoved: message};
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const commitMessage = `chore(${slug}): return to backlog for re-claiming`;
|
|
564
|
+
const handoff =
|
|
565
|
+
options.message && options.message.trim() !== ''
|
|
566
|
+
? options.message.trim()
|
|
567
|
+
: undefined;
|
|
568
|
+
// The body rests EITHER in the pool (`tasks-ready`) OR — for a staged item driven
|
|
569
|
+
// with `--allow-backlog` — in staging (`tasks-backlog`). Probe in the SAME
|
|
570
|
+
// precedence `resolveTask`/`--allow-backlog` uses (ready first, then backlog), so
|
|
571
|
+
// the handoff note finds a staged body too (obs
|
|
572
|
+
// `requeue-dash-m-fails-and-strands-lock-for-staged-backlog-item`).
|
|
573
|
+
const bodyResidenceCandidates: readonly WorkFolderKey[] = [
|
|
574
|
+
'tasks-ready',
|
|
575
|
+
'tasks-backlog',
|
|
576
|
+
];
|
|
577
|
+
|
|
578
|
+
// `-m "<note>"` (the handoff steer): APPEND a dated `## Requeue YYYY-MM-DD`
|
|
579
|
+
// section to the item BODY where it already rests (pool or staging), via the SAME
|
|
580
|
+
// tree-less CAS move (same-folder rewrite with the body transform) — it NEVER
|
|
581
|
+
// stages/commits in the cwd tree. The handoff is OPTIONAL and NON-FATAL: a failed
|
|
582
|
+
// append degrades to a WARNING and the lock release below STILL runs, because the
|
|
583
|
+
// lock release is the load-bearing recovery and must never be stranded by an
|
|
584
|
+
// optional note (obs `requeue-dash-m-fails-and-strands-lock-for-staged-backlog-item`).
|
|
585
|
+
if (handoff !== undefined) {
|
|
586
|
+
const noted = await runTreelessLedgerMove({
|
|
587
|
+
cwd,
|
|
588
|
+
slug,
|
|
589
|
+
arbiter,
|
|
590
|
+
kind: 'requeue',
|
|
591
|
+
onContended: 'requeue',
|
|
592
|
+
explicitMainRefspec: false,
|
|
593
|
+
env,
|
|
594
|
+
note,
|
|
595
|
+
plan: (base) => {
|
|
596
|
+
const bodyRel = bodyResidenceCandidates
|
|
597
|
+
.map((folder) => workItemRel(folder, `${slug}.md`))
|
|
598
|
+
.find((rel) => pathInCommit(base, rel, cwd, env));
|
|
599
|
+
if (bodyRel === undefined) {
|
|
600
|
+
// The body is in neither tasks/ready/ nor tasks/backlog/ on this base —
|
|
601
|
+
// nothing to annotate (the durable move that placed it must land first).
|
|
602
|
+
return 'missing';
|
|
603
|
+
}
|
|
604
|
+
return prepareTreelessMoveCommit({
|
|
605
|
+
cwd,
|
|
606
|
+
slug,
|
|
607
|
+
base,
|
|
608
|
+
sourceRel: bodyRel,
|
|
609
|
+
destRel: bodyRel,
|
|
610
|
+
transformBody: (body) => appendRequeueNoteText(body, handoff),
|
|
611
|
+
commitMessage: `chore(${slug}): requeue handoff note`,
|
|
612
|
+
refNamespace: 'requeue',
|
|
613
|
+
env,
|
|
614
|
+
});
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
if (!noted) {
|
|
618
|
+
// NON-FATAL: warn and fall through to the lock release. We do NOT strand the
|
|
619
|
+
// lock for a failed OPTIONAL note (the previous behaviour, which left a
|
|
620
|
+
// half-applied state: branch deleted on --reset, lock still held).
|
|
621
|
+
note(
|
|
622
|
+
`requeue for '${slug}': could not append the -m handoff note (the body ` +
|
|
623
|
+
`is in neither tasks/ready/ nor tasks/backlog/ on ${arbiter}/main, or ` +
|
|
624
|
+
'main kept moving). Releasing the lock anyway — the requeue still ' +
|
|
625
|
+
'recovers the item; only the note was skipped.',
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// RELEASE the held lock (`stuck → released` / give up the hold): the item
|
|
631
|
+
// returns to the claimable pool (its body already rests in `backlog/`). Use the
|
|
632
|
+
// tolerant {@link releaseItemLock} (idempotent) so requeue recovers BOTH a
|
|
633
|
+
// `stuck` hold (the resolved-recovery path) and an `active` hold (a killed run
|
|
634
|
+
// that never surfaced) — the human asserting "put it back".
|
|
635
|
+
const released = await releaseItemLock({
|
|
636
|
+
item: `task:${slug}`,
|
|
637
|
+
cwd,
|
|
638
|
+
arbiter,
|
|
639
|
+
env,
|
|
640
|
+
});
|
|
641
|
+
if (released.outcome === 'error') {
|
|
642
|
+
const message =
|
|
643
|
+
`requeue for '${slug}': could not release the per-item lock ` +
|
|
644
|
+
`(${released.message}). The item is left stuck. Try again shortly.`;
|
|
645
|
+
note(message);
|
|
646
|
+
return {moved: false, reasonNotMoved: message};
|
|
647
|
+
}
|
|
648
|
+
note(
|
|
649
|
+
`Returned '${slug}' to backlog (released the lock; body rests in pool).`,
|
|
650
|
+
);
|
|
651
|
+
return {moved: true, commitMessage, deletedRemoteBranch};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* **Promote a STAGED task into the agent-eligible pool** (prd
|
|
656
|
+
* `staging-pool-position-gate-and-trust-model`, task
|
|
657
|
+
* `pre-backlog-staging-folder-and-promote-step-a`, governing ADR
|
|
658
|
+
* `placement-is-runner-deterministic-humanonly-is-agent-judgement`). Moves
|
|
659
|
+
* `work/pre-backlog/<slug>.md → work/backlog/<slug>.md` as a durable `main`
|
|
660
|
+
* move, the same category as {@link returnToBacklog} (tree-less CAS via
|
|
661
|
+
* {@link runTreelessLedgerMove}). After this transition the task is in the
|
|
662
|
+
* pool and claimable.
|
|
663
|
+
*
|
|
664
|
+
* **RUNNER/human-owned.** There is no agent-facing path that performs this:
|
|
665
|
+
* the agent's tasking output lands STAGED in `work/pre-backlog/` (the runner's
|
|
666
|
+
* deterministic placement decision), and only a runner/human invocation moves
|
|
667
|
+
* it into the pool. The agent does no git here, as everywhere.
|
|
668
|
+
*
|
|
669
|
+
* Storage-agnostic: it names the slug + the arbiter, NOT *where* the move
|
|
670
|
+
* lands; the sole strategy publishes to `<arbiter>/main`. Like
|
|
671
|
+
* {@link returnToBacklog} the tree-less CAS needs a ref to push to, so an
|
|
672
|
+
* `arbiter` is REQUIRED. NEVER throws for the expected
|
|
673
|
+
* "not in pre-backlog/" / contention-exhausted cases — it returns
|
|
674
|
+
* `{moved: false, reasonNotMoved}` so callers can branch cleanly.
|
|
675
|
+
*/
|
|
676
|
+
export interface PromoteFromPreBacklogOptions {
|
|
677
|
+
/**
|
|
678
|
+
* The working clone the move is ORIGINATED from — purely the ORIGIN SOURCE
|
|
679
|
+
* (it resolves the arbiter remote + holds the object store the plumbing
|
|
680
|
+
* writes into), NEVER a write TARGET. Tree-less: the cwd index/HEAD/working
|
|
681
|
+
* tree are never touched (parity with {@link returnToBacklog}).
|
|
682
|
+
*/
|
|
683
|
+
cwd: string;
|
|
684
|
+
/** The slug of the staged task to promote into the pool. */
|
|
685
|
+
slug: string;
|
|
686
|
+
/**
|
|
687
|
+
* The arbiter remote the promotion is CAS-published to. REQUIRED — the
|
|
688
|
+
* tree-less CAS needs a ref to push to (parity with `requeue`/`claim`).
|
|
689
|
+
*/
|
|
690
|
+
arbiter: string;
|
|
691
|
+
/** Environment for child git processes (identity etc.). */
|
|
692
|
+
env?: NodeJS.ProcessEnv;
|
|
693
|
+
/** Sink for human-readable progress notes. */
|
|
694
|
+
note?: (message: string) => void;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export interface PromoteFromPreBacklogResult {
|
|
698
|
+
/** True iff the staged task was moved into the pool + committed. */
|
|
699
|
+
moved: boolean;
|
|
700
|
+
/** When `moved`, the committed transition message. */
|
|
701
|
+
commitMessage?: string;
|
|
702
|
+
/** When NOT moved, why (no such pre-backlog item, already in backlog, contention). */
|
|
703
|
+
reasonNotMoved?: string;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
export async function promoteFromPreBacklog(
|
|
707
|
+
options: PromoteFromPreBacklogOptions,
|
|
708
|
+
): Promise<PromoteFromPreBacklogResult> {
|
|
709
|
+
const note = options.note ?? (() => {});
|
|
710
|
+
const {cwd, slug, env} = options;
|
|
711
|
+
|
|
712
|
+
if (!options.arbiter) {
|
|
713
|
+
return {
|
|
714
|
+
moved: false,
|
|
715
|
+
reasonNotMoved:
|
|
716
|
+
`promote for '${slug}' needs an --arbiter: the move is published as a ` +
|
|
717
|
+
'tree-less compare-and-swap to the arbiter ref (like requeue/claim), so ' +
|
|
718
|
+
'there is no local-only mode — pass --arbiter.',
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
const arbiter = options.arbiter;
|
|
722
|
+
|
|
723
|
+
if (
|
|
724
|
+
(await gitSoftAsync(['remote', 'get-url', arbiter], cwd, env)).status !== 0
|
|
725
|
+
) {
|
|
726
|
+
return {
|
|
727
|
+
moved: false,
|
|
728
|
+
reasonNotMoved: `no git remote named '${arbiter}' (set one, or pass --arbiter).`,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Refresh `<arbiter>/main` so the residence probe + the CAS base see the
|
|
733
|
+
// arbiter's TRUTH. A fetch, not a checkout — the working tree is untouched.
|
|
734
|
+
await gitSoftAsync(['fetch', '--quiet', arbiter], cwd, env);
|
|
735
|
+
|
|
736
|
+
// UNIFIED PER-ITEM LOCK around the CAS window (prd
|
|
737
|
+
// `staging-surface-and-apply-promote-safety`, task
|
|
738
|
+
// `f3b-promote-takes-per-item-advancing-lock`): promote and apply BOTH key onto
|
|
739
|
+
// the item's `refs/dorfl/lock/<entry>` ref with `action: advance` (the
|
|
740
|
+
// SAME action `apply` takes via `advancing-lock.ts`), so an apply mid-flight
|
|
741
|
+
// and a promote attempt on the SAME item are mutually exclusive BY
|
|
742
|
+
// CONSTRUCTION (the second acquirer loses the create-only ref CAS). Reusing
|
|
743
|
+
// the existing `advance` action value (rather than introducing a distinct
|
|
744
|
+
// `'promote'` axis) is deliberate — the lock entry is keyed on the item
|
|
745
|
+
// identity, and what matters is that ALL three transitions of one item
|
|
746
|
+
// (implement/task/advance) serialise on ONE ref. A lock `lost` exits CLEAN
|
|
747
|
+
// (no partial state on `main`, mirroring claim-cas loss semantics); on success
|
|
748
|
+
// or failure we release the lock in `finally`. Crash-safe release mirrors the
|
|
749
|
+
// apply rung: a crashed promote leaves an `advance`-active lock that the
|
|
750
|
+
// existing `release-lock` / `gc --ledger` recovery surface clears.
|
|
751
|
+
const item = `task:${slug}`;
|
|
752
|
+
const acquired = await acquireItemLock({
|
|
753
|
+
item,
|
|
754
|
+
action: 'advance',
|
|
755
|
+
cwd,
|
|
756
|
+
arbiter,
|
|
757
|
+
env,
|
|
758
|
+
});
|
|
759
|
+
if (acquired.outcome !== 'acquired') {
|
|
760
|
+
const message =
|
|
761
|
+
acquired.outcome === 'lost'
|
|
762
|
+
? `promote for '${slug}' lost the per-item lock race (another implement/task/advance hold is in flight). No move on ${arbiter}/main. Try again shortly.`
|
|
763
|
+
: `promote for '${slug}': could not acquire the per-item lock (${acquired.message}).`;
|
|
764
|
+
note(message);
|
|
765
|
+
return {moved: false, reasonNotMoved: message};
|
|
766
|
+
}
|
|
767
|
+
try {
|
|
768
|
+
const sourceRel = workItemRel('tasks-backlog', `${slug}.md`);
|
|
769
|
+
const destRel = workItemRel('tasks-ready', `${slug}.md`);
|
|
770
|
+
|
|
771
|
+
// Early-exit message: if NEITHER staged nor already-in-pool, there is
|
|
772
|
+
// nothing to promote (the per-attempt `plan` is the authoritative
|
|
773
|
+
// resolution against the live base).
|
|
774
|
+
const hasSource =
|
|
775
|
+
(
|
|
776
|
+
await gitSoftAsync(
|
|
777
|
+
['cat-file', '-e', `${arbiter}/main:${sourceRel}`],
|
|
778
|
+
cwd,
|
|
779
|
+
env,
|
|
780
|
+
)
|
|
781
|
+
).status === 0;
|
|
782
|
+
const hasDest =
|
|
783
|
+
(
|
|
784
|
+
await gitSoftAsync(
|
|
785
|
+
['cat-file', '-e', `${arbiter}/main:${destRel}`],
|
|
786
|
+
cwd,
|
|
787
|
+
env,
|
|
788
|
+
)
|
|
789
|
+
).status === 0;
|
|
790
|
+
if (!hasSource && !hasDest) {
|
|
791
|
+
const message =
|
|
792
|
+
`'${slug}' is not staged in work/pre-backlog/ on ${arbiter}/main (and not ` +
|
|
793
|
+
'already in work/backlog/) — nothing to promote (wrong slug, or never ' +
|
|
794
|
+
'staged?).';
|
|
795
|
+
note(message);
|
|
796
|
+
return {moved: false, reasonNotMoved: message};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const commitMessage = `chore(${slug}): promote work/pre-backlog/ -> work/backlog/`;
|
|
800
|
+
const moved = await runTreelessLedgerMove({
|
|
801
|
+
cwd,
|
|
802
|
+
slug,
|
|
803
|
+
arbiter,
|
|
804
|
+
kind: 'promote',
|
|
805
|
+
onContended: 'promote',
|
|
806
|
+
// The surface direction's main-only refspec works here too: this runs in
|
|
807
|
+
// the project checkout, but we only need `<arbiter>/main` resolved, and
|
|
808
|
+
// the explicit refspec is the safer default (mirrors `surface`).
|
|
809
|
+
explicitMainRefspec: true,
|
|
810
|
+
env,
|
|
811
|
+
note,
|
|
812
|
+
plan: (base) => {
|
|
813
|
+
// If already in the pool on this base, a prior attempt landed
|
|
814
|
+
// (idempotent).
|
|
815
|
+
if (pathInCommit(base, destRel, cwd, env)) {
|
|
816
|
+
return 'already-done';
|
|
817
|
+
}
|
|
818
|
+
if (!pathInCommit(base, sourceRel, cwd, env)) {
|
|
819
|
+
return 'missing';
|
|
820
|
+
}
|
|
821
|
+
return prepareTreelessMoveCommit({
|
|
822
|
+
cwd,
|
|
823
|
+
slug,
|
|
824
|
+
base,
|
|
825
|
+
sourceRel,
|
|
826
|
+
destRel,
|
|
827
|
+
// The body is carried byte-for-byte from pre-backlog into the
|
|
828
|
+
// pool — promotion is a placement decision, not a content transform.
|
|
829
|
+
transformBody: (body) => body,
|
|
830
|
+
commitMessage,
|
|
831
|
+
refNamespace: 'promote',
|
|
832
|
+
env,
|
|
833
|
+
});
|
|
834
|
+
},
|
|
835
|
+
});
|
|
836
|
+
if (moved) {
|
|
837
|
+
note(`Promoted '${slug}' from pre-backlog to backlog (claimable).`);
|
|
838
|
+
return {moved: true, commitMessage};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const message =
|
|
842
|
+
`promote for '${slug}': the arbiter's main kept moving (contended) after ` +
|
|
843
|
+
`${TREELESS_CONTENTION_ATTEMPTS} attempts — item left in pre-backlog (no ` +
|
|
844
|
+
'move). Try again shortly.';
|
|
845
|
+
note(message);
|
|
846
|
+
return {moved: false, reasonNotMoved: message};
|
|
847
|
+
} finally {
|
|
848
|
+
await releaseItemLock({item, cwd, arbiter, env});
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* **Promote a STAGED prd into the auto-task pool** (prd
|
|
854
|
+
* `staging-pool-position-gate-and-trust-model`, task
|
|
855
|
+
* `pre-prd-staging-pool-split-and-untrusted-prd-placement`, governing ADR
|
|
856
|
+
* `placement-is-runner-deterministic-humanonly-is-agent-judgement`). The prd
|
|
857
|
+
* twin of {@link promoteFromPreBacklog}: moves
|
|
858
|
+
* `work/prds/proposed/<slug>.md → work/prds/ready/<slug>.md` as a durable `main` move
|
|
859
|
+
* (tree-less CAS via {@link runTreelessLedgerMove}). After this transition
|
|
860
|
+
* the prd is in the auto-task POOL and eligible to be auto-tasked (subject
|
|
861
|
+
* to the existing `autoTask`/`humanOnly`/`needsAnswers`/`taskedAfter` gates,
|
|
862
|
+
* which are UNCHANGED — the staging/pool split changes only WHICH folder is
|
|
863
|
+
* the auto-task pool, not the gates).
|
|
864
|
+
*
|
|
865
|
+
* **RUNNER/human-owned.** There is no agent-facing path that performs this:
|
|
866
|
+
* `intake`'s `prd` dispatch lands the prd STAGED in `work/prds/proposed/` (the
|
|
867
|
+
* runner's deterministic placement decision), and only a runner/human
|
|
868
|
+
* invocation moves it into the pool. The agent does no git here, as
|
|
869
|
+
* everywhere; this function is not reachable from any agent surface.
|
|
870
|
+
*
|
|
871
|
+
* Storage-agnostic + tree-less, exactly like {@link promoteFromPreBacklog}:
|
|
872
|
+
* cwd index/HEAD/working tree are never touched, an arbiter remote is
|
|
873
|
+
* REQUIRED, and "not in prds/proposed/" / contention-exhausted cases are returned
|
|
874
|
+
* (NEVER thrown) via `{moved: false, reasonNotMoved}` so callers branch
|
|
875
|
+
* cleanly. Idempotent: re-running after the move LANDED is a no-op success.
|
|
876
|
+
*/
|
|
877
|
+
export interface PromoteFromPreSpecOptions {
|
|
878
|
+
/** The working clone the move is originated from (origin source only; never written). */
|
|
879
|
+
cwd: string;
|
|
880
|
+
/** The slug of the staged prd to promote into the pool. */
|
|
881
|
+
slug: string;
|
|
882
|
+
/** The arbiter remote the promotion is CAS-published to. REQUIRED. */
|
|
883
|
+
arbiter: string;
|
|
884
|
+
/** Environment for child git processes (identity etc.). */
|
|
885
|
+
env?: NodeJS.ProcessEnv;
|
|
886
|
+
/** Sink for human-readable progress notes. */
|
|
887
|
+
note?: (message: string) => void;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
export interface PromoteFromPreSpecResult {
|
|
891
|
+
/** True iff the staged prd was moved into the pool + committed. */
|
|
892
|
+
moved: boolean;
|
|
893
|
+
/** When `moved`, the committed transition message. */
|
|
894
|
+
commitMessage?: string;
|
|
895
|
+
/** When NOT moved, why (no such prds/proposed item, already in prds/ready/, contention). */
|
|
896
|
+
reasonNotMoved?: string;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
export async function promoteFromPreSpec(
|
|
900
|
+
options: PromoteFromPreSpecOptions,
|
|
901
|
+
): Promise<PromoteFromPreSpecResult> {
|
|
902
|
+
const note = options.note ?? (() => {});
|
|
903
|
+
const {cwd, slug, env} = options;
|
|
904
|
+
|
|
905
|
+
if (!options.arbiter) {
|
|
906
|
+
return {
|
|
907
|
+
moved: false,
|
|
908
|
+
reasonNotMoved:
|
|
909
|
+
`promote for '${slug}' needs an --arbiter: the move is published as ` +
|
|
910
|
+
'a tree-less compare-and-swap to the arbiter ref (like requeue/claim), so ' +
|
|
911
|
+
'there is no local-only mode — pass --arbiter.',
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
const arbiter = options.arbiter;
|
|
915
|
+
|
|
916
|
+
if (
|
|
917
|
+
(await gitSoftAsync(['remote', 'get-url', arbiter], cwd, env)).status !== 0
|
|
918
|
+
) {
|
|
919
|
+
return {
|
|
920
|
+
moved: false,
|
|
921
|
+
reasonNotMoved: `no git remote named '${arbiter}' (set one, or pass --arbiter).`,
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Refresh `<arbiter>/main` so the residence probe + the CAS base see the
|
|
926
|
+
// arbiter's TRUTH. A fetch, not a checkout — the working tree is untouched.
|
|
927
|
+
await gitSoftAsync(['fetch', '--quiet', arbiter], cwd, env);
|
|
928
|
+
|
|
929
|
+
// UNIFIED PER-ITEM LOCK around the CAS window — symmetric with
|
|
930
|
+
// {@link promoteFromPreBacklog} (prd `staging-surface-and-apply-promote-safety`,
|
|
931
|
+
// task `f3b-promote-takes-per-item-advancing-lock`, decisive prd q4 answer:
|
|
932
|
+
// specs share the apply×promote mutual-exclusion fix with tasks). The lock
|
|
933
|
+
// keys on `spec:${slug}` (a distinct ref from a task with the same slug, via
|
|
934
|
+
// {@link lockEntryFor}'s `<type>-<slug>` encoding), with `action: advance` —
|
|
935
|
+
// the SAME action an apply for a spec would take — so spec promote and spec
|
|
936
|
+
// apply on the same item are mutually exclusive by construction. MIGRATE step
|
|
937
|
+
// (prd `prd-to-spec-vocabulary-cutover-and-migration-command`): the lock
|
|
938
|
+
// identity is `spec:${slug}` to match the `spec-<slug>` entry the tasking/apply
|
|
939
|
+
// path now acquires (`tasking.ts` releases under `spec:${slug}`); a stale
|
|
940
|
+
// `prd:${slug}` here would key a DIFFERENT ref and break the mutual exclusion.
|
|
941
|
+
// Loss / crash semantics mirror the task case.
|
|
942
|
+
const item = `spec:${slug}`;
|
|
943
|
+
const acquired = await acquireItemLock({
|
|
944
|
+
item,
|
|
945
|
+
action: 'advance',
|
|
946
|
+
cwd,
|
|
947
|
+
arbiter,
|
|
948
|
+
env,
|
|
949
|
+
});
|
|
950
|
+
if (acquired.outcome !== 'acquired') {
|
|
951
|
+
const message =
|
|
952
|
+
acquired.outcome === 'lost'
|
|
953
|
+
? `promote for '${slug}' lost the per-item lock race (another implement/task/advance hold is in flight). No move on ${arbiter}/main. Try again shortly.`
|
|
954
|
+
: `promote for '${slug}': could not acquire the per-item lock (${acquired.message}).`;
|
|
955
|
+
note(message);
|
|
956
|
+
return {moved: false, reasonNotMoved: message};
|
|
957
|
+
}
|
|
958
|
+
try {
|
|
959
|
+
const sourceRel = workItemRel('specs-proposed', `${slug}.md`);
|
|
960
|
+
const destRel = workItemRel('specs-ready', `${slug}.md`);
|
|
961
|
+
|
|
962
|
+
const hasSource =
|
|
963
|
+
(
|
|
964
|
+
await gitSoftAsync(
|
|
965
|
+
['cat-file', '-e', `${arbiter}/main:${sourceRel}`],
|
|
966
|
+
cwd,
|
|
967
|
+
env,
|
|
968
|
+
)
|
|
969
|
+
).status === 0;
|
|
970
|
+
const hasDest =
|
|
971
|
+
(
|
|
972
|
+
await gitSoftAsync(
|
|
973
|
+
['cat-file', '-e', `${arbiter}/main:${destRel}`],
|
|
974
|
+
cwd,
|
|
975
|
+
env,
|
|
976
|
+
)
|
|
977
|
+
).status === 0;
|
|
978
|
+
if (!hasSource && !hasDest) {
|
|
979
|
+
const message =
|
|
980
|
+
`'${slug}' is not staged in ${workFolderPrefix('specs-proposed')} on ${arbiter}/main ` +
|
|
981
|
+
`(and not already in ${workFolderPrefix('specs-ready')}) — nothing to promote ` +
|
|
982
|
+
'(wrong slug, or never staged?).';
|
|
983
|
+
note(message);
|
|
984
|
+
return {moved: false, reasonNotMoved: message};
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const commitMessage = `chore(${slug}): promote ${workFolderPrefix(
|
|
988
|
+
'specs-proposed',
|
|
989
|
+
)} -> ${workFolderPrefix('specs-ready')}`;
|
|
990
|
+
const moved = await runTreelessLedgerMove({
|
|
991
|
+
cwd,
|
|
992
|
+
slug,
|
|
993
|
+
arbiter,
|
|
994
|
+
kind: 'promote',
|
|
995
|
+
onContended: 'promote',
|
|
996
|
+
explicitMainRefspec: true,
|
|
997
|
+
env,
|
|
998
|
+
note,
|
|
999
|
+
plan: (base) => {
|
|
1000
|
+
if (pathInCommit(base, destRel, cwd, env)) {
|
|
1001
|
+
return 'already-done';
|
|
1002
|
+
}
|
|
1003
|
+
if (!pathInCommit(base, sourceRel, cwd, env)) {
|
|
1004
|
+
return 'missing';
|
|
1005
|
+
}
|
|
1006
|
+
return prepareTreelessMoveCommit({
|
|
1007
|
+
cwd,
|
|
1008
|
+
slug,
|
|
1009
|
+
base,
|
|
1010
|
+
sourceRel,
|
|
1011
|
+
destRel,
|
|
1012
|
+
// The body is carried byte-for-byte from prds/proposed into the pool —
|
|
1013
|
+
// promotion is a placement decision, not a content transform.
|
|
1014
|
+
transformBody: (body) => body,
|
|
1015
|
+
commitMessage,
|
|
1016
|
+
refNamespace: 'promote',
|
|
1017
|
+
env,
|
|
1018
|
+
});
|
|
1019
|
+
},
|
|
1020
|
+
});
|
|
1021
|
+
if (moved) {
|
|
1022
|
+
note(
|
|
1023
|
+
`Promoted prd '${slug}' from prds/proposed to prds/ready (auto-taskable).`,
|
|
1024
|
+
);
|
|
1025
|
+
return {moved: true, commitMessage};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const message =
|
|
1029
|
+
`promote for '${slug}': the arbiter's main kept moving (contended) ` +
|
|
1030
|
+
`after ${TREELESS_CONTENTION_ATTEMPTS} attempts — item left in prds/proposed ` +
|
|
1031
|
+
'(no move). Try again shortly.';
|
|
1032
|
+
note(message);
|
|
1033
|
+
return {moved: false, reasonNotMoved: message};
|
|
1034
|
+
} finally {
|
|
1035
|
+
await releaseItemLock({item, cwd, arbiter, env});
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/** One staged item awaiting promotion (a task in `pre-backlog/` or a spec in `specs/proposed/`). */
|
|
1040
|
+
export interface PromotableItem {
|
|
1041
|
+
/** `'task'` (staged in `work/pre-backlog/`) or `'spec'` (staged in `work/specs/proposed/`). */
|
|
1042
|
+
namespace: 'task' | 'spec';
|
|
1043
|
+
/** The slug (filename minus `.md`). */
|
|
1044
|
+
slug: string;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
export interface ListPromotableOptions {
|
|
1048
|
+
/** The working clone the arbiter remote is resolved FROM (origin source only). */
|
|
1049
|
+
cwd: string;
|
|
1050
|
+
/** The arbiter remote whose `main` the staging folders are read from. REQUIRED. */
|
|
1051
|
+
arbiter: string;
|
|
1052
|
+
/** Environment for child git processes. */
|
|
1053
|
+
env?: NodeJS.ProcessEnv;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
export interface ListPromotableResult {
|
|
1057
|
+
/** Every staged item awaiting promotion, tasks then prds, each sorted by slug. */
|
|
1058
|
+
items: PromotableItem[];
|
|
1059
|
+
/** When the listing could not run (no such remote), why. */
|
|
1060
|
+
error?: string;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* LIST every staged item awaiting a runner/human promotion — the tasks in
|
|
1065
|
+
* `work/pre-backlog/` and the prds in `work/prds/proposed/` on `<arbiter>/main` (the
|
|
1066
|
+
* discovery half of the `promote` verb, so `promote` with no argument answers
|
|
1067
|
+
* "what is staged waiting for me?"). It reads the ARBITER's truth (a fetch + a
|
|
1068
|
+
* tree read), NOT the local working tree (which may be stale) — the same source
|
|
1069
|
+
* the promotion functions act against, so the list and the move never disagree.
|
|
1070
|
+
* Read-only: it never fetches a checkout, never moves anything.
|
|
1071
|
+
*/
|
|
1072
|
+
export async function listPromotable(
|
|
1073
|
+
options: ListPromotableOptions,
|
|
1074
|
+
): Promise<ListPromotableResult> {
|
|
1075
|
+
const {cwd, arbiter, env} = options;
|
|
1076
|
+
if (
|
|
1077
|
+
(await gitSoftAsync(['remote', 'get-url', arbiter], cwd, env)).status !== 0
|
|
1078
|
+
) {
|
|
1079
|
+
return {
|
|
1080
|
+
items: [],
|
|
1081
|
+
error: `no git remote named '${arbiter}' (set one, or pass --arbiter).`,
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
// Refresh `<arbiter>/main` so the listing sees the arbiter's TRUTH (a fetch,
|
|
1085
|
+
// not a checkout — the working tree is untouched), exactly as the promote
|
|
1086
|
+
// functions do before their residence probe.
|
|
1087
|
+
await gitSoftAsync(['fetch', '--quiet', arbiter], cwd, env);
|
|
1088
|
+
const tasks = await listMarkdownSlugsInTree(
|
|
1089
|
+
`${arbiter}/main:${workFolderRel('tasks-backlog')}`,
|
|
1090
|
+
cwd,
|
|
1091
|
+
env,
|
|
1092
|
+
);
|
|
1093
|
+
const specs = await listMarkdownSlugsInTree(
|
|
1094
|
+
`${arbiter}/main:${workFolderRel('specs-proposed')}`,
|
|
1095
|
+
cwd,
|
|
1096
|
+
env,
|
|
1097
|
+
);
|
|
1098
|
+
return {
|
|
1099
|
+
items: [
|
|
1100
|
+
...tasks.map((slug) => ({namespace: 'task' as const, slug})),
|
|
1101
|
+
...specs.map((slug) => ({namespace: 'spec' as const, slug})),
|
|
1102
|
+
],
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* `git ls-tree --name-only <base>` → the `.md` filenames' SLUGS (filename minus
|
|
1108
|
+
* `.md`), sorted. An absent folder on the ref reads as empty (the staging folder
|
|
1109
|
+
* may not exist yet). The bare-repo-safe read (`ls-tree`, no working tree), the
|
|
1110
|
+
* SAME mechanism the ledger read seam uses.
|
|
1111
|
+
*/
|
|
1112
|
+
async function listMarkdownSlugsInTree(
|
|
1113
|
+
base: string,
|
|
1114
|
+
cwd: string,
|
|
1115
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
1116
|
+
): Promise<string[]> {
|
|
1117
|
+
const tree = await gitSoftAsync(['ls-tree', '--name-only', base], cwd, env);
|
|
1118
|
+
if (tree.status !== 0) {
|
|
1119
|
+
return [];
|
|
1120
|
+
}
|
|
1121
|
+
return tree.stdout
|
|
1122
|
+
.split('\n')
|
|
1123
|
+
.map((s) => s.trim())
|
|
1124
|
+
.filter((name) => isWorkItemFile(name))
|
|
1125
|
+
.map((name) => name.replace(/\.md$/i, ''))
|
|
1126
|
+
.sort();
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/** The contention-retry cap shared by the tree-less requeue + surface moves. */
|
|
1130
|
+
const TREELESS_CONTENTION_ATTEMPTS = 5;
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* The plan for ONE attempt of a tree-less move, computed FRESH against the
|
|
1134
|
+
* current (re-fetched) base so a retry never reuses a stale source/blob:
|
|
1135
|
+
* - `{ref, commit}` — a prepared move commit on a throwaway ref, ready to CAS.
|
|
1136
|
+
* - `'already-done'` — the item is ALREADY at the destination on this base (an
|
|
1137
|
+
* idempotent re-surface, or a prior attempt that actually landed but whose CAS
|
|
1138
|
+
* verify reported rejected) — treat as success, no push needed.
|
|
1139
|
+
* - `'missing'` — the item is in NEITHER the source nor the destination folder on
|
|
1140
|
+
* this base — nothing to move.
|
|
1141
|
+
*/
|
|
1142
|
+
type TreelessAttemptPlan =
|
|
1143
|
+
| {ref: string; commit: string}
|
|
1144
|
+
| 'already-done'
|
|
1145
|
+
| 'missing';
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* The SHARED tree-less ledger-move core (ONE mechanism for BOTH directions — the
|
|
1149
|
+
* requeue `needs-attention|in-progress → backlog` and the surface `in-progress →
|
|
1150
|
+
* needs-attention`). It runs the contention-retry loop: fetch `<arbiter>/main`,
|
|
1151
|
+
* resolve `expectedBase`, ask the caller's `plan(base)` to build the one-file move
|
|
1152
|
+
* on a SCRATCH INDEX via {@link prepareTreelessMoveCommit} (so the caller's
|
|
1153
|
+
* index/HEAD/working tree are NEVER touched), CAS-publish it THROUGH the shared
|
|
1154
|
+
* write seam (`ledgerWrite.applyTransition`, the very push+lease+verify `claim`
|
|
1155
|
+
* uses), drop the throwaway ref, and on a CONTENTION rejection refetch + REPLAN
|
|
1156
|
+
* against the advanced base and retry. Re-planning per attempt is what makes the
|
|
1157
|
+
* retry safe when the item itself moved under us (e.g. a prior attempt landed but
|
|
1158
|
+
* the CAS verify reported rejected): the next plan sees it `already-done`.
|
|
1159
|
+
*/
|
|
1160
|
+
async function runTreelessLedgerMove(params: {
|
|
1161
|
+
cwd: string;
|
|
1162
|
+
slug: string;
|
|
1163
|
+
arbiter: string;
|
|
1164
|
+
kind: LedgerTransitionKind;
|
|
1165
|
+
/** Build (or short-circuit) the move against the current base. Called per attempt. */
|
|
1166
|
+
plan: (base: string) => TreelessAttemptPlan;
|
|
1167
|
+
/** A label for the contention-progress note (the verb the caller surfaces). */
|
|
1168
|
+
onContended: string;
|
|
1169
|
+
/**
|
|
1170
|
+
* Fetch the arbiter's `main` with an EXPLICIT refspec
|
|
1171
|
+
* (`+refs/heads/main:refs/remotes/<arbiter>/main`) instead of a plain
|
|
1172
|
+
* `fetch <arbiter>`. The surface direction sets this `true` because it runs from
|
|
1173
|
+
* a JOB WORKTREE whose remote's default fetch refspec may NOT map `main →
|
|
1174
|
+
* refs/remotes/<arbiter>/main` (a bare-mirror worktree), so the plain fetch can
|
|
1175
|
+
* leave `<arbiter>/main` unresolved. The requeue direction leaves it `false`: it
|
|
1176
|
+
* needs ALL refs (the continue-branch guard reads `<arbiter>/work/<slug>`), so a
|
|
1177
|
+
* main-only refspec would be too narrow there.
|
|
1178
|
+
*/
|
|
1179
|
+
explicitMainRefspec: boolean;
|
|
1180
|
+
env: NodeJS.ProcessEnv | undefined;
|
|
1181
|
+
note: (message: string) => void;
|
|
1182
|
+
}): Promise<boolean> {
|
|
1183
|
+
const {
|
|
1184
|
+
cwd,
|
|
1185
|
+
arbiter,
|
|
1186
|
+
kind,
|
|
1187
|
+
plan,
|
|
1188
|
+
onContended,
|
|
1189
|
+
explicitMainRefspec,
|
|
1190
|
+
env,
|
|
1191
|
+
note,
|
|
1192
|
+
} = params;
|
|
1193
|
+
const fetchArgs = explicitMainRefspec
|
|
1194
|
+
? [
|
|
1195
|
+
'fetch',
|
|
1196
|
+
'--quiet',
|
|
1197
|
+
arbiter,
|
|
1198
|
+
`+refs/heads/main:refs/remotes/${arbiter}/main`,
|
|
1199
|
+
]
|
|
1200
|
+
: ['fetch', '--quiet', arbiter];
|
|
1201
|
+
|
|
1202
|
+
for (let i = 0; i < TREELESS_CONTENTION_ATTEMPTS; i++) {
|
|
1203
|
+
if (i > 0) {
|
|
1204
|
+
await gitSoftAsync(fetchArgs, cwd, env);
|
|
1205
|
+
}
|
|
1206
|
+
const base = (
|
|
1207
|
+
await gitHardAsync(['rev-parse', `${arbiter}/main`], cwd, env)
|
|
1208
|
+
).stdout.trim();
|
|
1209
|
+
|
|
1210
|
+
// Plan the move FRESH against this (possibly re-fetched) base. The item may
|
|
1211
|
+
// already be at the destination (idempotent / a prior landed-but-reported-
|
|
1212
|
+
// rejected attempt) or absent — both are terminal, no push.
|
|
1213
|
+
const prepared = plan(base);
|
|
1214
|
+
if (prepared === 'already-done') {
|
|
1215
|
+
return true;
|
|
1216
|
+
}
|
|
1217
|
+
if (prepared === 'missing') {
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Publish THROUGH the shared seam (the same `:main` push + force-with-lease +
|
|
1222
|
+
// verify `claim` uses). The transition's WHO stays the caller's ambient env
|
|
1223
|
+
// (threaded by `commit-tree` above) — tree-less is orthogonal to attribution.
|
|
1224
|
+
const result = await ledgerWrite.applyTransition({
|
|
1225
|
+
kind,
|
|
1226
|
+
arbiter,
|
|
1227
|
+
localBranch: prepared.ref,
|
|
1228
|
+
expectedBase: base,
|
|
1229
|
+
head: prepared.commit,
|
|
1230
|
+
cwd,
|
|
1231
|
+
env,
|
|
1232
|
+
note,
|
|
1233
|
+
});
|
|
1234
|
+
// Drop the throwaway ref either way (it served only as the push source).
|
|
1235
|
+
await gitSoftAsync(['update-ref', '-d', prepared.ref], cwd, env);
|
|
1236
|
+
|
|
1237
|
+
if (result.kind === 'published') {
|
|
1238
|
+
// Advance the LOCAL remote-tracking `<arbiter>/main` so it INCLUDES the
|
|
1239
|
+
// move (the push only moved the arbiter's main). Best-effort.
|
|
1240
|
+
await gitSoftAsync(fetchArgs, cwd, env);
|
|
1241
|
+
return true;
|
|
1242
|
+
}
|
|
1243
|
+
// rejected: main moved under us — refetch + REPLAN against the new base.
|
|
1244
|
+
note(
|
|
1245
|
+
`main advanced under us — ${onContended} refetch and retry (${i + 1}/${TREELESS_CONTENTION_ATTEMPTS})...`,
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/** True iff `path` exists in the given commit's tree (a soft cat-file probe). */
|
|
1252
|
+
function pathInCommit(
|
|
1253
|
+
commit: string,
|
|
1254
|
+
path: string,
|
|
1255
|
+
cwd: string,
|
|
1256
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
1257
|
+
): boolean {
|
|
1258
|
+
return (
|
|
1259
|
+
run('git', ['cat-file', '-e', `${commit}:${path}`], cwd, {env}).status === 0
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Build a one-file ledger MOVE as a commit off the arbiter's `main`, using
|
|
1265
|
+
* PLUMBING on a SCRATCH INDEX — it never touches the caller's index, HEAD, or
|
|
1266
|
+
* working tree (so a concurrent writer's uncommitted cwd files can never be swept
|
|
1267
|
+
* in). It loads `base`'s tree into a throwaway index, relocates ONLY this slug's
|
|
1268
|
+
* ledger file from `sourceRel` to `destRel` (applying `transformBody` to its body
|
|
1269
|
+
* first — read from the blob on `main`, NOT from any cwd file: the requeue note,
|
|
1270
|
+
* or the needs-attention reason), writes the tree, commits it parented on `base`,
|
|
1271
|
+
* and points a throwaway local ref at the commit. Returns that ref + the commit
|
|
1272
|
+
* sha for the seam's CAS push. The SHARED prep for BOTH tree-less directions.
|
|
1273
|
+
*/
|
|
1274
|
+
function prepareTreelessMoveCommit(params: {
|
|
1275
|
+
cwd: string;
|
|
1276
|
+
slug: string;
|
|
1277
|
+
base: string;
|
|
1278
|
+
sourceRel: string;
|
|
1279
|
+
destRel: string;
|
|
1280
|
+
transformBody: (body: string) => string;
|
|
1281
|
+
commitMessage: string;
|
|
1282
|
+
refNamespace: string;
|
|
1283
|
+
env: NodeJS.ProcessEnv | undefined;
|
|
1284
|
+
}): {ref: string; commit: string} {
|
|
1285
|
+
const {
|
|
1286
|
+
cwd,
|
|
1287
|
+
slug,
|
|
1288
|
+
base,
|
|
1289
|
+
sourceRel,
|
|
1290
|
+
destRel,
|
|
1291
|
+
transformBody,
|
|
1292
|
+
commitMessage,
|
|
1293
|
+
refNamespace,
|
|
1294
|
+
env,
|
|
1295
|
+
} = params;
|
|
1296
|
+
|
|
1297
|
+
// The item's body on `main`, with the caller's body transform applied.
|
|
1298
|
+
const original = catBlob(`${base}:${sourceRel}`, cwd, env);
|
|
1299
|
+
const content = transformBody(original);
|
|
1300
|
+
// Hash the (possibly transformed) blob INTO the cwd's object store. A blob
|
|
1301
|
+
// write does not touch the working tree.
|
|
1302
|
+
const blob = hashObject(content, cwd, env);
|
|
1303
|
+
|
|
1304
|
+
// A scratch index so read-tree/update-index never disturb the caller's index.
|
|
1305
|
+
const scratchIndex = join(
|
|
1306
|
+
tmpdir(),
|
|
1307
|
+
`dorfl-${refNamespace}-${process.pid}-${Date.now()}.index`,
|
|
1308
|
+
);
|
|
1309
|
+
const withIndex: NodeJS.ProcessEnv = {
|
|
1310
|
+
...(env ?? process.env),
|
|
1311
|
+
GIT_INDEX_FILE: scratchIndex,
|
|
1312
|
+
};
|
|
1313
|
+
try {
|
|
1314
|
+
gitHard(['read-tree', base], cwd, withIndex);
|
|
1315
|
+
// Remove the item from its source folder, add it under the dest folder. When
|
|
1316
|
+
// source === dest (an idempotent re-surface), force-remove + re-add the same
|
|
1317
|
+
// path is a no-op move that still carries any body change. One file changes.
|
|
1318
|
+
gitHard(['update-index', '--force-remove', sourceRel], cwd, withIndex);
|
|
1319
|
+
gitHard(
|
|
1320
|
+
['update-index', '--add', '--cacheinfo', `100644,${blob},${destRel}`],
|
|
1321
|
+
cwd,
|
|
1322
|
+
withIndex,
|
|
1323
|
+
);
|
|
1324
|
+
const tree = runHard(['write-tree'], cwd, withIndex).stdout.trim();
|
|
1325
|
+
// commit-tree threads the caller's ambient identity (env) — tree-less only
|
|
1326
|
+
// changed the WHERE-it-writes (the arbiter ref, not the cwd tree), not the WHO.
|
|
1327
|
+
const commit = runHard(
|
|
1328
|
+
['commit-tree', tree, '-p', base, '-m', commitMessage],
|
|
1329
|
+
cwd,
|
|
1330
|
+
env,
|
|
1331
|
+
).stdout.trim();
|
|
1332
|
+
// A throwaway local ref the seam's push uses as its source (`<ref>:main`).
|
|
1333
|
+
const ref = `refs/dorfl/${refNamespace}/${slug}`;
|
|
1334
|
+
gitHard(['update-ref', ref, commit], cwd, env);
|
|
1335
|
+
return {ref, commit};
|
|
1336
|
+
} finally {
|
|
1337
|
+
rmSync(scratchIndex, {force: true});
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
/** The heading that opens an appended requeue handoff note in the item body. */
|
|
1342
|
+
const REQUEUE_HEADING_PREFIX = '## Requeue';
|
|
1343
|
+
|
|
1344
|
+
/**
|
|
1345
|
+
* Append a dated `## Requeue YYYY-MM-DD` handoff section to an item body's TEXT
|
|
1346
|
+
* (append-only — never overwrites; repeated requeues accumulate a handoff log).
|
|
1347
|
+
* Body prose only (never a frontmatter field — WORK-CONTRACT rule 3). The date is
|
|
1348
|
+
* UTC `YYYY-MM-DD`; multiple notes on the same day are distinct appended blocks.
|
|
1349
|
+
*
|
|
1350
|
+
* A PURE string transform (it operates on the body CONTENT, not a file path) so
|
|
1351
|
+
* the tree-less requeue can apply it to the blob read from `<arbiter>/main`
|
|
1352
|
+
* without touching the cwd working tree.
|
|
1353
|
+
*/
|
|
1354
|
+
function appendRequeueNoteText(content: string, message: string): string {
|
|
1355
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
1356
|
+
const base = content.replace(/\s*$/, '');
|
|
1357
|
+
return [base, '', `${REQUEUE_HEADING_PREFIX} ${date}`, '', message, ''].join(
|
|
1358
|
+
'\n',
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* Extract the prose written under the `## Needs attention` heading from an item
|
|
1364
|
+
* body. Returns the first non-empty line(s) of the block as a single line
|
|
1365
|
+
* (stops at the next `## ` heading); '' when no block is present. The
|
|
1366
|
+
* needs-attention REASON is now recorded on the per-item lock entry (task
|
|
1367
|
+
* `cutover-needs-attention-becomes-lock-stuck-recovery-surface`, decision i+),
|
|
1368
|
+
* NOT in the body, but this extractor stays for any historical body text that
|
|
1369
|
+
* still carries the heading (a tolerant best-effort read).
|
|
1370
|
+
*/
|
|
1371
|
+
export function extractReason(content: string): string {
|
|
1372
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
1373
|
+
const lines = normalized.split('\n');
|
|
1374
|
+
const start = lines.findIndex((l) => l.trim() === REASON_HEADING);
|
|
1375
|
+
if (start === -1) {
|
|
1376
|
+
return '';
|
|
1377
|
+
}
|
|
1378
|
+
const collected: string[] = [];
|
|
1379
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
1380
|
+
const line = lines[i];
|
|
1381
|
+
if (/^##\s/.test(line)) {
|
|
1382
|
+
break;
|
|
1383
|
+
}
|
|
1384
|
+
if (/^###\s/.test(line)) {
|
|
1385
|
+
// The questions sub-section starts here; the reason itself is above it.
|
|
1386
|
+
break;
|
|
1387
|
+
}
|
|
1388
|
+
if (line.trim() === '') {
|
|
1389
|
+
if (collected.length > 0) {
|
|
1390
|
+
// Stop at the first blank line AFTER we captured the reason text.
|
|
1391
|
+
break;
|
|
1392
|
+
}
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
collected.push(line.trim());
|
|
1396
|
+
}
|
|
1397
|
+
return collected.join(' ').trim();
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/** Run git; throw on non-zero (genuinely unexpected plumbing failures). */
|
|
1401
|
+
function gitHard(
|
|
1402
|
+
args: string[],
|
|
1403
|
+
cwd: string,
|
|
1404
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
1405
|
+
): void {
|
|
1406
|
+
runHard(args, cwd, env);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/** Like {@link gitHard} but returns the raw result (for plumbing that emits stdout). */
|
|
1410
|
+
function runHard(
|
|
1411
|
+
args: string[],
|
|
1412
|
+
cwd: string,
|
|
1413
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
1414
|
+
): RunResult {
|
|
1415
|
+
const result = run('git', args, cwd, {env});
|
|
1416
|
+
if (result.status !== 0) {
|
|
1417
|
+
throw new Error(
|
|
1418
|
+
`git ${args.join(' ')} failed (exit ${result.status}): ${result.stderr.trim()}`,
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
return result;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/** Async soft git (no throw) — for the tree-less requeue's remote checks. */
|
|
1425
|
+
function gitSoftAsync(
|
|
1426
|
+
args: string[],
|
|
1427
|
+
cwd: string,
|
|
1428
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
1429
|
+
): Promise<RunResult> {
|
|
1430
|
+
return runAsync('git', args, cwd, {env});
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/** Async git; throw on non-zero (unexpected plumbing failures). */
|
|
1434
|
+
async function gitHardAsync(
|
|
1435
|
+
args: string[],
|
|
1436
|
+
cwd: string,
|
|
1437
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
1438
|
+
): Promise<RunResult> {
|
|
1439
|
+
const result = await runAsync('git', args, cwd, {env});
|
|
1440
|
+
if (result.status !== 0) {
|
|
1441
|
+
throw new Error(
|
|
1442
|
+
`git ${args.join(' ')} failed (exit ${result.status}): ${result.stderr.trim()}`,
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
return result;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/** Read an object's content (`git cat-file -p <object>`) from the cwd's store. */
|
|
1449
|
+
function catBlob(
|
|
1450
|
+
object: string,
|
|
1451
|
+
cwd: string,
|
|
1452
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
1453
|
+
): string {
|
|
1454
|
+
return runHard(['cat-file', '-p', object], cwd, env).stdout;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/** Write a blob into the cwd's object store (`git hash-object -w`), return its sha. */
|
|
1458
|
+
function hashObject(
|
|
1459
|
+
content: string,
|
|
1460
|
+
cwd: string,
|
|
1461
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
1462
|
+
): string {
|
|
1463
|
+
const result = run('git', ['hash-object', '-w', '--stdin'], cwd, {
|
|
1464
|
+
env,
|
|
1465
|
+
input: content,
|
|
1466
|
+
});
|
|
1467
|
+
if (result.status !== 0) {
|
|
1468
|
+
throw new Error(
|
|
1469
|
+
`git hash-object failed (exit ${result.status}): ${result.stderr.trim()}`,
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
return result.stdout.trim();
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/**
|
|
1476
|
+
* Run git, returning the raw result (no throw) — for soft checks like the
|
|
1477
|
+
* `--reset` remote-branch delete, whose non-zero exit is a meaningful outcome
|
|
1478
|
+
* (the requeue aborts) rather than an unexpected plumbing failure.
|
|
1479
|
+
*/
|
|
1480
|
+
function gitSoftRun(
|
|
1481
|
+
args: string[],
|
|
1482
|
+
cwd: string,
|
|
1483
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
1484
|
+
): {status: number; stdout: string; stderr: string} {
|
|
1485
|
+
return run('git', args, cwd, {env});
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/** True when the index has no staged changes against HEAD (nothing to commit). */
|
|
1489
|
+
function nothingStaged(
|
|
1490
|
+
cwd: string,
|
|
1491
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
1492
|
+
): boolean {
|
|
1493
|
+
// `diff --cached --quiet` exits 0 when NOTHING is staged, 1 when there is.
|
|
1494
|
+
return run('git', ['diff', '--cached', '--quiet'], cwd, {env}).status === 0;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/** The current HEAD commit sha (the just-made commit's tip). */
|
|
1498
|
+
function revParseHead(cwd: string, env: NodeJS.ProcessEnv | undefined): string {
|
|
1499
|
+
const result = run('git', ['rev-parse', 'HEAD'], cwd, {env});
|
|
1500
|
+
if (result.status !== 0) {
|
|
1501
|
+
throw new Error(
|
|
1502
|
+
`git rev-parse HEAD failed (exit ${result.status}): ${result.stderr.trim()}`,
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
return result.stdout.trim();
|
|
1506
|
+
}
|