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
package/dist/do.js
ADDED
|
@@ -0,0 +1,1882 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { performStart } from './start.js';
|
|
4
|
+
import { performComplete } from './complete.js';
|
|
5
|
+
import { performClaim } from './claim-cas.js';
|
|
6
|
+
import { resolveSlug, SlugResolutionError, workBranchRef, } from './slug-namespace.js';
|
|
7
|
+
import { performTask } from './tasking.js';
|
|
8
|
+
import { resolveTask, buildAgentPrompt, resolveContinueContext, resolvePromptGuidanceForItem, PromptError, } from './prompt.js';
|
|
9
|
+
import { NullHarness } from './harness.js';
|
|
10
|
+
import { PiHarness } from './pi-harness.js';
|
|
11
|
+
import { launchWithOptionalWatch } from './agent-launch.js';
|
|
12
|
+
import { ledgerRead } from './ledger-read.js';
|
|
13
|
+
import { ledgerWrite, } from './ledger-write.js';
|
|
14
|
+
import { jobWorktreeStrategy, selectIsolationStrategy, } from './isolation.js';
|
|
15
|
+
import { ensureMirror, encodeRepoKey, mirrorPath } from './repo-mirror.js';
|
|
16
|
+
import { jobWorktreePath } from './workspace.js';
|
|
17
|
+
import { reapJob } from './gc.js';
|
|
18
|
+
import { isGitHubArbiterUrl, GitHubProvider } from './github.js';
|
|
19
|
+
import { arbiterUrl } from './integration-core.js';
|
|
20
|
+
import { shouldFailProposePrIntent, PROPOSE_PR_INTENT_GH_UNAVAILABLE_MESSAGE, } from './do-config.js';
|
|
21
|
+
import { checkGatePreconditions, detectLockfileOnDisk, detectLockfileOnMirrorMain, } from './gate-readiness.js';
|
|
22
|
+
import { git, run, runAsync, localMainAheadCount } from './git.js';
|
|
23
|
+
import { identityEnv, assertTransportAllowed, } from './identity.js';
|
|
24
|
+
import { parseStopSentinel, isWorkBranchDiffEmpty, emptyDiffStopReason, } from './agent-stop.js';
|
|
25
|
+
import { classifyFailureCause, failureCauseLabel, } from './failure-cause.js';
|
|
26
|
+
const DEFAULT_ARBITER = 'origin';
|
|
27
|
+
/**
|
|
28
|
+
* Map a `do prd:<slug>` {@link TaskResult} onto the `do` {@link DoResult}
|
|
29
|
+
* contract: outcomes pass through (tasked / gate-refused / stale / agent-failed /
|
|
30
|
+
* usage-error), the lock-lost outcome splits into `lost` (exit 2) vs `contended`
|
|
31
|
+
* (exit 3) by its exit code, and the tasking-only exit 4 (stale) is reported on
|
|
32
|
+
* the `do` exit contract (`0|1|2|3`) as exit 1 — the needs-attention-class
|
|
33
|
+
* failure code, same as a stuck build.
|
|
34
|
+
*/
|
|
35
|
+
function taskResultToDoResult(tasked) {
|
|
36
|
+
let outcome;
|
|
37
|
+
let exitCode;
|
|
38
|
+
switch (tasked.outcome) {
|
|
39
|
+
case 'tasked':
|
|
40
|
+
outcome = 'tasked';
|
|
41
|
+
exitCode = 0;
|
|
42
|
+
break;
|
|
43
|
+
case 'gate-refused':
|
|
44
|
+
outcome = 'gate-refused';
|
|
45
|
+
exitCode = 1;
|
|
46
|
+
break;
|
|
47
|
+
case 'lock-lost':
|
|
48
|
+
if (tasked.exitCode === 3) {
|
|
49
|
+
outcome = 'contended';
|
|
50
|
+
exitCode = 3;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
outcome = 'lost';
|
|
54
|
+
exitCode = 2;
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
case 'stale':
|
|
58
|
+
outcome = 'stale';
|
|
59
|
+
exitCode = 1;
|
|
60
|
+
break;
|
|
61
|
+
case 'needs-attention':
|
|
62
|
+
// The tasker review→edit loop found the decomposition unclear and routed the
|
|
63
|
+
// prd to needs-attention (no guessed tasks). Same exit class as a stuck
|
|
64
|
+
// build (1).
|
|
65
|
+
outcome = 'needs-attention';
|
|
66
|
+
exitCode = 1;
|
|
67
|
+
break;
|
|
68
|
+
case 'agent-failed':
|
|
69
|
+
outcome = 'agent-failed';
|
|
70
|
+
exitCode = 1;
|
|
71
|
+
break;
|
|
72
|
+
default:
|
|
73
|
+
outcome = 'usage-error';
|
|
74
|
+
exitCode = 1;
|
|
75
|
+
}
|
|
76
|
+
return { exitCode, outcome, slug: tasked.slug, message: tasked.message };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Run the in-place `do` ritual end-to-end. Never throws for the expected
|
|
80
|
+
* lost/contended/refused/needs-attention cases — those are returned with the
|
|
81
|
+
* appropriate exit code and outcome. The runner owns all git; the agent only
|
|
82
|
+
* edits code.
|
|
83
|
+
*/
|
|
84
|
+
export async function performDo(options) {
|
|
85
|
+
const note = options.note ?? (() => { });
|
|
86
|
+
const arbiter = options.arbiter ?? DEFAULT_ARBITER;
|
|
87
|
+
const cwd = options.cwd;
|
|
88
|
+
// `env` here is the runner's GIT/provider env, scoped to the configured
|
|
89
|
+
// identity (claim, push, integrate, `gh`). The AGENT launch is the ONE thing
|
|
90
|
+
// that must NOT be the identity — it stays on the ambient `options.env`
|
|
91
|
+
// (`runDoAgent` reads `options.env` directly, never this local `env`), so the
|
|
92
|
+
// agent never commits as the bot; only the runner's own transitions do. Absent
|
|
93
|
+
// identity ⇒ `options.env` unchanged (byte-for-byte ambient). A configured
|
|
94
|
+
// identity that cannot be resolved (e.g. `tokenEnv` names an unset env var) is
|
|
95
|
+
// a clean usage error here, never a crash or a silent ambient fallback.
|
|
96
|
+
let env;
|
|
97
|
+
try {
|
|
98
|
+
env = identityEnv(options.identity, options.env ?? process.env);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
102
|
+
note(message);
|
|
103
|
+
return { exitCode: 1, outcome: 'usage-error', message };
|
|
104
|
+
}
|
|
105
|
+
// 0. `--watch` REQUIRES the pi harness (task `do-watch`): only the pi adapter
|
|
106
|
+
// writes a session `.jsonl` event log to tail. The null/shell adapter has no
|
|
107
|
+
// session log / event taxonomy, so there is nothing to observe — ERROR
|
|
108
|
+
// CLEARLY here, BEFORE any git transition (no claim, no branch), rather than
|
|
109
|
+
// silently running without the view. The injected `dorfl` (tests /
|
|
110
|
+
// custom embeddings) is its own launch path and is exempt.
|
|
111
|
+
if (options.watch === true &&
|
|
112
|
+
options.dorfl === undefined &&
|
|
113
|
+
!(options.harness instanceof PiHarness)) {
|
|
114
|
+
return {
|
|
115
|
+
exitCode: 1,
|
|
116
|
+
outcome: 'usage-error',
|
|
117
|
+
message: '`do --watch` requires the pi harness; configure `harness: pi` or drop ' +
|
|
118
|
+
'`--watch`.',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// 1. Resolve the slug across BOTH namespaces — `do` is the ONE command that
|
|
122
|
+
// spans them (ADR §3a): bare → task (after a no-prd-collision check;
|
|
123
|
+
// ERROR on collision), `task:`/`prd:` explicit. A collision / resolution
|
|
124
|
+
// failure is a loud usage error (exit 1).
|
|
125
|
+
let resolved;
|
|
126
|
+
try {
|
|
127
|
+
resolved = resolveSlug({
|
|
128
|
+
arg: options.arg,
|
|
129
|
+
repoPath: options.repoPath ?? cwd,
|
|
130
|
+
read: options.read ?? ledgerRead,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
if (err instanceof SlugResolutionError) {
|
|
135
|
+
return { exitCode: 1, outcome: 'usage-error', message: err.message };
|
|
136
|
+
}
|
|
137
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
138
|
+
return { exitCode: 1, outcome: 'usage-error', message };
|
|
139
|
+
}
|
|
140
|
+
// 2. `do spec:<slug>` → the spec-TASKING path (`autoslice-command`): the in-place
|
|
141
|
+
// `do` worker is AUTONOMOUS, so it tasks as the AGENT (gate-bound + lock).
|
|
142
|
+
// The orchestration (gate → lock → to-task harness → runner-owned commit)
|
|
143
|
+
// lives in `tasking.ts`; `do` dispatches `spec:` here. The agent only writes
|
|
144
|
+
// task files — the runner owns every git transition (same boundary as the
|
|
145
|
+
// build path). It does NOT run the task-build pipeline below.
|
|
146
|
+
// `resolveSlug` returns `{namespace:'spec'}` for a `spec:<slug>` arg, so
|
|
147
|
+
// `do spec:<slug>` routes here to tasking.
|
|
148
|
+
if (resolved.namespace === 'spec') {
|
|
149
|
+
const tasked = await performTask({
|
|
150
|
+
slug: resolved.slug,
|
|
151
|
+
cwd,
|
|
152
|
+
arbiter,
|
|
153
|
+
doer: 'agent',
|
|
154
|
+
autoTask: options.autoTask,
|
|
155
|
+
// EXPLICIT dispatch: a `do prd:<slug>` target was NAMED (the operator typed
|
|
156
|
+
// it, or the auto-pick POOL already filtered it on `autoTask` before
|
|
157
|
+
// dispatching here — the single policy-enforcement point). So the tasking gate
|
|
158
|
+
// drops the `autoTask` policy term and binds only the prd's own readiness
|
|
159
|
+
// (`humanOnly`/`needsAnswers`) + `taskedAfter`, EXACTLY as `do <task>` builds a
|
|
160
|
+
// named task regardless of `autoBuild` (the pool gates the policy, not the
|
|
161
|
+
// explicit claim).
|
|
162
|
+
explicit: true,
|
|
163
|
+
// The injected dorfl (tests) writes task files directly. The
|
|
164
|
+
// DoDorfl shape is a structural superset of TaskDorfl (its
|
|
165
|
+
// extra `output` is ignored by the tasking path), so it threads straight in.
|
|
166
|
+
dorfl: options.dorfl,
|
|
167
|
+
harness: options.harness,
|
|
168
|
+
agentCmd: options.agentCmd,
|
|
169
|
+
model: options.model,
|
|
170
|
+
sessionsDir: options.sessionsDir,
|
|
171
|
+
// The integrate-time args (task `slice-output-through-integration`): the
|
|
172
|
+
// `provider` is the SAME the task-build path threads (arg parity), but the
|
|
173
|
+
// MODE is the per-TRANSITION TASKING resolution (`per-transition-integration-
|
|
174
|
+
// mode-tasking-vs-build`): `taskingIntegration ?? integration`. Unset override ⇒
|
|
175
|
+
// falls back to `integration` (today's behaviour); a repo with
|
|
176
|
+
// `integration:'propose'` + `taskingIntegration:'merge'` lands the task FILES
|
|
177
|
+
// on main here while the BUILD path below still threads plain `integration`.
|
|
178
|
+
integration: options.taskingIntegration ?? options.integration,
|
|
179
|
+
// The per-repo TASK-PLACEMENT default + the operator's explicit
|
|
180
|
+
// override (task `runner-deterministic-slice-placement-policy-and-
|
|
181
|
+
// precedence`). The tasker reads them as the configured-default + the
|
|
182
|
+
// top rung of the runner-deterministic placement resolver; the
|
|
183
|
+
// `originTrust: untrusted` force is read inside the tasker from the
|
|
184
|
+
// prd's stamped frontmatter.
|
|
185
|
+
tasksLandIn: options.tasksLandIn,
|
|
186
|
+
explicitTasksLandIn: options.explicitTasksLandIn,
|
|
187
|
+
noPR: options.noPR,
|
|
188
|
+
providerInstance: options.providerInstance,
|
|
189
|
+
// The tasker review→edit→converge loop (slicer-review-edit-loop): improves the
|
|
190
|
+
// candidate tasks in place + routes the verdict through the needsAnswers /
|
|
191
|
+
// needs-attention sink. Threaded only on the `do prd:` path; omitted ⇒ no loop.
|
|
192
|
+
reviewLoop: options.reviewLoop,
|
|
193
|
+
taskerLoopMax: options.taskerLoopMax,
|
|
194
|
+
reviewExecutions: options.reviewExecutions,
|
|
195
|
+
taskerLoopModel: options.taskerLoopModel,
|
|
196
|
+
// The task-SET ACCEPTANCE GATE (slice-acceptance-gate): rides the BUILD
|
|
197
|
+
// `--review`/`--review-model` family — a fresh-context review of the produced
|
|
198
|
+
// SET before it integrates, ONE-SHOT, independent of the improver loop above.
|
|
199
|
+
review: options.review,
|
|
200
|
+
reviewGate: options.taskReviewGate,
|
|
201
|
+
acceptanceReviewModel: options.reviewModel,
|
|
202
|
+
env,
|
|
203
|
+
// The tasker + review AGENTS launch AMBIENT, never the identity env.
|
|
204
|
+
agentEnv: options.env,
|
|
205
|
+
note,
|
|
206
|
+
});
|
|
207
|
+
return taskResultToDoResult(tasked);
|
|
208
|
+
}
|
|
209
|
+
const slug = resolved.slug;
|
|
210
|
+
// 3. Refuse on a DIRTY working tree (ar-run.sh's first guard). `do` runs in a
|
|
211
|
+
// REAL checkout (the human's clone / the CI container); it must NOT
|
|
212
|
+
// entangle unrelated work or run over uncommitted changes. (Mirrors the
|
|
213
|
+
// bash driver: "error: working tree is dirty — commit/stash before
|
|
214
|
+
// running a task.")
|
|
215
|
+
if (await isDirtyTree(cwd, env)) {
|
|
216
|
+
const message = `working tree is dirty — commit or stash before running '${slug}' ` +
|
|
217
|
+
'(do runs in-place in this checkout and will not entangle unrelated ' +
|
|
218
|
+
'changes).';
|
|
219
|
+
return { exitCode: 1, outcome: 'refused', slug, message };
|
|
220
|
+
}
|
|
221
|
+
// 3b. Refuse on a DIVERGED local `main` (MERGE MODE ONLY — mirrors `complete`'s
|
|
222
|
+
// guard). A local `main` AHEAD of `<arbiter>/main` (unpushed commits) breaks
|
|
223
|
+
// ONLY the paths that fast-forward local `main`, and only merge mode ff's it:
|
|
224
|
+
// the task builds off `<arbiter>/main`, so a merge-back ff cannot apply over
|
|
225
|
+
// a diverged main. Propose mode never ff's local `main` (it pushes the work
|
|
226
|
+
// branch + opens a PR; completion only `switch`es to main, no ff), so the
|
|
227
|
+
// guard is irrelevant there and must NOT fire. Catch it UP FRONT — before the
|
|
228
|
+
// claim + agent run — so a whole build is not wasted. Resolve the mode the
|
|
229
|
+
// SAME way the rest of the flow does (the `options.integration` we thread into
|
|
230
|
+
// `complete`), then fetch (as the onboarding flow does) and compare.
|
|
231
|
+
// `--ignore-diverged-main` overrides (mirrors `--ignore-not-ready`); when
|
|
232
|
+
// overridden, Part 1's non-fatal sync handles the persisting divergence
|
|
233
|
+
// honestly at complete-time.
|
|
234
|
+
if ((options.integration ?? 'propose') === 'merge' &&
|
|
235
|
+
options.ignoreDivergedMain !== true) {
|
|
236
|
+
await runAsync('git', ['fetch', '--quiet', arbiter], cwd, { env });
|
|
237
|
+
const ahead = await localMainAheadCount(cwd, arbiter, env);
|
|
238
|
+
if (ahead > 0) {
|
|
239
|
+
const message = `local main is ahead of ${arbiter}/main by ${ahead} commit` +
|
|
240
|
+
`${ahead === 1 ? '' : 's'} (unpushed); the task builds off ${arbiter}/main ` +
|
|
241
|
+
"and the merge-back can't fast-forward — push or reconcile main first " +
|
|
242
|
+
'(or re-run with --ignore-diverged-main to proceed anyway).';
|
|
243
|
+
return { exitCode: 1, outcome: 'refused', slug, message };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// 3c. PR-INTENT pre-flight guard (the honest-failure value of the `noPR` axis).
|
|
247
|
+
// When this run is `propose` on a GITHUB arbiter and the operator INTENDS a PR
|
|
248
|
+
// (`noPR` unset), run a `gh` AUTH/AVAILABILITY PROBE UP FRONT — BEFORE the
|
|
249
|
+
// claim + agent run — and FAIL FAST if `gh` genuinely cannot open one, instead
|
|
250
|
+
// of letting integration silently degrade to manual-PR instructions. This sits
|
|
251
|
+
// alongside the dirty-tree / diverged-main guards (and mirrors the shared
|
|
252
|
+
// `doNeedsAgentCmd`/`NO_AGENT_CMD_MESSAGE` up-front refusal) so no build work
|
|
253
|
+
// is wasted. CRITICAL: the PROBE is the signal, NOT "is a `providers.github`
|
|
254
|
+
// identity present" — an absent identity falls back to AMBIENT `gh` auth (the
|
|
255
|
+
// common local-dev case), which the probe correctly reports as available, so a
|
|
256
|
+
// working ambient setup still PROCEEDS. A genuinely transient mid-run `gh`
|
|
257
|
+
// outage (probe passes here, the API fails later) is left to the runtime
|
|
258
|
+
// degrade. `noPR: true` skips the guard entirely (no PR is intended).
|
|
259
|
+
{
|
|
260
|
+
const url = await arbiterUrl(cwd, arbiter, env);
|
|
261
|
+
const probe = options.ghCanOpenPr ??
|
|
262
|
+
((probeCwd, probeEnv) => new GitHubProvider().available(probeCwd, probeEnv));
|
|
263
|
+
if (shouldFailProposePrIntent({
|
|
264
|
+
mode: options.integration ?? 'propose',
|
|
265
|
+
arbiterIsGitHub: url !== undefined && isGitHubArbiterUrl(url),
|
|
266
|
+
noPR: options.noPR,
|
|
267
|
+
ghCanOpenPr: () => probe(cwd, env),
|
|
268
|
+
})) {
|
|
269
|
+
return {
|
|
270
|
+
exitCode: 1,
|
|
271
|
+
outcome: 'refused',
|
|
272
|
+
slug,
|
|
273
|
+
message: PROPOSE_PR_INTENT_GH_UNAVAILABLE_MESSAGE,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// 3d. STATIC fresh-worktree-gate readiness guard (task
|
|
278
|
+
// `do-fails-fast-when-acceptance-gate-statically-unrunnable`). When the
|
|
279
|
+
// fresh-worktree gate is ON AND `prepare` resolves to no commands AND a
|
|
280
|
+
// lockfile is present, the throwaway worktree the gate runs in will have no
|
|
281
|
+
// `node_modules` and `verify`'s tools (`prettier`/`tsc`/`vitest`) will be
|
|
282
|
+
// "command not found" — fail fast HERE, BEFORE the claim and BEFORE spawning
|
|
283
|
+
// the build agent, instead of wasting a whole `do` run and (worse) routing
|
|
284
|
+
// correct work to needs-attention as if the task were at fault. A repo with
|
|
285
|
+
// NO lockfile is the intentional dep-free case (the design point preserved)
|
|
286
|
+
// and proceeds. There is NO verify-unset case — `resolveVerifyCommands`
|
|
287
|
+
// substitutes the default gate when verify is unset/all-blank, so verify is
|
|
288
|
+
// never statically unrunnable-because-unset (the guard is deps-only).
|
|
289
|
+
{
|
|
290
|
+
const guard = checkGatePreconditions({
|
|
291
|
+
freshWorktreeGate: options.freshWorktreeGate,
|
|
292
|
+
prepare: options.prepare,
|
|
293
|
+
lockfile: detectLockfileOnDisk(cwd),
|
|
294
|
+
});
|
|
295
|
+
if (guard !== undefined) {
|
|
296
|
+
return { exitCode: 1, outcome: 'refused', slug, message: guard.message };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// 4. Onboard via the ISOLATION SEAM (`selectIsolationStrategy`/`inPlaceStrategy`)
|
|
300
|
+
// with the SAME claim-first composition `do --remote` (and `run`) use — this
|
|
301
|
+
// is the consumer that finally puts in-place `do` on the seam, so all THREE
|
|
302
|
+
// `do`/`run` forms share the one `IsolatedTree`-handle post-claim shape:
|
|
303
|
+
//
|
|
304
|
+
// a. CLAIM explicitly via the CAS (the claim is the `do` driver's job,
|
|
305
|
+
// BEFORE prepare). A lost/contended/usage claim is propagated verbatim
|
|
306
|
+
// and NOTHING is onboarded — the same clean skip `run`/`do --remote` do.
|
|
307
|
+
// An already-in-progress / done / absent item is NOT claimable, so the
|
|
308
|
+
// CAS returns `lost` (exit 2): `do` (the autonomous CI worker) never
|
|
309
|
+
// re-claims an item someone else holds, exactly like its siblings.
|
|
310
|
+
// b. `selectIsolationStrategy({checkout})` → `inPlaceStrategy`, whose
|
|
311
|
+
// `prepare` does the ONBOARDING half (fetch + continue-detection +
|
|
312
|
+
// fresh-main `work/<slug>` switch, incl. the §14 continue/rebase path)
|
|
313
|
+
// WITHOUT re-claiming — the split the seam mandates (claim → driver,
|
|
314
|
+
// onboarding → strategy).
|
|
315
|
+
const claim = await performClaim({
|
|
316
|
+
slug,
|
|
317
|
+
cwd,
|
|
318
|
+
arbiter,
|
|
319
|
+
// `--allow-backlog`: widen the claimable predicate to ALSO accept a
|
|
320
|
+
// `tasks/backlog/`-resident body. Claim stays a pure lock (writes nothing to
|
|
321
|
+
// `main`, moves nothing); default off ⇒ pool-only, exactly as today.
|
|
322
|
+
allowBacklog: options.allowBacklog === true,
|
|
323
|
+
env,
|
|
324
|
+
note,
|
|
325
|
+
});
|
|
326
|
+
if (claim.outcome === 'lost') {
|
|
327
|
+
return { exitCode: 2, outcome: 'lost', slug, message: claim.message };
|
|
328
|
+
}
|
|
329
|
+
if (claim.outcome === 'contended') {
|
|
330
|
+
return { exitCode: 3, outcome: 'contended', slug, message: claim.message };
|
|
331
|
+
}
|
|
332
|
+
if (claim.exitCode !== 0) {
|
|
333
|
+
// usage/environment error (not inside a repo, no arbiter remote, dirty
|
|
334
|
+
// index, …): surface verbatim. NOTHING was onboarded.
|
|
335
|
+
return { exitCode: 1, outcome: 'usage-error', slug, message: claim.message };
|
|
336
|
+
}
|
|
337
|
+
// The claim landed (the item is now in-progress on the arbiter). Onboard the
|
|
338
|
+
// checkout onto its work branch THROUGH the seam — the in-place strategy puts
|
|
339
|
+
// `cwd` on `work/<slug>` off the freshly-fetched `<arbiter>/main` (or continues
|
|
340
|
+
// a kept requeue branch + rebases it, §14/§10). `prepare` can throw on a genuine
|
|
341
|
+
// plumbing failure (unreachable arbiter, …) — surface that as a usage error,
|
|
342
|
+
// never a false success.
|
|
343
|
+
let tree;
|
|
344
|
+
try {
|
|
345
|
+
tree = selectIsolationStrategy({ checkout: cwd, arbiter }).prepare({
|
|
346
|
+
slug,
|
|
347
|
+
// The task-build path: namespace the branch `work/task-<slug>`, and
|
|
348
|
+
// branch it from the EXACT claim commit (the defensive guard) so a stale
|
|
349
|
+
// same-named branch (e.g. one `intake` left) is re-pointed, never reused.
|
|
350
|
+
type: 'task',
|
|
351
|
+
claimCommit: claim.claimCommit,
|
|
352
|
+
env,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
357
|
+
return { exitCode: 1, outcome: 'usage-error', slug, message };
|
|
358
|
+
}
|
|
359
|
+
const branch = tree.branch;
|
|
360
|
+
// 4a. CONTINUE rebase conflict (ADR §14 + §10): a requeue kept a `work/<slug>`
|
|
361
|
+
// that did not replay onto the current main at onboard-time (aborted, never
|
|
362
|
+
// auto-resolved). Surface to needs-attention TREE-LESSLY via the SAME `#89`
|
|
363
|
+
// mechanism `requeue` uses for the reverse direction — the rebase was
|
|
364
|
+
// ABORTED, so the kept `work/<slug>` tip == the arbiter tip (already on the
|
|
365
|
+
// arbiter, after-commit, recoverable). The surface is purely the one-file
|
|
366
|
+
// `in-progress/ → needs-attention/` ledger move + reason (no branch push, no
|
|
367
|
+
// worktree mutation) instead of running the agent — the §10 path. The work
|
|
368
|
+
// did NOT onboard; the runner owns the bounce.
|
|
369
|
+
if (tree.continueRebaseConflict) {
|
|
370
|
+
const reason = `continuing the kept ${tree.branch}: rebase onto the latest main ` +
|
|
371
|
+
'conflicted (aborted, never auto-resolved) — resolve against the latest ' +
|
|
372
|
+
'main, or `requeue --reset` to discard and start fresh';
|
|
373
|
+
const surfaced = await ledgerWrite.applyTreelessNeedsAttentionTransition({
|
|
374
|
+
cwd: tree.dir,
|
|
375
|
+
slug,
|
|
376
|
+
reason,
|
|
377
|
+
arbiter: tree.arbiterRemote,
|
|
378
|
+
env,
|
|
379
|
+
note,
|
|
380
|
+
});
|
|
381
|
+
if (!surfaced.moved) {
|
|
382
|
+
return surfaceUnmovedDoResult({ slug, branch, reason, surfaced });
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
exitCode: 1,
|
|
386
|
+
outcome: 'needs-attention',
|
|
387
|
+
slug,
|
|
388
|
+
branch,
|
|
389
|
+
message: reason,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
// 4b. CONTINUE reconcile-push TERMINAL failure (the stale-lease-strand bug):
|
|
393
|
+
// the onboard reconcile push of the kept (already-committed) work branch
|
|
394
|
+
// FAILED terminally (stale-lease cap exhausted, or a non-stale-lease
|
|
395
|
+
// rejection / unreachable arbiter). The push helper THROWS; the strategy
|
|
396
|
+
// CATCHES it and flags `continuePushFailure` so the run does NOT crash
|
|
397
|
+
// leaving the task silently in-progress. Surface to needs-attention
|
|
398
|
+
// TREE-LESSLY via the SAME `#89` mechanism `requeue` uses — the kept branch
|
|
399
|
+
// is already on the arbiter (after-commit, recoverable), so the surface is
|
|
400
|
+
// purely the one-file ledger move + reason (no branch push, no worktree).
|
|
401
|
+
if (tree.continuePushFailure !== undefined) {
|
|
402
|
+
const reason = `continuing the kept ${tree.branch}: publishing the rebased work branch ` +
|
|
403
|
+
`to the arbiter failed terminally (${tree.continuePushFailure}) — the kept ` +
|
|
404
|
+
'branch is left intact on the arbiter (recoverable); `requeue` to retry ' +
|
|
405
|
+
'once the churn settles, or `requeue --reset` to discard and start fresh';
|
|
406
|
+
const surfaced = await ledgerWrite.applyTreelessNeedsAttentionTransition({
|
|
407
|
+
cwd: tree.dir,
|
|
408
|
+
slug,
|
|
409
|
+
reason,
|
|
410
|
+
arbiter: tree.arbiterRemote,
|
|
411
|
+
env,
|
|
412
|
+
note,
|
|
413
|
+
});
|
|
414
|
+
if (!surfaced.moved) {
|
|
415
|
+
return surfaceUnmovedDoResult({ slug, branch, reason, surfaced });
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
exitCode: 1,
|
|
419
|
+
outcome: 'needs-attention',
|
|
420
|
+
slug,
|
|
421
|
+
branch,
|
|
422
|
+
message: reason,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
// 5. Run the agent autonomously in the checkout, ON the work branch — the
|
|
426
|
+
// SAME prompt assembly `dorfl prompt` emits (canonical wrapper +
|
|
427
|
+
// source prd + the task's ## Prompt). The agent only edits code (it does
|
|
428
|
+
// no git). This is the one NEW middle step `ar-run.sh` shelled out for
|
|
429
|
+
// (`prompt | pi`).
|
|
430
|
+
// The post-claim pipeline reads the uniform `IsolatedTree` handle (`tree.dir`)
|
|
431
|
+
// — in-place that IS `cwd`, but reading the handle keeps the shared shape the
|
|
432
|
+
// future advance-loop tick wraps (no in-place-only special case).
|
|
433
|
+
let prompt;
|
|
434
|
+
try {
|
|
435
|
+
// CONTINUE-aware resolution: on a continue (the arbiter holds a kept
|
|
436
|
+
// `work/<slug>` whose tip is STRANDED off main) the task may already be in
|
|
437
|
+
// `work/done/`; admit `done/` ONLY behind the tip-vs-arbiter stranded gate
|
|
438
|
+
// (story 5), reusing the SAME refs the continue-detection uses.
|
|
439
|
+
const task = resolveTask(tree.dir, slug, {
|
|
440
|
+
cwd: tree.dir,
|
|
441
|
+
branchRef: `${tree.arbiterRemote}/${tree.branch}`,
|
|
442
|
+
mainRef: `${tree.arbiterRemote}/main`,
|
|
443
|
+
env,
|
|
444
|
+
},
|
|
445
|
+
// `--allow-backlog`: also resolve a `tasks/backlog/`-resident body
|
|
446
|
+
// (lowest priority). Default off ⇒ unchanged.
|
|
447
|
+
{ allowBacklog: options.allowBacklog === true });
|
|
448
|
+
// CONTINUE-mode (the `agent-prompt-continue-context` task): if the arbiter
|
|
449
|
+
// holds a kept `work/<slug>` ahead of main (a requeue) the checkout was
|
|
450
|
+
// CONTINUED onto it — inject the continue block (prior diff + reason + note).
|
|
451
|
+
// REUSE the SAME continue-detection the onboarding path used (in-place clone
|
|
452
|
+
// refs: `<arbiter>/work/<slug>` vs `<arbiter>/main`).
|
|
453
|
+
const continueContext = resolveContinueContext({
|
|
454
|
+
cwd: tree.dir,
|
|
455
|
+
slug,
|
|
456
|
+
arbiter: tree.arbiterRemote,
|
|
457
|
+
branchRef: `${tree.arbiterRemote}/${tree.branch}`,
|
|
458
|
+
mainRef: `${tree.arbiterRemote}/main`,
|
|
459
|
+
content: readFileSync(task.path, 'utf8'),
|
|
460
|
+
env,
|
|
461
|
+
});
|
|
462
|
+
// Per-item override layer: a task or prd may pin `promptGuidance.testFirst`
|
|
463
|
+
// in its frontmatter, superseding the resolved repo policy for THIS item.
|
|
464
|
+
const itemGuidance = resolvePromptGuidanceForItem({
|
|
465
|
+
cwd: tree.dir,
|
|
466
|
+
repoResolved: { testFirst: options.promptGuidance?.testFirst === true },
|
|
467
|
+
taskContent: readFileSync(task.path, 'utf8'),
|
|
468
|
+
});
|
|
469
|
+
prompt = buildAgentPrompt(task.slug, task.prd, task.taskPrompt, {
|
|
470
|
+
cwd: tree.dir,
|
|
471
|
+
continueContext,
|
|
472
|
+
promptGuidance: itemGuidance,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
if (err instanceof PromptError) {
|
|
477
|
+
return await saveAgentFailure({
|
|
478
|
+
slug,
|
|
479
|
+
branch,
|
|
480
|
+
cwd: tree.dir,
|
|
481
|
+
arbiter: tree.arbiterRemote,
|
|
482
|
+
detail: err.message,
|
|
483
|
+
env,
|
|
484
|
+
note,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
throw err;
|
|
488
|
+
}
|
|
489
|
+
let agent;
|
|
490
|
+
try {
|
|
491
|
+
agent = await runDoAgent(options, tree.dir, prompt, slug);
|
|
492
|
+
}
|
|
493
|
+
catch (err) {
|
|
494
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
495
|
+
return await saveAgentFailure({
|
|
496
|
+
slug,
|
|
497
|
+
branch,
|
|
498
|
+
cwd: tree.dir,
|
|
499
|
+
arbiter: tree.arbiterRemote,
|
|
500
|
+
detail: message,
|
|
501
|
+
env,
|
|
502
|
+
note,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
if (!agent.ok) {
|
|
506
|
+
const detail = agent.detail ?? `the agent failed to build '${slug}'.`;
|
|
507
|
+
return await saveAgentFailure({
|
|
508
|
+
slug,
|
|
509
|
+
branch,
|
|
510
|
+
cwd: tree.dir,
|
|
511
|
+
arbiter: tree.arbiterRemote,
|
|
512
|
+
detail,
|
|
513
|
+
env,
|
|
514
|
+
note,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
// 5b. HONOR a deliberate STOP (task `agent-stop-signal`). The agent exited
|
|
518
|
+
// cleanly (`agent.ok`), but the CLAIM-PROTOCOL wrapper tells it to STOP and
|
|
519
|
+
// report on a DRIFTED/ambiguous/stale-premise task WITHOUT building. Detect
|
|
520
|
+
// that BEFORE the gate via the in-band sentinel (the agent's reason is the
|
|
521
|
+
// needs-attention reason VERBATIM); a clean STOP with no source change is the
|
|
522
|
+
// deterministic empty-diff backstop. Either routes to needs-attention and
|
|
523
|
+
// SKIPS the acceptance gate AND Gate-2 — a clean STOP is NOT "a build that
|
|
524
|
+
// changed nothing".
|
|
525
|
+
const stopReason = await resolveStopReason({
|
|
526
|
+
output: agent.output,
|
|
527
|
+
slug,
|
|
528
|
+
cwd: tree.dir,
|
|
529
|
+
arbiter: tree.arbiterRemote,
|
|
530
|
+
env,
|
|
531
|
+
});
|
|
532
|
+
if (stopReason !== undefined) {
|
|
533
|
+
return await saveAgentStop({
|
|
534
|
+
slug,
|
|
535
|
+
branch,
|
|
536
|
+
cwd: tree.dir,
|
|
537
|
+
arbiter: tree.arbiterRemote,
|
|
538
|
+
reason: stopReason,
|
|
539
|
+
env,
|
|
540
|
+
note,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
// 6. Gate + done-move + commit + rebase + integrate + branch-tidy LIKE
|
|
544
|
+
// `complete` — but with the AUTONOMOUS needs-attention surfacing (pass
|
|
545
|
+
// `surfaceArbiter` so a red gate / rebase conflict surfaces on the
|
|
546
|
+
// arbiter's main, cross-machine visible — a stuck CI `do` that only routed
|
|
547
|
+
// locally would be invisible). The success path reuses `complete`'s
|
|
548
|
+
// machinery unchanged.
|
|
549
|
+
//
|
|
550
|
+
// Push-time transport-coherence guard (identity): refuse a forbidden transport
|
|
551
|
+
// for THIS arbiter's actual URL rather than silently pushing under an ambient
|
|
552
|
+
// credential. A no-op when no identity is configured.
|
|
553
|
+
try {
|
|
554
|
+
assertTransportAllowed(options.identity, tree.arbiterUrl);
|
|
555
|
+
}
|
|
556
|
+
catch (err) {
|
|
557
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
558
|
+
return await saveAgentFailure({
|
|
559
|
+
slug,
|
|
560
|
+
branch,
|
|
561
|
+
cwd: tree.dir,
|
|
562
|
+
arbiter: tree.arbiterRemote,
|
|
563
|
+
detail: message,
|
|
564
|
+
env,
|
|
565
|
+
note,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
const completed = await performComplete({
|
|
569
|
+
slug,
|
|
570
|
+
cwd: tree.dir,
|
|
571
|
+
arbiter: tree.arbiterRemote,
|
|
572
|
+
integration: options.integration,
|
|
573
|
+
// An explicit `--merge` overrides the untrusted-origin build-propose rule (task
|
|
574
|
+
// `untrusted-origin-forces-build-propose`); the autonomous path leaves it
|
|
575
|
+
// unset so untrusted-origin reliably forces propose.
|
|
576
|
+
explicitMerge: options.explicitMerge,
|
|
577
|
+
// `do` already ran the pre-flight divergence guard UP FRONT (step 3b), before
|
|
578
|
+
// the claim + agent; skip `complete`'s redundant re-check. When `do` was run
|
|
579
|
+
// with --ignore-diverged-main the guard was bypassed there too, so either way
|
|
580
|
+
// the (now non-fatal) local-main sync handles any persisting divergence.
|
|
581
|
+
ignoreDivergedMain: true,
|
|
582
|
+
// `--allow-backlog`: treat a `tasks/backlog/`-resident body as a valid build
|
|
583
|
+
// source so the done-move goes `tasks/backlog/ → tasks/done/` directly. Gated
|
|
584
|
+
// on the flag (without it `complete` refuses a staged strand). Default off.
|
|
585
|
+
allowBacklog: options.allowBacklog === true,
|
|
586
|
+
prepare: options.prepare,
|
|
587
|
+
verify: options.verify,
|
|
588
|
+
freshWorktreeGate: options.freshWorktreeGate,
|
|
589
|
+
// The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) — the
|
|
590
|
+
// git-alone FLOOR of the cross-job land queue (prd `land-time-reverify-and-
|
|
591
|
+
// parallel-merge-ceiling` Story 5 / Applied Answer q1 (a)). Threaded so a
|
|
592
|
+
// wide-matrix CI's raised cap actually reaches the merge loop.
|
|
593
|
+
mergeRetries: options.mergeRetries,
|
|
594
|
+
noPR: options.noPR,
|
|
595
|
+
// The resolved provider INSTANCE seam (tests/embeddings inject a stubbed
|
|
596
|
+
// GitHubProvider to drive the propose pipeline offline). Unset ⇒ the core
|
|
597
|
+
// selects from the arbiter URL.
|
|
598
|
+
providerInstance: options.providerInstance,
|
|
599
|
+
// Half B (propose-mode PR body): the build agent's FINAL SUMMARY, captured
|
|
600
|
+
// from the harness seam's `LaunchResult.output` (surfaced by `runDoAgent`
|
|
601
|
+
// below) and threaded as the PR description. `complete` scaffolds a
|
|
602
|
+
// deterministic header (task pointer) above it. Undefined ⇒ no body ⇒ the
|
|
603
|
+
// provider degrades to `gh ... --fill` (no regression).
|
|
604
|
+
body: agent.output,
|
|
605
|
+
// Gate 2 (PR/code review) rides INSIDE `complete`: run the `review` SKILL as a
|
|
606
|
+
// fresh-context agent after the green `verify` (the non-skippable floor) and
|
|
607
|
+
// before the done-move. A `block` re-uses the same needs-attention surfacing
|
|
608
|
+
// (`surfaceArbiter`) the red gate does; mapped to `needs-attention` below.
|
|
609
|
+
review: options.review,
|
|
610
|
+
reviewModel: options.reviewModel,
|
|
611
|
+
reviewMaxRounds: options.reviewMaxRounds,
|
|
612
|
+
reviewGate: options.reviewGate,
|
|
613
|
+
// `--watch` (task `watch-review-session`): tail the Gate-2 review agent's
|
|
614
|
+
// session live too, AFTER the build stream the `runDoAgent` watch surfaced
|
|
615
|
+
// (the gate prints a build→review boundary). Threaded into the gate launch via
|
|
616
|
+
// `complete`; OFF ⇒ the review path is byte-identical (sync launch, no tailer).
|
|
617
|
+
watch: options.watch,
|
|
618
|
+
watchSink: options.watchSink,
|
|
619
|
+
sessionsDir: options.sessionsDir,
|
|
620
|
+
// The autonomous failure-surfacing: route stuck items to the arbiter's
|
|
621
|
+
// main (the `run` semantics), NOT local-only (the human `complete`).
|
|
622
|
+
surfaceArbiter: tree.arbiterRemote,
|
|
623
|
+
color: options.color,
|
|
624
|
+
note,
|
|
625
|
+
noteBlock: options.noteBlock,
|
|
626
|
+
env,
|
|
627
|
+
// The review AGENT (Gate 2) launches AMBIENT — never the identity env (an
|
|
628
|
+
// agent must not act as the bot; only the runner's git ops carry identity).
|
|
629
|
+
agentEnv: options.env,
|
|
630
|
+
});
|
|
631
|
+
if (completed.outcome === 'completed' ||
|
|
632
|
+
completed.outcome === 'already-integrated') {
|
|
633
|
+
// `already-integrated` is the stranded-done auto-recover's clean no-op (the
|
|
634
|
+
// kept tip was already on `<arbiter>/main` — e.g. the prior PR merged
|
|
635
|
+
// out-of-band before the re-claim). It is a SUCCESSFUL terminal state — the
|
|
636
|
+
// work is integrated — so it folds into `DoOutcome 'completed'` (exit 0),
|
|
637
|
+
// not a new outcome value: the autonomous caller just needs to know the
|
|
638
|
+
// integrate path ended cleanly. The distinct `CompleteOutcome` value
|
|
639
|
+
// preserves the no-op signal for tests + the `complete` surface.
|
|
640
|
+
return {
|
|
641
|
+
exitCode: 0,
|
|
642
|
+
outcome: 'completed',
|
|
643
|
+
slug,
|
|
644
|
+
branch,
|
|
645
|
+
message: completed.message,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
if (completed.outcome === 'review-unparseable') {
|
|
649
|
+
// Gate 2 RAN but its verdict was UNPARSEABLE (malformed JSON). The core already
|
|
650
|
+
// routed it work-preservingly (branch pushed + surfaced). This is the FAILURE-
|
|
651
|
+
// CAUSE axis, NOT a reviewer block: classify the parse-failure phrase the core
|
|
652
|
+
// recorded → `transient-infra` (re-run the SAME work: the gate output is
|
|
653
|
+
// stochastic + the parser now repairs the control-char class, so a re-run is far
|
|
654
|
+
// more likely to parse). Same `classifyFailureCause` `run` uses, so `do`/`run`
|
|
655
|
+
// agree on the label. (`classifyFailureCause` only EVER returns config/transient/
|
|
656
|
+
// agent-failed; the malformed-JSON signature lands transient-infra, never a
|
|
657
|
+
// silent success.)
|
|
658
|
+
const cause = classifyFailureCause(completed.message);
|
|
659
|
+
return {
|
|
660
|
+
exitCode: 1,
|
|
661
|
+
outcome: failureCauseToDoOutcome(cause),
|
|
662
|
+
slug,
|
|
663
|
+
branch,
|
|
664
|
+
message: completed.message,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
if (completed.outcome === 'prepare-failed' ||
|
|
668
|
+
completed.outcome === 'gate-failed' ||
|
|
669
|
+
completed.outcome === 'review-blocked' ||
|
|
670
|
+
completed.outcome === 'rebase-conflict' ||
|
|
671
|
+
completed.outcome === 'strand-surfaced') {
|
|
672
|
+
// Failed env-prep / red gate / Gate-2 review block / rebase conflict — routed
|
|
673
|
+
// to needs-attention (surfaced on the arbiter). A `prepare-failed` (the env
|
|
674
|
+
// could not be made ready, so verify was NOT run) and a `review-blocked` are
|
|
675
|
+
// mapped HERE the SAME way `gate-failed` is. The work did NOT complete; the
|
|
676
|
+
// runner owns the bounce.
|
|
677
|
+
//
|
|
678
|
+
// `strand-surfaced` is the autonomous-strand parity (the SHARED `complete.ts`
|
|
679
|
+
// seam already surfaced the source-strand / empty-staged refusal to
|
|
680
|
+
// needs-attention on the arbiter) — in-place `performDo` inherits the fix
|
|
681
|
+
// here, mapped to the SAME `needs-attention` outcome shape `do --remote`
|
|
682
|
+
// (`runRemotePipeline`) uses, so `advance task:<slug>` (via the default
|
|
683
|
+
// `doDriver = performDo`) agrees with the remote path on the caller-visible
|
|
684
|
+
// label.
|
|
685
|
+
return {
|
|
686
|
+
exitCode: 1,
|
|
687
|
+
outcome: 'needs-attention',
|
|
688
|
+
slug,
|
|
689
|
+
branch,
|
|
690
|
+
message: completed.message,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
if (completed.outcome === 'surface-unmoved') {
|
|
694
|
+
// Strand-surface could not land on the arbiter (CAS contention exhausted /
|
|
695
|
+
// no arbiter) — HONESTLY still in-progress on the arbiter. Mirror
|
|
696
|
+
// `runRemotePipeline`'s `surface-unmoved` mapping so in-place `performDo`
|
|
697
|
+
// (and `advance task:<slug>` via the default `doDriver`) agrees with
|
|
698
|
+
// `do --remote` on the same signal, never a fake success.
|
|
699
|
+
return {
|
|
700
|
+
exitCode: 1,
|
|
701
|
+
outcome: 'surface-unmoved',
|
|
702
|
+
slug,
|
|
703
|
+
branch,
|
|
704
|
+
message: completed.message,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
// refused (nothing to commit, wrong folder) / usage-error: surface verbatim —
|
|
708
|
+
// BUT first reclassify a thrown CORE wiring/config error (which `performComplete`
|
|
709
|
+
// swallows into `usage-error`) onto the SAME `config-error` cause `run` records,
|
|
710
|
+
// closing the cross-path divergence (`do`: usage-error vs `run`: agent-failed for
|
|
711
|
+
// the identical thrown core error). Best-effort: a non-config usage-error stays
|
|
712
|
+
// `usage-error` (the conservative default — the classifier only re-labels what it
|
|
713
|
+
// recognises).
|
|
714
|
+
if (completed.outcome === 'usage-error') {
|
|
715
|
+
const cause = classifyFailureCause(completed.message);
|
|
716
|
+
if (cause === 'config-error') {
|
|
717
|
+
return {
|
|
718
|
+
exitCode: 1,
|
|
719
|
+
outcome: 'config-error',
|
|
720
|
+
slug,
|
|
721
|
+
branch,
|
|
722
|
+
message: completed.message,
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
const outcome = completed.outcome === 'refused' ? 'refused' : 'usage-error';
|
|
727
|
+
return { exitCode: 1, outcome, slug, branch, message: completed.message };
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* The EXACT recovery one-liner handed to the operator when an isolated/remote `do`
|
|
731
|
+
* integration fails terminally AFTER the work was committed + done-moved (the
|
|
732
|
+
* stale-lease-strand class surfaced by Part B #97), leaving the job worktree
|
|
733
|
+
* RETAINED. It points them straight at the recover-already-committed path
|
|
734
|
+
* (`complete --isolated <slug>`) so they need not reverse-engineer the encoded
|
|
735
|
+
* worktree path \u2014 the FINISH half of try-to-finish / else-surface. Detection is
|
|
736
|
+
* unspoofable (an already-integrated task is a clean no-op), so re-running it is
|
|
737
|
+
* always safe.
|
|
738
|
+
*
|
|
739
|
+
* `complete --isolated` recovers the retained WORKTREE, so it ONLY works ON THE
|
|
740
|
+
* MACHINE that ran the job (the worktree is local + reaped when that runner ends).
|
|
741
|
+
* From a DIFFERENT checkout (e.g. a CI-stranded job finished on your laptop),
|
|
742
|
+
* `--isolated` finds no local worktree and silently no-ops; there, check out the
|
|
743
|
+
* already-pushed work branch off the arbiter and run plain `complete`. Both named.
|
|
744
|
+
*/
|
|
745
|
+
export function recoverIsolatedOneLiner(slug) {
|
|
746
|
+
return (`To FINISH the stranded branch once the cause clears: ON THE SAME MACHINE that ` +
|
|
747
|
+
`ran the job, run \`dorfl complete --isolated ${slug}\` (integrates the ` +
|
|
748
|
+
`kept commit from the retained worktree; a no-op if already integrated). From ` +
|
|
749
|
+
`ANOTHER checkout (e.g. a CI-stranded job finished on your laptop), check out ` +
|
|
750
|
+
`the pushed work branch off the arbiter and run plain \`dorfl complete ` +
|
|
751
|
+
`${slug}\` instead.`);
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Build the HONEST per-op fragment describing WHAT actually reached the arbiter
|
|
755
|
+
* after a needs-attention route — reading the seam's captured per-op outcome
|
|
756
|
+
* (`branchPush`) rather than ASSUMING "pushed" off the local move. Shared by
|
|
757
|
+
* every save-failure / save-stop message site so they can never drift from
|
|
758
|
+
* reality (the observed bug: the report claimed "pushed" when the branch push
|
|
759
|
+
* was skipped-empty or failed). A PUSH failure (HIGH severity: work-at-risk /
|
|
760
|
+
* breaks cross-machine recovery) flips the fragment to a loud "saved LOCALLY
|
|
761
|
+
* only" with recovery guidance. The OBSERVABLE half (the stuck state) now rides
|
|
762
|
+
* on the per-item lock `state: stuck` amend — there is no separate on-`main`
|
|
763
|
+
* surface to report.
|
|
764
|
+
*/
|
|
765
|
+
function routeReport(routed, branch) {
|
|
766
|
+
const branchPush = routed.branchPush ?? 'not-attempted';
|
|
767
|
+
const branchFailed = branchPush === 'failed';
|
|
768
|
+
if (branchFailed) {
|
|
769
|
+
// HIGH severity: the recoverable branch push did not reach the arbiter — say
|
|
770
|
+
// so loudly, the work is saved LOCALLY only, and how to recover.
|
|
771
|
+
return {
|
|
772
|
+
fragment: `push of ${branch} FAILED — the work is saved LOCALLY only; push it ` +
|
|
773
|
+
'when online, then `requeue` (continue), or `requeue --reset` to discard',
|
|
774
|
+
pushFailed: true,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
// The push that was attempted succeeded (or was honestly skipped). Report
|
|
778
|
+
// the branch state truthfully.
|
|
779
|
+
let landed;
|
|
780
|
+
if (branchPush === 'pushed') {
|
|
781
|
+
landed = `pushed ${branch}`;
|
|
782
|
+
}
|
|
783
|
+
else if (branchPush === 'skipped-empty') {
|
|
784
|
+
landed = `branch ${branch} skipped (nothing to recover yet)`;
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
landed = 'saved locally';
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
fragment: `${landed}. Recover via \`requeue\` (continue) or \`requeue --reset\` to discard`,
|
|
791
|
+
pushFailed: false,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* SAVE the partial work of a FAILED agent instead of dropping it (the keystone of
|
|
796
|
+
* the `agent-fail-saves-work` task). An agent failure (`runDoAgent` returned
|
|
797
|
+
* `ok:false`, threw, or the prompt could not be assembled) used to BARE-RETURN
|
|
798
|
+
* `agent-failed`, leaving whatever the agent edited only on the local work branch
|
|
799
|
+
* in the (disposable, possibly remote) job worktree — silently lost.
|
|
800
|
+
*
|
|
801
|
+
* This routes it through the SAME work-preserving machinery a RED GATE uses: the
|
|
802
|
+
* ledger write seam's needs-attention transition (`git add -A` + a wip commit
|
|
803
|
+
* capturing the agent's work + the `git mv → needs-attention/` move-only commit
|
|
804
|
+
* with the failure detail recorded as the reason in the body), surfaced on the
|
|
805
|
+
* arbiter's `main` (the autonomous, cross-machine-visible mode-M surfacing `do`
|
|
806
|
+
* already uses for the gate-fail path) so `scan`/`status`/another machine see it.
|
|
807
|
+
*
|
|
808
|
+
* It ALSO pushes the `work/<slug>` branch to the arbiter so the saved partial
|
|
809
|
+
* commits travel cross-machine and the item is RECOVERABLE via `requeue`
|
|
810
|
+
* (continue): the continue-detection in `continue-branch.ts` looks for an arbiter
|
|
811
|
+
* `work/<slug>` ahead of main. That push now lives IN the ledger write seam (the
|
|
812
|
+
* RECOVERABLE half of the needs-attention transition — fired when an `arbiter` is
|
|
813
|
+
* given, best-effort, emptiness-guarded), consolidated there by
|
|
814
|
+
* `centralise-bounce-branch-push` so it cannot drift from the OBSERVABLE surface;
|
|
815
|
+
* this function no longer pushes separately.
|
|
816
|
+
*
|
|
817
|
+
* The EMPTY-failure case (the agent made NO commits / no changes) is handled
|
|
818
|
+
* without crashing on an empty commit: `routeToNeedsAttention` (under the seam)
|
|
819
|
+
* skips the wip commit when the tree is clean, and the move-only commit (reason +
|
|
820
|
+
* the `git mv`) is always non-empty, so the failure reason is still surfaced.
|
|
821
|
+
*
|
|
822
|
+
* The OUTCOME is the classified failure CAUSE (the FAILURE-CAUSE axis): the
|
|
823
|
+
* genuinely-new `transient-infra` / `config-error` where the surfaced detail makes
|
|
824
|
+
* the cause knowable, else the conservative generic `agent-failed` (still distinct
|
|
825
|
+
* from a clean success and from a red `gate-failed`/`needs-attention` — `do`'s exit
|
|
826
|
+
* contract stays coherent). Only the WORK-PRESERVING side-effect (unchanged here)
|
|
827
|
+
* matches the gate-failure path. We do NOT validate or "fix" the partial work — a broken
|
|
828
|
+
* tree committed + surfaced (with the reason) is recoverable; the human chooses
|
|
829
|
+
* `requeue` (continue) vs `requeue --reset` (discard).
|
|
830
|
+
*/
|
|
831
|
+
async function saveAgentFailure(params) {
|
|
832
|
+
const { slug, cwd, arbiter, detail, env, note } = params;
|
|
833
|
+
// The work branch is the namespaced build branch (`work/task-<slug>`; the
|
|
834
|
+
// onboarding switched the checkout to it before the agent ran); derive it from
|
|
835
|
+
// the slug so the push target is always defined even when the caller's `branch`
|
|
836
|
+
// was not narrowed.
|
|
837
|
+
const branch = params.branch ?? workBranchRef('task', slug);
|
|
838
|
+
// Classify the failure CAUSE (best-effort + conservative) from the surfaced
|
|
839
|
+
// detail — the SAME `classifyFailureCause` `run` uses, so `do`/`run` agree on the
|
|
840
|
+
// same error. The cause LABEL prefixes the recorded reason so the cause is legible
|
|
841
|
+
// on the needs-attention route without a second naming scheme; `agent-failed`
|
|
842
|
+
// keeps the historical "agent failed:" prefix (no reason-prose regression).
|
|
843
|
+
const cause = classifyFailureCause(detail);
|
|
844
|
+
const reason = `${failureCauseLabel(cause)}: ${detail}`;
|
|
845
|
+
// Route through the SAME seam the gate-fail path uses: save the agent's work as
|
|
846
|
+
// a wip commit (skipped when the tree is clean — the empty-failure case), amend
|
|
847
|
+
// the item's per-item lock ref to `state: stuck` with the reason in the body,
|
|
848
|
+
// surface that lock amend on the arbiter (OBSERVABLE, mode-M, cross-machine
|
|
849
|
+
// visible) AND push the `work/<slug>` branch (RECOVERABLE — so a requeue-continue
|
|
850
|
+
// reading <arbiter>/work/<slug> lands on the saved wip). Both halves fire from
|
|
851
|
+
// the single `arbiter` here; no separate push to forget.
|
|
852
|
+
const routed = await ledgerWrite.applyNeedsAttentionTransition({
|
|
853
|
+
cwd,
|
|
854
|
+
slug,
|
|
855
|
+
reason,
|
|
856
|
+
arbiter,
|
|
857
|
+
env,
|
|
858
|
+
note,
|
|
859
|
+
});
|
|
860
|
+
const report = routed.moved ? routeReport(routed, branch) : undefined;
|
|
861
|
+
const message = routed.moved
|
|
862
|
+
? `Agent run failed building '${slug}' [${cause}] (${detail}); SAVED the ` +
|
|
863
|
+
`partial work and marked '${slug}' stuck on its lock (${report.fragment}).`
|
|
864
|
+
: `Agent run failed building '${slug}' [${cause}] (${detail}); could not ` +
|
|
865
|
+
`mark '${slug}' stuck (${routed.reasonNotMoved ?? 'unknown'}).`;
|
|
866
|
+
note(message);
|
|
867
|
+
return {
|
|
868
|
+
exitCode: 1,
|
|
869
|
+
outcome: failureCauseToDoOutcome(cause),
|
|
870
|
+
slug,
|
|
871
|
+
branch,
|
|
872
|
+
routedToNeedsAttention: routed.moved,
|
|
873
|
+
message,
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Map a {@link FailureCause} onto the `do` {@link DoOutcome}. The cause names ARE
|
|
878
|
+
* the outcome names (the FAILURE-CAUSE axis reuses the terminal vocabulary), so
|
|
879
|
+
* this is identity — a single helper documents the mapping + keeps the `do`/`run`
|
|
880
|
+
* sites symmetric (`run` has the twin {@link failureCauseToItemStatus}).
|
|
881
|
+
*/
|
|
882
|
+
function failureCauseToDoOutcome(cause) {
|
|
883
|
+
return cause;
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Build the HONEST result for a CONTINUE-site surface that did NOT land on the
|
|
887
|
+
* arbiter (`{moved: false}`). The tree-less `in-progress/ → needs-attention/` move
|
|
888
|
+
* lost the CAS race against a busy arbiter (its contention-retry cap exhausted) or
|
|
889
|
+
* had no arbiter to publish to, so the item is STILL in-progress on the arbiter —
|
|
890
|
+
* a clean `needs-attention` would mislead (it claims the surface landed). Distinct
|
|
891
|
+
* `surface-unmoved` outcome, carrying `reasonNotMoved`, so the caller/human can
|
|
892
|
+
* tell it from a successful surface and retry/resolve. The `moved: true` path is
|
|
893
|
+
* left byte-for-byte unchanged (this branch is only reached on `!moved`).
|
|
894
|
+
*/
|
|
895
|
+
function surfaceUnmovedDoResult(params) {
|
|
896
|
+
const { slug, branch, reason, surfaced } = params;
|
|
897
|
+
const message = `'${slug}' could NOT be surfaced to needs-attention — the surface did not ` +
|
|
898
|
+
`reach the arbiter's main; the item is still IN-PROGRESS on the arbiter ` +
|
|
899
|
+
`(retry/resolve). ${surfaced.reasonNotMoved ?? reason}`;
|
|
900
|
+
return { exitCode: 1, outcome: 'surface-unmoved', slug, branch, message };
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Resolve the STOP reason for a clean (`agent.ok`) run, or `undefined` when the
|
|
904
|
+
* run is a genuine build that should proceed to the gate (task
|
|
905
|
+
* `agent-stop-signal`). TWO independent triggers, the sentinel winning:
|
|
906
|
+
*
|
|
907
|
+
* 1. The IN-BAND STOP sentinel in the agent's output ({@link parseStopSentinel})
|
|
908
|
+
* — the principled case: the agent declared the task drifted/ambiguous and
|
|
909
|
+
* reported WHY. Its reason is used VERBATIM (a non-empty diff with a sentinel
|
|
910
|
+
* is still a STOP — the agent may have left scratch; the sentinel wins).
|
|
911
|
+
* 2. The DETERMINISTIC empty-diff backstop ({@link isWorkBranchDiffEmpty}) — the
|
|
912
|
+
* observable safety net for when the agent stopped WITHOUT (or with a
|
|
913
|
+
* malformed) sentinel: `agent.ok` but no source change vs `<arbiter>/main` is
|
|
914
|
+
* never a successful build.
|
|
915
|
+
*
|
|
916
|
+
* Shared by `performDo` and `runRemotePipeline` so the in-place and remote forms
|
|
917
|
+
* detect a STOP identically.
|
|
918
|
+
*/
|
|
919
|
+
async function resolveStopReason(params) {
|
|
920
|
+
const { output, slug, cwd, arbiter, env } = params;
|
|
921
|
+
const sentinel = parseStopSentinel(output);
|
|
922
|
+
if (sentinel !== undefined) {
|
|
923
|
+
return sentinel.reason;
|
|
924
|
+
}
|
|
925
|
+
if (await isWorkBranchDiffEmpty({ cwd, arbiter, env })) {
|
|
926
|
+
return emptyDiffStopReason(slug);
|
|
927
|
+
}
|
|
928
|
+
return undefined;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Route a DELIBERATE agent STOP (task `agent-stop-signal`) to needs-attention
|
|
932
|
+
* through the SAME work-preserving seam `saveAgentFailure` uses (save the branch,
|
|
933
|
+
* surface on the arbiter) — but as the DISTINCT `agent-stopped` outcome, NOT
|
|
934
|
+
* `agent-failed` (the agent did not error) nor `needs-attention` (no red gate /
|
|
935
|
+
* rebase conflict). The agent's STOP reason is recorded VERBATIM as the
|
|
936
|
+
* needs-attention reason. The acceptance gate AND Gate-2 are NEVER reached.
|
|
937
|
+
*/
|
|
938
|
+
async function saveAgentStop(params) {
|
|
939
|
+
const { slug, cwd, arbiter, reason, env, note } = params;
|
|
940
|
+
const branch = params.branch ?? workBranchRef('task', slug);
|
|
941
|
+
const routed = await ledgerWrite.applyNeedsAttentionTransition({
|
|
942
|
+
cwd,
|
|
943
|
+
slug,
|
|
944
|
+
reason,
|
|
945
|
+
arbiter,
|
|
946
|
+
env,
|
|
947
|
+
note,
|
|
948
|
+
});
|
|
949
|
+
const report = routed.moved ? routeReport(routed, branch) : undefined;
|
|
950
|
+
const message = routed.moved
|
|
951
|
+
? `The agent STOPPED building '${slug}' (the task drifted / is ambiguous / ` +
|
|
952
|
+
`produced no change); marked '${slug}' stuck on its lock (${report.fragment}) ` +
|
|
953
|
+
`WITHOUT running the gate or Gate-2 review. Reason: ${reason}`
|
|
954
|
+
: `The agent STOPPED building '${slug}' but it could not be marked stuck ` +
|
|
955
|
+
`(${routed.reasonNotMoved ?? 'unknown'}). Reason: ${reason}`;
|
|
956
|
+
note(message);
|
|
957
|
+
return {
|
|
958
|
+
exitCode: 1,
|
|
959
|
+
outcome: 'agent-stopped',
|
|
960
|
+
slug,
|
|
961
|
+
branch,
|
|
962
|
+
routedToNeedsAttention: routed.moved,
|
|
963
|
+
message,
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Run the agent against the checkout. Prefers the injected `dorfl` (tests
|
|
968
|
+
* / custom embeddings); otherwise launches `agentCmd` through the SHARED
|
|
969
|
+
* {@link launchWithOptionalWatch} helper (the null adapter by default),
|
|
970
|
+
* forwarding the model routing intent.
|
|
971
|
+
*
|
|
972
|
+
* With `--watch` (pi harness only, validated earlier), the helper launches the
|
|
973
|
+
* agent NON-BLOCKING (`PiHarness.launchAsync` — `spawn`, not `spawnSync`) so a
|
|
974
|
+
* `SessionTailer` can READ the growing session `.jsonl` concurrently and surface
|
|
975
|
+
* the high-signal events live. The tailer is a pure observer: the launch result
|
|
976
|
+
* is IDENTICAL to the non-watch path, so outcome / gate / git / exit code are
|
|
977
|
+
* unchanged — only a concurrent log-tail is added.
|
|
978
|
+
*
|
|
979
|
+
* The build session-id is the SLUG (in-place `do` has no work-id), which the
|
|
980
|
+
* helper makes unique per launch; the Gate-2 REVIEW launch uses the SAME helper
|
|
981
|
+
* with a DISTINCT id (`<slug>-review`) so the two sessions never collide — one
|
|
982
|
+
* watch implementation, two callers (task `watch-review-session`).
|
|
983
|
+
*/
|
|
984
|
+
async function runDoAgent(options, cwd, prompt, slug) {
|
|
985
|
+
if (options.dorfl) {
|
|
986
|
+
return options.dorfl({ cwd, prompt, slug, env: options.env });
|
|
987
|
+
}
|
|
988
|
+
const harness = options.harness ?? new NullHarness();
|
|
989
|
+
const launched = await launchWithOptionalWatch({
|
|
990
|
+
harness,
|
|
991
|
+
dir: cwd,
|
|
992
|
+
slug,
|
|
993
|
+
command: options.agentCmd ?? '',
|
|
994
|
+
prompt,
|
|
995
|
+
model: options.model,
|
|
996
|
+
// In-place `do` has NO work-id, so the build session id is the slug.
|
|
997
|
+
sessionId: slug,
|
|
998
|
+
sessionsDir: options.sessionsDir,
|
|
999
|
+
watch: options.watch,
|
|
1000
|
+
watchSink: options.watchSink,
|
|
1001
|
+
color: options.color,
|
|
1002
|
+
env: options.env,
|
|
1003
|
+
});
|
|
1004
|
+
// Surface the agent's FINAL SUMMARY (`LaunchResult.output`) — the source channel
|
|
1005
|
+
// for the propose-mode PR body — instead of dropping it. Absent (no parseable
|
|
1006
|
+
// assistant text) ⇒ undefined ⇒ the body degrades to `--fill` (no regression).
|
|
1007
|
+
return { ok: launched.ok, detail: launched.detail, output: launched.output };
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* True when the working tree has uncommitted (unstaged OR staged) changes — the
|
|
1011
|
+
* dirty-tree refusal predicate (ar-run.sh: `git diff --quiet` AND
|
|
1012
|
+
* `git diff --cached --quiet`). Read-only.
|
|
1013
|
+
*/
|
|
1014
|
+
async function isDirtyTree(cwd, env) {
|
|
1015
|
+
const unstaged = await runAsync('git', ['diff', '--quiet'], cwd, { env });
|
|
1016
|
+
if (unstaged.status !== 0) {
|
|
1017
|
+
return true;
|
|
1018
|
+
}
|
|
1019
|
+
const staged = await runAsync('git', ['diff', '--cached', '--quiet'], cwd, {
|
|
1020
|
+
env,
|
|
1021
|
+
});
|
|
1022
|
+
return staged.status !== 0;
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* `dorfl do --remote <r> <arg>` — the per-repo `do` WORKER run against a
|
|
1026
|
+
* REGISTERED repo with NO checkout (`docs/adr/command-surface-and-journeys.md`
|
|
1027
|
+
* §3). Where the in-place {@link performDo} uses the CURRENT checkout AS its
|
|
1028
|
+
* isolation, this form materialises a **hub mirror + job worktree in the AGENTS'
|
|
1029
|
+
* area** (`workspacesDir`, the SAME isolation `run` uses — NEVER the human area),
|
|
1030
|
+
* runs the existing `do` pipeline against that worktree, then tears it down per
|
|
1031
|
+
* the §4 provably-safe deletion predicate.
|
|
1032
|
+
*
|
|
1033
|
+
* **Option A — materialise-then-reuse** (the drift correction; the full
|
|
1034
|
+
* IsolatedTree-seam unification is the SEPARATE `do-run-share-isolation-seam`
|
|
1035
|
+
* task). `performDo` composes the human verbs against a literal `cwd`; this
|
|
1036
|
+
* function does the same — it just points that `cwd` at a freshly-cut job
|
|
1037
|
+
* worktree instead of a checkout. It reuses (does NOT reimplement) the pipeline:
|
|
1038
|
+
* `performStart` (resume) → agent → `performComplete`.
|
|
1039
|
+
*
|
|
1040
|
+
* **The claim ↔ worktree ↔ start composition.** Both `createJob` (cuts the
|
|
1041
|
+
* `work/<slug>` branch off the mirror's fresh main) and `performStart` (claims +
|
|
1042
|
+
* switches to `work/<slug>`) overlap. To compose without double-claiming or
|
|
1043
|
+
* fighting over the branch we order them as the task mandates:
|
|
1044
|
+
*
|
|
1045
|
+
* 1. **CLAIM FIRST** — the CAS push to the arbiter, run in a throwaway clone of
|
|
1046
|
+
* the mirror (the CAS needs a non-bare checkout with an `origin/main`
|
|
1047
|
+
* tracking ref + a worktree to commit in, which a bare mirror does not
|
|
1048
|
+
* provide — exactly `work-on`'s remote-form claim context).
|
|
1049
|
+
* 2. **MATERIALISE the worktree** off the POST-CLAIM fresh main via the EXISTING
|
|
1050
|
+
* job-worktree machinery (`jobWorktreeStrategy`/`createJob`): `createJob`
|
|
1051
|
+
* re-`ensureMirror`s (fetching the claim move) then cuts `work/<slug>` off
|
|
1052
|
+
* the freshly-fetched mirror main.
|
|
1053
|
+
* 3. **RUN start/agent/complete** against the worktree dir as `cwd`. `start` is
|
|
1054
|
+
* driven with `resume: true` so it PLAIN-SWITCHES the branch `createJob`
|
|
1055
|
+
* already created (the item is in-progress on the arbiter after the claim;
|
|
1056
|
+
* `performStart`'s resume path switches without re-claiming) — no
|
|
1057
|
+
* double-claim, no branch fight. The worktree's arbiter remote is `origin`
|
|
1058
|
+
* (the bare mirror's clone remote), so both `start` and `complete` use
|
|
1059
|
+
* `origin`.
|
|
1060
|
+
*
|
|
1061
|
+
* **Teardown** re-applies the §4 predicate via the strategy handle's `teardown`
|
|
1062
|
+
* (`reapJob`): reap the worktree iff clean AND on the arbiter, retain otherwise
|
|
1063
|
+
* (the never-lose-work signal). NEVER `--force`.
|
|
1064
|
+
*
|
|
1065
|
+
* **Recovery contract.** The worktree is disposable; the durable artifact is the
|
|
1066
|
+
* `work/<slug>` BRANCH (pushed by the autonomous needs-attention surfacing on a
|
|
1067
|
+
* stuck/failed run). A human recovers via the human face (`requeue` + re-claim,
|
|
1068
|
+
* or `work-on`), NEVER by editing the agents'-area worktree.
|
|
1069
|
+
*/
|
|
1070
|
+
/**
|
|
1071
|
+
* Resolve the URL of a CHECKOUT's arbiter remote — the primitive `do --isolated`
|
|
1072
|
+
* needs to point the job-worktree pipeline ({@link performDoRemote}) at MY OWN
|
|
1073
|
+
* arbiter (`git -C <cwd> remote get-url <arbiter>`). Returns the URL, or
|
|
1074
|
+
* `undefined` when the cwd is not a git repo or has no such remote (the
|
|
1075
|
+
* "isolated against what?" case the CLI turns into a clear error naming
|
|
1076
|
+
* `--remote <url>`). Does NOT fork the isolation/integrate path — it only feeds
|
|
1077
|
+
* the EXISTING `performDoRemote` its `remote` URL.
|
|
1078
|
+
*/
|
|
1079
|
+
export function resolveArbiterUrlFromCheckout(cwd, arbiter, env) {
|
|
1080
|
+
const res = run('git', ['remote', 'get-url', arbiter], cwd, { env });
|
|
1081
|
+
if (res.status !== 0) {
|
|
1082
|
+
return undefined;
|
|
1083
|
+
}
|
|
1084
|
+
const url = res.stdout.trim();
|
|
1085
|
+
return url === '' ? undefined : url;
|
|
1086
|
+
}
|
|
1087
|
+
export async function performDoRemote(options) {
|
|
1088
|
+
const note = options.note ?? (() => { });
|
|
1089
|
+
// The runner's GIT/provider env, scoped to the configured identity (claim,
|
|
1090
|
+
// push, integrate, `gh`). The AGENT launch stays ambient via `options.env`
|
|
1091
|
+
// (`runDoAgent` reads it directly) — the agent must not commit as the bot.
|
|
1092
|
+
// Absent identity ⇒ `options.env` unchanged (byte-for-byte ambient).
|
|
1093
|
+
const workspacesDir = options.workspacesDir;
|
|
1094
|
+
// Resolve the identity env AND run the push-time transport-coherence guard:
|
|
1095
|
+
// refuse a forbidden transport for THIS remote's URL (the registered remote),
|
|
1096
|
+
// and fail cleanly on an unresolvable identity (e.g. `tokenEnv` unset) — never
|
|
1097
|
+
// a crash or a silent ambient fallback. Both are no-ops without an identity.
|
|
1098
|
+
let env;
|
|
1099
|
+
try {
|
|
1100
|
+
env = identityEnv(options.identity, options.env ?? process.env);
|
|
1101
|
+
assertTransportAllowed(options.identity, options.remote);
|
|
1102
|
+
}
|
|
1103
|
+
catch (err) {
|
|
1104
|
+
return {
|
|
1105
|
+
exitCode: 1,
|
|
1106
|
+
outcome: 'usage-error',
|
|
1107
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
// 0. `--watch` REQUIRES the pi harness (same guard as in-place `do`): only the
|
|
1111
|
+
// pi adapter writes a session `.jsonl` to tail. Error CLEARLY here, BEFORE
|
|
1112
|
+
// any mirror/claim/worktree side-effect. The injected `dorfl` (tests)
|
|
1113
|
+
// is its own launch path and is exempt.
|
|
1114
|
+
if (options.watch === true &&
|
|
1115
|
+
options.dorfl === undefined &&
|
|
1116
|
+
!(options.harness instanceof PiHarness)) {
|
|
1117
|
+
return {
|
|
1118
|
+
exitCode: 1,
|
|
1119
|
+
outcome: 'usage-error',
|
|
1120
|
+
message: '`do --watch` requires the pi harness; configure `harness: pi` or drop ' +
|
|
1121
|
+
'`--watch`.',
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
// 1. Resolve / auto-create the hub mirror for `<r>` (the `registry-remote` /
|
|
1125
|
+
// `work-on`-remote precedent: an unregistered remote is auto-mirrored
|
|
1126
|
+
// before use). `ensureMirror` creates it (`git clone --bare`) when absent
|
|
1127
|
+
// or fetches it when present, under `workspacesDir/repos/` — the agents'
|
|
1128
|
+
// area, NEVER the human area.
|
|
1129
|
+
let mirror;
|
|
1130
|
+
try {
|
|
1131
|
+
mirror = ensureMirror({ url: options.remote, workspacesDir, env });
|
|
1132
|
+
}
|
|
1133
|
+
catch (err) {
|
|
1134
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1135
|
+
return { exitCode: 1, outcome: 'usage-error', message };
|
|
1136
|
+
}
|
|
1137
|
+
note(mirror.created
|
|
1138
|
+
? `Auto-registered hub mirror for ${mirror.url} at ${mirror.path}.`
|
|
1139
|
+
: `Using hub mirror for ${mirror.url} at ${mirror.path}.`);
|
|
1140
|
+
// 1b. PR-INTENT pre-flight guard — the AUTONOMOUS mirror of in-place `performDo`
|
|
1141
|
+
// step 3c. The SAME predicate, run UP FRONT here (mirror resolved, BEFORE the
|
|
1142
|
+
// claim clone / worktree) so a `propose` run on a GitHub arbiter that INTENDS a
|
|
1143
|
+
// PR (`noPR` unset) fails fast when `gh` genuinely cannot open one, instead of
|
|
1144
|
+
// letting integration silently degrade to manual-PR instructions. NO claim/
|
|
1145
|
+
// build side-effect: it precedes even the throwaway claim CLONE below. The probe
|
|
1146
|
+
// runs in the bare mirror dir (`mirror.path`), which carries the arbiter remote
|
|
1147
|
+
// as `origin`, NOT a bare cwd. CRITICAL: the PROBE is the signal, NOT a config
|
|
1148
|
+
// check — an absent `providers.github` identity falls back to AMBIENT `gh` auth
|
|
1149
|
+
// (the probe reports it available), so a working ambient setup still PROCEEDS.
|
|
1150
|
+
// `arbiterIsGitHub` is derived from `mirror.url` (the resolved arbiter URL).
|
|
1151
|
+
// REUSES the predicate + message so the in-place + autonomous paths cannot
|
|
1152
|
+
// drift.
|
|
1153
|
+
{
|
|
1154
|
+
const probe = options.ghCanOpenPr ??
|
|
1155
|
+
((probeCwd, probeEnv) => new GitHubProvider().available(probeCwd, probeEnv));
|
|
1156
|
+
if (shouldFailProposePrIntent({
|
|
1157
|
+
mode: options.integration ?? 'propose',
|
|
1158
|
+
arbiterIsGitHub: isGitHubArbiterUrl(mirror.url),
|
|
1159
|
+
noPR: options.noPR,
|
|
1160
|
+
ghCanOpenPr: () => probe(mirror.path, env),
|
|
1161
|
+
})) {
|
|
1162
|
+
return {
|
|
1163
|
+
exitCode: 1,
|
|
1164
|
+
outcome: 'refused',
|
|
1165
|
+
message: PROPOSE_PR_INTENT_GH_UNAVAILABLE_MESSAGE,
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
// 1c. STATIC fresh-worktree-gate readiness guard — the AUTONOMOUS mirror of
|
|
1170
|
+
// `performDo` step 3d. When the fresh-worktree gate is ON AND `prepare`
|
|
1171
|
+
// resolves to no commands AND a lockfile is present IN THE MIRROR, the
|
|
1172
|
+
// throwaway worktree the gate runs in will have no installed deps, so the
|
|
1173
|
+
// gate cannot run. Probe the bare mirror's main tree (`git ls-tree main`)
|
|
1174
|
+
// so the guard fires BEFORE the throwaway claim clone is even cut. A repo
|
|
1175
|
+
// with NO lockfile is the intentional dep-free case and proceeds. Deps-only
|
|
1176
|
+
// (verify-unset is impossible — `resolveVerifyCommands` defaults the gate).
|
|
1177
|
+
{
|
|
1178
|
+
const guard = checkGatePreconditions({
|
|
1179
|
+
freshWorktreeGate: options.freshWorktreeGate,
|
|
1180
|
+
prepare: options.prepare,
|
|
1181
|
+
lockfile: detectLockfileOnMirrorMain(mirror.path, env),
|
|
1182
|
+
});
|
|
1183
|
+
if (guard !== undefined) {
|
|
1184
|
+
return { exitCode: 1, outcome: 'refused', message: guard.message };
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
// 2. A throwaway claim clone of the mirror (the CAS context). Slug resolution
|
|
1188
|
+
// + the claim both run here against `origin` (the arbiter URL). Keyed PER
|
|
1189
|
+
// ARG (not a single fixed `__remote` path per mirror) so two CONCURRENT
|
|
1190
|
+
// `do --remote` calls on the SAME mirror — the registry-set advance batch's
|
|
1191
|
+
// per-mirror concurrency (`perRepoMax > 1`) — get DISTINCT claim clones rather
|
|
1192
|
+
// than racing one shared dir (the SAME per-job-clone keying `run`'s
|
|
1193
|
+
// `claimAgainstRepo` uses; the prior single-shot caller never raced, so this
|
|
1194
|
+
// is a pure concurrency hardening, not a behaviour change for it).
|
|
1195
|
+
const claimKey = options.arg.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
1196
|
+
const claimDir = join(workspacesDir, 'claim', `${encodeRepoKey(mirror.url).split('/').join('__')}__${claimKey}`);
|
|
1197
|
+
rmSync(claimDir, { recursive: true, force: true });
|
|
1198
|
+
mkdirSync(dirname(claimDir), { recursive: true });
|
|
1199
|
+
git(['clone', '--quiet', mirror.url, claimDir], dirname(claimDir), { env });
|
|
1200
|
+
try {
|
|
1201
|
+
// 2a. Resolve the slug across BOTH namespaces against the claim clone (it
|
|
1202
|
+
// carries `work/` from the mirror's main). A collision / resolution
|
|
1203
|
+
// failure is a loud usage error; a `prd:` arg reaches the not-yet-wired
|
|
1204
|
+
// stub — identical behaviour to in-place `do`.
|
|
1205
|
+
let resolved;
|
|
1206
|
+
try {
|
|
1207
|
+
resolved = resolveSlug({
|
|
1208
|
+
arg: options.arg,
|
|
1209
|
+
repoPath: claimDir,
|
|
1210
|
+
read: options.read ?? ledgerRead,
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
catch (err) {
|
|
1214
|
+
if (err instanceof SlugResolutionError) {
|
|
1215
|
+
return { exitCode: 1, outcome: 'usage-error', message: err.message };
|
|
1216
|
+
}
|
|
1217
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1218
|
+
return { exitCode: 1, outcome: 'usage-error', message };
|
|
1219
|
+
}
|
|
1220
|
+
// `do --remote spec:<slug>` routes to tasking (the `spec` namespace).
|
|
1221
|
+
if (resolved.namespace === 'spec') {
|
|
1222
|
+
// `do --remote spec:<slug>`: task the spec as the AGENT, against the claim
|
|
1223
|
+
// clone (its `origin` IS the arbiter URL + it carries a working tree from the
|
|
1224
|
+
// mirror's main). No job worktree is needed — the tasking transition is a
|
|
1225
|
+
// runner-owned `spec → tasking → spec` move + emit-backlog on the arbiter, not
|
|
1226
|
+
// a build pipeline. The agent only writes task files; the runner does all git.
|
|
1227
|
+
const tasked = await performTask({
|
|
1228
|
+
slug: resolved.slug,
|
|
1229
|
+
cwd: claimDir,
|
|
1230
|
+
arbiter: 'origin',
|
|
1231
|
+
doer: 'agent',
|
|
1232
|
+
autoTask: options.autoTask,
|
|
1233
|
+
// EXPLICIT dispatch (same as the in-place path above): the `prd:<slug>` was
|
|
1234
|
+
// NAMED (typed, or pool-filtered on `autoTask` before reaching here), so the
|
|
1235
|
+
// tasking gate drops the policy term — only the prd's own readiness +
|
|
1236
|
+
// `taskedAfter` bind, mirroring the build path vs `autoBuild`.
|
|
1237
|
+
explicit: true,
|
|
1238
|
+
dorfl: options.dorfl,
|
|
1239
|
+
harness: options.harness,
|
|
1240
|
+
agentCmd: options.agentCmd,
|
|
1241
|
+
model: options.model,
|
|
1242
|
+
sessionsDir: options.sessionsDir,
|
|
1243
|
+
// The integrate-time args (task `slice-output-through-integration`): the
|
|
1244
|
+
// `provider` is the SAME the task-build path threads (arg parity), but the
|
|
1245
|
+
// MODE is the per-TRANSITION TASKING resolution
|
|
1246
|
+
// (`per-transition-integration-mode-slicing-vs-build`):
|
|
1247
|
+
// `taskingIntegration ?? integration`, so the `--remote prd:` output ALSO
|
|
1248
|
+
// routes through the shared core with the tasking-resolved mode.
|
|
1249
|
+
integration: options.taskingIntegration ?? options.integration,
|
|
1250
|
+
// The per-repo TASK-PLACEMENT default + the operator's explicit
|
|
1251
|
+
// override (task `runner-deterministic-slice-placement-policy-and-
|
|
1252
|
+
// precedence`). Same threading as the in-place `do prd:` path.
|
|
1253
|
+
tasksLandIn: options.tasksLandIn,
|
|
1254
|
+
explicitTasksLandIn: options.explicitTasksLandIn,
|
|
1255
|
+
noPR: options.noPR,
|
|
1256
|
+
providerInstance: options.providerInstance,
|
|
1257
|
+
// The tasker review→edit→converge loop on the `do --remote prd:` path too.
|
|
1258
|
+
reviewLoop: options.reviewLoop,
|
|
1259
|
+
taskerLoopMax: options.taskerLoopMax,
|
|
1260
|
+
reviewExecutions: options.reviewExecutions,
|
|
1261
|
+
taskerLoopModel: options.taskerLoopModel,
|
|
1262
|
+
// The task-SET ACCEPTANCE GATE on the `do --remote prd:` path too.
|
|
1263
|
+
review: options.review,
|
|
1264
|
+
reviewGate: options.taskReviewGate,
|
|
1265
|
+
acceptanceReviewModel: options.reviewModel,
|
|
1266
|
+
env,
|
|
1267
|
+
// The tasker + review AGENTS launch AMBIENT, never the identity env.
|
|
1268
|
+
agentEnv: options.env,
|
|
1269
|
+
note,
|
|
1270
|
+
});
|
|
1271
|
+
return taskResultToDoResult(tasked);
|
|
1272
|
+
}
|
|
1273
|
+
const slug = resolved.slug;
|
|
1274
|
+
// 3. CLAIM FIRST (the CAS push to the arbiter), in the throwaway clone.
|
|
1275
|
+
// `origin` there IS the arbiter URL. A lost/contended/usage claim is
|
|
1276
|
+
// propagated verbatim — NO worktree is materialised (clean failure, like
|
|
1277
|
+
// `run`'s `runOneItem`).
|
|
1278
|
+
const claim = await performClaim({
|
|
1279
|
+
slug,
|
|
1280
|
+
cwd: claimDir,
|
|
1281
|
+
arbiter: 'origin',
|
|
1282
|
+
// `--allow-backlog`: accept a `tasks/backlog/`-resident body (staging). The
|
|
1283
|
+
// claim stays a pure lock. Default off ⇒ pool-only.
|
|
1284
|
+
allowBacklog: options.allowBacklog === true,
|
|
1285
|
+
env,
|
|
1286
|
+
note,
|
|
1287
|
+
});
|
|
1288
|
+
if (claim.outcome === 'lost') {
|
|
1289
|
+
return { exitCode: 2, outcome: 'lost', slug, message: claim.message };
|
|
1290
|
+
}
|
|
1291
|
+
if (claim.outcome === 'contended') {
|
|
1292
|
+
return { exitCode: 3, outcome: 'contended', slug, message: claim.message };
|
|
1293
|
+
}
|
|
1294
|
+
if (claim.exitCode !== 0) {
|
|
1295
|
+
return {
|
|
1296
|
+
exitCode: 1,
|
|
1297
|
+
outcome: 'usage-error',
|
|
1298
|
+
slug,
|
|
1299
|
+
message: claim.message,
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
// 4. MATERIALISE the job worktree off the POST-CLAIM fresh main via the
|
|
1303
|
+
// EXISTING job-worktree machinery (the SAME path `run` uses). `createJob`
|
|
1304
|
+
// re-`ensureMirror`s (fetching the claim move) then cuts `work/<slug>`
|
|
1305
|
+
// off the freshly-fetched mirror main, in the agents' area.
|
|
1306
|
+
const strategy = jobWorktreeStrategy({
|
|
1307
|
+
fromRepo: claimDir,
|
|
1308
|
+
arbiter: 'origin',
|
|
1309
|
+
workspacesDir,
|
|
1310
|
+
});
|
|
1311
|
+
let tree;
|
|
1312
|
+
let result;
|
|
1313
|
+
try {
|
|
1314
|
+
try {
|
|
1315
|
+
tree = strategy.prepare({ slug, type: 'task', env });
|
|
1316
|
+
}
|
|
1317
|
+
catch (err) {
|
|
1318
|
+
// `prepare()`/`createJob` THREW before returning the handle (e.g. an
|
|
1319
|
+
// onboard reconcile/stale-lease push surfaced as a throw). `tree` is
|
|
1320
|
+
// undefined, so the normal teardown below would be skipped and a
|
|
1321
|
+
// partially-created worktree could LEAK with no teardown attempt at all —
|
|
1322
|
+
// and its checked-out `work/<slug>` branch would then poison the next
|
|
1323
|
+
// build's fetch. Best-effort reap the deterministic worktree path for this
|
|
1324
|
+
// slug (it is reaped ONLY if its branch is reachable on the arbiter —
|
|
1325
|
+
// never lose work), then re-throw so the failure is still reported.
|
|
1326
|
+
reapPreparedWorktreeLeak(mirror.url, slug, workspacesDir, env, note);
|
|
1327
|
+
throw err;
|
|
1328
|
+
}
|
|
1329
|
+
result = await runRemotePipeline(options, tree, slug, note, env);
|
|
1330
|
+
return result;
|
|
1331
|
+
}
|
|
1332
|
+
finally {
|
|
1333
|
+
// 7. Teardown via the strategy handle. On a CLEAN completion: reap iff clean
|
|
1334
|
+
// AND on the arbiter (the standard §4 predicate). On a FAILURE return
|
|
1335
|
+
// (needs-attention / config-error / refused etc. — the seam already
|
|
1336
|
+
// surfaced the item + pushed the branch): reap on REACHABILITY ALONE, so a
|
|
1337
|
+
// churn-dirty-but-arbiter-safe worktree does not linger to poison the next
|
|
1338
|
+
// build's config-read/materialisation fetch. A worktree whose work is NOT
|
|
1339
|
+
// yet on the arbiter is RETAINED either way (never lose work). NEVER
|
|
1340
|
+
// --force.
|
|
1341
|
+
if (tree) {
|
|
1342
|
+
const reachableOnly = result !== undefined && result.outcome !== 'completed';
|
|
1343
|
+
tree.teardown({ reachableOnly });
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
finally {
|
|
1348
|
+
// Remove the throwaway claim clone either way.
|
|
1349
|
+
rmSync(claimDir, { recursive: true, force: true });
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Best-effort reap of a worktree that {@link IsolationStrategy.prepare}/`createJob`
|
|
1354
|
+
* may have created at the deterministic per-job path BEFORE it threw (so the
|
|
1355
|
+
* handle was never returned and the normal teardown is skipped). Reaps it ONLY
|
|
1356
|
+
* if its branch is provably reachable on the arbiter — a `prepare` throw can
|
|
1357
|
+
* happen AFTER a clean continue-push (work is safe ⇒ reap so it can't poison the
|
|
1358
|
+
* next build) or BEFORE any push (work not safe ⇒ retain; never lose work). All
|
|
1359
|
+
* git ops are swallowed: this runs on an already-failing path and must never mask
|
|
1360
|
+
* the original throw.
|
|
1361
|
+
*/
|
|
1362
|
+
function reapPreparedWorktreeLeak(mirrorUrl, slug, workspacesDir, env, note) {
|
|
1363
|
+
try {
|
|
1364
|
+
const dir = jobWorktreePath(workspacesDir, mirrorUrl, slug);
|
|
1365
|
+
if (!existsSync(dir)) {
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
const mirrorDir = mirrorPath(workspacesDir, mirrorUrl);
|
|
1369
|
+
const result = reapJob({
|
|
1370
|
+
dir,
|
|
1371
|
+
branch: workBranchRef('task', slug),
|
|
1372
|
+
mirrorPath: mirrorDir,
|
|
1373
|
+
// Same failure-path stance as the normal teardown: reachable-on-arbiter is
|
|
1374
|
+
// enough (don't let incidental churn retain a worktree whose branch is
|
|
1375
|
+
// already safe). Reachability still gates — unsaved work is retained.
|
|
1376
|
+
reachableOnly: true,
|
|
1377
|
+
env,
|
|
1378
|
+
});
|
|
1379
|
+
if (result.removed) {
|
|
1380
|
+
note(`Reaped leaked worktree for ${slug} after prepare() threw ` +
|
|
1381
|
+
`(branch safe on the arbiter).`);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
catch {
|
|
1385
|
+
// best-effort — never mask the original prepare() throw
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Run the existing `do` pipeline (start[resume] → agent → complete) against an
|
|
1390
|
+
* already-materialised job worktree. Mirrors {@link performDo}'s middle/back —
|
|
1391
|
+
* but `cwd` is the worktree and the arbiter remote inside it is `origin`. The
|
|
1392
|
+
* needs-attention surfacing is the AUTONOMOUS, arbiter-passed variant (like
|
|
1393
|
+
* `run`/in-place `do`): a stuck remote `do` must be cross-machine visible.
|
|
1394
|
+
*/
|
|
1395
|
+
async function runRemotePipeline(options, tree, slug, note, env) {
|
|
1396
|
+
const cwd = tree.dir;
|
|
1397
|
+
const arbiterRemote = tree.arbiterRemote; // `origin` (the bare mirror's clone).
|
|
1398
|
+
// 4a. CONTINUE rebase conflict (ADR §14 + §10): a requeue kept a `work/<slug>`
|
|
1399
|
+
// that did not replay onto the current main at onboard-time (aborted, never
|
|
1400
|
+
// auto-resolved). Route to needs-attention via the seam (surfaced on the
|
|
1401
|
+
// arbiter + the kept branch already on the arbiter) instead of running the
|
|
1402
|
+
// agent — the §10 path `run` uses.
|
|
1403
|
+
if (tree.continueRebaseConflict) {
|
|
1404
|
+
const reason = `continuing the kept ${tree.branch}: rebase onto the latest main ` +
|
|
1405
|
+
'conflicted (aborted, never auto-resolved) — resolve against the latest ' +
|
|
1406
|
+
'main, or `requeue --reset` to discard and start fresh';
|
|
1407
|
+
await ledgerWrite.applyNeedsAttentionTransition({
|
|
1408
|
+
cwd,
|
|
1409
|
+
slug,
|
|
1410
|
+
reason,
|
|
1411
|
+
arbiter: arbiterRemote,
|
|
1412
|
+
env,
|
|
1413
|
+
note,
|
|
1414
|
+
});
|
|
1415
|
+
return {
|
|
1416
|
+
exitCode: 1,
|
|
1417
|
+
outcome: 'needs-attention',
|
|
1418
|
+
slug,
|
|
1419
|
+
branch: tree.branch,
|
|
1420
|
+
message: reason,
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
// 4b. CONTINUE reconcile-push TERMINAL failure (the stale-lease-strand bug this
|
|
1424
|
+
// fix kills): the onboard reconcile push of the kept (already-committed)
|
|
1425
|
+
// work branch to the arbiter FAILED terminally (the stale-lease retry cap
|
|
1426
|
+
// exhausted, or a non-stale-lease rejection / unreachable arbiter). The push
|
|
1427
|
+
// helper THROWS; `createJob` CATCHES it and flags `continuePushFailure`
|
|
1428
|
+
// rather than letting the throw escape (which crashed the run and left the
|
|
1429
|
+
// task silently in `work/in-progress/` on the arbiter, the work stranded in
|
|
1430
|
+
// the worktree). Route to needs-attention via the SAME seam the conflict path
|
|
1431
|
+
// uses — surfaced on the arbiter, the kept branch already on the arbiter from
|
|
1432
|
+
// the prior requeue (recoverable) — instead of running the agent.
|
|
1433
|
+
if (tree.continuePushFailure !== undefined) {
|
|
1434
|
+
const reason = `continuing the kept ${tree.branch}: publishing the rebased work branch ` +
|
|
1435
|
+
`to the arbiter failed terminally (${tree.continuePushFailure}) — the kept ` +
|
|
1436
|
+
'branch is left intact on the arbiter (recoverable); `requeue` to retry ' +
|
|
1437
|
+
'once the churn settles, or `requeue --reset` to discard and start fresh';
|
|
1438
|
+
await ledgerWrite.applyNeedsAttentionTransition({
|
|
1439
|
+
cwd,
|
|
1440
|
+
slug,
|
|
1441
|
+
reason,
|
|
1442
|
+
arbiter: arbiterRemote,
|
|
1443
|
+
env,
|
|
1444
|
+
note,
|
|
1445
|
+
});
|
|
1446
|
+
return {
|
|
1447
|
+
exitCode: 1,
|
|
1448
|
+
outcome: 'needs-attention',
|
|
1449
|
+
slug,
|
|
1450
|
+
branch: tree.branch,
|
|
1451
|
+
message: reason,
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
// 5. HARDEN start to work against a job-worktree `cwd` (the load-bearing work).
|
|
1455
|
+
// `performStart` reads/switches via the `<arbiter>/main` REMOTE-TRACKING ref
|
|
1456
|
+
// (`origin/main` here). A job worktree is cut from a BARE hub mirror whose
|
|
1457
|
+
// `origin` remote has NO fetch refspec, so `origin/main` /
|
|
1458
|
+
// `origin/work/<slug>` would not otherwise resolve — and `start` would
|
|
1459
|
+
// wrongly read the slug as "absent". Prime those two remote-tracking refs
|
|
1460
|
+
// EXPLICITLY (the SAME technique `integrator.rebaseOntoArbiterMain` /
|
|
1461
|
+
// `gc.fetchTracking` use against a bare-mirror worktree). After this the
|
|
1462
|
+
// existing `performStart` runs UNCHANGED against the worktree.
|
|
1463
|
+
await primeWorktreeTrackingRef(cwd, arbiterRemote, 'main', env);
|
|
1464
|
+
await primeWorktreeTrackingRef(cwd, arbiterRemote, workBranchRef('task', slug), env);
|
|
1465
|
+
// Onboard onto the work branch like in-place `do` — but the item is ALREADY
|
|
1466
|
+
// CLAIMED (step 3), so `performStart` runs with `resume: true`: it
|
|
1467
|
+
// PLAIN-SWITCHES the `work/<slug>` branch `createJob` already created WITHOUT
|
|
1468
|
+
// re-claiming (no double-claim, no branch fight). This proves `start` works
|
|
1469
|
+
// against a job-worktree `cwd`.
|
|
1470
|
+
const started = await performStart({
|
|
1471
|
+
slug,
|
|
1472
|
+
cwd,
|
|
1473
|
+
arbiter: arbiterRemote,
|
|
1474
|
+
resume: true,
|
|
1475
|
+
// `--allow-backlog`: a staged body (claimed, still in `tasks/backlog/`) is a
|
|
1476
|
+
// valid onboard residence; without this `start` would read it as `absent`.
|
|
1477
|
+
allowBacklog: options.allowBacklog === true,
|
|
1478
|
+
env,
|
|
1479
|
+
note,
|
|
1480
|
+
});
|
|
1481
|
+
if (started.outcome === 'needs-attention') {
|
|
1482
|
+
return {
|
|
1483
|
+
exitCode: 1,
|
|
1484
|
+
outcome: 'needs-attention',
|
|
1485
|
+
slug,
|
|
1486
|
+
branch: started.branch,
|
|
1487
|
+
message: started.message,
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
// FORWARD the honest un-moved signal end-to-end: a continue-site surface that
|
|
1491
|
+
// did NOT land on the arbiter (start's `surface-unmoved`) must NOT degrade to
|
|
1492
|
+
// `usage-error` here — the item is still in-progress on the arbiter (retry/resolve).
|
|
1493
|
+
if (started.outcome === 'surface-unmoved') {
|
|
1494
|
+
return {
|
|
1495
|
+
exitCode: 1,
|
|
1496
|
+
outcome: 'surface-unmoved',
|
|
1497
|
+
slug,
|
|
1498
|
+
branch: started.branch,
|
|
1499
|
+
message: started.message,
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
if (started.exitCode !== 0) {
|
|
1503
|
+
const outcome = started.outcome === 'refused' ? 'refused' : 'usage-error';
|
|
1504
|
+
return { exitCode: 1, outcome, slug, message: started.message };
|
|
1505
|
+
}
|
|
1506
|
+
const branch = started.branch;
|
|
1507
|
+
// 6. Build the prompt + run the agent autonomously in the worktree (the SAME
|
|
1508
|
+
// assembly in-place `do` uses). The agent only edits code.
|
|
1509
|
+
let prompt;
|
|
1510
|
+
try {
|
|
1511
|
+
// CONTINUE-aware resolution (job-worktree path): admit `work/done/` ONLY
|
|
1512
|
+
// behind the tip-vs-arbiter stranded gate (story 5), using the worktree's
|
|
1513
|
+
// primed `<origin>/work/<slug>` vs `<origin>/main` tracking refs.
|
|
1514
|
+
const task = resolveTask(cwd, slug, {
|
|
1515
|
+
cwd,
|
|
1516
|
+
branchRef: `${arbiterRemote}/${branch}`,
|
|
1517
|
+
mainRef: `${arbiterRemote}/main`,
|
|
1518
|
+
env,
|
|
1519
|
+
},
|
|
1520
|
+
// `--allow-backlog`: also resolve a `tasks/backlog/`-resident body.
|
|
1521
|
+
{ allowBacklog: options.allowBacklog === true });
|
|
1522
|
+
// CONTINUE-mode (job-worktree path): the worktree's tracking refs were primed
|
|
1523
|
+
// above (`primeWorktreeTrackingRef`), so reuse the SAME continue-detection with
|
|
1524
|
+
// the worktree's `<origin>/work/<slug>` vs `<origin>/main` refs.
|
|
1525
|
+
const continueContext = resolveContinueContext({
|
|
1526
|
+
cwd,
|
|
1527
|
+
slug,
|
|
1528
|
+
arbiter: arbiterRemote,
|
|
1529
|
+
branchRef: `${arbiterRemote}/${branch}`,
|
|
1530
|
+
mainRef: `${arbiterRemote}/main`,
|
|
1531
|
+
content: readFileSync(task.path, 'utf8'),
|
|
1532
|
+
env,
|
|
1533
|
+
});
|
|
1534
|
+
// Per-item override layer (mirrors in-place `do`): the task/prd frontmatter
|
|
1535
|
+
// may override the resolved repo `promptGuidance.testFirst` for THIS item.
|
|
1536
|
+
const itemGuidance = resolvePromptGuidanceForItem({
|
|
1537
|
+
cwd,
|
|
1538
|
+
repoResolved: { testFirst: options.promptGuidance?.testFirst === true },
|
|
1539
|
+
taskContent: readFileSync(task.path, 'utf8'),
|
|
1540
|
+
});
|
|
1541
|
+
prompt = buildAgentPrompt(task.slug, task.prd, task.taskPrompt, {
|
|
1542
|
+
cwd,
|
|
1543
|
+
continueContext,
|
|
1544
|
+
promptGuidance: itemGuidance,
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
catch (err) {
|
|
1548
|
+
if (err instanceof PromptError) {
|
|
1549
|
+
return await saveRemoteAgentFailure({
|
|
1550
|
+
slug,
|
|
1551
|
+
branch,
|
|
1552
|
+
cwd,
|
|
1553
|
+
arbiterRemote,
|
|
1554
|
+
detail: err.message,
|
|
1555
|
+
env,
|
|
1556
|
+
note,
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
throw err;
|
|
1560
|
+
}
|
|
1561
|
+
let agent;
|
|
1562
|
+
try {
|
|
1563
|
+
agent = await runDoAgent(options, cwd, prompt, slug);
|
|
1564
|
+
}
|
|
1565
|
+
catch (err) {
|
|
1566
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1567
|
+
return await saveRemoteAgentFailure({
|
|
1568
|
+
slug,
|
|
1569
|
+
branch,
|
|
1570
|
+
cwd,
|
|
1571
|
+
arbiterRemote,
|
|
1572
|
+
detail: message,
|
|
1573
|
+
env,
|
|
1574
|
+
note,
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
if (!agent.ok) {
|
|
1578
|
+
const detail = agent.detail ?? `the agent failed to build '${slug}'.`;
|
|
1579
|
+
return await saveRemoteAgentFailure({
|
|
1580
|
+
slug,
|
|
1581
|
+
branch,
|
|
1582
|
+
cwd,
|
|
1583
|
+
arbiterRemote,
|
|
1584
|
+
detail,
|
|
1585
|
+
env,
|
|
1586
|
+
note,
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
// 6b. HONOR a deliberate STOP (task `agent-stop-signal`) — the SAME detection
|
|
1590
|
+
// in-place `do` runs, shared via `resolveStopReason`/`saveAgentStop`. A
|
|
1591
|
+
// sentinel STOP (verbatim reason) or an empty work-branch diff routes to
|
|
1592
|
+
// needs-attention (surfaced on the arbiter) and SKIPS the gate + Gate-2. The
|
|
1593
|
+
// diff base is the worktree's `origin` (arbiterRemote).
|
|
1594
|
+
const stopReason = await resolveStopReason({
|
|
1595
|
+
output: agent.output,
|
|
1596
|
+
slug,
|
|
1597
|
+
cwd,
|
|
1598
|
+
arbiter: arbiterRemote,
|
|
1599
|
+
env,
|
|
1600
|
+
});
|
|
1601
|
+
if (stopReason !== undefined) {
|
|
1602
|
+
return await saveAgentStop({
|
|
1603
|
+
slug,
|
|
1604
|
+
branch,
|
|
1605
|
+
cwd,
|
|
1606
|
+
arbiter: arbiterRemote,
|
|
1607
|
+
reason: stopReason,
|
|
1608
|
+
env,
|
|
1609
|
+
note,
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
// 7. Gate + done-move + commit + rebase + integrate LIKE in-place `do`: the
|
|
1613
|
+
// AUTONOMOUS surfacing (`surfaceArbiter: origin`) so a red gate / rebase
|
|
1614
|
+
// conflict / review block surfaces on the arbiter's main (cross-machine
|
|
1615
|
+
// visible). The success path reuses `complete`'s machinery unchanged.
|
|
1616
|
+
const completed = await performComplete({
|
|
1617
|
+
slug,
|
|
1618
|
+
cwd,
|
|
1619
|
+
arbiter: arbiterRemote,
|
|
1620
|
+
integration: options.integration,
|
|
1621
|
+
// An explicit `--merge` overrides the untrusted-origin build-propose rule (task
|
|
1622
|
+
// `untrusted-origin-forces-build-propose`); unset on the autonomous path so
|
|
1623
|
+
// untrusted-origin reliably forces propose.
|
|
1624
|
+
explicitMerge: options.explicitMerge,
|
|
1625
|
+
// SCOPE: the divergence guard is in-place only. A job worktree is cut fresh off
|
|
1626
|
+
// the bare mirror and never ff's the operator's local main, so the guard does
|
|
1627
|
+
// not apply here — opt out explicitly (the task: do NOT touch do --remote/run).
|
|
1628
|
+
ignoreDivergedMain: true,
|
|
1629
|
+
// `--allow-backlog`: a staged build sources from `tasks/backlog/` (gated on
|
|
1630
|
+
// the flag; otherwise a staged strand still refuses). Default off.
|
|
1631
|
+
allowBacklog: options.allowBacklog === true,
|
|
1632
|
+
prepare: options.prepare,
|
|
1633
|
+
verify: options.verify,
|
|
1634
|
+
freshWorktreeGate: options.freshWorktreeGate,
|
|
1635
|
+
// The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) — see
|
|
1636
|
+
// the in-place site above for the rationale; mirrored here so the no-checkout
|
|
1637
|
+
// `do --remote`/`--isolated` path threads the same resolved cap.
|
|
1638
|
+
mergeRetries: options.mergeRetries,
|
|
1639
|
+
noPR: options.noPR,
|
|
1640
|
+
providerInstance: options.providerInstance,
|
|
1641
|
+
body: agent.output,
|
|
1642
|
+
review: options.review,
|
|
1643
|
+
reviewModel: options.reviewModel,
|
|
1644
|
+
reviewMaxRounds: options.reviewMaxRounds,
|
|
1645
|
+
reviewGate: options.reviewGate,
|
|
1646
|
+
watch: options.watch,
|
|
1647
|
+
watchSink: options.watchSink,
|
|
1648
|
+
sessionsDir: options.sessionsDir,
|
|
1649
|
+
surfaceArbiter: arbiterRemote,
|
|
1650
|
+
color: options.color,
|
|
1651
|
+
note,
|
|
1652
|
+
noteBlock: options.noteBlock,
|
|
1653
|
+
env,
|
|
1654
|
+
// The review AGENT (Gate 2) launches AMBIENT — never the identity env (an
|
|
1655
|
+
// agent must not act as the bot; only the runner's git ops carry identity).
|
|
1656
|
+
agentEnv: options.env,
|
|
1657
|
+
});
|
|
1658
|
+
if (completed.outcome === 'completed' ||
|
|
1659
|
+
completed.outcome === 'already-integrated') {
|
|
1660
|
+
// Stranded-done auto-recover's clean no-op folds into `completed` here too
|
|
1661
|
+
// (see the in-place performDo handler for the rationale): same SHARED
|
|
1662
|
+
// `complete.ts` seam — so `do --remote` (this `performDoRemote`) inherits
|
|
1663
|
+
// the auto-recover without per-caller duplication.
|
|
1664
|
+
return {
|
|
1665
|
+
exitCode: 0,
|
|
1666
|
+
outcome: 'completed',
|
|
1667
|
+
slug,
|
|
1668
|
+
branch,
|
|
1669
|
+
message: completed.message,
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
if (completed.outcome === 'review-unparseable') {
|
|
1673
|
+
// Gate 2 RAN but its verdict was UNPARSEABLE (malformed JSON). The core already
|
|
1674
|
+
// routed it work-preservingly (branch pushed + surfaced). FAILURE-CAUSE axis, NOT
|
|
1675
|
+
// a reviewer block: classify the parse-failure phrase → `transient-infra` (re-run
|
|
1676
|
+
// the SAME work), the SAME convergence in-place `do`/`run` apply, so `do --remote`
|
|
1677
|
+
// agrees on the label. The job worktree is RETAINED by the §4 reap (a
|
|
1678
|
+
// not-provably-safe tree); hand the operator the recover-one-liner like the other
|
|
1679
|
+
// retained-tree failures.
|
|
1680
|
+
note(recoverIsolatedOneLiner(slug));
|
|
1681
|
+
const cause = classifyFailureCause(completed.message);
|
|
1682
|
+
return {
|
|
1683
|
+
exitCode: 1,
|
|
1684
|
+
outcome: failureCauseToDoOutcome(cause),
|
|
1685
|
+
slug,
|
|
1686
|
+
branch,
|
|
1687
|
+
message: completed.message,
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
if (completed.outcome === 'prepare-failed' ||
|
|
1691
|
+
completed.outcome === 'gate-failed' ||
|
|
1692
|
+
completed.outcome === 'review-blocked' ||
|
|
1693
|
+
completed.outcome === 'rebase-conflict' ||
|
|
1694
|
+
completed.outcome === 'strand-surfaced') {
|
|
1695
|
+
// The job worktree is RETAINED (the §4 reap keeps a not-provably-safe tree).
|
|
1696
|
+
// When the work was committed + done-moved but the integrate failed terminally
|
|
1697
|
+
// (the stale-lease-strand class Part B #97 surfaces), the operator FINISHES the
|
|
1698
|
+
// stranded branch with the recover-already-committed path \u2014 hand them the EXACT
|
|
1699
|
+
// one-liner so they need not reverse-engineer the encoded worktree path.
|
|
1700
|
+
//
|
|
1701
|
+
// `strand-surfaced` is the autonomous-strand parity (the SHARED `complete.ts`
|
|
1702
|
+
// seam already surfaced the source-strand / empty-staged refusal to
|
|
1703
|
+
// needs-attention on the arbiter) — `do --remote` inherits the fix here
|
|
1704
|
+
// without per-caller duplication; mapped to the SAME `needs-attention`
|
|
1705
|
+
// outcome shape the in-place `performDo` uses.
|
|
1706
|
+
note(recoverIsolatedOneLiner(slug));
|
|
1707
|
+
return {
|
|
1708
|
+
exitCode: 1,
|
|
1709
|
+
outcome: 'needs-attention',
|
|
1710
|
+
slug,
|
|
1711
|
+
branch,
|
|
1712
|
+
message: completed.message,
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
if (completed.outcome === 'surface-unmoved') {
|
|
1716
|
+
// Strand-surface could not land on the arbiter (CAS contention exhausted) —
|
|
1717
|
+
// HONESTLY still in-progress on the arbiter. Mirror in-place `performDo`'s
|
|
1718
|
+
// `surface-unmoved` mapping so `do --remote` agrees on the same signal.
|
|
1719
|
+
return {
|
|
1720
|
+
exitCode: 1,
|
|
1721
|
+
outcome: 'surface-unmoved',
|
|
1722
|
+
slug,
|
|
1723
|
+
branch,
|
|
1724
|
+
message: completed.message,
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
// Reclassify a thrown CORE wiring/config error (swallowed into `usage-error` by
|
|
1728
|
+
// `performComplete`) onto `config-error` — the SAME convergence in-place `do`
|
|
1729
|
+
// applies, so `do --remote` agrees with `do`/`run` on the same error too.
|
|
1730
|
+
if (completed.outcome === 'usage-error') {
|
|
1731
|
+
const cause = classifyFailureCause(completed.message);
|
|
1732
|
+
if (cause === 'config-error') {
|
|
1733
|
+
return {
|
|
1734
|
+
exitCode: 1,
|
|
1735
|
+
outcome: 'config-error',
|
|
1736
|
+
slug,
|
|
1737
|
+
branch,
|
|
1738
|
+
message: completed.message,
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
const outcome = completed.outcome === 'refused' ? 'refused' : 'usage-error';
|
|
1743
|
+
return { exitCode: 1, outcome, slug, branch, message: completed.message };
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* SAVE the partial work of a FAILED agent in a remote `do` worktree — the same
|
|
1747
|
+
* work-preserving routing in-place `do`'s {@link saveAgentFailure} uses, but
|
|
1748
|
+
* against the worktree's `origin` arbiter remote. The agent's edits + the failure
|
|
1749
|
+
* reason are committed + surfaced on the arbiter's main AND the `work/<slug>`
|
|
1750
|
+
* branch is pushed (the RECOVERABLE durable artifact — the disposable worktree is
|
|
1751
|
+
* NOT the recovery surface). The outcome is the classified failure CAUSE
|
|
1752
|
+
* (`transient-infra` / `config-error` / the generic `agent-failed`), same as the
|
|
1753
|
+
* in-place form.
|
|
1754
|
+
*/
|
|
1755
|
+
async function saveRemoteAgentFailure(params) {
|
|
1756
|
+
const { slug, cwd, arbiterRemote, detail, env, note } = params;
|
|
1757
|
+
const branch = params.branch ?? workBranchRef('task', slug);
|
|
1758
|
+
// Same best-effort cause classification as in-place `do`'s `saveAgentFailure`
|
|
1759
|
+
// (shared `classifyFailureCause`), so the remote form labels the SAME error the
|
|
1760
|
+
// SAME way too.
|
|
1761
|
+
const cause = classifyFailureCause(detail);
|
|
1762
|
+
const reason = `${failureCauseLabel(cause)}: ${detail}`;
|
|
1763
|
+
const routed = await ledgerWrite.applyNeedsAttentionTransition({
|
|
1764
|
+
cwd,
|
|
1765
|
+
slug,
|
|
1766
|
+
reason,
|
|
1767
|
+
arbiter: arbiterRemote,
|
|
1768
|
+
env,
|
|
1769
|
+
note,
|
|
1770
|
+
});
|
|
1771
|
+
const report = routed.moved ? routeReport(routed, branch) : undefined;
|
|
1772
|
+
const message = routed.moved
|
|
1773
|
+
? `Agent run failed building '${slug}' [${cause}] (${detail}); SAVED the ` +
|
|
1774
|
+
`partial work and marked '${slug}' stuck on its lock (${report.fragment}).`
|
|
1775
|
+
: `Agent run failed building '${slug}' [${cause}] (${detail}); could not ` +
|
|
1776
|
+
`mark '${slug}' stuck (${routed.reasonNotMoved ?? 'unknown'}).`;
|
|
1777
|
+
note(message);
|
|
1778
|
+
return {
|
|
1779
|
+
exitCode: 1,
|
|
1780
|
+
outcome: failureCauseToDoOutcome(cause),
|
|
1781
|
+
slug,
|
|
1782
|
+
branch,
|
|
1783
|
+
routedToNeedsAttention: routed.moved,
|
|
1784
|
+
message,
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Prime ONE arbiter head into its remote-tracking ref inside a job worktree
|
|
1789
|
+
* (`+refs/heads/<head>:refs/remotes/<arbiter>/<head>`). A job worktree is cut
|
|
1790
|
+
* from a BARE hub mirror whose `origin` remote has no fetch refspec, so
|
|
1791
|
+
* `<arbiter>/main` / `<arbiter>/work/<slug>` would not otherwise resolve — which
|
|
1792
|
+
* makes the (otherwise unchanged) `performStart` read the slug as absent. This is
|
|
1793
|
+
* the EXACT technique `integrator.rebaseOntoArbiterMain` + `gc.fetchTracking`
|
|
1794
|
+
* already use for the same bare-mirror-worktree reason. Best-effort: an
|
|
1795
|
+
* unreachable arbiter / a missing head leaves the local ref absent, which `start`
|
|
1796
|
+
* then handles (the FRESH branch already exists locally from `createJob`).
|
|
1797
|
+
*/
|
|
1798
|
+
async function primeWorktreeTrackingRef(cwd, arbiter, head, env) {
|
|
1799
|
+
// SOFT (no throw): the head may not exist on the arbiter (e.g. a FRESH cut has
|
|
1800
|
+
// no pushed `work/<slug>` yet) — that is the common, expected case, and `start`
|
|
1801
|
+
// handles a missing remote-tracking ref (the local branch `createJob` created
|
|
1802
|
+
// is plain-switched). A genuine fetch error likewise leaves the ref absent →
|
|
1803
|
+
// the safe direction.
|
|
1804
|
+
await runAsync('git', [
|
|
1805
|
+
'fetch',
|
|
1806
|
+
'--quiet',
|
|
1807
|
+
arbiter,
|
|
1808
|
+
`+refs/heads/${head}:refs/remotes/${arbiter}/${head}`,
|
|
1809
|
+
], cwd, { env });
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Adapt the IN-PLACE {@link DoOptions} build/task driver onto the WORKTREE-
|
|
1813
|
+
* ISOLATED {@link performDoRemote} pipeline (task
|
|
1814
|
+
* `advance-loop-driver-registry-set-job-worktrees`) — the per-mirror job-worktree
|
|
1815
|
+
* `doDriver` the registry-set advance driver threads into the advance tick.
|
|
1816
|
+
*
|
|
1817
|
+
* The advance tick's build/task rung ORCHESTRATES `do` by calling the
|
|
1818
|
+
* {@link AdvanceContext.doDriver} seam with a resolved {@link DoOptions} (whose
|
|
1819
|
+
* `cwd` is the per-mirror in-place checkout). The DEFAULT driver is
|
|
1820
|
+
* {@link performDo} (in-place, the cwd checkout IS the isolation — the human-local
|
|
1821
|
+
* one-shot `advance` + today's `run --advance`). The DAEMON/CI registry-set path
|
|
1822
|
+
* wants the SAME per-job-worktree isolation `run`'s build tick gives `runOneItem`,
|
|
1823
|
+
* so it injects THIS driver instead: it re-routes the orchestration onto
|
|
1824
|
+
* {@link performDoRemote}, which materialises a hub mirror + job worktree off the
|
|
1825
|
+
* mirror's arbiter via the EXISTING `jobWorktreeStrategy` (no second isolation
|
|
1826
|
+
* mechanism), runs the SAME `do` pipeline there, and reaps per the §4 predicate.
|
|
1827
|
+
* The cwd checkout is NEVER touched (the equivalence the task asserts vs plain
|
|
1828
|
+
* `run`).
|
|
1829
|
+
*
|
|
1830
|
+
* The pipeline knobs (verify / integration / review family / agent launch /
|
|
1831
|
+
* identity) ride VERBATIM from the threaded `DoOptions` onto the structurally-
|
|
1832
|
+
* matching {@link DoRemoteOptions} fields; `cwd` is DROPPED (the worktree replaces
|
|
1833
|
+
* it) and `remote` + `workspacesDir` are supplied from this driver's closure (the
|
|
1834
|
+
* mirror's arbiter URL + the agents' execution area). A `prd:` arg flows through
|
|
1835
|
+
* unchanged — `performDoRemote` tasks it against the claim clone with NO build
|
|
1836
|
+
* worktree (the tasking/surface/triage/apply rungs are tree-less ledger moves, the
|
|
1837
|
+
* substrate the task's criterion 4 preserves).
|
|
1838
|
+
*/
|
|
1839
|
+
export function jobWorktreeDoDriver(closure) {
|
|
1840
|
+
return (options) => {
|
|
1841
|
+
// Map the in-place DoOptions onto the worktree DoRemoteOptions. `cwd` is
|
|
1842
|
+
// intentionally DROPPED (the worktree is the isolation, not the checkout);
|
|
1843
|
+
// every other pipeline knob rides verbatim onto its structural twin.
|
|
1844
|
+
const remoteOptions = {
|
|
1845
|
+
arg: options.arg,
|
|
1846
|
+
remote: closure.remote,
|
|
1847
|
+
workspacesDir: closure.workspacesDir,
|
|
1848
|
+
arbiter: options.arbiter,
|
|
1849
|
+
identity: options.identity,
|
|
1850
|
+
autoTask: options.autoTask,
|
|
1851
|
+
reviewLoop: options.reviewLoop,
|
|
1852
|
+
taskerLoopMax: options.taskerLoopMax,
|
|
1853
|
+
taskerLoopModel: options.taskerLoopModel,
|
|
1854
|
+
reviewExecutions: options.reviewExecutions,
|
|
1855
|
+
integration: options.integration,
|
|
1856
|
+
// The explicit `--merge` override for the untrusted-origin build-propose rule.
|
|
1857
|
+
explicitMerge: options.explicitMerge,
|
|
1858
|
+
prepare: options.prepare,
|
|
1859
|
+
verify: options.verify,
|
|
1860
|
+
noPR: options.noPR,
|
|
1861
|
+
review: options.review,
|
|
1862
|
+
reviewModel: options.reviewModel,
|
|
1863
|
+
reviewMaxRounds: options.reviewMaxRounds,
|
|
1864
|
+
reviewGate: options.reviewGate,
|
|
1865
|
+
taskReviewGate: options.taskReviewGate,
|
|
1866
|
+
dorfl: options.dorfl,
|
|
1867
|
+
harness: options.harness,
|
|
1868
|
+
agentCmd: options.agentCmd,
|
|
1869
|
+
model: options.model,
|
|
1870
|
+
sessionsDir: options.sessionsDir,
|
|
1871
|
+
watch: options.watch,
|
|
1872
|
+
watchSink: options.watchSink,
|
|
1873
|
+
color: options.color,
|
|
1874
|
+
read: options.read,
|
|
1875
|
+
env: options.env,
|
|
1876
|
+
note: options.note,
|
|
1877
|
+
noteBlock: options.noteBlock,
|
|
1878
|
+
};
|
|
1879
|
+
return performDoRemote(remoteOptions);
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
//# sourceMappingURL=do.js.map
|