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/cli.js
ADDED
|
@@ -0,0 +1,3094 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command, Option } from 'commander';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { realpathSync, mkdirSync, rmSync } from 'node:fs';
|
|
6
|
+
import { join as joinPath } from 'node:path';
|
|
7
|
+
import { git } from './git.js';
|
|
8
|
+
import { loadConfig, mergeConfig, defaultConfigPath, } from './config.js';
|
|
9
|
+
import { defaultConfigOverridePath, loadConfigOverride, } from './config-override.js';
|
|
10
|
+
import { envOverrides } from './env-config.js';
|
|
11
|
+
import { scan } from './scan.js';
|
|
12
|
+
import { remoteAdd, remoteRm, listMirrors, RegistryError } from './registry.js';
|
|
13
|
+
import { findParticipatingRepos } from './detect.js';
|
|
14
|
+
import { formatReport } from './format.js';
|
|
15
|
+
import { resolveCwdSection, cwdSectionDisposition } from './cwd-section.js';
|
|
16
|
+
import { runLoop, } from './run.js';
|
|
17
|
+
import { performClaim } from './claim-cas.js';
|
|
18
|
+
import { createClaimSpinner } from './cli-spinner.js';
|
|
19
|
+
import { performStart } from './start.js';
|
|
20
|
+
import { performWorkOn, loadHumanWorktreesDir, persistHumanWorktreesDir, } from './work-on.js';
|
|
21
|
+
import { performComplete, integrationFromFlags } from './complete.js';
|
|
22
|
+
import { performRecoverIsolated, locateIsolatedRecovery, } from './recover-isolated.js';
|
|
23
|
+
import { performDo, performDoRemote, resolveArbiterUrlFromCheckout, } from './do.js';
|
|
24
|
+
import { performDoRemoteAuto, performDoRemoteArgs } from './do-remote-auto.js';
|
|
25
|
+
import { performAdvance } from './advance.js';
|
|
26
|
+
import { performAdvanceAuto, performAdvanceArgs, runAdvanceTickWithTreelessPublish, } from './advance-drivers.js';
|
|
27
|
+
import { performAdvanceIsolated, performAdvanceIsolatedAuto, performAdvanceIsolatedArgs, } from './advance-isolated.js';
|
|
28
|
+
import { advanceRegistrySetRunTick } from './advance-loop-driver.js';
|
|
29
|
+
import { performIntake, resolveIntakeIntegrationModes } from './intake.js';
|
|
30
|
+
import { workFolderPrefix } from './work-layout.js';
|
|
31
|
+
import { performDoAuto, performDoArgs, } from './do-autopick.js';
|
|
32
|
+
import { createHarness } from './pi-harness.js';
|
|
33
|
+
import { generateSessionPath } from './session-path.js';
|
|
34
|
+
import { shouldUseColor } from './output.js';
|
|
35
|
+
import { resolveRepoConfig, resolveRepoConfigFromLoaded, loadRepoConfigFromContent, REPO_CONFIG_FILENAME, } from './repo-config.js';
|
|
36
|
+
import { ensureMirrorMain, readRepoConfigFromMirrorMain, encodeRepoKey, } from './repo-mirror.js';
|
|
37
|
+
import { identityEnv } from './identity.js';
|
|
38
|
+
import { harnessFlagOverrides, doFlagOverrides, doNeedsAgentCmd, NO_AGENT_CMD_MESSAGE, reviewFlagOverrides, freshWorktreeGateFlagOverrides, mergeRetriesFlagOverrides, noPRFlagOverrides, } from './do-config.js';
|
|
39
|
+
import { harnessReviewGate, harnessTaskAcceptanceGate } from './review-gate.js';
|
|
40
|
+
import { harnessSurfaceGate } from './surface-gate.js';
|
|
41
|
+
import { harnessTriageGate } from './triage-gate.js';
|
|
42
|
+
import { harnessApplyDecider } from './apply-decide.js';
|
|
43
|
+
import { harnessTaskReviewGate } from './tasker-review-loop.js';
|
|
44
|
+
import { runVerify } from './verify.js';
|
|
45
|
+
import { renderPrompt } from './prompt.js';
|
|
46
|
+
import { resolvePromptGuidance } from './config.js';
|
|
47
|
+
import { gc, RETAIN_REASON_TEXT } from './gc.js';
|
|
48
|
+
import { runPrdToSpec, } from './prd-to-spec.js';
|
|
49
|
+
import { sweepRemoteMergedBranches } from './reap-branches.js';
|
|
50
|
+
import { sweepOrphanSidecars } from './orphan-sidecar.js';
|
|
51
|
+
import { sweepLedgerDuplicates, formatLedgerSweep } from './ledger-lint.js';
|
|
52
|
+
import { status, formatStatus } from './status.js';
|
|
53
|
+
import { ledgerWrite } from './ledger-write.js';
|
|
54
|
+
import { releaseItemLock, reportItemLocks, formatItemLockReport, itemLockReportNeedsAttention, reapStaleItemLocks, formatReapReport, reapReportNeedsAttention, } from './item-lock.js';
|
|
55
|
+
import { promoteFromPreBacklog, promoteFromPreSpec, listPromotable, } from './needs-attention.js';
|
|
56
|
+
import { parseSlugArg } from './slug-namespace.js';
|
|
57
|
+
import { dropSource } from './drop-source.js';
|
|
58
|
+
import { arbiterStatus, DEFAULT_ARBITER_REMOTE } from './arbiter.js';
|
|
59
|
+
import { resolveTaskOnlyArg, workBranchRef, SlugResolutionError, } from './slug-namespace.js';
|
|
60
|
+
import { brand } from './brand.js';
|
|
61
|
+
import { installCI } from './install-ci.js';
|
|
62
|
+
import { GitHubCIContext } from './install-ci-github.js';
|
|
63
|
+
import { loadCapabilityRegistry } from './install-ci-core.js';
|
|
64
|
+
import { performCloseMergedIssues } from './close-job.js';
|
|
65
|
+
/**
|
|
66
|
+
* Whether `--auto-build` / `--no-auto-build` was explicitly passed on the command
|
|
67
|
+
* line. Commander gives a negatable boolean option a default of `true`, so we
|
|
68
|
+
* must check the value SOURCE to distinguish "user set it" from "default"; only an
|
|
69
|
+
* explicit flag becomes a config override (so config/defaults still win
|
|
70
|
+
* otherwise).
|
|
71
|
+
*/
|
|
72
|
+
function autoBuildFromCli(command) {
|
|
73
|
+
if (!command) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
if (command.getOptionValueSource('autoBuild') === 'cli') {
|
|
77
|
+
return command.getOptionValue('autoBuild');
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Build the overrides a user supplied via CLI flags. Discovery is the registry
|
|
83
|
+
* (the hub-mirror set, ADR §1), so there are no `--root`/`--include`/`--exclude`
|
|
84
|
+
* flags any more — only the autonomy-gate `--auto-build` toggle.
|
|
85
|
+
*/
|
|
86
|
+
function flagOverrides(flags, command) {
|
|
87
|
+
const overrides = {};
|
|
88
|
+
const autoBuild = autoBuildFromCli(command);
|
|
89
|
+
if (autoBuild !== undefined) {
|
|
90
|
+
overrides.autoBuild = autoBuild;
|
|
91
|
+
}
|
|
92
|
+
return overrides;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolve the global (non-per-repo) config along the chain
|
|
96
|
+
*
|
|
97
|
+
* flag > ENV (DORFL_*) > global file > built-in default
|
|
98
|
+
*
|
|
99
|
+
* by layering the file config, then the `DORFL_*` env layer, then the
|
|
100
|
+
* flag overrides on top. Env is a per-machine source (like a flag or the global
|
|
101
|
+
* file) and may set ANY key, host-only included. Used by the commands that build
|
|
102
|
+
* a single global config (`scan`, `run`); per-repo commands fold env in via
|
|
103
|
+
* `resolveRepoConfig` instead.
|
|
104
|
+
*/
|
|
105
|
+
function resolveGlobalConfig(fileConfig, flags) {
|
|
106
|
+
return mergeConfig({ ...fileConfig, ...envOverrides(), ...flags });
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Load BOTH the global config file AND the per-machine `config.override.json`
|
|
110
|
+
* sibling for the given `--config` flag (or the default paths). This is the
|
|
111
|
+
* SINGLE entry point CLI commands use so the override layer is wired uniformly
|
|
112
|
+
* everywhere a per-repo resolution happens (ADR
|
|
113
|
+
* `per-machine-config-override-layer`); a missing override file is a no-op
|
|
114
|
+
* (empty map) — byte-identical to the pre-override behaviour.
|
|
115
|
+
*/
|
|
116
|
+
function loadGlobalAndOverride(configPath) {
|
|
117
|
+
return {
|
|
118
|
+
global: loadConfig(configPath),
|
|
119
|
+
override: loadConfigOverride(defaultConfigOverridePath(configPath)),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Resolve the effective config for a `do --remote <r>` run, layering the target
|
|
124
|
+
* repo's COMMITTED `.dorfl.json` (read from `<arbiter>/main` via the hub
|
|
125
|
+
* mirror) into the SAME `flag > env > per-repo > global > default` chain in-place
|
|
126
|
+
* `do` uses. This is the no-checkout analogue of {@link resolveRepoConfig}: there
|
|
127
|
+
* is no working tree, so the bytes come from the arbiter's `main` instead of the
|
|
128
|
+
* cwd — but the parse + allow/reject FILTER (`loadRepoConfigFromContent`) and the
|
|
129
|
+
* layering (`resolveRepoConfigFromLoaded`) are the EXISTING per-repo machinery,
|
|
130
|
+
* reused verbatim. Host-only keys in the committed file are rejected + reported
|
|
131
|
+
* exactly as the in-place read rejects them.
|
|
132
|
+
*
|
|
133
|
+
* Resilient by design: a config-less repo (no file on main) OR an unreachable
|
|
134
|
+
* mirror falls back to global+default (the pre-task behaviour), with a warning
|
|
135
|
+
* on a genuine fetch/read fault — a `--remote` build must not be blocked because
|
|
136
|
+
* the arbiter was momentarily offline.
|
|
137
|
+
*
|
|
138
|
+
* The config read uses {@link ensureMirrorMain} (main-only, NO-prune), NOT the
|
|
139
|
+
* all-heads pruning {@link ensureMirror}: `git show main:.dorfl.json` only
|
|
140
|
+
* needs `main`, and the all-heads `+refs/heads/*:refs/heads/*` fetch would let a
|
|
141
|
+
* `work/<slug>` branch CHECKED OUT in some stale job worktree block it (git
|
|
142
|
+
* refuses to fetch into a checked-out branch), throwing the read into its
|
|
143
|
+
* fallback and silently dropping the per-repo `harness`/`verify`/etc. The build's
|
|
144
|
+
* own worktree MATERIALISATION still calls the all-heads `ensureMirror` later
|
|
145
|
+
* (continue-detection needs the kept `work/<slug>` head) — that is a separate,
|
|
146
|
+
* untouched concern; only the CONFIG-READ fetch is narrowed here.
|
|
147
|
+
*/
|
|
148
|
+
function resolveRemoteRepoConfig(options) {
|
|
149
|
+
const { remote, workspacesDir, global, flags, identity, note, override } = options;
|
|
150
|
+
let loaded;
|
|
151
|
+
try {
|
|
152
|
+
const env = identityEnv(identity, process.env);
|
|
153
|
+
const mirror = ensureMirrorMain({ url: remote, workspacesDir, env });
|
|
154
|
+
const content = readRepoConfigFromMirrorMain(mirror.path, env);
|
|
155
|
+
loaded =
|
|
156
|
+
content === undefined
|
|
157
|
+
? {
|
|
158
|
+
path: `${remote}#main:${REPO_CONFIG_FILENAME}`,
|
|
159
|
+
config: {},
|
|
160
|
+
rejected: [],
|
|
161
|
+
}
|
|
162
|
+
: loadRepoConfigFromContent(content, `${remote}#main:${REPO_CONFIG_FILENAME}`);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
// A fetch/read fault (offline arbiter, corrupt mirror) must NOT block the
|
|
166
|
+
// build: warn + fall back to global+default (today's no-per-repo behaviour).
|
|
167
|
+
note(`could not read the target repo's ${REPO_CONFIG_FILENAME} from ` +
|
|
168
|
+
`${remote}/main; resolving config from global + flags only. ` +
|
|
169
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
170
|
+
loaded = { path: `${remote}#main`, config: {}, rejected: [] };
|
|
171
|
+
}
|
|
172
|
+
if (loaded.message) {
|
|
173
|
+
note(loaded.message);
|
|
174
|
+
}
|
|
175
|
+
return resolveRepoConfigFromLoaded(loaded, {
|
|
176
|
+
global,
|
|
177
|
+
flags,
|
|
178
|
+
override,
|
|
179
|
+
arbiterUrl: remote,
|
|
180
|
+
}).config;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Build the {@link RunTick} that **plain `run`** (no flag) loops: the REGISTRY-SET
|
|
184
|
+
* ADVANCE tick (task `run-uses-advance-tick`). This points the deliberate
|
|
185
|
+
* {@link RunTick} swap seam at the precursor's registry-set advance driver
|
|
186
|
+
* ({@link advanceRegistrySetRunTick}) instead of the build-only `runOnce` tick, so
|
|
187
|
+
* plain `run` ≡ advance with calm-default gates: behaviour-preserving today
|
|
188
|
+
* (both lifecycle gates default off ⇒ build ready tasks / task ready prds /
|
|
189
|
+
* route failures to needs-attention, over the SAME registry-set discovery +
|
|
190
|
+
* per-mirror job-worktree isolation the build tick used), lifecycle-capable the
|
|
191
|
+
* moment a gate is flipped (triage / surface / apply).
|
|
192
|
+
*
|
|
193
|
+
* Where the deprecated single-mirror advance wiring drained ONE named mirror
|
|
194
|
+
* IN-PLACE in the cwd checkout (the library {@link advanceRunTick}, now reached
|
|
195
|
+
* only by the precursor's single-mirror tests, no longer the CLI), this discovers
|
|
196
|
+
* the WHOLE registry via
|
|
197
|
+
* `scan(config)` (the SAME discovery the build tick uses) and the registry-set
|
|
198
|
+
* driver threads a PER-MIRROR job-worktree `doDriver` so each mirror's build/task
|
|
199
|
+
* rungs run isolated off THAT mirror's arbiter (NOT `process.cwd()`). The
|
|
200
|
+
* tree-less surface/triage/apply rungs commit in a per-mirror working CLONE of the
|
|
201
|
+
* mirror's arbiter (materialised lazily under the agents' workspace), since a bare
|
|
202
|
+
* hub mirror has no work tree to `git mv`/`git commit` in.
|
|
203
|
+
*/
|
|
204
|
+
function buildRegistrySetAdvanceTick(options) {
|
|
205
|
+
const { config, workspace, arbiter, env, override } = options;
|
|
206
|
+
const gitEnv = identityEnv(config.identity, env);
|
|
207
|
+
const harness = createHarness({ harness: config.harness, piBin: config.piBin });
|
|
208
|
+
return advanceRegistrySetRunTick({
|
|
209
|
+
config,
|
|
210
|
+
override,
|
|
211
|
+
workspace,
|
|
212
|
+
// The SELECTION-layer gates for the loop/CI path, IDENTICAL to the
|
|
213
|
+
// single-mirror wiring above: `observationTriage != off` enumerates the
|
|
214
|
+
// observation (triage) pool; `surfaceBlockers` enumerates the `needsAnswers`-
|
|
215
|
+
// blocked (surface) pool. `off`/`false` drops the respective pool. Apply
|
|
216
|
+
// (consume) is always-on (never gated here). Both default to their calm state,
|
|
217
|
+
// so plain `run` out of the box is behaviour-identical to the old build tick.
|
|
218
|
+
lifecycleGates: {
|
|
219
|
+
triage: config.observationTriage !== 'off',
|
|
220
|
+
surface: config.surfaceBlockers,
|
|
221
|
+
// `surfaceStaging` widens the SURFACE candidate set into STAGING (prd
|
|
222
|
+
// `staging-surface-and-apply-promote-safety` F2). Default `true` — a
|
|
223
|
+
// tasked `needsAnswers` task in `tasks/backlog/` (or prd in
|
|
224
|
+
// `prds/proposed/`) surfaces its questions BEFORE promotion. BUILD/claim
|
|
225
|
+
// stays pool-only either way.
|
|
226
|
+
surfaceStaging: config.surfaceStaging,
|
|
227
|
+
},
|
|
228
|
+
// Build the per-mirror advance CONTEXT the registry-set driver injects its
|
|
229
|
+
// per-mirror job-worktree `doDriver` on top of: the build/task `doOptions`
|
|
230
|
+
// base + the surface/triage gate seams + a tree-less working clone of THIS
|
|
231
|
+
// mirror's arbiter (the ledger-write cwd the surface/triage/apply rungs commit
|
|
232
|
+
// in — a bare mirror cannot `git mv`/`git commit`).
|
|
233
|
+
contextFor: ({ mirrorPath, originUrl }) => {
|
|
234
|
+
// A per-mirror working clone of the mirror's arbiter for the tree-less
|
|
235
|
+
// lifecycle rungs (surface/triage/apply). Keyed by the mirror's repo key so
|
|
236
|
+
// distinct mirrors get distinct clones; re-created fresh each tick so the
|
|
237
|
+
// rungs always commit onto the latest mirror `main` (idempotent, cheap
|
|
238
|
+
// local clone). The build/task rungs DO NOT use this cwd (the worktree
|
|
239
|
+
// `doDriver` replaces it); it serves ONLY the tree-less moves.
|
|
240
|
+
const treelessCwd = joinPath(workspace, 'advance-cwd', encodeRepoKey(originUrl).split('/').join('__'));
|
|
241
|
+
rmSync(treelessCwd, { recursive: true, force: true });
|
|
242
|
+
mkdirSync(joinPath(treelessCwd, '..'), { recursive: true });
|
|
243
|
+
git(['clone', '--quiet', mirrorPath, treelessCwd], workspace, {
|
|
244
|
+
env: gitEnv,
|
|
245
|
+
});
|
|
246
|
+
const doOptions = {
|
|
247
|
+
cwd: treelessCwd,
|
|
248
|
+
arbiter: arbiter ?? config.defaultArbiter,
|
|
249
|
+
identity: config.identity,
|
|
250
|
+
autoTask: config.autoTask,
|
|
251
|
+
integration: config.integration,
|
|
252
|
+
// The per-TRANSITION TASKING override: the `do prd:` tasking path threads
|
|
253
|
+
// `taskingIntegration ?? integration`; the build path stays on `integration`.
|
|
254
|
+
taskingIntegration: config.taskingIntegration,
|
|
255
|
+
// The TASK-PLACEMENT configured default (`do prd:` tasking output:
|
|
256
|
+
// `pre-backlog` staged vs `ready` pool). No operator flag on this
|
|
257
|
+
// registry-driven advance context, so only the configured default rung is
|
|
258
|
+
// threaded (the resolver still layers untrusted-origin force + built-in floor).
|
|
259
|
+
tasksLandIn: config.tasksLandIn,
|
|
260
|
+
prepare: config.prepare,
|
|
261
|
+
verify: config.verify,
|
|
262
|
+
// Single-job build path: gate the REBASED tip (the default) unconditionally.
|
|
263
|
+
freshWorktreeGate: config.freshWorktreeGate,
|
|
264
|
+
// Cross-job merge-serialiser CAS-retry cap (prd `land-time-reverify-and-
|
|
265
|
+
// parallel-merge-ceiling` Story 5 / Applied Answer q1 (a)) — resolved per-repo
|
|
266
|
+
// and threaded so the registry-driven advance path's `do` inherits it.
|
|
267
|
+
mergeRetries: config.mergeRetries,
|
|
268
|
+
noPR: config.noPR,
|
|
269
|
+
harness,
|
|
270
|
+
agentCmd: config.agentCmd,
|
|
271
|
+
model: config.model,
|
|
272
|
+
sessionsDir: config.sessionsDir,
|
|
273
|
+
review: config.review,
|
|
274
|
+
reviewModel: config.reviewModel,
|
|
275
|
+
reviewMaxRounds: config.reviewMaxRounds,
|
|
276
|
+
reviewGate: config.review
|
|
277
|
+
? harnessReviewGate({ harness, agentCmd: config.agentCmd })
|
|
278
|
+
: undefined,
|
|
279
|
+
reviewLoop: config.taskerLoop
|
|
280
|
+
? harnessTaskReviewGate({ harness, agentCmd: config.agentCmd })
|
|
281
|
+
: undefined,
|
|
282
|
+
taskerLoopMax: config.taskerLoopMax,
|
|
283
|
+
taskerLoopModel: config.taskerLoopModel,
|
|
284
|
+
taskReviewGate: config.review
|
|
285
|
+
? harnessTaskAcceptanceGate({ harness, agentCmd: config.agentCmd })
|
|
286
|
+
: undefined,
|
|
287
|
+
color: shouldUseColor(process.stdout),
|
|
288
|
+
note: (message) => console.error(`>> ${message}`),
|
|
289
|
+
noteBlock: (message) => console.error(message),
|
|
290
|
+
};
|
|
291
|
+
const context = {
|
|
292
|
+
cwd: treelessCwd,
|
|
293
|
+
arbiter: arbiter ?? config.defaultArbiter,
|
|
294
|
+
doOptions,
|
|
295
|
+
surfaceGate: harnessSurfaceGate({ harness, agentCmd: config.agentCmd }),
|
|
296
|
+
surfaceModel: config.model,
|
|
297
|
+
applyDecide: harnessApplyDecider({ harness, agentCmd: config.agentCmd }),
|
|
298
|
+
applyModel: config.model,
|
|
299
|
+
observationTriage: config.observationTriage,
|
|
300
|
+
triageGate: harnessTriageGate({ harness, agentCmd: config.agentCmd }),
|
|
301
|
+
triageModel: config.model,
|
|
302
|
+
// The ANSWERED-MERGE LAND DISPATCH context (task
|
|
303
|
+
// `apply-rung-merge-disposition`, prd `land-time-reverify-and-parallel-
|
|
304
|
+
// merge-ceiling`): the dispatcher cuts a per-job worktree via
|
|
305
|
+
// `workspace.ts` `createJob` off the hub mirror (so we thread the resolved
|
|
306
|
+
// `workspacesDir` + the real arbiter URL — the per-mirror tree-less
|
|
307
|
+
// `treelessCwd` has `origin` pointing at the LOCAL mirror path, NOT the
|
|
308
|
+
// arbiter URL, so we MUST pass `originUrl` directly here), then drives
|
|
309
|
+
// `performIntegration` with `committedRecovery: true` +
|
|
310
|
+
// `freshWorktreeGate: true` (the rebased tip is re-verified, the RED
|
|
311
|
+
// route refuses, the GREEN route lands). `prepare`/`verify` are the SAME
|
|
312
|
+
// per-repo gate the build path uses; `strictMergeApproval` is the OQ6
|
|
313
|
+
// opt-in resolved by the sibling task `strict-merge-approval-gate`
|
|
314
|
+
// (default OFF ⇒ honour + land on a green re-verify).
|
|
315
|
+
workspacesDir: workspace,
|
|
316
|
+
arbiterUrl: originUrl,
|
|
317
|
+
prepare: config.prepare,
|
|
318
|
+
verify: config.verify,
|
|
319
|
+
strictMergeApproval: config.strictMergeApproval,
|
|
320
|
+
note: (message) => console.error(`>> ${message}`),
|
|
321
|
+
};
|
|
322
|
+
return context;
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Resolve the arbiter URL for `do --isolated <slug>` from the CURRENT repo (cwd).
|
|
328
|
+
*
|
|
329
|
+
* `--isolated` builds in a job worktree off MY OWN arbiter (the same isolation +
|
|
330
|
+
* integrate pipeline `do --remote <url>` uses), so it needs the URL of the cwd's
|
|
331
|
+
* arbiter remote. It uses the SAME arbiter-remote resolution in-place `do` does:
|
|
332
|
+
* `--arbiter` > the resolved cwd `defaultArbiter` (the per-repo/global config), as
|
|
333
|
+
* the remote NAME, then `git remote get-url <name>` in the checkout to get its URL.
|
|
334
|
+
* That URL is then fed into the EXISTING `performDoRemote` pipeline as `remote`.
|
|
335
|
+
*
|
|
336
|
+
* Returns the URL, or `undefined` when there is no resolvable arbiter (cwd is not
|
|
337
|
+
* a git repo, or the named arbiter remote does not exist) — the "isolated against
|
|
338
|
+
* what?" case the caller turns into a clear error naming `--remote <url>`.
|
|
339
|
+
*/
|
|
340
|
+
function resolveDefaultArbiterForCwd(cwd, global, flags, override) {
|
|
341
|
+
// The SAME per-repo config read in-place `do` uses (`resolveRepoConfig` on the
|
|
342
|
+
// cwd), so `--isolated` resolves the arbiter remote NAME (`defaultArbiter`)
|
|
343
|
+
// through the identical `flag > env > per-repo > global > default` chain. An
|
|
344
|
+
// absent `.dorfl.json` falls back to the global/default (`origin`).
|
|
345
|
+
return resolveRepoConfig({ repoPath: cwd, global, flags, override }).config
|
|
346
|
+
.defaultArbiter;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* First-use prompt for the human worktree root (`work-on`). Offers `suggestion`
|
|
350
|
+
* as the default (Enter accepts it); a blank non-interactive answer aborts. The
|
|
351
|
+
* prompt goes to stderr so `--print-dir`'s stdout stays clean.
|
|
352
|
+
*/
|
|
353
|
+
function promptForWorktreesRoot(suggestion) {
|
|
354
|
+
return new Promise((resolvePrompt) => {
|
|
355
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
356
|
+
rl.question('work-on needs a human worktree root (NOT under ~/.dorfl). ' +
|
|
357
|
+
`Where should parallel worktrees live? [${suggestion}] `, (answer) => {
|
|
358
|
+
rl.close();
|
|
359
|
+
const trimmed = answer.trim();
|
|
360
|
+
resolvePrompt(trimmed === '' ? suggestion : trimmed);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
function runFlagOverrides(flags, command) {
|
|
365
|
+
const overrides = flagOverrides(flags, command);
|
|
366
|
+
if (flags.maxParallel !== undefined) {
|
|
367
|
+
overrides.maxParallel = Number(flags.maxParallel);
|
|
368
|
+
}
|
|
369
|
+
if (flags.perRepoMax !== undefined) {
|
|
370
|
+
overrides.perRepoMax = Number(flags.perRepoMax);
|
|
371
|
+
}
|
|
372
|
+
if (flags.arbiter !== undefined) {
|
|
373
|
+
overrides.defaultArbiter = flags.arbiter;
|
|
374
|
+
}
|
|
375
|
+
if (flags.integration === 'propose' || flags.integration === 'merge') {
|
|
376
|
+
overrides.integration = flags.integration;
|
|
377
|
+
}
|
|
378
|
+
// `--no-pr` (the PR-INTENT axis): suppress the PR even on an authed GitHub
|
|
379
|
+
// arbiter. Commander stores the negatable flag as `pr` (false when `--no-pr` is
|
|
380
|
+
// passed). Rides the SAME flag-override chain as `integration`.
|
|
381
|
+
if (flags.pr === false) {
|
|
382
|
+
overrides.noPR = true;
|
|
383
|
+
}
|
|
384
|
+
// The harness/adapter flags (--agent-cmd/--model/--harness/--pi-bin) map via
|
|
385
|
+
// the SHARED per-key mapping `do` also reuses (do-config.harnessFlagOverrides),
|
|
386
|
+
// so there is exactly ONE override path for them.
|
|
387
|
+
Object.assign(overrides, harnessFlagOverrides(flags));
|
|
388
|
+
// Gate 2 (PR/code review) flags ride the SAME flag-override path so
|
|
389
|
+
// `--review`/`--review-model`/`--review-max-rounds` resolve
|
|
390
|
+
// flag > env > per-repo > global > default — mirroring the `do` command (the
|
|
391
|
+
// fleet inherits the review gate via the converged `performIntegration` core).
|
|
392
|
+
Object.assign(overrides, reviewFlagOverrides(flags));
|
|
393
|
+
// `--fresh-worktree-gate`/`--no-fresh-worktree-gate` rides the SAME chain: gate
|
|
394
|
+
// the REBASED tip in a clean throwaway worktree (ON by default). The `run` fleet
|
|
395
|
+
// caller additionally downgrades it to today's gate at `perRepoMax > 1` (the
|
|
396
|
+
// fleet conditional lives in `runOnce`, not in this flag mapping).
|
|
397
|
+
Object.assign(overrides, freshWorktreeGateFlagOverrides(flags));
|
|
398
|
+
// `--merge-retries <n>` rides the SAME chain: the cross-job merge-serialiser
|
|
399
|
+
// CAS-retry cap (prd `land-time-reverify-and-parallel-merge-ceiling` Story 5 /
|
|
400
|
+
// Applied Answer q1 (a)). The `run` fleet inherits the resolved cap through
|
|
401
|
+
// the converged `performIntegration` core (config.mergeRetries threads into
|
|
402
|
+
// the merge loop, replacing the bare `DEFAULT_MERGE_RETRIES` fallback).
|
|
403
|
+
Object.assign(overrides, mergeRetriesFlagOverrides(flags));
|
|
404
|
+
return overrides;
|
|
405
|
+
}
|
|
406
|
+
function formatItemLine(item) {
|
|
407
|
+
const extra = item.detail ? ` — ${item.detail}` : '';
|
|
408
|
+
return ` [${item.status}] ${item.repoPath} :: ${item.slug}${extra}`;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Resolve the EXPLICIT operator placement override from `--tasks-land-in <where>`
|
|
412
|
+
* (the top of the `do prd:` tasking-placement precedence — task
|
|
413
|
+
* `runner-deterministic-slice-placement-policy-and-precedence`). Mirrors the
|
|
414
|
+
* `flagMode === 'merge'` ⇒ `explicitMerge: true` shape: it contributes
|
|
415
|
+
* `explicitTasksLandIn` ONLY when the operator actually typed the flag, so an
|
|
416
|
+
* untrusted-origin's staging force still wins when the value came from config, not
|
|
417
|
+
* the flag. An invalid value FAILS LOUDLY (a usage error, never silently dropped
|
|
418
|
+
* — the SAME discipline the `--observation-triage` enum + the
|
|
419
|
+
* `DORFL_TASKS_LAND_IN` env coercion use).
|
|
420
|
+
*/
|
|
421
|
+
function explicitTasksLandInFromFlag(raw) {
|
|
422
|
+
if (raw === undefined) {
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
if (raw !== 'pre-backlog' && raw !== 'ready') {
|
|
426
|
+
throw new Error(`--tasks-land-in must be 'pre-backlog' or 'ready' (got '${raw}').`);
|
|
427
|
+
}
|
|
428
|
+
return raw;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* The SPEC twin of {@link explicitTasksLandInFromFlag} (spec
|
|
432
|
+
* `prd-to-spec-vocabulary-cutover-and-migration-command`). Resolve the EXPLICIT
|
|
433
|
+
* operator spec-placement override from `--specs-land-in <where>` for `intake`'s
|
|
434
|
+
* `spec` dispatch — the TOP of the same precedence chain that the tasking
|
|
435
|
+
* placement uses. Contributes `explicitSpecsLandIn` ONLY when the operator
|
|
436
|
+
* actually typed the flag, so an untrusted-origin's staging force still wins when
|
|
437
|
+
* the value came from config. An invalid value FAILS LOUDLY (a usage error,
|
|
438
|
+
* never silently dropped), mirroring the task helper above and the
|
|
439
|
+
* `DORFL_SPECS_LAND_IN` env coercion.
|
|
440
|
+
*
|
|
441
|
+
* HARD CUTOVER: the legacy `--prds-land-in` flag is GONE (clean break, spec US
|
|
442
|
+
* #8); only `--specs-land-in` is accepted.
|
|
443
|
+
*/
|
|
444
|
+
function explicitSpecsLandInFromFlag(raw, flagName = '--specs-land-in') {
|
|
445
|
+
if (raw === undefined) {
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
if (raw !== 'pre-proposed' && raw !== 'ready') {
|
|
449
|
+
throw new Error(`${flagName} must be 'pre-proposed' or 'ready' (got '${raw}').`);
|
|
450
|
+
}
|
|
451
|
+
return raw;
|
|
452
|
+
}
|
|
453
|
+
/** Human-readable report for the `prd-to-spec` migration result. */
|
|
454
|
+
function printPrdToSpecReport(result) {
|
|
455
|
+
if (result.refused) {
|
|
456
|
+
const v = result.refused;
|
|
457
|
+
const label = v.kind === 'dirty-tree'
|
|
458
|
+
? 'dirty working tree'
|
|
459
|
+
: v.kind === 'held-lock'
|
|
460
|
+
? 'a held per-item lock'
|
|
461
|
+
: 'an in-progress work-branch carrying unlanded work';
|
|
462
|
+
console.error(`REFUSED: the repo is not quiescent (${label}): ${v.offender}. ` +
|
|
463
|
+
'Land or discard the in-flight work, then re-run. (prd-to-spec ' +
|
|
464
|
+
'never migrates over uncommitted/in-flight state.)');
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const verb = result.dryRun ? 'WOULD' : 'DID';
|
|
468
|
+
console.log(result.dryRun
|
|
469
|
+
? '=== prd-to-spec (DRY RUN — nothing written) ==='
|
|
470
|
+
: '=== prd-to-spec ===');
|
|
471
|
+
if (result.resync) {
|
|
472
|
+
const changed = result.resync.docs.filter((d) => !d.unchanged).length;
|
|
473
|
+
console.log(`Contract re-sync: ${verb} sync ${result.resync.docs.length} protocol ` +
|
|
474
|
+
`doc(s) (${changed} changed) + bump ${result.resync.versionPath}.`);
|
|
475
|
+
}
|
|
476
|
+
console.log(`Folders: ${verb} move ${result.folderMoves.length} folder(s).`);
|
|
477
|
+
for (const m of result.folderMoves) {
|
|
478
|
+
console.log(` ${m.from} -> ${m.to}`);
|
|
479
|
+
}
|
|
480
|
+
console.log(`Item content: ${verb} rewrite ${result.contentRewrites.length} item(s).`);
|
|
481
|
+
console.log(`Config: ${verb} rename ${result.configRewrites.length} key(s)` +
|
|
482
|
+
(result.configRewrites.length > 0
|
|
483
|
+
? ` (${result.configRewrites.map((c) => `${c.from}->${c.to}`).join(', ')})`
|
|
484
|
+
: '') +
|
|
485
|
+
'.');
|
|
486
|
+
console.log(`Refs: ${verb} rename ${result.refRenames.length} inert ref(s).`);
|
|
487
|
+
for (const r of result.refRenames) {
|
|
488
|
+
console.log(` ${r.from} -> ${r.to}`);
|
|
489
|
+
}
|
|
490
|
+
if (result.dryRun) {
|
|
491
|
+
console.log('(Re-run without --dry-run to apply; the leak scan gates the output.)');
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (result.leaks.length === 0) {
|
|
495
|
+
console.log('Leak scan: GREEN (no surviving prd data ref).');
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
console.error(`Leak scan: FAILED (${result.leaks.length} leak(s)):`);
|
|
499
|
+
for (const leak of result.leaks) {
|
|
500
|
+
printLeak(leak);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function printLeak(leak) {
|
|
505
|
+
console.error(` [${leak.lens}] ${leak.where}: '${leak.token}' — ${leak.why}`);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Resolve a task-only command's slug argument through the §3a namespace guard
|
|
509
|
+
* (`resolveTaskOnlyArg`): accept bare (= task) + `task:` (explicit alias),
|
|
510
|
+
* REJECT `spec:` with a clear "operates on tasks, not specs" error (and the
|
|
511
|
+
* legacy `prd:` with "operates on tasks, not prds", still accepted through the
|
|
512
|
+
* cutover). On rejection it prints the error to stderr and exits 1 (the task-only
|
|
513
|
+
* commands never act on a spec). An OMITTED slug (`start`/`complete`/`prompt`
|
|
514
|
+
* infer it from the branch) passes through untouched.
|
|
515
|
+
*
|
|
516
|
+
* `do` is the ONE command that spans both namespaces; it consumes the full
|
|
517
|
+
* `resolveSlug` (with the cross-namespace collision check) in the `do-in-place`
|
|
518
|
+
* task. This guard is the task-only half of ADR §3a.
|
|
519
|
+
*/
|
|
520
|
+
function resolveTaskOnlySlug(slug) {
|
|
521
|
+
if (slug === undefined) {
|
|
522
|
+
return undefined;
|
|
523
|
+
}
|
|
524
|
+
try {
|
|
525
|
+
return resolveTaskOnlyArg(slug);
|
|
526
|
+
}
|
|
527
|
+
catch (err) {
|
|
528
|
+
if (err instanceof SlugResolutionError) {
|
|
529
|
+
console.error(`error: ${err.message}`);
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
throw err;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Build the INTERACTIVE launcher closure for `--agent` (task
|
|
537
|
+
* `agent-interactive-launch`), or `undefined` when `--agent` was not passed.
|
|
538
|
+
*
|
|
539
|
+
* It resolves the harness + model the SAME way the autonomous `do`/`run` path
|
|
540
|
+
* does — per-repo config layered flag > env > per-repo > global > default (ADR
|
|
541
|
+
* §13) — so the human starts pinned to the intended model (decision #4). The
|
|
542
|
+
* returned closure is what `start.ts`/`work-on.ts` call AFTER onboarding: it
|
|
543
|
+
* generates the pi `--session` path for the onboarded working tree and calls
|
|
544
|
+
* `harness.launchInteractive` (which inherits stdio, drops `--print`, feeds no
|
|
545
|
+
* prompt, foreground). A NON-pi harness throws a clear pi-only error from the
|
|
546
|
+
* adapter (decision #2). This keeps the git-logic modules decoupled from
|
|
547
|
+
* `createHarness`/config (they only receive the thin {@link InteractiveLauncher}).
|
|
548
|
+
*
|
|
549
|
+
* `repoPath` is the per-repo config root (the current checkout for `start` /
|
|
550
|
+
* in-repo `work-on`); `undefined` (remote `work-on`, no checkout) resolves from
|
|
551
|
+
* the global config only — mirroring `do --remote`.
|
|
552
|
+
*/
|
|
553
|
+
function buildInteractiveLauncher(flags, configPath, repoPath) {
|
|
554
|
+
if (flags.agent !== true) {
|
|
555
|
+
return undefined;
|
|
556
|
+
}
|
|
557
|
+
const { global, override } = loadGlobalAndOverride(configPath);
|
|
558
|
+
const overrides = harnessFlagOverrides(flags);
|
|
559
|
+
const config = repoPath !== undefined
|
|
560
|
+
? resolveRepoConfig({ repoPath, global, flags: overrides, override }).config
|
|
561
|
+
: resolveGlobalConfig(global, overrides);
|
|
562
|
+
const harness = createHarness({
|
|
563
|
+
harness: config.harness,
|
|
564
|
+
piBin: config.piBin,
|
|
565
|
+
});
|
|
566
|
+
return (site) => {
|
|
567
|
+
// Generate the pi `--session` path for the onboarded working tree so the
|
|
568
|
+
// human session is recorded + dashboard-visible (decision #2); the resolved
|
|
569
|
+
// model flows in (decision #4). The harness's `launchInteractive` runs pi
|
|
570
|
+
// WITHOUT `--print`, inherited stdio, no piped prompt, in `site.dir`.
|
|
571
|
+
const session = generateSessionPath({
|
|
572
|
+
sessionsDir: config.sessionsDir,
|
|
573
|
+
cwd: site.dir,
|
|
574
|
+
id: site.slug,
|
|
575
|
+
});
|
|
576
|
+
harness.launchInteractive({
|
|
577
|
+
slug: site.slug,
|
|
578
|
+
dir: site.dir,
|
|
579
|
+
model: config.model,
|
|
580
|
+
session,
|
|
581
|
+
env: site.env,
|
|
582
|
+
});
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* The shared `start`/`resume` action body. `start` and `resume` are the two
|
|
587
|
+
* human in-place verbs of ADR §4: `start` BEGINS work here (claim if needed +
|
|
588
|
+
* switch); `resume` CONTINUES here (re-engage an already-in-progress item by
|
|
589
|
+
* switching to its `work/<slug>` branch WITHOUT claiming). The runtime
|
|
590
|
+
* difference is exactly the `resume` flag — `resume` forces it on (its only mode
|
|
591
|
+
* is to re-engage), while `start` honours the (now hidden) `--resume` alias.
|
|
592
|
+
* Both are task-only (§3a: accept bare + `task:`, reject `prd:`).
|
|
593
|
+
*/
|
|
594
|
+
async function runStartAction(rawSlug, flags, resume) {
|
|
595
|
+
// Task-only command (§3a): accept bare + `task:`, reject `prd:`.
|
|
596
|
+
const slug = resolveTaskOnlySlug(rawSlug);
|
|
597
|
+
const cwd = process.cwd();
|
|
598
|
+
// `resume --isolated <slug>`: re-engage the slug's RETAINED job worktree (the
|
|
599
|
+
// inverse of `do --isolated`) WITHOUT claiming \u2014 locate it off THIS repo's
|
|
600
|
+
// arbiter and report its path so the operator can cd in. The symmetric
|
|
601
|
+
// companion of `complete --isolated` (finish the stranded worktree). `start`
|
|
602
|
+
// (begin-here) has no isolated form \u2014 there is nothing retained to re-engage yet.
|
|
603
|
+
if (resume && flags.isolated === true) {
|
|
604
|
+
if (slug === undefined || slug === '') {
|
|
605
|
+
console.error('error: resume --isolated requires <slug> (the retained worktree to re-engage).');
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
const { config } = loadHumanWorktreesDir(flags.config ?? defaultConfigPath());
|
|
609
|
+
const located = locateIsolatedRecovery({
|
|
610
|
+
slug,
|
|
611
|
+
cwd,
|
|
612
|
+
arbiter: flags.arbiter ?? config.defaultArbiter,
|
|
613
|
+
workspacesDir: flags.workspace ?? config.workspacesDir,
|
|
614
|
+
env: process.env,
|
|
615
|
+
});
|
|
616
|
+
if ('error' in located) {
|
|
617
|
+
console.error(`error: ${located.error}`);
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
if (!located.present) {
|
|
621
|
+
console.error(`>> No retained isolated worktree for '${slug}' (already integrated and ` +
|
|
622
|
+
'reaped, or never stranded) \u2014 nothing to resume.');
|
|
623
|
+
process.exit(0);
|
|
624
|
+
}
|
|
625
|
+
console.error(`>> Re-engaging the retained worktree for '${slug}'. cd into it to ` +
|
|
626
|
+
`continue, then 'dorfl complete --isolated ${slug}' to finish:`);
|
|
627
|
+
process.stdout.write(`${located.dir}\n`);
|
|
628
|
+
process.exit(0);
|
|
629
|
+
}
|
|
630
|
+
const result = await performStart({
|
|
631
|
+
slug,
|
|
632
|
+
cwd,
|
|
633
|
+
arbiter: flags.arbiter ?? 'origin',
|
|
634
|
+
// `resume` (the verb) always asserts ownership; `start` honours --resume.
|
|
635
|
+
resume: resume || flags.resume === true,
|
|
636
|
+
override: flags.ignoreNotReady === true,
|
|
637
|
+
// `--agent`: launch the configured harness INTERACTIVELY in the checkout
|
|
638
|
+
// after onboarding (task `agent-interactive-launch`). The per-repo config
|
|
639
|
+
// root is the current checkout.
|
|
640
|
+
launchInteractive: buildInteractiveLauncher(flags, flags.config, cwd),
|
|
641
|
+
// HUMAN commands (`start` = "begin here", `resume` = "continue here"): the
|
|
642
|
+
// onboard/branch/switch is the human's, so it is NOT given the runner
|
|
643
|
+
// `config.identity` (the autonomous onboard is `do`/`run`, identity-aware).
|
|
644
|
+
// Ambient `process.env` threaded EXPLICITLY so the choice is declared here,
|
|
645
|
+
// not left to the seam's silent `?? process.env` fallback.
|
|
646
|
+
env: process.env,
|
|
647
|
+
note: (message) => console.error(`>> ${message}`),
|
|
648
|
+
});
|
|
649
|
+
if (result.exitCode !== 0) {
|
|
650
|
+
console.error(`error: ${result.message}`);
|
|
651
|
+
}
|
|
652
|
+
process.exit(result.exitCode);
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Help GROUP labels for the two-tier surface (ADR §7). commander v14's
|
|
656
|
+
* `command.helpGroup(...)` renders each command under its label heading, so the
|
|
657
|
+
* HEADLINE tier (the surface a user reaches for) lists first and the
|
|
658
|
+
* ADVANCED/PLUMBING tier (kept, but de-emphasised) lists under its own heading
|
|
659
|
+
* — without removing or hiding anything. Headline: run/do/work-on/start/resume/
|
|
660
|
+
* complete/requeue/scan/status + remote add/ls/find. Advanced: claim/prompt/
|
|
661
|
+
* verify/gc + remote rm.
|
|
662
|
+
*/
|
|
663
|
+
const HEADLINE_GROUP = 'Commands:';
|
|
664
|
+
const ADVANCED_GROUP = 'Advanced / plumbing:';
|
|
665
|
+
/** Help group for the de-emphasised plumbing FLAGS named in ADR §7. */
|
|
666
|
+
const ADVANCED_OPT_GROUP = 'Advanced / plumbing options:';
|
|
667
|
+
export function buildProgram() {
|
|
668
|
+
const program = new Command();
|
|
669
|
+
program
|
|
670
|
+
.name(brand.bin)
|
|
671
|
+
.description('Autonomous parallel agents over file-based work/ queues.');
|
|
672
|
+
program
|
|
673
|
+
.command('scan')
|
|
674
|
+
.helpGroup(HEADLINE_GROUP)
|
|
675
|
+
.description('Read-only: list the cross-repo queue of work items (across the registered hub mirrors) and whether each is runnable now. Discovery is the registry — the hub-mirror set under workspacesDir/repos/ (no --root/roots).')
|
|
676
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
677
|
+
.option('--auto-build', 'allow agents to auto-build undeclared (not humanOnly) tasks')
|
|
678
|
+
.option('--no-auto-build', 'forbid agents from auto-building undeclared tasks (default)')
|
|
679
|
+
.option('--arbiter-remote <name>', `the current repo's arbiter remote to fetch + diff its local section against (default: ${DEFAULT_ARBITER_REMOTE})`)
|
|
680
|
+
.option('--arbiter <remote>', 'the COORDINATION arbiter remote whose per-item lock refs (refs/dorfl/lock/*) gate the cwd selection pool (held in-flight items are subtracted); default: origin (the same remote claim/do use, NOT the --arbiter-remote divergence name)')
|
|
681
|
+
.option('--here', 'report ONLY the current repo (the cwd working tree, fetch-first): skip the cross-repo registry loop entirely. The fast, focused path — no N-mirror fetches.')
|
|
682
|
+
.option('--json', 'output the raw report as JSON')
|
|
683
|
+
.action(async (flags, command) => {
|
|
684
|
+
const fileConfig = loadConfig(flags.config);
|
|
685
|
+
const override = loadConfigOverride(defaultConfigOverridePath(flags.config));
|
|
686
|
+
const config = resolveGlobalConfig(fileConfig, flagOverrides(flags, command));
|
|
687
|
+
const warn = (message) => console.error(`>> ${message}`);
|
|
688
|
+
const resolveCwd = () => resolveCwdSection({
|
|
689
|
+
cwd: process.cwd(),
|
|
690
|
+
config,
|
|
691
|
+
override,
|
|
692
|
+
arbiterRemote: flags.arbiterRemote,
|
|
693
|
+
lockArbiterRemote: flags.arbiter ?? 'origin',
|
|
694
|
+
warn,
|
|
695
|
+
});
|
|
696
|
+
// `--here`: report ONLY the cwd — skip the registry loop ENTIRELY (the fast,
|
|
697
|
+
// focused path, and the CI shape). The report carries an empty `repos[]` so
|
|
698
|
+
// the `--json` consumers (the CI matrix `jq`) read `.cwd.repo.*` exactly as
|
|
699
|
+
// before, with `.repos[]` simply yielding nothing.
|
|
700
|
+
if (flags.here === true) {
|
|
701
|
+
const cwdSection = await resolveCwd();
|
|
702
|
+
const emptyReport = { repos: [], totalItems: 0, totalEligible: 0 };
|
|
703
|
+
if (flags.json) {
|
|
704
|
+
console.log(JSON.stringify({ ...emptyReport, cwd: cwdSection }, (_key, value) => (value instanceof Set ? [...value] : value), 2));
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
console.log(formatReport(emptyReport, cwdSection));
|
|
708
|
+
}
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const report = await scan(config, { warn, override });
|
|
712
|
+
// The cwd-local section: resolve it ONLY when a participating cwd is NOT
|
|
713
|
+
// already covered by a registered mirror. A FETCH-FREE pre-check
|
|
714
|
+
// (`cwdSectionDisposition`) decides this with zero network I/O; an
|
|
715
|
+
// already-registered cwd is skipped so we never re-fetch the SAME arbiter the
|
|
716
|
+
// registry loop just fetched (the `scan-here-and-skip-redundant-cwd`
|
|
717
|
+
// decision), and an UNregistered cwd is still shown standalone so a
|
|
718
|
+
// mirror-less repo you are standing in is never invisible.
|
|
719
|
+
const disposition = cwdSectionDisposition({
|
|
720
|
+
cwd: process.cwd(),
|
|
721
|
+
config,
|
|
722
|
+
arbiterRemote: flags.arbiterRemote,
|
|
723
|
+
});
|
|
724
|
+
const cwdSection = disposition.participating && !disposition.alsoRegistered
|
|
725
|
+
? await resolveCwd()
|
|
726
|
+
: undefined;
|
|
727
|
+
if (flags.json) {
|
|
728
|
+
console.log(JSON.stringify({ ...report, cwd: cwdSection }, (_key, value) => (value instanceof Set ? [...value] : value), 2));
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
console.log(formatReport(report, cwdSection));
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
program
|
|
735
|
+
.command('run')
|
|
736
|
+
.helpGroup(HEADLINE_GROUP)
|
|
737
|
+
.description('The cross-repo, parallel daemon: loop the supervised tick over the registry — each tick claims up to maxParallel eligible items (perRepoMax per repo), runs the agents CONCURRENTLY in isolation, integrates, then loops (forever, or until a stop bound). Stuck items surface via the needs-attention seam (on main). `run --once` = one debug tick (NOT the CI path — CI is `do`).')
|
|
738
|
+
.option('--once', 'run a SINGLE supervised tick then stop — the debug/test affordance on the daemon (NOT the CI path; CI uses `do`)')
|
|
739
|
+
.option('--advance', 'DEPRECATED no-op alias: plain `run` ALREADY drives the registry-set advance tick (build/task with calm-default gates; flip observationTriage / surfaceBlockers for the lifecycle). Passing this warns and is otherwise ignored. The old `--advance <mirror>` single-mirror form is gone — the daemon discovers the whole registry via scan.')
|
|
740
|
+
.option('--max-iterations <n>', 'stop after N ticks (a bounded session; default: loop forever)')
|
|
741
|
+
.option('--max-duration <seconds>', 'stop after this many seconds of wall-clock (a bounded session; default: no bound)')
|
|
742
|
+
.option('--interval <seconds>', 'pause this many seconds between ticks (default: 0, back-to-back)')
|
|
743
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
744
|
+
.option('--auto-build', 'allow agents to auto-build undeclared (not humanOnly) tasks')
|
|
745
|
+
.option('--no-auto-build', 'forbid agents from auto-building undeclared tasks (default)')
|
|
746
|
+
.option('--max-parallel <n>', 'global cap on items claimed+run this tick')
|
|
747
|
+
.option('--per-repo-max <n>', 'per-repo cap on concurrent claims')
|
|
748
|
+
.option('--arbiter <remote>', 'name of the arbiter git remote')
|
|
749
|
+
.option('--integration <mode>', 'integration mode: propose (default) or merge')
|
|
750
|
+
.option('--no-pr', 'propose without opening a PR: push the branch but deliberately skip the review request, even on an authed GitHub arbiter (the explicit suppress-PR intent). Resolved flag > env > per-repo > global > default off.')
|
|
751
|
+
.option('--agent-cmd <cmd>', 'command to run one agent on a task prompt')
|
|
752
|
+
.option('--model <id>', 'model the agent runs on (routing intent; auth/keys stay the harness\u2019s job). pi: passed as --model; null/shell: substitutes a {model} placeholder in agentCmd. Resolved flag > env > per-repo > global > default (unset).')
|
|
753
|
+
.option('--harness <adapter>', 'harness adapter that launches the agent + reports liveness: null (default, shells out to agentCmd) or pi (the pi CLI)')
|
|
754
|
+
.option('--pi-bin <path>', 'pi CLI binary the pi harness invokes (default: pi on PATH)')
|
|
755
|
+
.option('--sessions-dir <dir>', 'HOST-ONLY root folder under which pi session files are generated (--session <dir>/<id>.jsonl). Default: pi per-cwd folder under ~/.pi/agent/sessions. Resolved flag > env > global > default (no per-repo).')
|
|
756
|
+
.option('--workspace <dir>', 'execution working area for hub mirrors + job worktrees (default: workspacesDir / ~/.dorfl)')
|
|
757
|
+
.option('--review', 'run Gate 2 (PR/code review) after verify, before the done-move, on every item (overrides config). Resolved flag > env > per-repo > global > default off.')
|
|
758
|
+
.option('--no-review', 'do NOT run Gate 2 this tick (overrides config)')
|
|
759
|
+
.option('--review-model <id>', 'model the Gate-2 review agent runs on (de-correlated from the builder; routing intent). Resolved flag > env > per-repo > global > default.')
|
|
760
|
+
.option('--review-max-rounds <n>', 'bound the revise/review loop; on exhaustion force needs-attention (default 2)')
|
|
761
|
+
.option('--fresh-worktree-gate', 'run the acceptance gate (prepare then verify) against the REBASED tip in a CLEAN throwaway worktree (the tree that integrates). ON by default; the `run` fleet uses it only when same-repo concurrency is off (perRepoMax=1), else today\u2019s in-build-worktree gate.')
|
|
762
|
+
.option('--no-fresh-worktree-gate', 'run the acceptance gate in the build worktree (the pre-rebase tree) — the opt-out for when the per-gate install cost is too high')
|
|
763
|
+
.option('--merge-retries <n>', 'cross-job merge-serialiser CAS-retry cap: a non-fast-forward `${branch}:main` push re-rebases onto the moved <arbiter>/main and retries up to <n> times before a contender bounces to needs-attention. The CAS loop IS the cross-job queue (the in-process integrateLock only serialises sibling integrates in one process), so a wide-matrix CI raises this. Default 1000 (a large liveness ceiling, NOT a small contention budget). Resolved flag > env > per-repo > global > default.')
|
|
764
|
+
.option('--json', 'output the raw result as JSON')
|
|
765
|
+
.action(async (flags, command) => {
|
|
766
|
+
const fileConfig = loadConfig(flags.config);
|
|
767
|
+
const override = loadConfigOverride(defaultConfigOverridePath(flags.config));
|
|
768
|
+
const config = resolveGlobalConfig(fileConfig, runFlagOverrides(flags, command));
|
|
769
|
+
// The null adapter shells out to agentCmd, so it is required there; the
|
|
770
|
+
// pi adapter invokes the pi CLI directly and does not consume agentCmd.
|
|
771
|
+
// Share the ONE predicate (doNeedsAgentCmd) with `do`/`--remote`.
|
|
772
|
+
if (doNeedsAgentCmd(config)) {
|
|
773
|
+
throw new Error(NO_AGENT_CMD_MESSAGE);
|
|
774
|
+
}
|
|
775
|
+
const workspace = flags.workspace ?? config.workspacesDir;
|
|
776
|
+
// Gate 2 (PR/code review): wire the PRODUCTION harness-backed gate ONLY when
|
|
777
|
+
// `config.review` resolves on (mirror the `do`/`complete` commands). The
|
|
778
|
+
// per-repo review flags are resolved per-item inside `runOneItem`; only the
|
|
779
|
+
// gate SEAM is threaded here. Off ⇒ undefined ⇒ no review (the default).
|
|
780
|
+
const reviewGate = config.review ? harnessReviewGate() : undefined;
|
|
781
|
+
const onWarn = (message) => console.error(`>> ${message}`);
|
|
782
|
+
// Plain `run` (no flag) NOW drives the REGISTRY-SET ADVANCE tick as its
|
|
783
|
+
// per-item unit (task `run-uses-advance-tick`), via the deliberate
|
|
784
|
+
// {@link RunTick} swap seam: the loop machinery (`runLoop`) is UNCHANGED, the
|
|
785
|
+
// tick it loops is the precursor's registry-set advance driver instead of the
|
|
786
|
+
// build-only `runOnce`. With BOTH lifecycle gates at their calm defaults
|
|
787
|
+
// (observationTriage off, surfaceBlockers off) the advance tick degrades to
|
|
788
|
+
// EXACTLY the old build tick's behaviour over the SAME substrate (registry-set
|
|
789
|
+
// discovery + per-mirror job-worktree isolation) — behaviour-preserving today;
|
|
790
|
+
// flip a gate and the SAME tick performs the lifecycle (triage/surface/apply).
|
|
791
|
+
// `run` ≡ CI: the same advance tick, a different cadence.
|
|
792
|
+
const advanceTick = buildRegistrySetAdvanceTick({
|
|
793
|
+
config,
|
|
794
|
+
workspace,
|
|
795
|
+
arbiter: flags.arbiter,
|
|
796
|
+
env: process.env,
|
|
797
|
+
override,
|
|
798
|
+
});
|
|
799
|
+
// `--advance` is now a DEPRECATED NO-OP ALIAS: plain `run` already IS advance,
|
|
800
|
+
// so there is no separate mode to opt into. Warn (but do not fail) so an
|
|
801
|
+
// existing `run --advance` invocation keeps working without surprise.
|
|
802
|
+
if (flags.advance) {
|
|
803
|
+
onWarn('`run --advance` is deprecated and ignored: plain `run` already runs the ' +
|
|
804
|
+
'advance tick (build/task with calm-default gates; set observationTriage ' +
|
|
805
|
+
'/ surfaceBlockers for the lifecycle).');
|
|
806
|
+
}
|
|
807
|
+
const printTick = (result) => {
|
|
808
|
+
if (flags.json) {
|
|
809
|
+
console.log(JSON.stringify(result, null, 2));
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
for (const item of result.items) {
|
|
813
|
+
console.log(formatItemLine(item));
|
|
814
|
+
}
|
|
815
|
+
console.log(`Summary: ${result.claimedAndDone} done, ${result.skipped} skipped, ${result.failed} failed.`);
|
|
816
|
+
};
|
|
817
|
+
// `run --once` = ONE debug tick (NOT the CI path; CI is `do`). The existing
|
|
818
|
+
// `runOnce` IS this tick.
|
|
819
|
+
if (flags.once) {
|
|
820
|
+
// The advance tick IS a RunTick, so `run --once` debug-ticks it (one
|
|
821
|
+
// registry-set advance batch) identically to how it looped.
|
|
822
|
+
const result = await advanceTick({
|
|
823
|
+
config,
|
|
824
|
+
workspace,
|
|
825
|
+
reviewGate,
|
|
826
|
+
onWarn,
|
|
827
|
+
});
|
|
828
|
+
printTick(result);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
// `run` (no flag) = the cross-repo, parallel, forever-looping DAEMON: loop
|
|
832
|
+
// the concurrent tick over the registry until a stop bound (--max-iterations
|
|
833
|
+
// / --max-duration) or a SIGINT/SIGTERM (graceful shutdown after the current
|
|
834
|
+
// tick). Stuck items surface via the existing needs-attention seam inside the
|
|
835
|
+
// tick — the loop never infinite-retries and adds no bespoke reporting.
|
|
836
|
+
let stopRequested = false;
|
|
837
|
+
const requestStop = () => {
|
|
838
|
+
if (!stopRequested) {
|
|
839
|
+
stopRequested = true;
|
|
840
|
+
console.error('>> stop requested — finishing the current tick, then exiting.');
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
process.on('SIGINT', requestStop);
|
|
844
|
+
process.on('SIGTERM', requestStop);
|
|
845
|
+
try {
|
|
846
|
+
const summary = await runLoop({
|
|
847
|
+
config,
|
|
848
|
+
workspace,
|
|
849
|
+
reviewGate,
|
|
850
|
+
onWarn,
|
|
851
|
+
// The swap seam: plain `run` ALWAYS drives the registry-set ADVANCE tick
|
|
852
|
+
// (build/task with calm-default gates; the lifecycle when a gate is on).
|
|
853
|
+
tick: advanceTick,
|
|
854
|
+
maxIterations: flags.maxIterations !== undefined
|
|
855
|
+
? Number(flags.maxIterations)
|
|
856
|
+
: undefined,
|
|
857
|
+
maxDurationMs: flags.maxDuration !== undefined
|
|
858
|
+
? Number(flags.maxDuration) * 1000
|
|
859
|
+
: undefined,
|
|
860
|
+
intervalMs: flags.interval !== undefined ? Number(flags.interval) * 1000 : 0,
|
|
861
|
+
stop: () => stopRequested,
|
|
862
|
+
onTick: (result, iteration) => {
|
|
863
|
+
if (!flags.json) {
|
|
864
|
+
console.error(`>> tick ${iteration}:`);
|
|
865
|
+
}
|
|
866
|
+
printTick(result);
|
|
867
|
+
},
|
|
868
|
+
});
|
|
869
|
+
if (!flags.json) {
|
|
870
|
+
console.log(`Loop ended (${summary.stoppedBy}) after ${summary.iterations} tick(s): ` +
|
|
871
|
+
`${summary.claimedAndDone} done, ${summary.skipped} skipped, ${summary.failed} failed.`);
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
finally {
|
|
878
|
+
process.off('SIGINT', requestStop);
|
|
879
|
+
process.off('SIGTERM', requestStop);
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
program
|
|
883
|
+
.command('verify')
|
|
884
|
+
.helpGroup(ADVANCED_GROUP)
|
|
885
|
+
.description("Run the repo's declared acceptance gate (per-repo `verify` config) and exit with its status (0 = pass). Deterministic shell gate; no model. Read-only with respect to work/.")
|
|
886
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
887
|
+
.action(async (flags) => {
|
|
888
|
+
const config = resolveGlobalConfig(loadConfig(flags.config), {});
|
|
889
|
+
// DELIBERATELY verify-ONLY: the standalone `verify` command does NOT run the
|
|
890
|
+
// `prepare` env-prep step first. `verify` is the PURE acceptance gate (env-
|
|
891
|
+
// ready is a separate concern); a human invoking it prepares their own
|
|
892
|
+
// checkout. `prepare` runs only in the runner's fresh-worktree lifecycle
|
|
893
|
+
// (`do`/`run`/`complete` → `performIntegration`), where a fresh job worktree
|
|
894
|
+
// off the hub mirror genuinely needs deps before the gate can be trusted.
|
|
895
|
+
const result = await runVerify({
|
|
896
|
+
cwd: process.cwd(),
|
|
897
|
+
verify: config.verify,
|
|
898
|
+
});
|
|
899
|
+
process.exit(result.exitCode);
|
|
900
|
+
});
|
|
901
|
+
program
|
|
902
|
+
.command('claim')
|
|
903
|
+
.helpGroup(ADVANCED_GROUP)
|
|
904
|
+
.description('Atomically claim a work/backlog/<slug>.md item via a compare-and-swap push to the arbiter (in-process; mirrors scripts/claim.sh).')
|
|
905
|
+
.argument('<slug>', 'the slug of the backlog item to claim')
|
|
906
|
+
.option('--arbiter <remote>', 'name of the arbiter git remote (default: origin)', 'origin')
|
|
907
|
+
.option('--retries <n>', 'cap on push retries when main advances', '3')
|
|
908
|
+
.option('--dry-run', 'show the intended push without mutating the arbiter')
|
|
909
|
+
.option('--ignore-not-ready', 'override the readiness guard: claim despite an unmet blockedBy, and silence the needsAnswers warning (loud, never default)')
|
|
910
|
+
.action(async (rawSlug, flags) => {
|
|
911
|
+
// Task-only command (§3a): accept bare + `task:`, reject `prd:`.
|
|
912
|
+
const slug = resolveTaskOnlySlug(rawSlug);
|
|
913
|
+
// Wrap ONLY this CLI surface's `performClaim` call with the spinner
|
|
914
|
+
// helper (task `claim-cas-spinner`): the push can take seconds, so the
|
|
915
|
+
// terminal looked frozen. In non-TTY mode the helper is a no-op and
|
|
916
|
+
// stderr stays byte-identical to today (silent on success,
|
|
917
|
+
// `error: <message>` on failure, `>> <note>` lines unchanged). The
|
|
918
|
+
// autonomous `performClaim` call sites (`do`/`run`/`start`/`work-on`/
|
|
919
|
+
// `continue-branch`) are explicitly OUT OF SCOPE.
|
|
920
|
+
const spinner = createClaimSpinner({
|
|
921
|
+
stream: process.stderr,
|
|
922
|
+
isTTY: process.stdout.isTTY === true,
|
|
923
|
+
clock: {
|
|
924
|
+
setInterval: (fn, ms) => setInterval(fn, ms),
|
|
925
|
+
clearInterval: (handle) => clearInterval(handle),
|
|
926
|
+
},
|
|
927
|
+
label: `Claiming ${slug}\u2026`,
|
|
928
|
+
});
|
|
929
|
+
const onSigint = () => {
|
|
930
|
+
spinner.stop();
|
|
931
|
+
process.exit(130);
|
|
932
|
+
};
|
|
933
|
+
process.on('SIGINT', onSigint);
|
|
934
|
+
spinner.start();
|
|
935
|
+
let result;
|
|
936
|
+
try {
|
|
937
|
+
result = await performClaim({
|
|
938
|
+
slug,
|
|
939
|
+
cwd: process.cwd(),
|
|
940
|
+
arbiter: flags.arbiter ?? 'origin',
|
|
941
|
+
retries: flags.retries !== undefined ? Number(flags.retries) : undefined,
|
|
942
|
+
dryRun: flags.dryRun,
|
|
943
|
+
humanPath: true,
|
|
944
|
+
override: flags.ignoreNotReady === true,
|
|
945
|
+
// HUMAN command (the `humanPath: true` above already says so): the
|
|
946
|
+
// standalone `claim` CAS micro-commit + push is the human's, so it is
|
|
947
|
+
// NOT given the runner `config.identity`. The AUTONOMOUS claim is the one
|
|
948
|
+
// inside `do`/`run`/`intake` (identity-aware). Thread the ambient
|
|
949
|
+
// `process.env` EXPLICITLY so the human-identity choice is declared at the
|
|
950
|
+
// call site, not left to the seam's silent `?? process.env` fallback.
|
|
951
|
+
env: process.env,
|
|
952
|
+
note: (message) => spinner.note(message),
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
catch (err) {
|
|
956
|
+
// Unhandled error: tear the spinner down cleanly BEFORE the throw
|
|
957
|
+
// propagates so the cursor is restored + no orphaned ANSI state.
|
|
958
|
+
spinner.stop();
|
|
959
|
+
process.off('SIGINT', onSigint);
|
|
960
|
+
throw err;
|
|
961
|
+
}
|
|
962
|
+
spinner.finish(result);
|
|
963
|
+
process.off('SIGINT', onSigint);
|
|
964
|
+
process.exit(result.exitCode);
|
|
965
|
+
});
|
|
966
|
+
program
|
|
967
|
+
.command('start')
|
|
968
|
+
.helpGroup(HEADLINE_GROUP)
|
|
969
|
+
.description('Claim a backlog item (only if needed) and onboard onto its work/<slug> branch in the CURRENT checkout. Decides on the folder on <arbiter>/main, never on a frontmatter field. Launches no agent/editor.')
|
|
970
|
+
.argument('[slug]', 'the slug to start (inferred from a work/<slug> branch if omitted)')
|
|
971
|
+
.option('--arbiter <remote>', 'name of the arbiter git remote (default: origin)', 'origin')
|
|
972
|
+
// `--resume` is now the HIDDEN alias of the `resume` verb (ADR §4/§7): the
|
|
973
|
+
// documented surface is `start` = begin here, `resume` = continue here. Kept
|
|
974
|
+
// (hidden) for muscle memory; addHelpText below points at the verb.
|
|
975
|
+
.addOption(new Option('--resume', '(hidden alias of the `resume` verb) assert ownership of an already in-progress item: switch to its work branch without claiming').hideHelp())
|
|
976
|
+
.option('--ignore-not-ready', 'override the readiness guard: claim despite an unmet blockedBy, and silence the needsAnswers warning (loud, never default)')
|
|
977
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
978
|
+
.option('--agent', 'after onboarding, launch the configured harness INTERACTIVELY in the checkout (foreground, you drive it — no prepared prompt). Requires harness: pi. Not a tracked job (no record/gate); you still run `complete`/`requeue`.')
|
|
979
|
+
.option('--harness <name>', 'harness adapter for --agent: pi (interactive launch requires pi). Resolved flag > env > per-repo > global > default.')
|
|
980
|
+
.option('--model <model>', 'model the interactive --agent session starts pinned to (routing intent; you may switch inside pi). Resolved flag > env > per-repo > global > default.')
|
|
981
|
+
.option('--pi-bin <path>', 'path to the pi CLI binary (for --agent)')
|
|
982
|
+
.option('--sessions-dir <dir>', 'HOST-ONLY root folder under which the --agent pi session file is generated')
|
|
983
|
+
.action((rawSlug, flags) => runStartAction(rawSlug, flags, false));
|
|
984
|
+
program
|
|
985
|
+
.command('resume')
|
|
986
|
+
.helpGroup(HEADLINE_GROUP)
|
|
987
|
+
.description('Re-engage an already in-progress item in the CURRENT checkout: switch to its work/<slug> branch WITHOUT claiming (the item is already in-progress; you assert ownership). The human “continue here” verb — the counterpart to `start` (“begin here”). Decides on the folder on <arbiter>/main, never on a frontmatter field. Launches no agent/editor.')
|
|
988
|
+
.argument('[slug]', 'the slug to resume (inferred from a work/<slug> branch if omitted)')
|
|
989
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
990
|
+
.option('--arbiter <remote>', 'name of the arbiter git remote (default: origin)', 'origin')
|
|
991
|
+
.option('--isolated', "re-engage the slug's RETAINED isolated job worktree (the inverse of `do --isolated`) WITHOUT claiming: locate it off THIS repo's arbiter and print its path to cd into. The symmetric companion of `complete --isolated` (finish the stranded worktree).")
|
|
992
|
+
.option('--workspace <dir>', 'execution working area for job worktrees (--isolated; default: workspacesDir / ~/.dorfl)')
|
|
993
|
+
.action((rawSlug, flags) => runStartAction(rawSlug, flags, true));
|
|
994
|
+
program
|
|
995
|
+
.command('work-on')
|
|
996
|
+
.helpGroup(HEADLINE_GROUP)
|
|
997
|
+
.description('HUMAN command: claim a task and create an isolated worktree in a human-friendly location (under config humanWorktreesDir, NEVER ~/.dorfl) for parallel work, and cd you in by default (via the shell wrapper). Two forms: `work-on <slug>` (in-repo: infer the arbiter from the current repo) and `work-on --remote <r> <slug>` (ensure a hub mirror via repo-mirror, creating if absent) — consistent with `do --remote` (bare = current repo; --remote = anywhere). BOTH claim, then always fetch + branch work/<slug> off the freshly-fetched <arbiter>/main — same claim, same starting commit; only the worktree LOCATION differs. --copy <patterns> copies named gitignored files (copy, not symlink; --copy-from required in remote mode) with a security notice. A binary cannot cd your shell, so install the wrapper `work-on(){ cd "$(dorfl work-on "$@" --print-dir)"; }`; --print-dir is that wrapper’s plumbing (emits ONLY the path).')
|
|
998
|
+
.argument('<slug>', 'the slug to work on (bare = the task; the target repo is the current one, or --remote <r>)')
|
|
999
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
1000
|
+
.option('--remote <r>', 'work on a REGISTERED repo with NO checkout: ensure a hub mirror via repo-mirror (creating if absent) and claim against it (consistent with `do --remote`). Omit for the in-repo form (the arbiter is inferred from the current repo).')
|
|
1001
|
+
.option('--arbiter <remote>', 'name of the arbiter git remote in the current repo (in-repo form; default: origin)', 'origin')
|
|
1002
|
+
.addOption(new Option('--copy <patterns>', 'comma-separated gitignored filenames to COPY into the worktree (e.g. .env.local,.env). In-repo: from the current repo; remote: requires --copy-from. Copy, not symlink.').helpGroup(ADVANCED_OPT_GROUP))
|
|
1003
|
+
.addOption(new Option('--copy-from <path>', 'source dir for --copy in the remote form (required there; there is no implicit current repo)').helpGroup(ADVANCED_OPT_GROUP))
|
|
1004
|
+
.addOption(new Option('--print-dir', 'print ONLY the worktree path to stdout (for a shell wrapper: work-on(){ cd "$(dorfl work-on "$@" --print-dir)"; })').helpGroup(ADVANCED_OPT_GROUP))
|
|
1005
|
+
.option('--workspace <dir>', 'execution working area for hub mirrors (default: workspacesDir / ~/.dorfl)')
|
|
1006
|
+
.option('--ignore-not-ready', 'override the readiness guard: claim despite an unmet blockedBy, and silence the needsAnswers warning (loud, never default)')
|
|
1007
|
+
.option('--agent', 'after creating the worktree, launch the configured harness INTERACTIVELY in it (foreground, you drive it — no prepared prompt). Requires harness: pi. Not a tracked job (no record/gate); you still run `complete`/`requeue`.')
|
|
1008
|
+
.option('--harness <name>', 'harness adapter for --agent: pi (interactive launch requires pi). Resolved flag > env > per-repo > global > default.')
|
|
1009
|
+
.option('--model <model>', 'model the interactive --agent session starts pinned to (routing intent; you may switch inside pi). Resolved flag > env > per-repo > global > default.')
|
|
1010
|
+
.option('--pi-bin <path>', 'path to the pi CLI binary (for --agent)')
|
|
1011
|
+
.option('--sessions-dir <dir>', 'HOST-ONLY root folder under which the --agent pi session file is generated')
|
|
1012
|
+
.action(async (rawSlug, flags) => {
|
|
1013
|
+
// The two forms are now distinguished by the `--remote` FLAG (ADR §4,
|
|
1014
|
+
// consistent with `do --remote`), not a positional <remote>: bare =
|
|
1015
|
+
// the current repo, `--remote <r>` = any registered repo.
|
|
1016
|
+
const remote = flags.remote !== undefined && flags.remote.trim() !== ''
|
|
1017
|
+
? flags.remote
|
|
1018
|
+
: undefined;
|
|
1019
|
+
// Task-only command (§3a): accept bare + `task:`, reject `prd:`.
|
|
1020
|
+
const theSlug = resolveTaskOnlySlug(rawSlug);
|
|
1021
|
+
const configPath = flags.config ?? defaultConfigPath();
|
|
1022
|
+
const { dir: configuredRoot, config } = loadHumanWorktreesDir(configPath);
|
|
1023
|
+
const workspace = flags.workspace ?? config.workspacesDir;
|
|
1024
|
+
// --print-dir wants a clean stdout, so all human-facing notes go to
|
|
1025
|
+
// stderr; the path is the ONLY thing on stdout (printed below).
|
|
1026
|
+
const printDir = flags.printDir === true;
|
|
1027
|
+
const result = await performWorkOn({
|
|
1028
|
+
slug: theSlug,
|
|
1029
|
+
remote,
|
|
1030
|
+
cwd: process.cwd(),
|
|
1031
|
+
arbiter: flags.arbiter ?? 'origin',
|
|
1032
|
+
copy: flags.copy,
|
|
1033
|
+
copyFrom: flags.copyFrom,
|
|
1034
|
+
override: flags.ignoreNotReady === true,
|
|
1035
|
+
workspacesDir: workspace,
|
|
1036
|
+
humanWorktreesDir: configuredRoot,
|
|
1037
|
+
promptForRoot: (suggestion) => promptForWorktreesRoot(suggestion),
|
|
1038
|
+
saveRoot: (chosen) => persistHumanWorktreesDir(chosen, configPath),
|
|
1039
|
+
// `--agent`: launch the configured harness INTERACTIVELY in the new
|
|
1040
|
+
// worktree after creation (task `agent-interactive-launch`). In-repo
|
|
1041
|
+
// mode resolves per-repo config from the current checkout; remote mode
|
|
1042
|
+
// (no checkout) resolves from the global config only (like `do --remote`).
|
|
1043
|
+
launchInteractive: buildInteractiveLauncher(flags, configPath, remote === undefined ? process.cwd() : undefined),
|
|
1044
|
+
// HUMAN command (the description says so): claim + worktree + branch is
|
|
1045
|
+
// the human's, NOT given the runner `config.identity`. Ambient
|
|
1046
|
+
// `process.env` threaded EXPLICITLY (not the seam's silent fallback).
|
|
1047
|
+
env: process.env,
|
|
1048
|
+
note: (message) => console.error(`>> ${message}`),
|
|
1049
|
+
});
|
|
1050
|
+
if (result.exitCode !== 0) {
|
|
1051
|
+
console.error(`error: ${result.message}`);
|
|
1052
|
+
process.exit(result.exitCode);
|
|
1053
|
+
}
|
|
1054
|
+
if (printDir) {
|
|
1055
|
+
// Path only on stdout, so `cd "$(... --print-dir)"` works.
|
|
1056
|
+
process.stdout.write(`${result.dir}\n`);
|
|
1057
|
+
}
|
|
1058
|
+
process.exit(0);
|
|
1059
|
+
});
|
|
1060
|
+
program
|
|
1061
|
+
.command('prompt')
|
|
1062
|
+
.helpGroup(ADVANCED_GROUP)
|
|
1063
|
+
.description("Print to stdout the work-agent prompt for a task: the canonical CLAIM-PROTOCOL wrapper + the task's own ## Prompt (with <slug> and source prd substituted). Resolves work/in-progress/<slug>.md then work/backlog/<slug>.md; infers <slug> from a work/<slug> branch when omitted. Read-only, stdout only — the same assembly the autonomous runner feeds agentCmd.")
|
|
1064
|
+
.argument('[slug]', 'the slug to render (inferred from a work/<slug> branch if omitted)')
|
|
1065
|
+
.action((rawSlug) => {
|
|
1066
|
+
// Task-only command (§3a): accept bare + `task:`, reject `prd:`.
|
|
1067
|
+
const slug = resolveTaskOnlySlug(rawSlug);
|
|
1068
|
+
// Resolve the `promptGuidance` NUDGE namespace through the SAME chain the
|
|
1069
|
+
// gate family uses (env > per-repo > global > default), so e.g. a
|
|
1070
|
+
// `promptGuidance.testFirst:true` in `.dorfl.json` strengthens the
|
|
1071
|
+
// wrapper line for `dorfl prompt` exactly as it would in `do`/`run`.
|
|
1072
|
+
const cwd = process.cwd();
|
|
1073
|
+
const global = loadConfig();
|
|
1074
|
+
const resolved = resolveRepoConfig({ repoPath: cwd, global }).config;
|
|
1075
|
+
const output = renderPrompt({
|
|
1076
|
+
slug,
|
|
1077
|
+
cwd,
|
|
1078
|
+
promptGuidance: resolvePromptGuidance(resolved),
|
|
1079
|
+
});
|
|
1080
|
+
process.stdout.write(output);
|
|
1081
|
+
});
|
|
1082
|
+
program
|
|
1083
|
+
.command('complete')
|
|
1084
|
+
.helpGroup(HEADLINE_GROUP)
|
|
1085
|
+
.description('On a work/<slug> branch (slug inferred if omitted): run the gate, mark done (git mv in-progress\u2192done), commit (<type>(<slug>): <summary>; done) the agent\u2019s uncommitted work + the move, rebase onto <arbiter>/main, and integrate. Mode resolved at completion time (--merge/--propose > per-repo > global > default propose): merge\u2192push to main + switch+ff local main; propose\u2192push branch + switch to main (no ff). Then delete the LOCAL work branch iff provably on the arbiter (never the remote); --no-switch stays on the branch and keeps it. Never --force.')
|
|
1086
|
+
.argument('[slug]', 'the slug to complete (inferred from a work/<slug> branch if omitted)')
|
|
1087
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
1088
|
+
.option('--arbiter <remote>', 'name of the arbiter git remote (default: origin)', 'origin')
|
|
1089
|
+
.option('--merge', 'integrate in merge mode this invocation (mutually exclusive with --propose; overrides config)')
|
|
1090
|
+
.option('--propose', 'integrate in propose mode this invocation (mutually exclusive with --merge; overrides config)')
|
|
1091
|
+
.option('--no-pr', 'propose without opening a PR: push the branch but deliberately skip the review request, even on an authed GitHub arbiter (the explicit suppress-PR intent). Resolved flag > env > per-repo > global > default off.')
|
|
1092
|
+
.option('--no-switch', 'stay on the work/<slug> branch (and keep it) instead of switching back to main')
|
|
1093
|
+
.option('--ignore-diverged-main', 'override the merge-mode divergence guard: complete --merge even when local main is ahead of <arbiter>/main (unpushed). The work still lands on the arbiter; local main is left for you to `git rebase`. Loud, never default.')
|
|
1094
|
+
.option('--isolated', "FINISH a STRANDED isolated worktree: integrate the slug's already-committed, already-done-moved retained job worktree (a terminal push failed AFTER the done-move+commit) by running ONLY the rebase\u2192integrate tail from the kept commit \u2014 the locate-EXISTING inverse of `do --isolated`. Detection is unspoofable: an already-integrated task is a clean no-op; no retained worktree is a clean \u201cnothing to recover\u201d. --merge/--propose/--arbiter resolve identically to a normal integrate; the already-passed gate is skipped.")
|
|
1095
|
+
.option('--workspace <dir>', 'execution working area for job worktrees (--isolated; default: workspacesDir / ~/.dorfl)')
|
|
1096
|
+
.addOption(new Option('--skip-verify', 'skip the acceptance gate (human-only escape hatch; the runner never skips)').helpGroup(ADVANCED_OPT_GROUP))
|
|
1097
|
+
.addOption(new Option('--type <type>', 'conventional-commit type for the commit')
|
|
1098
|
+
.default('feat')
|
|
1099
|
+
.helpGroup(ADVANCED_OPT_GROUP))
|
|
1100
|
+
.addOption(new Option('--message <summary>', 'commit summary (default: the task title, minus a leading "slug \u2014 " prefix)').helpGroup(ADVANCED_OPT_GROUP))
|
|
1101
|
+
.option('--review', 'run Gate 2 (PR/code review) after verify, before the done-move (overrides config). Resolved flag > per-repo > global > default off.')
|
|
1102
|
+
.option('--no-review', 'do NOT run Gate 2 this invocation (overrides config)')
|
|
1103
|
+
.option('--review-model <id>', 'model the Gate-2 review agent runs on (de-correlated from the builder; routing intent). Resolved flag > env > per-repo > global > default.')
|
|
1104
|
+
.option('--review-max-rounds <n>', 'bound the revise/review loop; on exhaustion force needs-attention (default 2)')
|
|
1105
|
+
.option('--fresh-worktree-gate', 'run the acceptance gate (prepare then verify) against the REBASED tip in a CLEAN throwaway worktree (the tree that integrates). ON by default. Resolved flag > env > per-repo > global > default on.')
|
|
1106
|
+
.option('--no-fresh-worktree-gate', 'run the acceptance gate in the current checkout (the pre-rebase tree) — the opt-out for when the per-gate install cost is too high')
|
|
1107
|
+
.option('--merge-retries <n>', 'cross-job merge-serialiser CAS-retry cap (see `run --help`); resolved flag > env > per-repo > global > default 1000.')
|
|
1108
|
+
.action(async (rawSlug, flags) => {
|
|
1109
|
+
// Task-only command (§3a): accept bare + `task:`, reject `prd:`.
|
|
1110
|
+
const slug = resolveTaskOnlySlug(rawSlug);
|
|
1111
|
+
const cwd = process.cwd();
|
|
1112
|
+
const { global, override } = loadGlobalAndOverride(flags.config);
|
|
1113
|
+
// `--isolated`: FINISH a stranded isolated worktree (the recover-already-
|
|
1114
|
+
// committed path) instead of completing the current checkout. It LOCATES the
|
|
1115
|
+
// slug's retained job worktree off THIS repo's arbiter and runs ONLY the
|
|
1116
|
+
// rebase\u2192integrate tail from the kept commit. The slug is REQUIRED (there is no
|
|
1117
|
+
// branch to infer it from in the operator's checkout).
|
|
1118
|
+
if (flags.isolated === true) {
|
|
1119
|
+
if (slug === undefined || slug === '') {
|
|
1120
|
+
console.error('error: complete --isolated requires <slug> (the stranded item to finish).');
|
|
1121
|
+
process.exit(1);
|
|
1122
|
+
}
|
|
1123
|
+
const flagMode = integrationFromFlags(flags);
|
|
1124
|
+
const resolved = resolveRepoConfig({
|
|
1125
|
+
repoPath: cwd,
|
|
1126
|
+
global,
|
|
1127
|
+
override,
|
|
1128
|
+
flags: {
|
|
1129
|
+
...(flagMode ? { integration: flagMode } : {}),
|
|
1130
|
+
...noPRFlagOverrides(flags),
|
|
1131
|
+
},
|
|
1132
|
+
});
|
|
1133
|
+
if (resolved.message) {
|
|
1134
|
+
console.error(`>> ${resolved.message}`);
|
|
1135
|
+
}
|
|
1136
|
+
const isoConfig = resolved.config;
|
|
1137
|
+
const recovered = await performRecoverIsolated({
|
|
1138
|
+
slug,
|
|
1139
|
+
cwd,
|
|
1140
|
+
arbiter: flags.arbiter ?? isoConfig.defaultArbiter,
|
|
1141
|
+
workspacesDir: flags.workspace ?? isoConfig.workspacesDir,
|
|
1142
|
+
integration: isoConfig.integration,
|
|
1143
|
+
noPR: isoConfig.noPR,
|
|
1144
|
+
note: (message) => console.error(`>> ${message}`),
|
|
1145
|
+
env: process.env,
|
|
1146
|
+
});
|
|
1147
|
+
if (recovered.exitCode !== 0) {
|
|
1148
|
+
console.error(`error: ${recovered.message}`);
|
|
1149
|
+
}
|
|
1150
|
+
process.exit(recovered.exitCode);
|
|
1151
|
+
}
|
|
1152
|
+
// Resolve the integration mode at completion time, highest first:
|
|
1153
|
+
// --merge/--propose flag > per-repo .dorfl.json > global > default.
|
|
1154
|
+
// The flag sits at the TOP of the same chain the autonomous runner uses
|
|
1155
|
+
// (per-repo > global > default), so human and autonomous paths agree.
|
|
1156
|
+
const flagMode = integrationFromFlags(flags);
|
|
1157
|
+
const resolved = resolveRepoConfig({
|
|
1158
|
+
repoPath: cwd,
|
|
1159
|
+
global,
|
|
1160
|
+
override,
|
|
1161
|
+
// The integrate-time mode AND the Gate-2 review flags ride the SAME
|
|
1162
|
+
// flag > env > per-repo > global > default chain.
|
|
1163
|
+
flags: {
|
|
1164
|
+
...(flagMode ? { integration: flagMode } : {}),
|
|
1165
|
+
...reviewFlagOverrides(flags),
|
|
1166
|
+
// `--fresh-worktree-gate`/`--no-fresh-worktree-gate` rides the SAME chain.
|
|
1167
|
+
...freshWorktreeGateFlagOverrides(flags),
|
|
1168
|
+
// `--merge-retries <n>` rides the SAME chain: the cross-job merge-serialiser
|
|
1169
|
+
// CAS-retry cap (prd `land-time-reverify-and-parallel-merge-ceiling` Story 5
|
|
1170
|
+
// / Applied Answer q1 (a)).
|
|
1171
|
+
...mergeRetriesFlagOverrides(flags),
|
|
1172
|
+
// `--no-pr` (the PR-INTENT axis) rides the SAME chain.
|
|
1173
|
+
...noPRFlagOverrides(flags),
|
|
1174
|
+
},
|
|
1175
|
+
});
|
|
1176
|
+
if (resolved.message) {
|
|
1177
|
+
console.error(`>> ${resolved.message}`);
|
|
1178
|
+
}
|
|
1179
|
+
const config = resolved.config;
|
|
1180
|
+
const result = await performComplete({
|
|
1181
|
+
slug,
|
|
1182
|
+
cwd,
|
|
1183
|
+
arbiter: flags.arbiter ?? config.defaultArbiter,
|
|
1184
|
+
integration: config.integration,
|
|
1185
|
+
// An EXPLICIT `--merge` overrides the untrusted-origin build-propose rule (task
|
|
1186
|
+
// `untrusted-origin-forces-build-propose`): `flagMode` is the typed flag
|
|
1187
|
+
// (undefined when none), so this is true ONLY when the operator typed
|
|
1188
|
+
// `--merge`, never when `merge` was resolved from config.
|
|
1189
|
+
explicitMerge: flagMode === 'merge',
|
|
1190
|
+
noPR: config.noPR,
|
|
1191
|
+
noSwitch: flags.switch === false,
|
|
1192
|
+
ignoreDivergedMain: flags.ignoreDivergedMain === true,
|
|
1193
|
+
prepare: config.prepare,
|
|
1194
|
+
verify: config.verify,
|
|
1195
|
+
skipVerify: flags.skipVerify,
|
|
1196
|
+
// Gate 2 (PR/code review): when `review` resolves on, run the `review`
|
|
1197
|
+
// SKILL as a fresh-context agent (the production harness-backed gate)
|
|
1198
|
+
// AFTER the green verify and BEFORE the done-move. The `reviewModel`
|
|
1199
|
+
// override flows to the launch through the existing harness seam.
|
|
1200
|
+
review: config.review,
|
|
1201
|
+
reviewModel: config.reviewModel,
|
|
1202
|
+
reviewMaxRounds: config.reviewMaxRounds,
|
|
1203
|
+
reviewGate: config.review ? harnessReviewGate() : undefined,
|
|
1204
|
+
// Run the acceptance gate against the REBASED tip in a clean throwaway
|
|
1205
|
+
// worktree (the tree that integrates) when ON (the default). `complete` is
|
|
1206
|
+
// a single-job path, so the resolved flag is passed UNCONDITIONALLY (no
|
|
1207
|
+
// fleet downgrade).
|
|
1208
|
+
freshWorktreeGate: config.freshWorktreeGate,
|
|
1209
|
+
// Cross-job merge-serialiser CAS-retry cap (prd `land-time-reverify-and-
|
|
1210
|
+
// parallel-merge-ceiling` Story 5 / Applied Answer q1 (a)) — the resolved
|
|
1211
|
+
// per-repo value reaches the merge loop via `performComplete`→
|
|
1212
|
+
// `performIntegration`.
|
|
1213
|
+
mergeRetries: config.mergeRetries,
|
|
1214
|
+
type: flags.type,
|
|
1215
|
+
message: flags.message,
|
|
1216
|
+
// Color the propose-mode next-step block only on an interactive
|
|
1217
|
+
// stdout TTY (and not under NO_COLOR); plain when piped/redirected.
|
|
1218
|
+
color: shouldUseColor(process.stdout),
|
|
1219
|
+
note: (message) => console.error(`>> ${message}`),
|
|
1220
|
+
// The propose next-step block is printed verbatim (no `>> ` prefix)
|
|
1221
|
+
// so its blank lines + heading stand out as the human call-to-action.
|
|
1222
|
+
noteBlock: (message) => console.error(message),
|
|
1223
|
+
// `complete` is a HUMAN command: a human finishing/merging the work, so
|
|
1224
|
+
// the commit/push/PR is THEIRS — it is deliberately NOT given the runner
|
|
1225
|
+
// `config.identity` (the autonomous completion is `do`'s own integrated
|
|
1226
|
+
// complete, which IS identity-aware). Thread the ambient `process.env`
|
|
1227
|
+
// EXPLICITLY so the human-identity choice is declared at the call site,
|
|
1228
|
+
// not left to the seam's silent `?? process.env` fallback (parity with
|
|
1229
|
+
// `requeue`).
|
|
1230
|
+
env: process.env,
|
|
1231
|
+
});
|
|
1232
|
+
if (result.exitCode !== 0) {
|
|
1233
|
+
console.error(`error: ${result.message}`);
|
|
1234
|
+
}
|
|
1235
|
+
process.exit(result.exitCode);
|
|
1236
|
+
});
|
|
1237
|
+
program
|
|
1238
|
+
.command('do')
|
|
1239
|
+
.helpGroup(HEADLINE_GROUP)
|
|
1240
|
+
.description('The per-repo WORKER (the CI command): claim + onboard onto work/<slug>, run the agent, gate, integrate, and exit. In the CURRENT checkout by default (refuses on a dirty tree, integrates in-place). With --remote <r>: against a REGISTERED repo with NO checkout — materialise a hub mirror + job worktree in the agents\u2019 area, run the same pipeline there, then reap. do <slug> | do task:<slug> | do spec:<slug> (the tasking path; the legacy prd:<slug> is still accepted) | do (auto-pick one) | do <a> <b> (those, in sequence) | do -n <x> (x eligible, in sequence). Auto-pick draws TASKS-FIRST then SPECS-to-task by default (per-repo selectionOrder reorders the pools). --propose (default) / --merge resolved at integrate-time. Supersedes ar-run.sh.')
|
|
1241
|
+
// EXTENSIBLE argument grammar (the three do-* tasks grow this one block):
|
|
1242
|
+
// `do-autopick` widens the single optional positional into a VARIADIC one so
|
|
1243
|
+
// `do` (zero args = auto-pick), `do <a> <b> …` (named, in sequence), and
|
|
1244
|
+
// `do <slug>` (exactly one) all share the one command. `-n <x>` is the count
|
|
1245
|
+
// for the auto-pick form. `do` stays SEQUENTIAL (parallelism is `run`).
|
|
1246
|
+
.argument('[slugs...]', 'the item(s) to do: bare (= the task), task:<slug>, or spec:<slug> (task the spec; the legacy prd:<slug> is still accepted). Zero args = auto-pick; multiple = do them in sequence.')
|
|
1247
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
1248
|
+
.option('--arbiter <remote>', 'name of the arbiter git remote (default: per-repo/global defaultArbiter)')
|
|
1249
|
+
.option('-n, --number <x>', 'AUTO-PICK x eligible items and do them IN SEQUENCE (ordered by selectionOrder, default drain = tasks-first then prds-to-task). Sequential — never a parallelism knob (that is `run`). Mutually exclusive with naming items.')
|
|
1250
|
+
.option('--selection-order <order>', 'order the auto-pick pools (build/task/surface/triage; apply is always first): a preset keyword (drain (default) | groom) or an explicit comma-separated pool list (e.g. build,task,surface,triage). Resolved flag > env > per-repo > global > default.')
|
|
1251
|
+
.option('--remote <r>', 'run against a REGISTERED repo with NO checkout: materialise a hub mirror + job worktree in the agents\u2019 area (auto-registers an unknown remote), run the pipeline there, then reap (never touches the human area)')
|
|
1252
|
+
.option('--isolated', "build in an ISOLATED job worktree off THIS repo's arbiter (inferred from cwd) instead of taking over the current checkout, then integrate + reap \u2014 the in-place-but-isolated form. Shares the same grammar as the no-checkout forms: a single named item, multiple named items (in sequence), AND -n/auto-pick over the mirror-side eligible-pool scan. Always SEQUENTIAL (parallelism is `run` / the CI matrix). Orthogonal to --remote (a foreign repo); with --remote, remote wins (isolation is already implied).")
|
|
1253
|
+
.option('--merge', 'integrate in merge mode this invocation (mutually exclusive with --propose; overrides config)')
|
|
1254
|
+
.option('--propose', 'integrate in propose mode this invocation (default; mutually exclusive with --merge; overrides config)')
|
|
1255
|
+
.option('--tasks-land-in <where>', 'where `do prd:<slug>` tasking output lands: `pre-backlog` (staged, not agent-eligible) or `ready` (the agent POOL). The EXPLICIT operator override at the top of the placement precedence (explicit flag > untrusted-origin forces staging > tasksLandIn default > built-in). Resolved flag > env (DORFL_TASKS_LAND_IN) > per-repo > global > built-in.')
|
|
1256
|
+
.option('--no-pr', 'propose without opening a PR: push the branch but deliberately skip the review request, even on an authed GitHub arbiter (the explicit suppress-PR intent). Resolved flag > env > per-repo > global > default off.')
|
|
1257
|
+
.option('--ignore-diverged-main', 'override the in-place divergence guard: run even when local main is ahead of <arbiter>/main (unpushed). The work still lands on the arbiter; local main is left for you to `git rebase`. In-place only; loud, never default.')
|
|
1258
|
+
.option('--allow-backlog', 'do task:<slug> ONLY: also FIND, CLAIM, and COMPLETE a task that lives in tasks/backlog/ (staging), driving it in place WITHOUT promoting it to the pool (so no advance leg / run daemon can claim it out from under you). The done-move goes tasks/backlog/ -> tasks/done/ directly (your explicit drive IS the promotion). EXPLICIT-INVOCATION-ONLY: default off, never set by run/auto-pick/advance or config/env.')
|
|
1259
|
+
.option('--agent-cmd <cmd>', 'command to run the agent on the task prompt')
|
|
1260
|
+
.option('--model <id>', 'model the agent runs on (routing intent; resolved flag > env > per-repo > global > default)')
|
|
1261
|
+
.option('--harness <adapter>', 'harness adapter that launches the agent: null (default, shells out to agentCmd) or pi (the pi CLI)')
|
|
1262
|
+
.option('--pi-bin <path>', 'pi CLI binary the pi harness invokes (default: pi on PATH)')
|
|
1263
|
+
.option('--sessions-dir <dir>', 'HOST-ONLY root folder under which the pi session file is generated (--session <dir>/<id>.jsonl). Default: pi per-cwd folder under ~/.pi/agent/sessions. Resolved flag > env > global > default (no per-repo).')
|
|
1264
|
+
.option('--watch', "stream the agent's high-signal events live by tailing the pi session log (requires harness: pi; READ-ONLY observer — does not change outcome/gate/git)")
|
|
1265
|
+
.option('--review', 'run Gate 2 (PR/code review) after verify, before the done-move (overrides config). Resolved flag > env > per-repo > global > default off.')
|
|
1266
|
+
.option('--no-review', 'do NOT run Gate 2 this invocation (overrides config)')
|
|
1267
|
+
.option('--review-model <id>', 'model the Gate-2 review agent runs on (de-correlated from the builder; routing intent). Resolved flag > env > per-repo > global > default.')
|
|
1268
|
+
.option('--review-max-rounds <n>', 'bound the revise/review loop; on exhaustion force needs-attention (default 2)')
|
|
1269
|
+
.option('--tasker-loop', 'run the tasker IMPROVER loop on `do prd:<slug>` (review→edit→converge over the produced task set). ON by default; --no-tasker-loop skips it. DISTINCT from the acceptance gate (--review).')
|
|
1270
|
+
.option('--no-tasker-loop', 'skip the tasker improver loop on `do prd:<slug>`')
|
|
1271
|
+
.option('--tasker-loop-max <n>', 'cap the tasker improver loop on `do prd:<slug>` (in-context review passes); on exhaustion with blockers, reject via needsAnswers / route the prd to needs-attention (default 3)')
|
|
1272
|
+
.option('--tasker-loop-model <id>', 'model the tasker improver loop review agent runs on (de-correlated from the tasker; routing intent). Resolved flag > env > per-repo > global > default. DISTINCT from --review-model.')
|
|
1273
|
+
.option('--fresh-worktree-gate', 'run the acceptance gate (prepare then verify) against the REBASED tip in a CLEAN throwaway worktree (the tree that actually integrates), so a green gate provably describes the merged artifact. ON by default. Resolved flag > env > per-repo > global > default on.')
|
|
1274
|
+
.option('--no-fresh-worktree-gate', "run the acceptance gate in the agent's build worktree (the pre-rebase tree) as before — the opt-out for when the per-gate install cost is too high")
|
|
1275
|
+
.option('--merge-retries <n>', 'cross-job merge-serialiser CAS-retry cap (see `run --help`); resolved flag > env > per-repo > global > default 1000.')
|
|
1276
|
+
.action(async (rawSlugs, flags) => {
|
|
1277
|
+
// Variadic grammar (`do-autopick`): zero args = AUTO-PICK; one = the single
|
|
1278
|
+
// named item; many = those, IN SEQUENCE. `-n <x>` is the auto-pick count.
|
|
1279
|
+
const args = rawSlugs ?? [];
|
|
1280
|
+
// `-n <x>` parse + validation. It is the AUTO-PICK count (sequential), so it
|
|
1281
|
+
// is mutually exclusive with NAMING items (you either auto-pick a count or
|
|
1282
|
+
// name the items, not both).
|
|
1283
|
+
let count;
|
|
1284
|
+
if (flags.number !== undefined) {
|
|
1285
|
+
const n = Number(flags.number);
|
|
1286
|
+
if (flags.number.trim() === '' || !Number.isInteger(n) || n < 1) {
|
|
1287
|
+
console.error(`error: -n/--number must be a positive integer (got '${flags.number}').`);
|
|
1288
|
+
process.exit(1);
|
|
1289
|
+
}
|
|
1290
|
+
if (args.length > 0) {
|
|
1291
|
+
console.error('error: -n/--number auto-picks a COUNT of eligible items; do not also ' +
|
|
1292
|
+
'name items. Use `do -n <x>` OR `do <a> <b> ...`, not both.');
|
|
1293
|
+
process.exit(1);
|
|
1294
|
+
}
|
|
1295
|
+
count = n;
|
|
1296
|
+
}
|
|
1297
|
+
const cwd = process.cwd();
|
|
1298
|
+
const { global, override } = loadGlobalAndOverride(flags.config);
|
|
1299
|
+
// Resolve the integration mode at integrate-time, highest first:
|
|
1300
|
+
// --merge/--propose flag > per-repo .dorfl.json > global > default.
|
|
1301
|
+
// (Same chain `complete` uses — `do` is the autonomous twin.)
|
|
1302
|
+
let flagMode;
|
|
1303
|
+
try {
|
|
1304
|
+
flagMode = integrationFromFlags(flags);
|
|
1305
|
+
}
|
|
1306
|
+
catch (err) {
|
|
1307
|
+
console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1308
|
+
process.exit(1);
|
|
1309
|
+
}
|
|
1310
|
+
// `do --remote <r>` / `do --isolated <slug>`: run the NO-CHECKOUT job-worktree
|
|
1311
|
+
// pipeline. Both materialise a hub mirror + job worktree in the agents' area
|
|
1312
|
+
// (`workspacesDir`) and reap per ADR §4 — the human area is NEVER touched.
|
|
1313
|
+
//
|
|
1314
|
+
// `--remote <r>` names the TARGETING axis (a FOREIGN repo, no checkout); the
|
|
1315
|
+
// arbiter spec is the `<r>` URL. `--isolated` names the ISOLATION intent (a
|
|
1316
|
+
// worktree off MY OWN arbiter, even though I am inside the repo); its arbiter
|
|
1317
|
+
// URL is RESOLVED FROM THE CWD's arbiter remote (the same `--arbiter` >
|
|
1318
|
+
// per-repo/global `defaultArbiter` name in-place `do` uses). The two are
|
|
1319
|
+
// ORTHOGONAL: `--isolated` + `--remote` is REDUNDANT (a foreign `--remote` is
|
|
1320
|
+
// already isolated), so we accept it and `--remote` WINS (see `## Decisions`).
|
|
1321
|
+
//
|
|
1322
|
+
// In BOTH cases the repo's COMMITTED `.dorfl.json` is reachable on
|
|
1323
|
+
// `<arbiter>/main` (the mirror), so we layer it — `flag > env > per-repo >
|
|
1324
|
+
// global > default` parity with in-place `do` (task
|
|
1325
|
+
// `remote-do-reads-per-repo-config-from-arbiter-main`). Only the whitelisted
|
|
1326
|
+
// `REPO_ALLOWED_KEYS` are layered (host-only keys stay global/flag/env-only,
|
|
1327
|
+
// rejected by the SAME `repo-config.ts` split).
|
|
1328
|
+
const isolatedNoRemote = flags.isolated === true && flags.remote === undefined;
|
|
1329
|
+
if (flags.remote !== undefined || isolatedNoRemote) {
|
|
1330
|
+
// The form's user-facing name + canonical usage, for the shared error
|
|
1331
|
+
// messages below (so `--isolated` errors read in its own terms).
|
|
1332
|
+
const form = isolatedNoRemote ? '--isolated' : '--remote';
|
|
1333
|
+
const usage = isolatedNoRemote
|
|
1334
|
+
? '`do --isolated <slug>`'
|
|
1335
|
+
: '`do --remote <r> <slug>`';
|
|
1336
|
+
// The no-checkout forms now support the SAME variadic grammar the in-place
|
|
1337
|
+
// form does: a single NAMED item, MULTIPLE named items (sequential), and
|
|
1338
|
+
// AUTO-PICK / `-n <x>` (sequential) over the MIRROR-SIDE eligible-pool scan
|
|
1339
|
+
// (`mirror-side-eligible-pool-scan`). The old inline `-n`×`--remote` REFUSAL
|
|
1340
|
+
// is GONE — the mirror scan backs it now (US #25); `-n` stays ALWAYS
|
|
1341
|
+
// SEQUENTIAL (parallelism is `run` / the CI matrix). `-n` is still mutually
|
|
1342
|
+
// exclusive with naming items (validated above, shared with the in-place form).
|
|
1343
|
+
const remoteFlags = doFlagOverrides(flags, flagMode);
|
|
1344
|
+
// Resolve the arbiter spec the rest of the pipeline consumes as `remote`.
|
|
1345
|
+
// `--remote` supplies it directly (a foreign URL). `--isolated` resolves it
|
|
1346
|
+
// from the CWD's arbiter remote (`git remote get-url`); no resolvable
|
|
1347
|
+
// arbiter ⇒ a CLEAR error naming `--remote <url>` as the foreign-repo
|
|
1348
|
+
// alternative — NOT a confusing URL-parse failure downstream.
|
|
1349
|
+
let effectiveRemote;
|
|
1350
|
+
if (isolatedNoRemote) {
|
|
1351
|
+
const bootstrapIdentity = resolveGlobalConfig(global, remoteFlags).identity;
|
|
1352
|
+
const arbiterName = flags.arbiter ??
|
|
1353
|
+
resolveDefaultArbiterForCwd(cwd, global, remoteFlags, override);
|
|
1354
|
+
const resolvedUrl = resolveArbiterUrlFromCheckout(cwd, arbiterName, identityEnv(bootstrapIdentity, process.env));
|
|
1355
|
+
if (resolvedUrl === undefined) {
|
|
1356
|
+
console.error(`error: --isolated builds in a worktree off this repo's arbiter ` +
|
|
1357
|
+
`('${arbiterName}'), but no such arbiter remote is configured/found ` +
|
|
1358
|
+
`here. Run inside a participating repo (a clone with an arbiter ` +
|
|
1359
|
+
`remote), or use --remote <url> to target another repo.`);
|
|
1360
|
+
process.exit(1);
|
|
1361
|
+
}
|
|
1362
|
+
effectiveRemote = resolvedUrl;
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
effectiveRemote = flags.remote;
|
|
1366
|
+
}
|
|
1367
|
+
// BOOTSTRAP resolution (global + flags, no per-repo layer) — it supplies
|
|
1368
|
+
// the HOST-ONLY keys needed to even reach the arbiter's committed file:
|
|
1369
|
+
// `workspacesDir` (where the mirror lives) and `identity` (the git env the
|
|
1370
|
+
// mirror fetch runs under). These are host-only by definition (rejected
|
|
1371
|
+
// per-repo), so reading them from global+flags first is correct and stable.
|
|
1372
|
+
const bootstrap = resolveGlobalConfig(global, remoteFlags);
|
|
1373
|
+
// Source the committed `.dorfl.json` from `<arbiter>/main` via the
|
|
1374
|
+
// hub mirror, then layer ONLY its whitelisted keys through the EXISTING
|
|
1375
|
+
// per-repo machinery. The read refreshes ONLY `main` (no-prune), so a
|
|
1376
|
+
// `work/<slug>` branch checked out in a stale worktree can never block it,
|
|
1377
|
+
// and the build's later all-heads materialisation fetch is unaffected. A
|
|
1378
|
+
// config-less repo (no file on
|
|
1379
|
+
// main, or an unreachable mirror) → exactly the bootstrap config, i.e.
|
|
1380
|
+
// byte-identical to the pre-task global+default behaviour.
|
|
1381
|
+
const remoteConfig = resolveRemoteRepoConfig({
|
|
1382
|
+
remote: effectiveRemote,
|
|
1383
|
+
workspacesDir: bootstrap.workspacesDir,
|
|
1384
|
+
global,
|
|
1385
|
+
flags: remoteFlags,
|
|
1386
|
+
identity: bootstrap.identity,
|
|
1387
|
+
note: (message) => console.error(`>> ${message}`),
|
|
1388
|
+
override,
|
|
1389
|
+
});
|
|
1390
|
+
if (doNeedsAgentCmd(remoteConfig)) {
|
|
1391
|
+
console.error(`error: ${NO_AGENT_CMD_MESSAGE}`);
|
|
1392
|
+
process.exit(1);
|
|
1393
|
+
}
|
|
1394
|
+
const remoteHarness = createHarness({
|
|
1395
|
+
harness: remoteConfig.harness,
|
|
1396
|
+
piBin: remoteConfig.piBin,
|
|
1397
|
+
});
|
|
1398
|
+
// The per-item `DoRemoteOptions` (everything BUT `arg`) — built ONCE and
|
|
1399
|
+
// reused for the single-item path AND threaded by the mirror-side auto-pick
|
|
1400
|
+
// driver (`performDoRemoteAuto`) to each sequential `performDoRemote`.
|
|
1401
|
+
const baseRemoteOptions = {
|
|
1402
|
+
remote: effectiveRemote,
|
|
1403
|
+
workspacesDir: remoteConfig.workspacesDir,
|
|
1404
|
+
arbiter: flags.arbiter ?? remoteConfig.defaultArbiter,
|
|
1405
|
+
// Host-only runner IDENTITY — scopes git/provider ops only (not the
|
|
1406
|
+
// agent launch); absent ⇒ ambient.
|
|
1407
|
+
identity: remoteConfig.identity,
|
|
1408
|
+
// `do --remote prd:<slug>` tasking-gate policy (task-build path ignores it).
|
|
1409
|
+
autoTask: remoteConfig.autoTask,
|
|
1410
|
+
// The resolved `promptGuidance` nudge — threaded into the remote worker
|
|
1411
|
+
// prompt (runRemotePipeline → buildAgentPrompt), mirroring in-place `do`.
|
|
1412
|
+
promptGuidance: resolvePromptGuidance(remoteConfig),
|
|
1413
|
+
integration: remoteConfig.integration,
|
|
1414
|
+
// EXPLICIT `--merge` override for the untrusted-origin build-propose rule.
|
|
1415
|
+
explicitMerge: flagMode === 'merge',
|
|
1416
|
+
// Per-TRANSITION TASKING override (the `do --remote prd:` tasking path).
|
|
1417
|
+
taskingIntegration: remoteConfig.taskingIntegration,
|
|
1418
|
+
// TASK-PLACEMENT: the configured default + the EXPLICIT operator override
|
|
1419
|
+
// (`--tasks-land-in`), the top of the placement precedence — mirrors
|
|
1420
|
+
// `explicitMerge` (set only when the flag was typed).
|
|
1421
|
+
tasksLandIn: remoteConfig.tasksLandIn,
|
|
1422
|
+
explicitTasksLandIn: explicitTasksLandInFromFlag(flags.tasksLandIn),
|
|
1423
|
+
prepare: remoteConfig.prepare,
|
|
1424
|
+
verify: remoteConfig.verify,
|
|
1425
|
+
// Single-job build path: gate the REBASED tip (the default) unconditionally.
|
|
1426
|
+
freshWorktreeGate: remoteConfig.freshWorktreeGate,
|
|
1427
|
+
// Cross-job merge-serialiser CAS-retry cap (resolved through the per-repo
|
|
1428
|
+
// chain on the arbiter-side `.dorfl.json` too) — prd
|
|
1429
|
+
// `land-time-reverify-and-parallel-merge-ceiling` Story 5.
|
|
1430
|
+
mergeRetries: remoteConfig.mergeRetries,
|
|
1431
|
+
noPR: remoteConfig.noPR,
|
|
1432
|
+
harness: remoteHarness,
|
|
1433
|
+
agentCmd: remoteConfig.agentCmd,
|
|
1434
|
+
model: remoteConfig.model,
|
|
1435
|
+
sessionsDir: remoteConfig.sessionsDir,
|
|
1436
|
+
review: remoteConfig.review,
|
|
1437
|
+
reviewModel: remoteConfig.reviewModel,
|
|
1438
|
+
reviewMaxRounds: remoteConfig.reviewMaxRounds,
|
|
1439
|
+
reviewGate: remoteConfig.review
|
|
1440
|
+
? harnessReviewGate({
|
|
1441
|
+
harness: remoteHarness,
|
|
1442
|
+
agentCmd: remoteConfig.agentCmd,
|
|
1443
|
+
})
|
|
1444
|
+
: undefined,
|
|
1445
|
+
// The tasker IMPROVER loop on the `do --remote prd:` path is ON by default
|
|
1446
|
+
// (auto-tasking has no `verify` floor, so the loop is the task path's
|
|
1447
|
+
// quality engine). `--tasker-loop`/`--no-tasker-loop` gates wiring the seam;
|
|
1448
|
+
// `taskerLoopMax`/`taskerLoopModel` resolve per-repo (flag > env > per-repo
|
|
1449
|
+
// > global > default). DISTINCT from the gate's `--review*` family.
|
|
1450
|
+
reviewLoop: remoteConfig.taskerLoop
|
|
1451
|
+
? harnessTaskReviewGate({
|
|
1452
|
+
harness: remoteHarness,
|
|
1453
|
+
agentCmd: remoteConfig.agentCmd,
|
|
1454
|
+
})
|
|
1455
|
+
: undefined,
|
|
1456
|
+
taskerLoopMax: remoteConfig.taskerLoopMax,
|
|
1457
|
+
taskerLoopModel: remoteConfig.taskerLoopModel,
|
|
1458
|
+
// The task-SET ACCEPTANCE GATE on the `do --remote prd:` path too.
|
|
1459
|
+
taskReviewGate: remoteConfig.review
|
|
1460
|
+
? harnessTaskAcceptanceGate({
|
|
1461
|
+
harness: remoteHarness,
|
|
1462
|
+
agentCmd: remoteConfig.agentCmd,
|
|
1463
|
+
})
|
|
1464
|
+
: undefined,
|
|
1465
|
+
watch: flags.watch === true,
|
|
1466
|
+
color: shouldUseColor(process.stdout),
|
|
1467
|
+
note: (message) => console.error(`>> ${message}`),
|
|
1468
|
+
noteBlock: (message) => console.error(message),
|
|
1469
|
+
};
|
|
1470
|
+
// DISPATCH the variadic grammar (the NO-CHECKOUT forms):
|
|
1471
|
+
// zero args -> AUTO-PICK `count` (default 1) over the MIRROR-SIDE
|
|
1472
|
+
// eligible-pool scan, run SEQUENTIALLY.
|
|
1473
|
+
// one named arg -> the single-item remote pipeline (unchanged).
|
|
1474
|
+
// many named args -> those, IN SEQUENCE (operator's order; no pool).
|
|
1475
|
+
// `--watch` tails ONE session, so it only fits the single-named-item form;
|
|
1476
|
+
// the auto/`-n`/multi forms run many ticks and do not stream a single log.
|
|
1477
|
+
const remoteMulti = args.length === 0 || count !== undefined || args.length > 1;
|
|
1478
|
+
if (remoteMulti && flags.watch === true) {
|
|
1479
|
+
console.error(`error: --watch streams ONE session; it does not combine with the ` +
|
|
1480
|
+
`${form} auto-pick / -n / multi-item forms. Name a single item: ${usage}.`);
|
|
1481
|
+
process.exit(1);
|
|
1482
|
+
}
|
|
1483
|
+
// `--allow-backlog` is EXPLICIT-SINGLE-TASK-ONLY (the leak-fence): it must
|
|
1484
|
+
// not combine with the no-checkout auto-pick / -n / multi-item forms
|
|
1485
|
+
// (those select FROM the pool). Reject the misuse loudly, mirroring the
|
|
1486
|
+
// in-place guard + the `--watch` multi guard above.
|
|
1487
|
+
if (remoteMulti && flags.allowBacklog === true) {
|
|
1488
|
+
console.error(`error: --allow-backlog drives ONE named staged task in place; it does ` +
|
|
1489
|
+
`not combine with the ${form} auto-pick / -n / multi-item forms ` +
|
|
1490
|
+
`(those select from the pool). Name a single task: ${usage} --allow-backlog.`);
|
|
1491
|
+
process.exit(1);
|
|
1492
|
+
}
|
|
1493
|
+
if (args.length === 0 || count !== undefined) {
|
|
1494
|
+
// AUTO-PICK / `-n <x>` over the MIRROR-SIDE eligible-pool scan, SEQUENTIAL.
|
|
1495
|
+
const multi = await performDoRemoteAuto({
|
|
1496
|
+
...baseRemoteOptions,
|
|
1497
|
+
config: remoteConfig,
|
|
1498
|
+
count,
|
|
1499
|
+
warn: (message) => console.error(`>> ${message}`),
|
|
1500
|
+
});
|
|
1501
|
+
console.error(`>> ${multi.message}`);
|
|
1502
|
+
process.exit(multi.exitCode);
|
|
1503
|
+
}
|
|
1504
|
+
if (args.length > 1) {
|
|
1505
|
+
// EXPLICIT named items, IN SEQUENCE (the operator's order; no pool).
|
|
1506
|
+
const multi = await performDoRemoteArgs(args, {
|
|
1507
|
+
...baseRemoteOptions,
|
|
1508
|
+
config: remoteConfig,
|
|
1509
|
+
});
|
|
1510
|
+
console.error(`>> ${multi.message}`);
|
|
1511
|
+
process.exit(multi.exitCode);
|
|
1512
|
+
}
|
|
1513
|
+
// Exactly one named item: the single-item remote pipeline.
|
|
1514
|
+
// `--allow-backlog` rides ONLY this single-named-task call (never the
|
|
1515
|
+
// shared base used by auto-pick / multi) — the leak-fence.
|
|
1516
|
+
const remoteResult = await performDoRemote({
|
|
1517
|
+
...baseRemoteOptions,
|
|
1518
|
+
arg: args[0],
|
|
1519
|
+
allowBacklog: flags.allowBacklog === true,
|
|
1520
|
+
});
|
|
1521
|
+
if (remoteResult.exitCode !== 0) {
|
|
1522
|
+
console.error(`error: ${remoteResult.message}`);
|
|
1523
|
+
}
|
|
1524
|
+
process.exit(remoteResult.exitCode);
|
|
1525
|
+
}
|
|
1526
|
+
// Thread the `do` CLI flags (--harness/--agent-cmd/--pi-bin/--model)
|
|
1527
|
+
// AND the integrate-time mode into the resolved config — the SAME flag
|
|
1528
|
+
// override path `run` uses (do-config.doFlagOverrides reuses
|
|
1529
|
+
// harnessFlagOverrides). Passing only `{integration}` here silently
|
|
1530
|
+
// DROPPED --harness pi etc.; now flag > env > per-repo > global > default
|
|
1531
|
+
// holds for `do` as for `run`.
|
|
1532
|
+
const resolved = resolveRepoConfig({
|
|
1533
|
+
repoPath: cwd,
|
|
1534
|
+
global,
|
|
1535
|
+
override,
|
|
1536
|
+
flags: doFlagOverrides(flags, flagMode),
|
|
1537
|
+
});
|
|
1538
|
+
if (resolved.message) {
|
|
1539
|
+
console.error(`>> ${resolved.message}`);
|
|
1540
|
+
}
|
|
1541
|
+
const config = resolved.config;
|
|
1542
|
+
// The null adapter shells out to agentCmd, so it is required there; the
|
|
1543
|
+
// pi adapter invokes the pi CLI directly and does not consume agentCmd.
|
|
1544
|
+
if (doNeedsAgentCmd(config)) {
|
|
1545
|
+
console.error(`error: ${NO_AGENT_CMD_MESSAGE}`);
|
|
1546
|
+
process.exit(1);
|
|
1547
|
+
}
|
|
1548
|
+
const harness = createHarness({
|
|
1549
|
+
harness: config.harness,
|
|
1550
|
+
piBin: config.piBin,
|
|
1551
|
+
});
|
|
1552
|
+
// The per-item `DoOptions` (everything BUT `arg`) — built ONCE and reused for
|
|
1553
|
+
// the single-item path AND threaded by the multi-item layer to each
|
|
1554
|
+
// sequential `performDo` (do-autopick runs the EXISTING pipeline per item).
|
|
1555
|
+
const baseDoOptions = {
|
|
1556
|
+
cwd,
|
|
1557
|
+
arbiter: flags.arbiter ?? config.defaultArbiter,
|
|
1558
|
+
// The host-only runner IDENTITY (a bot): scopes the runner's git/provider
|
|
1559
|
+
// ops (claim, push, integrate, `gh`) — NEVER the agent launch. Absent ⇒
|
|
1560
|
+
// ambient (today's behaviour). Mapped Config → DoOptions like model/agentCmd.
|
|
1561
|
+
identity: config.identity,
|
|
1562
|
+
// `do prd:<slug>` tasking-gate policy (the task-build path ignores it).
|
|
1563
|
+
autoTask: config.autoTask,
|
|
1564
|
+
// The resolved `promptGuidance` NUDGE namespace (e.g. `testFirst`),
|
|
1565
|
+
// threaded into the worker prompt by performDo → buildAgentPrompt so a
|
|
1566
|
+
// per-repo `promptGuidance.testFirst:true` actually strengthens the
|
|
1567
|
+
// autonomous `do` worker's wrapper line (not just `dorfl prompt`).
|
|
1568
|
+
promptGuidance: resolvePromptGuidance(config),
|
|
1569
|
+
integration: config.integration,
|
|
1570
|
+
// EXPLICIT `--merge` override for the untrusted-origin build-propose rule (task
|
|
1571
|
+
// `untrusted-origin-forces-build-propose`): true ONLY when the operator
|
|
1572
|
+
// typed `--merge` (`flagMode`), never when `merge` came from config — so an
|
|
1573
|
+
// untrusted-origin task still forces propose under a config `merge`.
|
|
1574
|
+
explicitMerge: flagMode === 'merge',
|
|
1575
|
+
// Per-TRANSITION TASKING override: the `do prd:` tasking path threads
|
|
1576
|
+
// `taskingIntegration ?? integration`; the task-build path stays on
|
|
1577
|
+
// `integration`. Unset ⇒ tasking falls back to `integration` (today's behaviour).
|
|
1578
|
+
taskingIntegration: config.taskingIntegration,
|
|
1579
|
+
// TASK-PLACEMENT (`do prd:` tasking output): the configured default rung +
|
|
1580
|
+
// the EXPLICIT operator override `--tasks-land-in` (top of the precedence).
|
|
1581
|
+
// `explicitTasksLandIn` is set ONLY when the flag was typed (mirrors
|
|
1582
|
+
// `explicitMerge`), so an untrusted-origin staging force still wins under a
|
|
1583
|
+
// config default.
|
|
1584
|
+
tasksLandIn: config.tasksLandIn,
|
|
1585
|
+
explicitTasksLandIn: explicitTasksLandInFromFlag(flags.tasksLandIn),
|
|
1586
|
+
// In-place divergence guard override (mirrors --ignore-not-ready).
|
|
1587
|
+
ignoreDivergedMain: flags.ignoreDivergedMain === true,
|
|
1588
|
+
prepare: config.prepare,
|
|
1589
|
+
verify: config.verify,
|
|
1590
|
+
// Single-job build path: gate the REBASED tip (the default) unconditionally.
|
|
1591
|
+
freshWorktreeGate: config.freshWorktreeGate,
|
|
1592
|
+
// Cross-job merge-serialiser CAS-retry cap (prd `land-time-reverify-and-
|
|
1593
|
+
// parallel-merge-ceiling` Story 5 / Applied Answer q1 (a)) — resolved per-repo
|
|
1594
|
+
// and threaded to `performComplete`→`performIntegration`.
|
|
1595
|
+
mergeRetries: config.mergeRetries,
|
|
1596
|
+
noPR: config.noPR,
|
|
1597
|
+
harness,
|
|
1598
|
+
agentCmd: config.agentCmd,
|
|
1599
|
+
model: config.model,
|
|
1600
|
+
// The HOST-ONLY sessions root (resolved Config → DoOptions bridge, like
|
|
1601
|
+
// model/agentCmd): the path generator turns it into
|
|
1602
|
+
// `<sessionsDir>/<id>.jsonl` for `--session`. Without this map the key
|
|
1603
|
+
// resolves but never reaches the launch (a silent no-op).
|
|
1604
|
+
sessionsDir: config.sessionsDir,
|
|
1605
|
+
// Gate 2 (PR/code review) rides inside `complete` (so CI inherits it for
|
|
1606
|
+
// free): when `review` resolves on, run the `review` SKILL as a
|
|
1607
|
+
// fresh-context agent (its OWN harness launch — same adapter + agentCmd,
|
|
1608
|
+
// `reviewModel` via the existing model-routing seam) after the green
|
|
1609
|
+
// verify, before the done-move. A block routes to needs-attention.
|
|
1610
|
+
review: config.review,
|
|
1611
|
+
reviewModel: config.reviewModel,
|
|
1612
|
+
reviewMaxRounds: config.reviewMaxRounds,
|
|
1613
|
+
reviewGate: config.review
|
|
1614
|
+
? harnessReviewGate({ harness, agentCmd: config.agentCmd })
|
|
1615
|
+
: undefined,
|
|
1616
|
+
// The tasker IMPROVER loop on the `do prd:` tasking path is ON by default
|
|
1617
|
+
// (auto-tasking has no `verify` floor — the loop is the task path's quality
|
|
1618
|
+
// engine). `--tasker-loop`/`--no-tasker-loop` gates wiring the seam;
|
|
1619
|
+
// `taskerLoopMax`/`taskerLoopModel` resolve per-repo (flag > env > per-repo
|
|
1620
|
+
// > global > default); the task-build path ignores all of these. DISTINCT
|
|
1621
|
+
// from the acceptance gate's `--review*` family.
|
|
1622
|
+
reviewLoop: config.taskerLoop
|
|
1623
|
+
? harnessTaskReviewGate({
|
|
1624
|
+
harness,
|
|
1625
|
+
agentCmd: config.agentCmd,
|
|
1626
|
+
})
|
|
1627
|
+
: undefined,
|
|
1628
|
+
taskerLoopMax: config.taskerLoopMax,
|
|
1629
|
+
taskerLoopModel: config.taskerLoopModel,
|
|
1630
|
+
// The task-SET ACCEPTANCE GATE (slice-acceptance-gate): the task-path
|
|
1631
|
+
// mirror of Gate-2, on the SAME `--review` family (so `--no-review` skips
|
|
1632
|
+
// it). ONE-SHOT (no rounds); production wires the task-SET-prompt gate.
|
|
1633
|
+
taskReviewGate: config.review
|
|
1634
|
+
? harnessTaskAcceptanceGate({ harness, agentCmd: config.agentCmd })
|
|
1635
|
+
: undefined,
|
|
1636
|
+
// `--watch`: tail the pi session log live (pi harness only; the
|
|
1637
|
+
// performDo guard errors clearly on any other adapter). READ-ONLY.
|
|
1638
|
+
watch: flags.watch === true,
|
|
1639
|
+
color: shouldUseColor(process.stdout),
|
|
1640
|
+
note: (message) => console.error(`>> ${message}`),
|
|
1641
|
+
noteBlock: (message) => console.error(message),
|
|
1642
|
+
};
|
|
1643
|
+
// `--allow-backlog` is EXPLICIT-SINGLE-TASK-ONLY (prd
|
|
1644
|
+
// `do-allow-backlog-drive-staged-tasks-without-promotion`, decision 4): it
|
|
1645
|
+
// drives ONE named staged task in place. It must NOT combine with the
|
|
1646
|
+
// AUTO-PICK (zero-args / -n) or MULTI-ITEM forms — those select FROM the
|
|
1647
|
+
// pool, and letting the flag widen a pool selection is exactly the
|
|
1648
|
+
// competition-bug-one-layer-down the fence forbids. Reject the misuse loudly
|
|
1649
|
+
// (mirroring the `--watch` multi guard) rather than silently widen a pool.
|
|
1650
|
+
if (flags.allowBacklog === true &&
|
|
1651
|
+
(args.length !== 1 || count !== undefined)) {
|
|
1652
|
+
console.error('error: --allow-backlog drives ONE named staged task in place; it does ' +
|
|
1653
|
+
'not combine with auto-pick / -n / multi-item forms (those select from ' +
|
|
1654
|
+
`the pool). Name a single task: dorfl do task:<slug> --allow-backlog.`);
|
|
1655
|
+
process.exit(1);
|
|
1656
|
+
}
|
|
1657
|
+
// DISPATCH the variadic grammar (in-place forms):
|
|
1658
|
+
// zero args -> AUTO-PICK `count` (default 1) across the two pools
|
|
1659
|
+
// (ordered by selectionOrder; default drain = tasks-first)
|
|
1660
|
+
// one named arg -> the single-item pipeline (unchanged from do-in-place)
|
|
1661
|
+
// many named args -> those, IN SEQUENCE (operator's order; no pool)
|
|
1662
|
+
// Auto-pick / multi-arg run the EXISTING `performDo` pipeline per item,
|
|
1663
|
+
// sequentially (`do` is sequential; parallelism is `run`).
|
|
1664
|
+
if (args.length === 0) {
|
|
1665
|
+
const multi = await performDoAuto({
|
|
1666
|
+
...baseDoOptions,
|
|
1667
|
+
config,
|
|
1668
|
+
override,
|
|
1669
|
+
count,
|
|
1670
|
+
});
|
|
1671
|
+
console.error(`>> ${multi.message}`);
|
|
1672
|
+
process.exit(multi.exitCode);
|
|
1673
|
+
}
|
|
1674
|
+
if (args.length > 1) {
|
|
1675
|
+
const multi = await performDoArgs(args, {
|
|
1676
|
+
...baseDoOptions,
|
|
1677
|
+
config,
|
|
1678
|
+
});
|
|
1679
|
+
console.error(`>> ${multi.message}`);
|
|
1680
|
+
process.exit(multi.exitCode);
|
|
1681
|
+
}
|
|
1682
|
+
// Exactly one named item: the single-item in-place pipeline (do-in-place).
|
|
1683
|
+
// `--allow-backlog` rides ONLY this single-named-task call (never the
|
|
1684
|
+
// auto-pick / multi base above) — the leak-fence: the flag is read from the
|
|
1685
|
+
// typed CLI flag here, not from config/env, and never reaches a pool path.
|
|
1686
|
+
const result = await performDo({
|
|
1687
|
+
...baseDoOptions,
|
|
1688
|
+
arg: args[0],
|
|
1689
|
+
allowBacklog: flags.allowBacklog === true,
|
|
1690
|
+
});
|
|
1691
|
+
if (result.exitCode !== 0) {
|
|
1692
|
+
console.error(`error: ${result.message}`);
|
|
1693
|
+
}
|
|
1694
|
+
process.exit(result.exitCode);
|
|
1695
|
+
});
|
|
1696
|
+
// `advance` — the SIBLING top-level verb (NOT a `do` subcommand; `do`
|
|
1697
|
+
// subcommands + a standalone `task` verb are REJECTED in prd `advance-loop`).
|
|
1698
|
+
// It reuses the SAME shared `prefix:arg` resolver `do` uses, EXTENDED with the
|
|
1699
|
+
// `obs:` namespace, and wires the classify → lock → execute SKELETON: classify
|
|
1700
|
+
// the rung (read-only, no model, no lock), take the `advancing` CAS borrow, then
|
|
1701
|
+
// dispatch winner-only — build/task rungs ORCHESTRATE `do`/`do prd:` (never a
|
|
1702
|
+
// duplicate), surface/apply/triage dispatch to a named executor seam later
|
|
1703
|
+
// tasks fill. The DRIVERS (one-shot/loop) + `-n` + per-action gates and the
|
|
1704
|
+
// rung BODIES are LATER tasks; the bare eligible-SET form errors clearly here.
|
|
1705
|
+
program
|
|
1706
|
+
.command('advance')
|
|
1707
|
+
.helpGroup(HEADLINE_GROUP)
|
|
1708
|
+
.description('Advance work/ item(s) one lifecycle rung toward ready/built (PRD advance-loop), the SEQUENTIAL one-shot driver over the advance tick. advance <slug> (bare = the task) | advance spec:<slug> (the spec tasking rung; the legacy prd:<slug> is still accepted) | advance obs:<slug> (triage an observation) | advance (auto-pick one eligible) | advance <a> <b> (those, in sequence) | advance -n <x> (x eligible, in sequence). Each item: classify (read-only, no model, no lock) → take the `advancing` CAS lock → dispatch winner-only — build/task rungs ORCHESTRATE `do`/`do spec:`, surface/apply always run, triage respects observationTriage (off|ask|auto). The bare/`-n` selection respects the per-action gates (build→autoBuild, task→autoTask, triage→observationTriage); `-n` is ALWAYS sequential (parallelism is `run` / the CI matrix).')
|
|
1709
|
+
.argument('[slugs...]', 'the item(s) to advance: bare (= the task), task:<slug>, spec:<slug> (the legacy prd:<slug> is still accepted), or obs:<slug> (an observation). Zero args = auto-pick one eligible; multiple = advance them in sequence.')
|
|
1710
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
1711
|
+
.option('--arbiter <remote>', 'name of the arbiter git remote (default: per-repo/global defaultArbiter)')
|
|
1712
|
+
.option('-n, --number <x>', 'AUTO-PICK x eligible items and advance them IN SEQUENCE (ordered by selectionOrder, default drain = tasks-first then prds-to-task). Sequential — never a parallelism knob (that is `run` / the CI matrix). Mutually exclusive with naming items.')
|
|
1713
|
+
.option('--isolated', "advance in an ISOLATED worktree off THIS repo's arbiter (inferred from cwd) instead of taking over the current checkout, then integrate + reap — the in-place-but-isolated form. Shares the same grammar: a single named item, multiple named items (in sequence), AND -n/auto-pick over the mirror-side eligible-pool scan. Always SEQUENTIAL (parallelism is `run` / the CI matrix). Lets you advance from a busy/dirty checkout or anywhere with a participating arbiter.")
|
|
1714
|
+
.option('--selection-order <order>', 'order the auto-pick pools (build/task/surface/triage; apply is always first): a preset keyword (drain (default) | groom) or an explicit comma-separated pool list. Resolved flag > env > per-repo > global > default.')
|
|
1715
|
+
.option('--observation-triage <mode>', 'the observation-inbox gate (off|ask|auto): off (default) leaves observations untouched (the triage pool is dropped from auto-pick); ask surfaces a promote/keep/delete question for each untriaged observation; auto auto-disposes the no-question cases (duplicate/map) and asks about the rest. Resolved flag > env > per-repo > global > default. An explicit `advance obs:<slug>` bypasses the selection gate and runs in ask-mode (auto-disposes only under `auto`).')
|
|
1716
|
+
.option('--surface-blockers', 'the declared-blocked-work gate (the orthogonal peer of --observation-triage): render a task/prd carrying needsAnswers:true into an answerable question sidecar (the needsAnswers-blocked pool is enumerated into auto-pick). Resolved flag > env > per-repo > global > default off. An explicit `advance <slug>`/`advance prd:<slug>` bypasses this selection gate and surfaces regardless. Does NOT gate apply (an answered sidecar still applies) or needs-attention (always on).')
|
|
1717
|
+
.option('--no-surface-blockers', 'leave a needsAnswers:true task/prd silently blocked (default; the blocked pool is dropped from auto-pick)')
|
|
1718
|
+
.option('--strict-merge-approval', 'opt in to the host-agnostic "dismiss stale approvals on base change" discipline (prd `land-time-reverify-and-parallel-merge-ceiling` sidecar OQ6): when the merge-base CHANGED between the human’s merge-answer and the apply step, RE-SURFACE the merge-question (clear the answer back to no-answer; re-author the question on main/runner under the advancing lock) instead of auto-landing on a green re-verify. Default OFF (a green re-verify is trusted as sufficient; honour the prior answer). Story #16’s RED-re-verify refusal is UNCHANGED and independent of this flag. Resolved flag > env > per-repo > global > default off.')
|
|
1719
|
+
.option('--no-strict-merge-approval', 'honour the prior merge-answer and land when the rebased tip re-verifies GREEN even if the merge-base changed (default; the cheap green-re-verify-is-enough path)')
|
|
1720
|
+
.option('--merge-questions <mode>', 'the merge-question SURFACER gate (off|ask|auto): off drops the surfacer (only for a repo that lands by some other means); ask (default) enumerates unmerged `work/*` branches and surfaces a merge-question sidecar a human answers; auto self-supplies the `merge` answer and lands via the SAME deterministic apply-time re-verify (the merge-mode-like fast path). SEPARATE axis from --observation-triage with a HIGHER default (a dropped merge-question means pushed work never lands). Resolved flag > env > per-repo > global > default ask.')
|
|
1721
|
+
.option('--merge', 'integrate the advanced item(s) in merge mode this invocation (mutually exclusive with --propose; overrides config). The CI merge shape is a SINGLE SEQUENTIAL job, so this rides the `-n`/named-sequence path, never the matrix.')
|
|
1722
|
+
.option('--propose', 'integrate the advanced item(s) in propose mode this invocation (default; mutually exclusive with --merge; overrides config). The CI propose shape is the parallel matrix (one PR per item).')
|
|
1723
|
+
.option('--tasks-land-in <where>', 'where `advance prd:<slug>` tasking output lands: `pre-backlog` (staged) or `ready` (the agent POOL). The EXPLICIT operator override at the top of the placement precedence. Resolved flag > env (DORFL_TASKS_LAND_IN) > per-repo > global > built-in.')
|
|
1724
|
+
.option('--watch', "stream the build agent's high-signal events live by tailing the pi session log (requires harness: pi; READ-ONLY observer — does not change outcome/gate/git). The same view `do --watch` gives, threaded through the build rung; CI uses it so the job log shows the agent working instead of freezing.")
|
|
1725
|
+
.action(async (rawSlugs, flags) => {
|
|
1726
|
+
// Variadic grammar (mirrors `do`): zero args = AUTO-PICK; one = the single
|
|
1727
|
+
// named item; many = those, IN SEQUENCE. `-n <x>` is the auto-pick count
|
|
1728
|
+
// (ALWAYS sequential, US #25).
|
|
1729
|
+
const args = rawSlugs ?? [];
|
|
1730
|
+
// `-n <x>` parse + validation — the AUTO-PICK count (sequential), mutually
|
|
1731
|
+
// exclusive with NAMING items (the SAME contract `do -n` enforces).
|
|
1732
|
+
let count;
|
|
1733
|
+
if (flags.number !== undefined) {
|
|
1734
|
+
const n = Number(flags.number);
|
|
1735
|
+
if (flags.number.trim() === '' || !Number.isInteger(n) || n < 1) {
|
|
1736
|
+
console.error(`error: -n/--number must be a positive integer (got '${flags.number}').`);
|
|
1737
|
+
process.exit(1);
|
|
1738
|
+
}
|
|
1739
|
+
if (args.length > 0) {
|
|
1740
|
+
console.error('error: -n/--number auto-picks a COUNT of eligible items; do not also ' +
|
|
1741
|
+
'name items. Use `advance -n <x>` OR `advance <a> <b> ...`, not both.');
|
|
1742
|
+
process.exit(1);
|
|
1743
|
+
}
|
|
1744
|
+
count = n;
|
|
1745
|
+
}
|
|
1746
|
+
const cwd = process.cwd();
|
|
1747
|
+
const { global, override } = loadGlobalAndOverride(flags.config);
|
|
1748
|
+
// Resolve the integration mode this invocation asks for, highest first:
|
|
1749
|
+
// --merge/--propose flag > per-repo .dorfl.json > global > default.
|
|
1750
|
+
// The SAME chain `do`/`complete` use (via `integrationFromFlags`), so the
|
|
1751
|
+
// human, the autonomous runner, and the CI workflow all resolve the SAME
|
|
1752
|
+
// order. This is what ties the CI dispatch `integrationMode` to the actual
|
|
1753
|
+
// open-PR-vs-merge-to-main behaviour: the propose-matrix legs pass
|
|
1754
|
+
// `--propose` and the single sequential merge job passes `--merge`, so the
|
|
1755
|
+
// integration mode can never DESYNC from the job shape the input selected.
|
|
1756
|
+
let flagMode;
|
|
1757
|
+
try {
|
|
1758
|
+
flagMode = integrationFromFlags(flags);
|
|
1759
|
+
}
|
|
1760
|
+
catch (err) {
|
|
1761
|
+
console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1762
|
+
process.exit(1);
|
|
1763
|
+
}
|
|
1764
|
+
// Build the flag overrides (the `--observation-triage` enum FAILS LOUDLY on a
|
|
1765
|
+
// typo, like the env coercion) before resolving — a bad gate value is a clean
|
|
1766
|
+
// usage error, never silently dropped.
|
|
1767
|
+
let doOverrides;
|
|
1768
|
+
try {
|
|
1769
|
+
doOverrides = doFlagOverrides(flags, flagMode);
|
|
1770
|
+
}
|
|
1771
|
+
catch (err) {
|
|
1772
|
+
console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1773
|
+
process.exit(1);
|
|
1774
|
+
}
|
|
1775
|
+
// `advance --isolated`: run the advance TICK in an ISOLATED worktree off
|
|
1776
|
+
// THIS repo's arbiter (resolved from cwd), then integrate + reap — the
|
|
1777
|
+
// in-place-but-isolated form, the SAME ergonomic `do --isolated` has. We
|
|
1778
|
+
// REUSE `do --isolated`'s arbiter-from-cwd resolver + the isolation substrate
|
|
1779
|
+
// (`ensureMirror` + the job-worktree `doDriver` + reap), threading the
|
|
1780
|
+
// arbiter URL into the NEW isolated advance-tick runner. `--isolated` is the
|
|
1781
|
+
// only ISOLATION axis here: `advance --remote <url>` is a SEPARATE concern
|
|
1782
|
+
// (the action already TYPES `flags.remote` via `DoFlags`, but no `--remote`
|
|
1783
|
+
// plumbing exists on `advance` — see `## Decisions`), so `--isolated` always
|
|
1784
|
+
// resolves the arbiter from the CWD.
|
|
1785
|
+
if (flags.isolated === true) {
|
|
1786
|
+
// BOOTSTRAP resolution (global + flags, no per-repo layer) supplies the
|
|
1787
|
+
// host-only keys needed to even reach the arbiter (`workspacesDir`,
|
|
1788
|
+
// `identity`), exactly as `do --isolated` bootstraps them.
|
|
1789
|
+
const bootstrap = resolveGlobalConfig(global, doOverrides);
|
|
1790
|
+
const arbiterName = flags.arbiter ??
|
|
1791
|
+
resolveDefaultArbiterForCwd(cwd, global, doOverrides, override);
|
|
1792
|
+
const arbiterUrl = resolveArbiterUrlFromCheckout(cwd, arbiterName, identityEnv(bootstrap.identity, process.env));
|
|
1793
|
+
if (arbiterUrl === undefined) {
|
|
1794
|
+
// The SAME clear "isolated against what?" error `do --isolated` gives —
|
|
1795
|
+
// naming `--remote <url>` as the foreign-repo alternative, NOT a
|
|
1796
|
+
// downstream URL-parse failure.
|
|
1797
|
+
console.error(`error: --isolated advances in a worktree off this repo's arbiter ` +
|
|
1798
|
+
`('${arbiterName}'), but no such arbiter remote is configured/found ` +
|
|
1799
|
+
`here. Run inside a participating repo (a clone with an arbiter ` +
|
|
1800
|
+
`remote), or use --remote <url> to target another repo.`);
|
|
1801
|
+
process.exit(1);
|
|
1802
|
+
}
|
|
1803
|
+
// Source the arbiter's COMMITTED `.dorfl.json` from `<arbiter>/main`
|
|
1804
|
+
// via the hub mirror + layer ONLY its whitelisted keys — the SAME
|
|
1805
|
+
// resolution `do --isolated` uses, so the gate family (autoBuild/autoTask/
|
|
1806
|
+
// observationTriage/surfaceBlockers) + selectionOrder + integration resolve
|
|
1807
|
+
// off the arbiter exactly as the in-place advance resolves them off cwd.
|
|
1808
|
+
const remoteConfig = resolveRemoteRepoConfig({
|
|
1809
|
+
remote: arbiterUrl,
|
|
1810
|
+
workspacesDir: bootstrap.workspacesDir,
|
|
1811
|
+
global,
|
|
1812
|
+
flags: doOverrides,
|
|
1813
|
+
identity: bootstrap.identity,
|
|
1814
|
+
note: (message) => console.error(`>> ${message}`),
|
|
1815
|
+
override,
|
|
1816
|
+
});
|
|
1817
|
+
if (doNeedsAgentCmd(remoteConfig)) {
|
|
1818
|
+
console.error(`error: ${NO_AGENT_CMD_MESSAGE}`);
|
|
1819
|
+
process.exit(1);
|
|
1820
|
+
}
|
|
1821
|
+
const isoHarness = createHarness({
|
|
1822
|
+
harness: remoteConfig.harness,
|
|
1823
|
+
piBin: remoteConfig.piBin,
|
|
1824
|
+
});
|
|
1825
|
+
// The base `do` options the build/task rungs ORCHESTRATE through the
|
|
1826
|
+
// INJECTED job-worktree driver (the isolated advance-tick runner wires it).
|
|
1827
|
+
const isoDoOptions = {
|
|
1828
|
+
cwd,
|
|
1829
|
+
// `--watch`: stream the build agent's session live (pi harness only;
|
|
1830
|
+
// validated in `performDo`). Threaded through the orchestrated build rung
|
|
1831
|
+
// so `advance --isolated --watch` (and CI) shows the agent working.
|
|
1832
|
+
watch: flags.watch === true,
|
|
1833
|
+
arbiter: flags.arbiter ?? remoteConfig.defaultArbiter,
|
|
1834
|
+
identity: remoteConfig.identity,
|
|
1835
|
+
autoTask: remoteConfig.autoTask,
|
|
1836
|
+
integration: remoteConfig.integration,
|
|
1837
|
+
// EXPLICIT `--merge` override for the untrusted-origin build-propose rule.
|
|
1838
|
+
explicitMerge: flagMode === 'merge',
|
|
1839
|
+
// Per-TRANSITION TASKING override (the isolated `do --remote prd:` path).
|
|
1840
|
+
taskingIntegration: remoteConfig.taskingIntegration,
|
|
1841
|
+
// TASK-PLACEMENT: configured default + EXPLICIT `--tasks-land-in` override
|
|
1842
|
+
// (set only when typed, mirroring `explicitMerge`).
|
|
1843
|
+
tasksLandIn: remoteConfig.tasksLandIn,
|
|
1844
|
+
explicitTasksLandIn: explicitTasksLandInFromFlag(flags.tasksLandIn),
|
|
1845
|
+
prepare: remoteConfig.prepare,
|
|
1846
|
+
verify: remoteConfig.verify,
|
|
1847
|
+
// Single-job build path: gate the REBASED tip (the default) unconditionally.
|
|
1848
|
+
freshWorktreeGate: remoteConfig.freshWorktreeGate,
|
|
1849
|
+
// Cross-job merge-serialiser CAS-retry cap — prd
|
|
1850
|
+
// `land-time-reverify-and-parallel-merge-ceiling` Story 5.
|
|
1851
|
+
mergeRetries: remoteConfig.mergeRetries,
|
|
1852
|
+
noPR: remoteConfig.noPR,
|
|
1853
|
+
harness: isoHarness,
|
|
1854
|
+
agentCmd: remoteConfig.agentCmd,
|
|
1855
|
+
model: remoteConfig.model,
|
|
1856
|
+
sessionsDir: remoteConfig.sessionsDir,
|
|
1857
|
+
review: remoteConfig.review,
|
|
1858
|
+
reviewModel: remoteConfig.reviewModel,
|
|
1859
|
+
reviewMaxRounds: remoteConfig.reviewMaxRounds,
|
|
1860
|
+
reviewGate: remoteConfig.review
|
|
1861
|
+
? harnessReviewGate({
|
|
1862
|
+
harness: isoHarness,
|
|
1863
|
+
agentCmd: remoteConfig.agentCmd,
|
|
1864
|
+
})
|
|
1865
|
+
: undefined,
|
|
1866
|
+
reviewLoop: remoteConfig.taskerLoop
|
|
1867
|
+
? harnessTaskReviewGate({
|
|
1868
|
+
harness: isoHarness,
|
|
1869
|
+
agentCmd: remoteConfig.agentCmd,
|
|
1870
|
+
})
|
|
1871
|
+
: undefined,
|
|
1872
|
+
taskerLoopMax: remoteConfig.taskerLoopMax,
|
|
1873
|
+
taskerLoopModel: remoteConfig.taskerLoopModel,
|
|
1874
|
+
taskReviewGate: remoteConfig.review
|
|
1875
|
+
? harnessTaskAcceptanceGate({
|
|
1876
|
+
harness: isoHarness,
|
|
1877
|
+
agentCmd: remoteConfig.agentCmd,
|
|
1878
|
+
})
|
|
1879
|
+
: undefined,
|
|
1880
|
+
color: shouldUseColor(process.stdout),
|
|
1881
|
+
note: (message) => console.error(`>> ${message}`),
|
|
1882
|
+
noteBlock: (message) => console.error(message),
|
|
1883
|
+
};
|
|
1884
|
+
// The shared per-item ISOLATED advance CONTEXT (everything BUT `arg` and
|
|
1885
|
+
// `cwd`/`doDriver`, which the runner supplies from the isolated clone).
|
|
1886
|
+
const isoContext = {
|
|
1887
|
+
remote: arbiterUrl,
|
|
1888
|
+
workspacesDir: remoteConfig.workspacesDir,
|
|
1889
|
+
arbiter: flags.arbiter ?? remoteConfig.defaultArbiter,
|
|
1890
|
+
doOptions: isoDoOptions,
|
|
1891
|
+
surfaceGate: harnessSurfaceGate({
|
|
1892
|
+
harness: isoHarness,
|
|
1893
|
+
agentCmd: remoteConfig.agentCmd,
|
|
1894
|
+
}),
|
|
1895
|
+
surfaceModel: remoteConfig.model,
|
|
1896
|
+
applyDecide: harnessApplyDecider({
|
|
1897
|
+
harness: isoHarness,
|
|
1898
|
+
agentCmd: remoteConfig.agentCmd,
|
|
1899
|
+
}),
|
|
1900
|
+
applyModel: remoteConfig.model,
|
|
1901
|
+
observationTriage: remoteConfig.observationTriage,
|
|
1902
|
+
triageGate: harnessTriageGate({
|
|
1903
|
+
harness: isoHarness,
|
|
1904
|
+
agentCmd: remoteConfig.agentCmd,
|
|
1905
|
+
}),
|
|
1906
|
+
triageModel: remoteConfig.model,
|
|
1907
|
+
note: (message) => console.error(`>> ${message}`),
|
|
1908
|
+
env: process.env,
|
|
1909
|
+
};
|
|
1910
|
+
// DISPATCH the variadic grammar, ISOLATED + SEQUENTIAL (mirrors
|
|
1911
|
+
// `do --isolated`): zero args / `-n` -> AUTO-PICK over the mirror-side
|
|
1912
|
+
// eligible-pool scan; many named -> those in sequence; one named -> the
|
|
1913
|
+
// single isolated tick. `-n` stays ALWAYS SEQUENTIAL (US #25).
|
|
1914
|
+
if (args.length === 0 || count !== undefined) {
|
|
1915
|
+
const multi = await performAdvanceIsolatedAuto({
|
|
1916
|
+
...isoContext,
|
|
1917
|
+
config: remoteConfig,
|
|
1918
|
+
count,
|
|
1919
|
+
warn: (message) => console.error(`>> ${message}`),
|
|
1920
|
+
lifecycleGates: {
|
|
1921
|
+
triage: remoteConfig.observationTriage !== 'off',
|
|
1922
|
+
surface: remoteConfig.surfaceBlockers,
|
|
1923
|
+
surfaceStaging: remoteConfig.surfaceStaging,
|
|
1924
|
+
},
|
|
1925
|
+
});
|
|
1926
|
+
console.error(`>> ${multi.message}`);
|
|
1927
|
+
process.exit(multi.exitCode);
|
|
1928
|
+
}
|
|
1929
|
+
if (args.length > 1) {
|
|
1930
|
+
const multi = await performAdvanceIsolatedArgs(args, {
|
|
1931
|
+
...isoContext,
|
|
1932
|
+
config: remoteConfig,
|
|
1933
|
+
});
|
|
1934
|
+
console.error(`>> ${multi.message}`);
|
|
1935
|
+
process.exit(multi.exitCode);
|
|
1936
|
+
}
|
|
1937
|
+
// Exactly one named item: the single ISOLATED advance tick.
|
|
1938
|
+
const result = await performAdvanceIsolated({
|
|
1939
|
+
...isoContext,
|
|
1940
|
+
arg: args[0],
|
|
1941
|
+
});
|
|
1942
|
+
if (result.exitCode !== 0) {
|
|
1943
|
+
console.error(`error: ${result.message}`);
|
|
1944
|
+
}
|
|
1945
|
+
process.exit(result.exitCode);
|
|
1946
|
+
}
|
|
1947
|
+
const resolved = resolveRepoConfig({
|
|
1948
|
+
repoPath: cwd,
|
|
1949
|
+
global,
|
|
1950
|
+
override,
|
|
1951
|
+
flags: doOverrides,
|
|
1952
|
+
});
|
|
1953
|
+
if (resolved.message) {
|
|
1954
|
+
console.error(`>> ${resolved.message}`);
|
|
1955
|
+
}
|
|
1956
|
+
const config = resolved.config;
|
|
1957
|
+
const harness = createHarness({
|
|
1958
|
+
harness: config.harness,
|
|
1959
|
+
piBin: config.piBin,
|
|
1960
|
+
});
|
|
1961
|
+
// The base `do` options the build/task rungs ORCHESTRATE `performDo` with
|
|
1962
|
+
// (the ONE build path / ONE task path). `advance` is a driver ON TOP — it
|
|
1963
|
+
// hands the resolved arg to `performDo`, never re-implementing it.
|
|
1964
|
+
const doOptions = {
|
|
1965
|
+
cwd,
|
|
1966
|
+
// `--watch`: stream the build agent's session live (pi harness only;
|
|
1967
|
+
// validated in `performDo`). Threaded through the orchestrated build rung
|
|
1968
|
+
// so `advance --watch` (and CI) shows the agent working, not a frozen log.
|
|
1969
|
+
watch: flags.watch === true,
|
|
1970
|
+
arbiter: flags.arbiter ?? config.defaultArbiter,
|
|
1971
|
+
identity: config.identity,
|
|
1972
|
+
autoTask: config.autoTask,
|
|
1973
|
+
integration: config.integration,
|
|
1974
|
+
// EXPLICIT `--merge` override for the untrusted-origin build-propose rule (a
|
|
1975
|
+
// bare `advance` auto-pick passes no flag ⇒ unset ⇒ untrusted forces propose).
|
|
1976
|
+
explicitMerge: flagMode === 'merge',
|
|
1977
|
+
// Per-TRANSITION TASKING override (the `do prd:` tasking path threads
|
|
1978
|
+
// `taskingIntegration ?? integration`; the build path stays on `integration`).
|
|
1979
|
+
taskingIntegration: config.taskingIntegration,
|
|
1980
|
+
// TASK-PLACEMENT: configured default + EXPLICIT `--tasks-land-in` override
|
|
1981
|
+
// (set only when typed, mirroring `explicitMerge`).
|
|
1982
|
+
tasksLandIn: config.tasksLandIn,
|
|
1983
|
+
explicitTasksLandIn: explicitTasksLandInFromFlag(flags.tasksLandIn),
|
|
1984
|
+
prepare: config.prepare,
|
|
1985
|
+
verify: config.verify,
|
|
1986
|
+
// Single-job build path: gate the REBASED tip (the default) unconditionally.
|
|
1987
|
+
freshWorktreeGate: config.freshWorktreeGate,
|
|
1988
|
+
// Cross-job merge-serialiser CAS-retry cap (prd `land-time-reverify-and-
|
|
1989
|
+
// parallel-merge-ceiling` Story 5 / Applied Answer q1 (a)).
|
|
1990
|
+
mergeRetries: config.mergeRetries,
|
|
1991
|
+
noPR: config.noPR,
|
|
1992
|
+
harness,
|
|
1993
|
+
agentCmd: config.agentCmd,
|
|
1994
|
+
model: config.model,
|
|
1995
|
+
sessionsDir: config.sessionsDir,
|
|
1996
|
+
review: config.review,
|
|
1997
|
+
reviewModel: config.reviewModel,
|
|
1998
|
+
reviewMaxRounds: config.reviewMaxRounds,
|
|
1999
|
+
reviewGate: config.review
|
|
2000
|
+
? harnessReviewGate({ harness, agentCmd: config.agentCmd })
|
|
2001
|
+
: undefined,
|
|
2002
|
+
reviewLoop: config.taskerLoop
|
|
2003
|
+
? harnessTaskReviewGate({ harness, agentCmd: config.agentCmd })
|
|
2004
|
+
: undefined,
|
|
2005
|
+
taskerLoopMax: config.taskerLoopMax,
|
|
2006
|
+
taskerLoopModel: config.taskerLoopModel,
|
|
2007
|
+
taskReviewGate: config.review
|
|
2008
|
+
? harnessTaskAcceptanceGate({ harness, agentCmd: config.agentCmd })
|
|
2009
|
+
: undefined,
|
|
2010
|
+
color: shouldUseColor(process.stdout),
|
|
2011
|
+
note: (message) => console.error(`>> ${message}`),
|
|
2012
|
+
noteBlock: (message) => console.error(message),
|
|
2013
|
+
};
|
|
2014
|
+
// The shared per-item advance CONTEXT (everything BUT `arg`) — built ONCE
|
|
2015
|
+
// and threaded by the one-shot DRIVER to each sequential tick. The SURFACE
|
|
2016
|
+
// rung spawns `surface-questions` fresh-context through the SAME harness seam
|
|
2017
|
+
// the review gate uses (the engine then PERSISTS); the TRIAGE rung is
|
|
2018
|
+
// question-gated by default; `observationTriage: 'auto'` enables the
|
|
2019
|
+
// conservative auto-disposition exception (`ask`/`off` surface the question).
|
|
2020
|
+
// Surface + apply stay ALWAYS allowed regardless of the gate family.
|
|
2021
|
+
const advanceContext = {
|
|
2022
|
+
cwd,
|
|
2023
|
+
arbiter: flags.arbiter ?? config.defaultArbiter,
|
|
2024
|
+
doOptions,
|
|
2025
|
+
surfaceGate: harnessSurfaceGate({ harness, agentCmd: config.agentCmd }),
|
|
2026
|
+
surfaceModel: config.model,
|
|
2027
|
+
applyDecide: harnessApplyDecider({ harness, agentCmd: config.agentCmd }),
|
|
2028
|
+
applyModel: config.model,
|
|
2029
|
+
observationTriage: config.observationTriage,
|
|
2030
|
+
triageGate: harnessTriageGate({ harness, agentCmd: config.agentCmd }),
|
|
2031
|
+
triageModel: config.model,
|
|
2032
|
+
note: (message) => console.error(`>> ${message}`),
|
|
2033
|
+
};
|
|
2034
|
+
// `--watch` tails ONE pi session, so it only fits the single-named-item form
|
|
2035
|
+
// (mirrors `do --watch`). The auto-pick / `-n` / multi-item forms run many
|
|
2036
|
+
// ticks in sequence and would tail several logs; reject rather than silently
|
|
2037
|
+
// stream only one. The CI propose matrix names a single item per leg, so it
|
|
2038
|
+
// satisfies this; the `-n` merge job must NOT pass `--watch`.
|
|
2039
|
+
const advanceMulti = args.length === 0 || count !== undefined || args.length > 1;
|
|
2040
|
+
if (advanceMulti && flags.watch === true) {
|
|
2041
|
+
console.error('error: --watch streams ONE session; it does not combine with the ' +
|
|
2042
|
+
'auto-pick / -n / multi-item forms. Name a single item.');
|
|
2043
|
+
process.exit(1);
|
|
2044
|
+
}
|
|
2045
|
+
// DISPATCH the variadic grammar (the one-shot SEQUENTIAL driver):
|
|
2046
|
+
// zero args -> AUTO-PICK `count` (default 1) over the eligible pool
|
|
2047
|
+
// (tasks-first then prds-to-task; per-action gates
|
|
2048
|
+
// respected by the SELECTION layer; ordered by selectionOrder).
|
|
2049
|
+
// one named arg -> the single-item tick (the always-allowed surface/apply
|
|
2050
|
+
// path runs regardless of the gate family).
|
|
2051
|
+
// many named args -> those, IN SEQUENCE (operator's order; no pool).
|
|
2052
|
+
// `-n` / auto-pick / multi-arg all run the EXISTING tick per item,
|
|
2053
|
+
// SEQUENTIALLY (parallelism is `run` / the CI matrix, never `-n`).
|
|
2054
|
+
if (args.length === 0) {
|
|
2055
|
+
const multi = await performAdvanceAuto({
|
|
2056
|
+
...advanceContext,
|
|
2057
|
+
config,
|
|
2058
|
+
override,
|
|
2059
|
+
count,
|
|
2060
|
+
// The SELECTION-layer gates: `observationTriage != off` enumerates the
|
|
2061
|
+
// observation (triage) pool into auto-pick; `surfaceBlockers` enumerates
|
|
2062
|
+
// the `needsAnswers`-blocked (surface) pool. `off`/`false` drops the
|
|
2063
|
+
// respective pool (its item is left untouched / silently blocked). The
|
|
2064
|
+
// two gates are orthogonal peers. The triage rung's ask-vs-auto
|
|
2065
|
+
// distinction is read inside the tick from `observationTriage` (threaded
|
|
2066
|
+
// on `advanceContext`). Apply (consume) is always-on (never gated here).
|
|
2067
|
+
lifecycleGates: {
|
|
2068
|
+
triage: config.observationTriage !== 'off',
|
|
2069
|
+
surface: config.surfaceBlockers,
|
|
2070
|
+
surfaceStaging: config.surfaceStaging,
|
|
2071
|
+
},
|
|
2072
|
+
});
|
|
2073
|
+
console.error(`>> ${multi.message}`);
|
|
2074
|
+
process.exit(multi.exitCode);
|
|
2075
|
+
}
|
|
2076
|
+
if (args.length > 1) {
|
|
2077
|
+
const multi = await performAdvanceArgs(args, {
|
|
2078
|
+
...advanceContext,
|
|
2079
|
+
config,
|
|
2080
|
+
});
|
|
2081
|
+
console.error(`>> ${multi.message}`);
|
|
2082
|
+
process.exit(multi.exitCode);
|
|
2083
|
+
}
|
|
2084
|
+
// Exactly one named item: the single-item tick. Wrapped in the shared
|
|
2085
|
+
// in-place tree-less publish so a surfaced sidecar / `triaged:` marker /
|
|
2086
|
+
// applied-answer commit reaches the arbiter's `main` (the CI ephemeral-runner
|
|
2087
|
+
// case — the local commit would otherwise be lost). The wrapper is the SAME
|
|
2088
|
+
// gate the `--isolated` / loop drivers use (`TREELESS_RUNGS` + exit 0 +
|
|
2089
|
+
// arbiter configured); a build/task rung integrates through the `doDriver`
|
|
2090
|
+
// band already, and a no-arbiter laptop checkout sits on the real `main`.
|
|
2091
|
+
const result = await runAdvanceTickWithTreelessPublish({ ...advanceContext, arg: args[0] }, performAdvance);
|
|
2092
|
+
if (result.exitCode !== 0) {
|
|
2093
|
+
console.error(`error: ${result.message}`);
|
|
2094
|
+
}
|
|
2095
|
+
process.exit(result.exitCode);
|
|
2096
|
+
});
|
|
2097
|
+
program
|
|
2098
|
+
.command('gc')
|
|
2099
|
+
.helpGroup(ADVANCED_GROUP)
|
|
2100
|
+
.description('Re-apply the provably-safe deletion predicate (ADR \u00a74) across every job worktree under workspacesDir/work/*: reap the provably-safe ones (clean tree AND branch tip reachable on the arbiter \u2014 merged or pushed) via git worktree remove (+ prune, never rm -rf), and report each RETAINED one with a reason. The catch-up for when end-of-job auto-reap did not run (runner crash/kill). --force overrides the predicate (discards un-saved work) \u2014 loud, never default.')
|
|
2101
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
2102
|
+
.option('--workspace <dir>', 'execution working area to sweep (default: workspacesDir / ~/.dorfl)')
|
|
2103
|
+
.option('--force', 'OVERRIDE the predicate: remove worktrees even with un-saved work (requires --yes; never the default)')
|
|
2104
|
+
.option('--yes', 'confirm a destructive --force sweep non-interactively')
|
|
2105
|
+
.option('--ledger [repoPath]', 'SWEEP the work/ lifecycle LEDGER instead of job worktrees: REPORT (never delete) every slug present in more than one work/ status folder (the one-slug-one-folder belt-and-suspenders), with its folders + candidate canonical folder, for a HUMAN to resolve. Defaults to the cwd repo.')
|
|
2106
|
+
.option('--reap-stale-locks', '(with --ledger) OPT-IN: also CLEAR every STALE terminal lock the report finds (a held `active` per-item lock whose item is already TERMINAL on <arbiter>/main — the `cleared-stale` class) via the SAME leased delete `release-lock` uses, so one command sweeps all orphaned terminal locks instead of N hand-run release-locks. SCOPED to `cleared-stale` ONLY: a `kept-stuck` (terminal + stuck) or a `kept-in-flight` (active, non-terminal) lock is NEVER reaped, even with this flag. A concurrent change to a lock ref makes its leased delete REJECT (reported), never --force. WITHOUT this flag `gc --ledger` stays report-only (fail-loud, deletes nothing).')
|
|
2107
|
+
.option('--remote-branches', 'SWEEP the arbiter’s remote work/* BRANCHES instead of job worktrees: delete (via git push --delete, NEVER --force) exactly those PROVABLY MERGED into <arbiter>/main (git merge-base --is-ancestor, the SAME predicate the worktree reaper uses), and RETAIN the rest with a reason. An in-flight/un-merged branch (the recovery point) is NEVER touched. Provider-agnostic plain git — works on a --bare arbiter. The merged-only complement of `requeue --reset`.')
|
|
2108
|
+
.option('--arbiter <remote>', '(with --remote-branches) the arbiter git remote whose work/* branches to sweep (default: origin); resolved from --cwd')
|
|
2109
|
+
.option('--cwd <dir>', '(with --remote-branches) the local repo/clone whose --arbiter remote points at the arbiter to sweep (default: cwd); only remote refs are read + deleted, never the working tree')
|
|
2110
|
+
.option('--dry-run', '(with --remote-branches) REPORT which merged branches WOULD be reaped without deleting anything (a read-only preview)')
|
|
2111
|
+
.option('--json', 'output the raw result as JSON')
|
|
2112
|
+
.action(async (flags) => {
|
|
2113
|
+
const config = resolveGlobalConfig(loadConfig(flags.config), {});
|
|
2114
|
+
const workspacesDir = flags.workspace ?? config.workspacesDir;
|
|
2115
|
+
// The `gc`-STYLE ledger SWEEP (prd `ledger-integrity` story 3): a SEPARATE
|
|
2116
|
+
// surface from the worktree reaper below — it REPORTS one-slug-one-folder
|
|
2117
|
+
// violations in a repo's `work/` lifecycle ledger and NEVER deletes (a human
|
|
2118
|
+
// resolves each). Distinct `work/`: the ledger, not the execution substrate.
|
|
2119
|
+
if (flags.ledger !== undefined) {
|
|
2120
|
+
const repoPath = typeof flags.ledger === 'string' ? flags.ledger : process.cwd();
|
|
2121
|
+
const result = sweepLedgerDuplicates(repoPath);
|
|
2122
|
+
// The UNIFIED-LOCK stuck/orphaned-lock REPORT (task
|
|
2123
|
+
// `release-lock-verb-and-gc-stuck-report`, prd
|
|
2124
|
+
// `ledger-status-per-item-lock-refs` US #12/#13/#14): generalises the
|
|
2125
|
+
// advancing-marker report from advancing-only to the unified per-item
|
|
2126
|
+
// lock. The locks live on the ARBITER ref (`refs/dorfl/lock/*`),
|
|
2127
|
+
// not in the local tree, so this reads the arbiter (cwd's `--arbiter`
|
|
2128
|
+
// remote). Best-effort: an absent lock-ref namespace / unreachable arbiter
|
|
2129
|
+
// degrades to an EMPTY report ("all locks released" — recoverable, US #12),
|
|
2130
|
+
// exactly as an absent lock-ref namespace reads. It REPORTS only,
|
|
2131
|
+
// wiring `reconcileItemLockAgainstMain`'s read-only twin to DISTINGUISH a
|
|
2132
|
+
// held/stuck lock from a stale-active lock over a terminal item WITHOUT
|
|
2133
|
+
// clearing (no auto-sweep; a human asserts a lock is dead via
|
|
2134
|
+
// `release-lock`).
|
|
2135
|
+
// OPT-IN SWEEP (`--reap-stale-locks`): the WRITE twin of the report. A
|
|
2136
|
+
// human asserting "clear the dead TERMINAL locks now": for EXACTLY the
|
|
2137
|
+
// `cleared-stale` class (terminal-on-main + active = stranded) perform the
|
|
2138
|
+
// SAME leased delete `release-lock` / the recovery use, so one command
|
|
2139
|
+
// sweeps every orphaned terminal lock. A `kept-stuck` / `kept-in-flight`
|
|
2140
|
+
// lock is NEVER reaped (scope fence); a concurrent change makes a clear
|
|
2141
|
+
// REJECT (reported `lost`), never --force. WITHOUT the flag the surface
|
|
2142
|
+
// below stays report-only (fail-loud, deletes nothing).
|
|
2143
|
+
if (flags.reapStaleLocks) {
|
|
2144
|
+
const reap = await reapStaleItemLocks(flags.cwd ?? repoPath, flags.arbiter ?? 'origin', process.env);
|
|
2145
|
+
if (flags.json) {
|
|
2146
|
+
console.log(JSON.stringify({ ...result, reap }, null, 2));
|
|
2147
|
+
}
|
|
2148
|
+
else {
|
|
2149
|
+
const reapLines = formatReapReport(reap);
|
|
2150
|
+
const blocks = [];
|
|
2151
|
+
if (result.duplicates.length > 0) {
|
|
2152
|
+
blocks.push(formatLedgerSweep(result));
|
|
2153
|
+
}
|
|
2154
|
+
if (reapLines.length > 0) {
|
|
2155
|
+
blocks.push(reapLines.join('\n'));
|
|
2156
|
+
}
|
|
2157
|
+
console.log(blocks.length > 0
|
|
2158
|
+
? blocks.join('\n\n')
|
|
2159
|
+
: formatLedgerSweep(result));
|
|
2160
|
+
}
|
|
2161
|
+
// Fail-loud AFTER the sweep: a `kept-stuck` (rightly left for a human) or
|
|
2162
|
+
// a `lost`/`error` (a stale lock whose leased delete lost the race) still
|
|
2163
|
+
// needs attention; a clean sweep that reaped every stale lock and left
|
|
2164
|
+
// only healthy in-flight holds exits 0.
|
|
2165
|
+
process.exit(result.duplicates.length > 0 || reapReportNeedsAttention(reap)
|
|
2166
|
+
? 1
|
|
2167
|
+
: 0);
|
|
2168
|
+
}
|
|
2169
|
+
const lockReport = await reportItemLocks(flags.cwd ?? repoPath, flags.arbiter ?? 'origin', process.env);
|
|
2170
|
+
if (flags.json) {
|
|
2171
|
+
console.log(JSON.stringify({ ...result, lockReport }, null, 2));
|
|
2172
|
+
}
|
|
2173
|
+
else {
|
|
2174
|
+
const lockLines = formatItemLockReport(lockReport);
|
|
2175
|
+
if (result.duplicates.length === 0 && lockLines.length === 0) {
|
|
2176
|
+
console.log(formatLedgerSweep(result));
|
|
2177
|
+
}
|
|
2178
|
+
else {
|
|
2179
|
+
const blocks = [];
|
|
2180
|
+
const sweepText = formatLedgerSweep(result);
|
|
2181
|
+
// Only print the duplicate block when it found something (otherwise
|
|
2182
|
+
// it returns the "clean" line, which is misleading when there ARE
|
|
2183
|
+
// lingering locks below it).
|
|
2184
|
+
if (result.duplicates.length > 0) {
|
|
2185
|
+
blocks.push(sweepText);
|
|
2186
|
+
}
|
|
2187
|
+
if (lockLines.length > 0) {
|
|
2188
|
+
blocks.push(lockLines.join('\n'));
|
|
2189
|
+
}
|
|
2190
|
+
console.log(blocks.join('\n\n'));
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
// A corrupt ledger, a stuck advancing-lock marker, OR a per-item lock that
|
|
2194
|
+
// NEEDS HUMAN ATTENTION is a fail-loud condition: exit non-zero so a human
|
|
2195
|
+
// (or a script) cannot miss it, mirroring the integration core's refusal.
|
|
2196
|
+
// ALL are REPORTED here (never auto-deleted — no automatic sweep exists; a
|
|
2197
|
+
// human clears a NAMED unified lock via `release-lock`).
|
|
2198
|
+
//
|
|
2199
|
+
// SCOPED to the ATTENTION verdicts only (prd US#14/#21, ADR
|
|
2200
|
+
// `ledger-status-on-per-item-lock-refs`: this surface is the STUCK /
|
|
2201
|
+
// crash-orphaned lock, NOT every held one): a `kept-stuck` (terminal +
|
|
2202
|
+
// stuck) or a `cleared-stale`-eligible (terminal + stale active = orphaned)
|
|
2203
|
+
// lock fails loud, but a `kept-in-flight` (active, non-terminal) lock is the
|
|
2204
|
+
// NORMAL in-flight state of a healthy concurrent build (read by `status` as
|
|
2205
|
+
// healthy) — it is reported informationally and does NOT make a routine
|
|
2206
|
+
// `gc --ledger` health check exit non-zero.
|
|
2207
|
+
process.exit(result.duplicates.length > 0 ||
|
|
2208
|
+
itemLockReportNeedsAttention(lockReport)
|
|
2209
|
+
? 1
|
|
2210
|
+
: 0);
|
|
2211
|
+
}
|
|
2212
|
+
// The REMOTE merged-BRANCH sweep (this task): a SEPARATE surface from the
|
|
2213
|
+
// worktree reaper below — it deletes PROVABLY-MERGED remote `work/*` branches
|
|
2214
|
+
// on the arbiter (the cross-machine counterpart of reaping local worktrees),
|
|
2215
|
+
// guarded by the SAME ancestor-of-main predicate. Provider-agnostic plain git
|
|
2216
|
+
// (works on a `--bare` arbiter); NEVER `--force` (a merged ref needs none),
|
|
2217
|
+
// NEVER touches an in-flight branch.
|
|
2218
|
+
if (flags.remoteBranches) {
|
|
2219
|
+
const sweepCwd = flags.cwd ?? process.cwd();
|
|
2220
|
+
const sweep = sweepRemoteMergedBranches({
|
|
2221
|
+
cwd: sweepCwd,
|
|
2222
|
+
arbiter: flags.arbiter ?? 'origin',
|
|
2223
|
+
dryRun: flags.dryRun === true,
|
|
2224
|
+
note: (message) => console.error(`>> ${message}`),
|
|
2225
|
+
});
|
|
2226
|
+
// The ORPHAN-SIDECAR sweep (prd
|
|
2227
|
+
// `agentic-question-resolution-retire-disposition-vocabulary`, US #10) rides
|
|
2228
|
+
// the SAME `--remote-branches` invocation the SCHEDULED CI lifecycle workflow
|
|
2229
|
+
// runs (`dorfl gc --remote-branches --arbiter origin`) — so the reap of a
|
|
2230
|
+
// `work/questions/<type>-<slug>.md` whose source item was deleted out-of-band
|
|
2231
|
+
// actually FIRES on the cron tick (not behind an un-passed flag). It operates
|
|
2232
|
+
// on the WORKING TREE of the checkout `gc` runs in (the same `cwd` the branch
|
|
2233
|
+
// sweep targets) — CI checks out the repo — so no arbiter ref query is needed
|
|
2234
|
+
// beyond the by-identity source-existence check
|
|
2235
|
+
// (`resolveItemPathByIdentity`). A `git rm` deletion (notes/sidecars leave by
|
|
2236
|
+
// deletion; git history is the archive), so a wrong source-delete is
|
|
2237
|
+
// recoverable from history. Honours `--dry-run` (report-only preview).
|
|
2238
|
+
const orphans = sweepOrphanSidecars({
|
|
2239
|
+
cwd: sweepCwd,
|
|
2240
|
+
dryRun: flags.dryRun === true,
|
|
2241
|
+
note: (message) => console.error(`>> ${message}`),
|
|
2242
|
+
});
|
|
2243
|
+
if (flags.json) {
|
|
2244
|
+
console.log(JSON.stringify({ ...sweep, orphanSidecars: orphans }, null, 2));
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
if (flags.dryRun === true) {
|
|
2248
|
+
for (const w of sweep.wouldReap) {
|
|
2249
|
+
console.log(` [would-reap] ${w.branch} \u2014 merged`);
|
|
2250
|
+
}
|
|
2251
|
+
for (const w of orphans.wouldReap) {
|
|
2252
|
+
console.log(` [would-reap] ${w.path} \u2014 orphan sidecar`);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
else {
|
|
2256
|
+
for (const r of sweep.reaped) {
|
|
2257
|
+
console.log(` [reaped] ${r.branch} \u2014 merged`);
|
|
2258
|
+
}
|
|
2259
|
+
for (const r of orphans.reaped) {
|
|
2260
|
+
console.log(` [reaped] ${r.path} \u2014 orphan sidecar`);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
for (const ret of sweep.retained) {
|
|
2264
|
+
console.log(` [retained] ${ret.branch} \u2014 ${ret.reasonText}`);
|
|
2265
|
+
}
|
|
2266
|
+
const reapedCount = flags.dryRun === true
|
|
2267
|
+
? sweep.wouldReap.length + orphans.wouldReap.length
|
|
2268
|
+
: sweep.reaped.length + orphans.reaped.length;
|
|
2269
|
+
const verb = flags.dryRun === true ? 'would reap' : 'reaped';
|
|
2270
|
+
console.log(`Summary: ${reapedCount} ${verb}, ${sweep.retained.length} retained.`);
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
// `--force` discards un-saved work, so it is gated behind an explicit
|
|
2274
|
+
// confirmation (`--yes`) — loud + intentional, NEVER the default (ADR §4).
|
|
2275
|
+
if (flags.force && !flags.yes) {
|
|
2276
|
+
console.error('refusing to --force without --yes: this DISCARDS un-saved work in ' +
|
|
2277
|
+
'retained worktrees (commits not on the arbiter, dirty trees). ' +
|
|
2278
|
+
'Re-run with `gc --force --yes` to confirm.');
|
|
2279
|
+
process.exit(1);
|
|
2280
|
+
}
|
|
2281
|
+
if (flags.force) {
|
|
2282
|
+
console.error('>> --force: OVERRIDING the deletion-safety predicate; un-saved work ' +
|
|
2283
|
+
'in retained worktrees will be DISCARDED.');
|
|
2284
|
+
}
|
|
2285
|
+
const result = gc({
|
|
2286
|
+
workspacesDir,
|
|
2287
|
+
force: flags.force === true,
|
|
2288
|
+
note: (message) => console.error(`>> ${message}`),
|
|
2289
|
+
});
|
|
2290
|
+
if (flags.json) {
|
|
2291
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
for (const reaped of result.reaped) {
|
|
2295
|
+
const how = reaped.forced
|
|
2296
|
+
? 'FORCED (discarded un-saved work)'
|
|
2297
|
+
: (reaped.verdict.reachableVia ?? 'safe');
|
|
2298
|
+
console.log(` [reaped] ${reaped.slug} \u2014 ${how}`);
|
|
2299
|
+
}
|
|
2300
|
+
for (const retained of result.retained) {
|
|
2301
|
+
console.log(` [retained] ${retained.slug} \u2014 ${RETAIN_REASON_TEXT[retained.reason]}`);
|
|
2302
|
+
}
|
|
2303
|
+
console.log(`Summary: ${result.reaped.length} reaped, ${result.retained.length} retained.`);
|
|
2304
|
+
});
|
|
2305
|
+
program
|
|
2306
|
+
.command('prd-to-spec')
|
|
2307
|
+
.helpGroup(ADVANCED_GROUP)
|
|
2308
|
+
.description("Migrate THIS repo's work/ DATA + config + inert git refs from the legacy `prd` vocabulary to `spec`, after upgrading the dorfl package (whose code/contract already speak `spec`). Self-contained: (1) REFUSES unless the repo is quiescent (clean tree AND no held per-item lock AND no in-progress work/prd-* branch), naming the offender; (2) re-syncs work/protocol/* to the new `spec` contract; (3) mechanically converts all four data layers (folders work/prds/* -> work/specs/* via git mv, `prd:` frontmatter + inert refs across ALL items incl. tasks/done + specs/tasked, `prdsLandIn` config key, inert lock-refs/work-branches). Idempotent; --dry-run previews every layer without writing; the forward+reverse leak scan over the converted tree is the acceptance gate. Runs IN-PLACE on the current repo (does no git commit/push -- the human commits the result).")
|
|
2309
|
+
.option('--repo <dir>', 'the repo working-tree root to migrate (default: cwd)')
|
|
2310
|
+
.option('--dry-run', 'REPORT exactly what each layer WOULD change, touching nothing (no writes, no ref renames, no leak-scan)')
|
|
2311
|
+
.option('--json', 'output the raw result as JSON')
|
|
2312
|
+
.action((flags) => {
|
|
2313
|
+
const repoPath = flags.repo ?? process.cwd();
|
|
2314
|
+
const result = runPrdToSpec({
|
|
2315
|
+
repoPath,
|
|
2316
|
+
dryRun: flags.dryRun === true,
|
|
2317
|
+
});
|
|
2318
|
+
if (flags.json) {
|
|
2319
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2320
|
+
}
|
|
2321
|
+
else {
|
|
2322
|
+
printPrdToSpecReport(result);
|
|
2323
|
+
}
|
|
2324
|
+
// Exit non-zero on a REFUSAL (quiescence gate) or a non-green leak scan
|
|
2325
|
+
// (a converted tree that still carries a dangling `prd` ref). A clean
|
|
2326
|
+
// dry-run or a green migration exits 0.
|
|
2327
|
+
if (result.refused || result.leaks.length > 0) {
|
|
2328
|
+
process.exit(1);
|
|
2329
|
+
}
|
|
2330
|
+
});
|
|
2331
|
+
program
|
|
2332
|
+
.command('status')
|
|
2333
|
+
.helpGroup(HEADLINE_GROUP)
|
|
2334
|
+
.description('Read-only operational dashboard of JOBS (distinct from scan’s backlog queue): list every job under workspacesDir/work/* from its .dorfl-job.json record + worktree state, grouped active (running + alive) vs failed/retained (needs-attention with its reason, a crashed running-but-dead job, or a done-but-un-reaped one). Liveness comes from the harness seam (PID/session), NOT mtime. Never claims/runs/moves/deletes (deletion is gc).')
|
|
2335
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
2336
|
+
.option('--workspace <dir>', 'execution working area to inspect (default: workspacesDir / ~/.dorfl)')
|
|
2337
|
+
.option('--arbiter-remote <name>', `the current repo's arbiter remote to report on (folds in the old \`arbiter status\`; default: ${DEFAULT_ARBITER_REMOTE})`)
|
|
2338
|
+
.option('--arbiter <remote>', 'the COORDINATION arbiter remote whose per-item lock refs gate the cwd selection pool (held in-flight items are subtracted); default: origin')
|
|
2339
|
+
.option('--no-arbiter', "skip the current repo's arbiter section")
|
|
2340
|
+
.option('--here', 'report ONLY the current repo (the cwd working tree, fetch-first): skip the jobs, registry-mirror, and arbiter sections entirely. "This repo, nothing else" — the fast, focused path.')
|
|
2341
|
+
.option('--json', 'output the raw report as JSON')
|
|
2342
|
+
.action(async (flags) => {
|
|
2343
|
+
const config = resolveGlobalConfig(loadConfig(flags.config), {});
|
|
2344
|
+
const override = loadConfigOverride(defaultConfigOverridePath(flags.config));
|
|
2345
|
+
const workspacesDir = flags.workspace ?? config.workspacesDir;
|
|
2346
|
+
const warn = (message) => console.error(`>> ${message}`);
|
|
2347
|
+
const resolveCwd = () => resolveCwdSection({
|
|
2348
|
+
cwd: process.cwd(),
|
|
2349
|
+
config,
|
|
2350
|
+
override,
|
|
2351
|
+
arbiterRemote: flags.arbiterRemote ?? DEFAULT_ARBITER_REMOTE,
|
|
2352
|
+
lockArbiterRemote: flags.arbiter ?? 'origin',
|
|
2353
|
+
warn,
|
|
2354
|
+
});
|
|
2355
|
+
// `--here`: report ONLY the cwd — skip the jobs, registry-mirror, and arbiter
|
|
2356
|
+
// sections entirely ("this repo, nothing else"). `status` is built with NO
|
|
2357
|
+
// jobs (empty workspace view), NO mirrors, and NO arbiter, so only the cwd
|
|
2358
|
+
// block renders.
|
|
2359
|
+
if (flags.here === true) {
|
|
2360
|
+
const cwdSection = await resolveCwd();
|
|
2361
|
+
const report = await status({
|
|
2362
|
+
workspacesDir,
|
|
2363
|
+
mirrorPaths: [],
|
|
2364
|
+
cwd: cwdSection,
|
|
2365
|
+
warn,
|
|
2366
|
+
});
|
|
2367
|
+
if (flags.json) {
|
|
2368
|
+
console.log(JSON.stringify(report, null, 2));
|
|
2369
|
+
}
|
|
2370
|
+
else {
|
|
2371
|
+
console.log(formatStatus(report));
|
|
2372
|
+
}
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
2375
|
+
// Surface the folder-native needs-attention set (ADR §12) from each
|
|
2376
|
+
// REGISTERED HUB MIRROR (the registry), read from its bare `main` ref
|
|
2377
|
+
// through the read seam (mirrors have no working tree).
|
|
2378
|
+
const mirrorPaths = listMirrors({ workspacesDir }).map((m) => m.path);
|
|
2379
|
+
// Fold in the current repo's arbiter state (the old `arbiter status`, ADR
|
|
2380
|
+
// §1/§7) unless --no-arbiter. Read-only; tolerates not being in a repo.
|
|
2381
|
+
const arbiter = flags.noArbiter === true
|
|
2382
|
+
? undefined
|
|
2383
|
+
: arbiterStatus({
|
|
2384
|
+
cwd: process.cwd(),
|
|
2385
|
+
remote: flags.arbiterRemote ?? DEFAULT_ARBITER_REMOTE,
|
|
2386
|
+
});
|
|
2387
|
+
// The cwd-local section: resolve it ONLY when a participating cwd is NOT
|
|
2388
|
+
// already covered by a registered mirror, via the SAME fetch-free pre-check
|
|
2389
|
+
// `scan` uses (`cwdSectionDisposition`) — so an already-registered cwd is not
|
|
2390
|
+
// re-fetched (the registry/jobs view already covers it) while a mirror-less
|
|
2391
|
+
// cwd is still shown standalone.
|
|
2392
|
+
const disposition = cwdSectionDisposition({
|
|
2393
|
+
cwd: process.cwd(),
|
|
2394
|
+
config,
|
|
2395
|
+
arbiterRemote: flags.arbiterRemote ?? DEFAULT_ARBITER_REMOTE,
|
|
2396
|
+
});
|
|
2397
|
+
const cwdSection = disposition.participating && !disposition.alsoRegistered
|
|
2398
|
+
? await resolveCwd()
|
|
2399
|
+
: undefined;
|
|
2400
|
+
const report = await status({
|
|
2401
|
+
workspacesDir,
|
|
2402
|
+
mirrorPaths,
|
|
2403
|
+
arbiter,
|
|
2404
|
+
cwd: cwdSection,
|
|
2405
|
+
warn,
|
|
2406
|
+
});
|
|
2407
|
+
if (flags.json) {
|
|
2408
|
+
console.log(JSON.stringify(report, null, 2));
|
|
2409
|
+
}
|
|
2410
|
+
else {
|
|
2411
|
+
console.log(formatStatus(report));
|
|
2412
|
+
}
|
|
2413
|
+
});
|
|
2414
|
+
program
|
|
2415
|
+
.command('requeue <slug>')
|
|
2416
|
+
.helpGroup(HEADLINE_GROUP)
|
|
2417
|
+
.description('Requeue a STUCK task to the backlog for re-claiming (ADR §12/§14). Recovers a task whose per-item lock is held — stuck (the resolved-recovery path: a previously-routed needs-attention item, now lock `state: stuck`) OR active (a claim that never surfaced — an un-surfaced abort, a killed run, or an in-place requeue note). The body rests in work/tasks/backlog/<slug>.md (claim never moves it under the per-item-lock model); requeue releases the lock so the item is claimable again. The release is published as a TREE-LESS compare-and-swap to the arbiter ref, EXACTLY like claim — it NEVER stages or commits in the cwd working tree, so a requeue in a shared checkout can never sweep up a concurrent writer’s uncommitted files. DEFAULT = keep + continue: leave the work/<slug> branch UNTOUCHED so the next claim CONTINUES from its tip (rebased onto fresh main at onboard-time). --reset = discard + fresh: delete the remote work/<slug> branch FIRST (then release the lock) so the next claim starts fresh (guarded; never the default). -m/--message appends a dated handoff note to the item body (both modes; append-only).')
|
|
2418
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
2419
|
+
.option('--cwd <dir>', 'the repo/working clone whose work/ tree the arbiter remote is resolved FROM (default: cwd) — an ORIGIN SOURCE only; the move is published to the arbiter, never to this tree')
|
|
2420
|
+
.option('--arbiter <remote>', 'the arbiter git remote the tree-less move is CAS-published to (default: origin). --cwd resolves this remote; the move is never written to the cwd tree.')
|
|
2421
|
+
.option('--reset', 'DISCARD the kept work: delete the remote work/<slug> branch FIRST, then move to backlog so the next claim starts FRESH (guarded; a deliberate departure from the never-delete-the-remote-branch invariant). Never the default.')
|
|
2422
|
+
.option('-m, --message <note>', 'append a dated handoff note to the item body for the next agent (append-only; applies to both default and --reset)')
|
|
2423
|
+
.action(async (rawSlug, flags) => {
|
|
2424
|
+
// Task-only command (§3a): accept bare + `task:`, reject `prd:`.
|
|
2425
|
+
const slug = resolveTaskOnlySlug(rawSlug);
|
|
2426
|
+
const cwd = flags.cwd ?? process.cwd();
|
|
2427
|
+
// Route the requeue (default keep+continue / --reset discard / -m handoff)
|
|
2428
|
+
// THROUGH the ledger write seam's transition (same seam the needs-attention
|
|
2429
|
+
// move uses), not the helper.
|
|
2430
|
+
//
|
|
2431
|
+
// `requeue` is a HUMAN command (like `complete`): the human is putting a
|
|
2432
|
+
// resolved item back, so the move/commit/push is THEIRS — it is NOT given
|
|
2433
|
+
// the runner identity (`config.identity`). The autonomous re-attempt is
|
|
2434
|
+
// `do` (which IS identity-aware), not this. We thread the ambient
|
|
2435
|
+
// `process.env` EXPLICITLY so the human-identity choice is visible at the
|
|
2436
|
+
// call site, rather than relying on the seam's silent `?? process.env`
|
|
2437
|
+
// default by omission (the implicit fallback that made `requeue`'s human
|
|
2438
|
+
// attribution accidental rather than declared).
|
|
2439
|
+
const result = await ledgerWrite.applyReturnToBacklogTransition({
|
|
2440
|
+
cwd,
|
|
2441
|
+
slug,
|
|
2442
|
+
// Tree-less CAS needs a ref to push to (parity with `claim`): default the
|
|
2443
|
+
// arbiter to `origin` so the common case Just Works; `--arbiter` overrides.
|
|
2444
|
+
// `--cwd` is purely the ORIGIN SOURCE the remote is resolved from.
|
|
2445
|
+
arbiter: flags.arbiter ?? 'origin',
|
|
2446
|
+
reset: flags.reset,
|
|
2447
|
+
message: flags.message,
|
|
2448
|
+
env: process.env,
|
|
2449
|
+
note: (message) => console.error(`>> ${message}`),
|
|
2450
|
+
});
|
|
2451
|
+
if (!result.moved) {
|
|
2452
|
+
console.error(`error: ${result.reasonNotMoved}`);
|
|
2453
|
+
process.exit(1);
|
|
2454
|
+
}
|
|
2455
|
+
const how = result.deletedRemoteBranch
|
|
2456
|
+
? ` (--reset: deleted the remote ${workBranchRef('task', slug)} branch; next claim starts fresh)`
|
|
2457
|
+
: ' (kept the work branch; next claim continues from its tip)';
|
|
2458
|
+
console.log(`Requeued '${slug}' to backlog for re-claiming.${how}`);
|
|
2459
|
+
});
|
|
2460
|
+
// `promote [item]` (prd `staging-pool-position-gate-and-trust-model`, tasks
|
|
2461
|
+
// `pre-backlog-staging-folder-and-promote-step-a` /
|
|
2462
|
+
// `pre-prd-staging-pool-split-and-untrusted-prd-placement`): the HUMAN/runner-
|
|
2463
|
+
// owned verb that moves a STAGED item into its agent-eligible POOL — a task
|
|
2464
|
+
// `work/pre-backlog/<slug>.md → work/backlog/<slug>.md`, a prd
|
|
2465
|
+
// `work/prds/proposed/<slug>.md → work/prds/ready/<slug>.md` — as a tree-less CAS on the
|
|
2466
|
+
// arbiter, the SAME trust model + mechanism as `requeue`. The agent emits STAGED;
|
|
2467
|
+
// only this verb (a human, or the runner) admits it to the pool. With NO argument
|
|
2468
|
+
// it LISTS what is promotable (the "what is staged waiting for me?" discovery), so
|
|
2469
|
+
// a human need not remember the staged slugs.
|
|
2470
|
+
program
|
|
2471
|
+
.command('promote [item]')
|
|
2472
|
+
.helpGroup(HEADLINE_GROUP)
|
|
2473
|
+
.description('Admit a STAGED item into its agent-eligible POOL (the runner/human side of the staging gate): a task `work/pre-backlog/<slug>.md → work/backlog/<slug>.md`, a prd `work/prds/proposed/<slug>.md → work/prds/ready/<slug>.md`, published as a TREE-LESS compare-and-swap to the arbiter ref (EXACTLY like requeue/claim — it never stages/commits in the cwd tree). The agent only ever CREATES staged; this verb is the gate a human (or the runner) opens. Accepts `task:<slug>` / `prd:<slug>` / a bare `<slug>` (= task). With NO argument, LISTS every promotable item (the tasks in pre-backlog/ + the prds in prds/proposed/ on the arbiter) so you can see what is staged waiting for promotion. Idempotent: promoting an already-pooled slug is a clean no-op success.')
|
|
2474
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
2475
|
+
.option('--cwd <dir>', 'the repo/working clone whose arbiter remote the tree-less move is resolved FROM (default: cwd) — an ORIGIN SOURCE only; the move is published to the arbiter, never to this tree')
|
|
2476
|
+
.option('--arbiter <remote>', 'the arbiter git remote the promotion is CAS-published to / the staging folders are listed from (default: origin)')
|
|
2477
|
+
.action(async (rawItem, flags) => {
|
|
2478
|
+
const cwd = flags.cwd ?? process.cwd();
|
|
2479
|
+
const arbiter = flags.arbiter ?? 'origin';
|
|
2480
|
+
// `promote` is a HUMAN command (like `requeue`): the move/commit/push is
|
|
2481
|
+
// THEIRS — NOT the runner identity. Thread the ambient env explicitly.
|
|
2482
|
+
const env = process.env;
|
|
2483
|
+
const note = (message) => console.error(`>> ${message}`);
|
|
2484
|
+
// NO ARGUMENT → LIST what is promotable (read-only discovery).
|
|
2485
|
+
if (rawItem === undefined) {
|
|
2486
|
+
const listed = await listPromotable({ cwd, arbiter, env });
|
|
2487
|
+
if (listed.error) {
|
|
2488
|
+
console.error(`error: ${listed.error}`);
|
|
2489
|
+
process.exit(1);
|
|
2490
|
+
}
|
|
2491
|
+
if (listed.items.length === 0) {
|
|
2492
|
+
console.log(`Nothing staged to promote on ${arbiter}/main (work/pre-backlog/ and work/prds/proposed/ are empty).`);
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
console.log('Staged, awaiting promotion (run `promote <item>`):');
|
|
2496
|
+
for (const item of listed.items) {
|
|
2497
|
+
console.log(` ${item.namespace}:${item.slug}`);
|
|
2498
|
+
}
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
// AN ITEM → promote it. `task:`/`spec:` are explicit (the legacy `prd:`
|
|
2502
|
+
// prefix is still ACCEPTED as an input alias through the cutover — the
|
|
2503
|
+
// contract task drops it); a bare slug defaults to a task (mirrors
|
|
2504
|
+
// `requeue`). An `obs:`/`observation:` prefix is rejected (observations have
|
|
2505
|
+
// no pool).
|
|
2506
|
+
const parsed = parseSlugArg(rawItem);
|
|
2507
|
+
if (parsed.explicit === 'observation') {
|
|
2508
|
+
console.error(`error: promote takes a task or spec, not an observation ('${rawItem}'). Observations have no agent pool.`);
|
|
2509
|
+
process.exit(1);
|
|
2510
|
+
}
|
|
2511
|
+
// A `spec:` prefix produces the `spec` namespace (the dispatch + messages
|
|
2512
|
+
// speak `spec`); only bare/`task:` stays `task`.
|
|
2513
|
+
const namespace = parsed.explicit === 'spec' ? 'spec' : 'task';
|
|
2514
|
+
const slug = parsed.slug;
|
|
2515
|
+
const result = namespace === 'spec'
|
|
2516
|
+
? await promoteFromPreSpec({ cwd, slug, arbiter, env, note })
|
|
2517
|
+
: await promoteFromPreBacklog({ cwd, slug, arbiter, env, note });
|
|
2518
|
+
if (!result.moved) {
|
|
2519
|
+
console.error(`error: ${result.reasonNotMoved}`);
|
|
2520
|
+
process.exit(1);
|
|
2521
|
+
}
|
|
2522
|
+
const dest = namespace === 'spec'
|
|
2523
|
+
? workFolderPrefix('specs-ready')
|
|
2524
|
+
: workFolderPrefix('tasks-ready');
|
|
2525
|
+
console.log(`Promoted ${namespace} '${slug}' into the pool (${dest}); it is now ${namespace === 'spec' ? 'auto-taskable' : 'claimable'}.`);
|
|
2526
|
+
});
|
|
2527
|
+
// NOTE: the legacy `release-advancing <item>` verb is RETIRED by the capstone
|
|
2528
|
+
// cut-over (task `cutover-retire-slicing-advancing-markers-and-trim-folder-sets`):
|
|
2529
|
+
// the `work/advancing/<entry>.md` marker is gone and an advance hold is now just
|
|
2530
|
+
// `action: advance` on the UNIFIED per-item lock, so `release-lock <item>` (below)
|
|
2531
|
+
// is the SOLE named human release for ALL holds (implement/task/advance).
|
|
2532
|
+
// `release-lock <item>` (task `release-lock-verb-and-gc-stuck-report`, prd
|
|
2533
|
+
// `ledger-status-per-item-lock-refs` US #14): the HUMAN-invoked named release of
|
|
2534
|
+
// a stuck/orphaned UNIFIED per-item lock (`refs/dorfl/lock/<entry>`) —
|
|
2535
|
+
// the GENERALISATION of `release-advancing` from the advancing-only marker to
|
|
2536
|
+
// the one lock per item (implement/task/advance × active/stuck). Same trust
|
|
2537
|
+
// model as `release-advancing` / `requeue`: a HUMAN asserts the lock is dead by
|
|
2538
|
+
// NAMING it; the tool never guesses liveness (there is NO heartbeat, NO
|
|
2539
|
+
// auto-sweep — the `gc --ledger` report only REPORTS lingering locks). Routes
|
|
2540
|
+
// through the existing leased-delete `releaseItemLock` (deleting the ref IS the
|
|
2541
|
+
// release; the parentless lock commit becomes gc-reclaimable). NEVER `--force`.
|
|
2542
|
+
// Idempotent: deleting an absent ref is a clean exit-0 "nothing to clear"
|
|
2543
|
+
// (`not-held`), NOT a failure — deleting the lock ref(s) is "all locks released"
|
|
2544
|
+
// and recoverable (the work is safe on the `work/<slug>` branches + `main`).
|
|
2545
|
+
program
|
|
2546
|
+
.command('release-lock <item>')
|
|
2547
|
+
.helpGroup(HEADLINE_GROUP)
|
|
2548
|
+
.description('Clear a NAMED stuck/orphaned UNIFIED per-item lock (refs/dorfl/lock/<entry>) by DELETING the ref on the arbiter — the recovery verb for a lock the system orphaned (a crashed build/task/advance that left the hold behind). The generalisation of `release-advancing` from the advancing marker to the ONE lock per item. Same trust model as `requeue`: a HUMAN asserts the lock is dead by NAMING it; the tool never guesses liveness (the lock has NO heartbeat, so there is NO automatic sweep / age-based reaper anywhere). Accepts the same item forms as the lock API: `task:<slug>` / `prd:<slug>` / `obs:<slug>` / a bare `<slug>` (= task). Idempotent — re-running on an already-cleared lock is a clean exit-0 no-op (deleting the lock ref is “all locks released”, recoverable). NEVER `--force`. Discoverable via `gc --ledger` (it REPORTS every lingering lock, never deletes).')
|
|
2549
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
2550
|
+
.option('--cwd <dir>', 'the repo/working clone whose arbiter remote the lock ref is DELETED on (default: cwd)')
|
|
2551
|
+
.option('--arbiter <remote>', 'the arbiter git remote the lock ref is deleted on (default: origin)')
|
|
2552
|
+
.action(async (item, flags) => {
|
|
2553
|
+
const cwd = flags.cwd ?? process.cwd();
|
|
2554
|
+
const arbiter = flags.arbiter ?? 'origin';
|
|
2555
|
+
const result = await releaseItemLock({
|
|
2556
|
+
item,
|
|
2557
|
+
cwd,
|
|
2558
|
+
arbiter,
|
|
2559
|
+
env: process.env,
|
|
2560
|
+
});
|
|
2561
|
+
if (result.outcome === 'released') {
|
|
2562
|
+
console.log(`Released lock '${result.entry}' (${result.ref} deleted on ${arbiter}; the item itself was untouched — it rests on main / its work/<slug> branch).`);
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
// IDEMPOTENT exit semantics: `releaseItemLock` returns `not-held` when the
|
|
2566
|
+
// ref is ALREADY absent. For a HUMAN re-running the verb on an
|
|
2567
|
+
// already-cleared lock that is the CORRECT "nothing to clear" outcome —
|
|
2568
|
+
// deleting the lock ref(s) is "all locks released" and recoverable — so map
|
|
2569
|
+
// it to a clean exit-0 with an honest message (NOT a failure).
|
|
2570
|
+
if (result.outcome === 'not-held') {
|
|
2571
|
+
console.log(`No lock to release for '${result.entry}' (${result.ref} is already absent on ${arbiter} — “all locks released”, recoverable).`);
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
console.error(`error: ${result.message}`);
|
|
2575
|
+
process.exit(1);
|
|
2576
|
+
});
|
|
2577
|
+
// `drop <slug>` (prd `agentic-question-resolution-retire-disposition-vocabulary`,
|
|
2578
|
+
// US #5/#11; task `direct-delete-question-cli-helper`): the DIRECT "throw it
|
|
2579
|
+
// away" verb — `git rm` a source item AND its question sidecar (when present) in
|
|
2580
|
+
// ONE revertible commit, the reason in the commit MESSAGE (git history is the
|
|
2581
|
+
// archive). It does NOT round-trip through the decision engine or spawn an agent
|
|
2582
|
+
// (that is the SEPARATE agentic `delete-source` verdict in apply-persist.ts);
|
|
2583
|
+
// this is the human/skill/CLI no-ceremony delete of decision 7. DISTINCT from
|
|
2584
|
+
// the existing `remote rm` (the hub-MIRROR deleter) — different concern, no
|
|
2585
|
+
// collision. A LOCAL one-commit primitive over the working tree (like apply): it
|
|
2586
|
+
// does NOT touch the arbiter; the human pushes/integrates the revertible commit
|
|
2587
|
+
// however they normally do.
|
|
2588
|
+
program
|
|
2589
|
+
.command('drop <slug>')
|
|
2590
|
+
.helpGroup(HEADLINE_GROUP)
|
|
2591
|
+
.description('DIRECTLY delete a source item + its question sidecar (when present) in ONE revertible commit — the "I just want to throw this away" path that does NOT round-trip through the decision engine or any agent. Resolves the source by its namespaced identity (`task:<slug>` / `prd:<slug>` / `obs:<slug>` / a bare `<slug>` = task), `git rm`s the source AND its sidecar together, and records your --reason in the commit MESSAGE (git history is the archive). A single revertible commit, so a wrong delete is recoverable via `git revert`. DISTINCT from `remote rm` (the hub-mirror deleter). A LOCAL working-tree commit (like the apply rung); it does not touch the arbiter — push/integrate it as you normally would. If the named source is already gone it is a clean no-op (nothing to throw away).')
|
|
2592
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
2593
|
+
.option('--cwd <dir>', 'the working clone the revertible delete commit is made in (default: cwd)')
|
|
2594
|
+
.option('--reason <text>', 'why you are throwing this away — recorded in the commit MESSAGE (git history is the archive). Optional; recorded as "(no reason given)" when omitted.')
|
|
2595
|
+
.action((slug, flags) => {
|
|
2596
|
+
const cwd = flags.cwd ?? process.cwd();
|
|
2597
|
+
// `drop` is a DIRECT HUMAN action (like the apply rung's local commit): the
|
|
2598
|
+
// delete/commit is THEIRS, so thread the ambient env explicitly.
|
|
2599
|
+
const env = process.env;
|
|
2600
|
+
const note = (message) => console.error(`>> ${message}`);
|
|
2601
|
+
const result = dropSource({
|
|
2602
|
+
cwd,
|
|
2603
|
+
item: slug,
|
|
2604
|
+
reason: flags.reason,
|
|
2605
|
+
env,
|
|
2606
|
+
note,
|
|
2607
|
+
});
|
|
2608
|
+
if (result.outcome === 'not-found') {
|
|
2609
|
+
// Nothing to throw away (the source is already gone). A clean exit-0
|
|
2610
|
+
// no-op, NOT a failure — deleting something already absent is success.
|
|
2611
|
+
console.log(`Nothing to drop for '${result.item}' — no source item resolves by identity (already gone).`);
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
console.log(`Dropped '${result.item}'${result.sidecarPath ? ' + its sidecar' : ''} in one revertible commit (${result.commit?.slice(0, 8)}; reason in the message). Recover with \`git revert\` if this was wrong.`);
|
|
2615
|
+
});
|
|
2616
|
+
program
|
|
2617
|
+
.command('intake')
|
|
2618
|
+
.helpGroup(HEADLINE_GROUP)
|
|
2619
|
+
.description('Front-of-funnel: turn a GitHub issue into the right work/ artifact. Reads issue #N + its comment thread via the issue seam (gh), runs a prompt→verdict decision, and dispatches it: a clear, small issue → a proposed work/backlog/<slug>.md PR carrying an `issue: N` closure link (read by a future CI close-job; not `Fixes #N`). GATE-FREE — your explicit invocation IS the authorization (autoTask/autoBuild do NOT apply), exactly as `do`. A LOCAL one-shot AND the SAME command CI schedules. PER-OUTCOME integration modes (the artifact TYPE is decided at runtime): --merge/--propose set BOTH; --merge-spec/--propose-spec and --merge-task/--propose-task override per type; granular overrides the aggregate; unset ⇒ propose for both.')
|
|
2620
|
+
.argument('<number>', 'the GitHub issue number to intake (e.g. `intake 42`)')
|
|
2621
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
2622
|
+
.option('--arbiter <remote>', 'name of the arbiter git remote (default: per-repo/global defaultArbiter)')
|
|
2623
|
+
.option('--merge', 'integrate BOTH outcomes (task AND spec) in merge mode (aggregate; overridden per type by --merge-*/--propose-*; mutually exclusive with --propose)')
|
|
2624
|
+
.option('--propose', 'integrate BOTH outcomes (task AND spec) in propose mode (aggregate; default; overridden per type; mutually exclusive with --merge)')
|
|
2625
|
+
.option('--no-pr', 'propose without opening a PR for intake emissions: push the branch but deliberately skip the review request (the explicit suppress-PR intent). Resolved flag > env > per-repo > global > default off.')
|
|
2626
|
+
.option('--merge-spec', 'integrate a spec outcome in merge mode (granular; overrides --merge/--propose for a spec; mutually exclusive with --propose-spec)')
|
|
2627
|
+
.option('--propose-spec', 'integrate a spec outcome in propose mode (granular; overrides --merge/--propose for a spec; mutually exclusive with --merge-spec)')
|
|
2628
|
+
.option('--merge-task', 'integrate a task outcome in merge mode (granular; overrides --merge/--propose for a task; mutually exclusive with --propose-task)')
|
|
2629
|
+
.option('--propose-task', 'integrate a task outcome in propose mode (granular; overrides --merge/--propose for a task; mutually exclusive with --merge-task)')
|
|
2630
|
+
.option('--origin-trust <trusted|untrusted>', "the author-trust verdict to STAMP onto the emitted prd/task (origin: issue + originTrust: <value>), so an untrusted origin survives the merge boundary and later forces the task's BUILD transition to propose. CI's intake.yml derives it from the SAME author_association case as the integration flags. UNSET (a local intake) ⇒ emitted unstamped (human/trusted) — the human running intake IS the checkpoint.")
|
|
2631
|
+
.option('--specs-land-in <where>', 'where an intake-authored spec lands: `pre-proposed` (staged, not auto-taskable) or `ready` (the auto-tasking pool). The EXPLICIT operator override at the top of the placement precedence (explicit flag > untrusted-origin forces staging > specsLandIn default > built-in). Resolved flag > env (DORFL_SPECS_LAND_IN) > per-repo > global > built-in.')
|
|
2632
|
+
.option('--agent-cmd <cmd>', 'command to run the decision agent')
|
|
2633
|
+
.option('--model <id>', 'model the decision agent runs on (routing intent; resolved flag > env > per-repo > global > default)')
|
|
2634
|
+
.option('--harness <adapter>', 'harness adapter that launches the decision agent: null (default) or pi')
|
|
2635
|
+
.option('--pi-bin <path>', 'pi CLI binary the pi harness invokes (default: pi on PATH)')
|
|
2636
|
+
.option('--sessions-dir <dir>', 'HOST-ONLY root folder under which the pi session file is generated')
|
|
2637
|
+
.action(async (rawNumber, flags) => {
|
|
2638
|
+
const issueNumber = Number(rawNumber);
|
|
2639
|
+
if (rawNumber.trim() === '' ||
|
|
2640
|
+
!Number.isInteger(issueNumber) ||
|
|
2641
|
+
issueNumber < 1) {
|
|
2642
|
+
console.error(`error: intake takes a positive issue NUMBER (got '${rawNumber}').`);
|
|
2643
|
+
process.exit(1);
|
|
2644
|
+
}
|
|
2645
|
+
const cwd = process.cwd();
|
|
2646
|
+
const { global, override } = loadGlobalAndOverride(flags.config);
|
|
2647
|
+
const resolved = resolveRepoConfig({
|
|
2648
|
+
repoPath: cwd,
|
|
2649
|
+
global,
|
|
2650
|
+
override,
|
|
2651
|
+
flags: {
|
|
2652
|
+
...harnessFlagOverrides(flags),
|
|
2653
|
+
// `--no-pr` (the PR-INTENT axis) rides the SAME chain.
|
|
2654
|
+
...noPRFlagOverrides(flags),
|
|
2655
|
+
},
|
|
2656
|
+
});
|
|
2657
|
+
if (resolved.message) {
|
|
2658
|
+
console.error(`>> ${resolved.message}`);
|
|
2659
|
+
}
|
|
2660
|
+
const config = resolved.config;
|
|
2661
|
+
// Resolve the PER-OUTCOME integration modes (prd US #9): `intake` decides
|
|
2662
|
+
// the artifact TYPE at runtime, so a single --merge/--propose can't express
|
|
2663
|
+
// a type-conditional policy. The granular flags override the aggregate; an
|
|
2664
|
+
// UNSET type falls back to the per-repo/global `integration` (the SAME chain
|
|
2665
|
+
// `do`/`complete` use — flag > per-repo > global > default propose). `intake`
|
|
2666
|
+
// is GATE-FREE, so autoTask/autoBuild are NOT consulted (the explicit
|
|
2667
|
+
// invocation is its own authorization). `intake` owns only these KNOBS; WHICH
|
|
2668
|
+
// knobs CI sets is CI's POLICY (`runner-in-ci`), NOT here.
|
|
2669
|
+
let modes;
|
|
2670
|
+
try {
|
|
2671
|
+
modes = resolveIntakeIntegrationModes(flags, config.integration);
|
|
2672
|
+
}
|
|
2673
|
+
catch (err) {
|
|
2674
|
+
console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2675
|
+
process.exit(1);
|
|
2676
|
+
}
|
|
2677
|
+
// The ORIGIN-TRUST stamp (task `untrusted-origin-forces-build-propose`):
|
|
2678
|
+
// the CI shell passes `--origin-trust <trusted|untrusted>`; `intake` writes
|
|
2679
|
+
// it onto the emitted artifact (it does NOT resolve trust — the ~L296
|
|
2680
|
+
// boundary). UNSET ⇒ undefined ⇒ emit unstamped (a local intake is
|
|
2681
|
+
// human/trusted). An INVALID value FAILS LOUDLY (an autonomy/trust signal
|
|
2682
|
+
// must never be quietly ignored), mirroring the observation-triage enum.
|
|
2683
|
+
let originTrust;
|
|
2684
|
+
if (flags.originTrust !== undefined) {
|
|
2685
|
+
if (flags.originTrust !== 'trusted' &&
|
|
2686
|
+
flags.originTrust !== 'untrusted') {
|
|
2687
|
+
console.error(`error: --origin-trust must be 'trusted' or 'untrusted' (got '${flags.originTrust}').`);
|
|
2688
|
+
process.exit(1);
|
|
2689
|
+
}
|
|
2690
|
+
originTrust = flags.originTrust;
|
|
2691
|
+
}
|
|
2692
|
+
const harness = createHarness({
|
|
2693
|
+
harness: config.harness,
|
|
2694
|
+
piBin: config.piBin,
|
|
2695
|
+
});
|
|
2696
|
+
// The OPERATOR's EXPLICIT spec-placement override (`--specs-land-in`), the
|
|
2697
|
+
// TOP of the placement precedence — mirrors `explicitTasksLandInFromFlag` on
|
|
2698
|
+
// the `do spec:` path. Fails loudly on a bad value.
|
|
2699
|
+
let explicitSpecsLandIn;
|
|
2700
|
+
try {
|
|
2701
|
+
explicitSpecsLandIn = explicitSpecsLandInFromFlag(flags.specsLandIn);
|
|
2702
|
+
}
|
|
2703
|
+
catch (err) {
|
|
2704
|
+
console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2705
|
+
process.exit(1);
|
|
2706
|
+
}
|
|
2707
|
+
const result = await performIntake({
|
|
2708
|
+
issueNumber,
|
|
2709
|
+
cwd,
|
|
2710
|
+
arbiter: flags.arbiter ?? config.defaultArbiter,
|
|
2711
|
+
integration: modes,
|
|
2712
|
+
// The origin-trust stamp the CI shell passes IN (unset ⇒ unstamped).
|
|
2713
|
+
originTrust,
|
|
2714
|
+
noPR: config.noPR,
|
|
2715
|
+
// SPEC-PLACEMENT: the configured-default `specsLandIn` rung + the EXPLICIT
|
|
2716
|
+
// `--specs-land-in` override (top of the precedence). The shared placement
|
|
2717
|
+
// resolver in `intake.ts` overlays the untrusted-origin staging force.
|
|
2718
|
+
specsLandIn: config.specsLandIn,
|
|
2719
|
+
explicitSpecsLandIn,
|
|
2720
|
+
harness,
|
|
2721
|
+
agentCmd: config.agentCmd,
|
|
2722
|
+
model: config.model,
|
|
2723
|
+
sessionsDir: config.sessionsDir,
|
|
2724
|
+
// Host-only runner IDENTITY — scopes intake's `gh`/git ops (not the
|
|
2725
|
+
// decision/review AGENT launches); absent ⇒ ambient.
|
|
2726
|
+
identity: config.identity,
|
|
2727
|
+
note: (message) => console.error(`>> ${message}`),
|
|
2728
|
+
});
|
|
2729
|
+
if (result.exitCode !== 0) {
|
|
2730
|
+
console.error(`error: ${result.message}`);
|
|
2731
|
+
}
|
|
2732
|
+
else {
|
|
2733
|
+
console.error(`>> ${result.message}`);
|
|
2734
|
+
}
|
|
2735
|
+
process.exit(result.exitCode);
|
|
2736
|
+
});
|
|
2737
|
+
// The CI CLOSE-JOB driver (prd `runner-in-ci`, capability E; task
|
|
2738
|
+
// `install-ci-close-job-workflow`). The thin JOB the emitted close-job workflow
|
|
2739
|
+
// invokes on a merge to main: resolve which source issue(s) the landed work
|
|
2740
|
+
// closes (resolveClosingIssue), run the "prd complete?" query for the prd case
|
|
2741
|
+
// (prd-complete-query, done), and close via the IssueProvider seam — all
|
|
2742
|
+
// UNCHANGED engine pieces, CONSUMED not re-built (the Out-of-Scope fence). CI
|
|
2743
|
+
// owns ONLY the job + trigger. Local-runnable too (a manual catch-up close).
|
|
2744
|
+
program
|
|
2745
|
+
.command('close-merged-issues')
|
|
2746
|
+
.helpGroup(ADVANCED_GROUP)
|
|
2747
|
+
.description('Close source issues whose work has landed on main (CI capability E, prd runner-in-ci). Resolves each closing issue from the work/ tree (resolveClosingIssue: a lone task closes its own `issue:`; a fanned task reaches the number via `task.prd: → prd issue:`), runs the existing "prd complete?" query for the prd case (closes ONLY when ALL its prd:<slug> tasks are in work/done/), and closes via the IssueProvider seam (atomic comment+close; NO direct gh). Re-implements NONE of the resolution/query/close — it WIRES them. Invoked by the emitted close-job workflow on a merge to main; DEGRADES (never crashes) on a missing/unauthenticated gh.')
|
|
2748
|
+
.option('--cwd <dir>', 'the repo working dir whose work/ tree to scan (default: cwd)')
|
|
2749
|
+
.option('--gh-bin <bin>', 'the gh CLI binary (default: gh on PATH)')
|
|
2750
|
+
.option('--json', 'output the raw result as JSON')
|
|
2751
|
+
.action(async (flags) => {
|
|
2752
|
+
const repoPath = flags.cwd ?? process.cwd();
|
|
2753
|
+
const result = await performCloseMergedIssues({
|
|
2754
|
+
repoPath,
|
|
2755
|
+
ghBin: flags.ghBin,
|
|
2756
|
+
env: process.env,
|
|
2757
|
+
});
|
|
2758
|
+
if (flags.json) {
|
|
2759
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2760
|
+
}
|
|
2761
|
+
else {
|
|
2762
|
+
for (const c of result.candidates) {
|
|
2763
|
+
if (c.decision === 'closed') {
|
|
2764
|
+
console.error(`>> closed issue #${c.issueNumber} (${c.via} ${c.slug}).`);
|
|
2765
|
+
}
|
|
2766
|
+
else if (c.decision === 'close-failed') {
|
|
2767
|
+
console.error(`>> issue #${c.issueNumber} (${c.via} ${c.slug}) NOT closed: ${c.reason}`);
|
|
2768
|
+
}
|
|
2769
|
+
else {
|
|
2770
|
+
console.error(`>> issue #${c.issueNumber} (${c.via} ${c.slug}) left open (${c.decision}).`);
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
console.error(`>> close-merged-issues: closed ${result.closed.length} issue(s).`);
|
|
2774
|
+
}
|
|
2775
|
+
// The close-job is a terminal CI tick: a degraded close is reported, not a
|
|
2776
|
+
// crash (exit 0), exactly like intake's bounce close.
|
|
2777
|
+
process.exit(0);
|
|
2778
|
+
});
|
|
2779
|
+
// The REGISTRY command group (ADR §1): the registered set of targets IS the
|
|
2780
|
+
// hub-mirror set on disk. `remote add --local` absorbs the old `arbiter init`;
|
|
2781
|
+
// `arbiter status` is folded into `status`. There is no standalone `arbiter`
|
|
2782
|
+
// command group, and no `roots`/`remotes` config field.
|
|
2783
|
+
const remote = program
|
|
2784
|
+
.command('remote')
|
|
2785
|
+
.helpGroup(HEADLINE_GROUP)
|
|
2786
|
+
.description('The registry: the registered set of targets IS the hub mirrors on disk under workspacesDir/repos/ (no roots/remotes config). add/rm/ls/find manage that set. `remote add --local` provisions a bare arbiter (absorbing `arbiter init`); `arbiter status` is folded into `status`.');
|
|
2787
|
+
remote
|
|
2788
|
+
.command('add <target>')
|
|
2789
|
+
.helpGroup(HEADLINE_GROUP)
|
|
2790
|
+
.description('Register a target by creating its hub mirror (idempotent). <target> is the arbiter URL; with --local it is a WORKING REPO whose bare arbiter is provisioned under arbitersDir (~/git, precious DATA, NEVER ~/.dorfl) and THAT arbiter is registered (absorbing `arbiter init`). The project-identity guard refuses registering one project (same projectId tail) under a second key unless --force; --force REPLACES the existing mirror but STILL refuses if a worktree of the replaced mirror holds un-pushed work (data-loss guard).')
|
|
2791
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
2792
|
+
.option('--local', 'provision a local --bare arbiter from <target> (a working repo) and register it (absorbs `arbiter init`)')
|
|
2793
|
+
.option('--arbiter-remote <name>', `name of the arbiter remote to wire in the working repo on --local (default: ${DEFAULT_ARBITER_REMOTE})`)
|
|
2794
|
+
.option('--force', 'REPLACE this project’s existing mirror (re-link remote ↔ bare arbiter deliberately); overrides the registration POLICY block ONLY — still refuses if a worktree of the replaced mirror holds un-pushed work (the data-loss block is never overridden)')
|
|
2795
|
+
.action((target, flags) => {
|
|
2796
|
+
const config = resolveGlobalConfig(loadConfig(flags.config), {});
|
|
2797
|
+
try {
|
|
2798
|
+
const result = remoteAdd({
|
|
2799
|
+
target,
|
|
2800
|
+
local: flags.local,
|
|
2801
|
+
workspacesDir: config.workspacesDir,
|
|
2802
|
+
arbitersDir: config.arbitersDir,
|
|
2803
|
+
arbiterRemote: flags.arbiterRemote ?? DEFAULT_ARBITER_REMOTE,
|
|
2804
|
+
force: flags.force,
|
|
2805
|
+
note: (message) => console.error(`>> ${message}`),
|
|
2806
|
+
});
|
|
2807
|
+
if (result.arbiter) {
|
|
2808
|
+
const a = result.arbiter;
|
|
2809
|
+
console.log(a.created
|
|
2810
|
+
? `Provisioned bare arbiter at ${a.path}`
|
|
2811
|
+
: `Arbiter already exists at ${a.path} (not clobbered)`);
|
|
2812
|
+
console.log(`Wired remote '${a.remote}' -> ${a.url}`);
|
|
2813
|
+
}
|
|
2814
|
+
console.log(result.created
|
|
2815
|
+
? `Registered '${result.key}' (${result.transport}) — hub mirror at ${result.mirrorPath}`
|
|
2816
|
+
: `'${result.key}' already registered (mirror at ${result.mirrorPath})`);
|
|
2817
|
+
}
|
|
2818
|
+
catch (err) {
|
|
2819
|
+
if (err instanceof RegistryError) {
|
|
2820
|
+
console.error(`error: ${err.message}`);
|
|
2821
|
+
process.exit(1);
|
|
2822
|
+
}
|
|
2823
|
+
throw err;
|
|
2824
|
+
}
|
|
2825
|
+
});
|
|
2826
|
+
remote
|
|
2827
|
+
.command('rm <target>')
|
|
2828
|
+
.helpGroup(ADVANCED_GROUP)
|
|
2829
|
+
.description('Delete a hub mirror by key (host/org/name) or origin URL. The ONLY mirror deleter — `gc` NEVER reaps mirrors. Plumbing tier.')
|
|
2830
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
2831
|
+
.action((target, flags) => {
|
|
2832
|
+
const config = resolveGlobalConfig(loadConfig(flags.config), {});
|
|
2833
|
+
const result = remoteRm({ target, workspacesDir: config.workspacesDir });
|
|
2834
|
+
if (!result.removed) {
|
|
2835
|
+
console.error(`error: no registered mirror matches '${target}'.`);
|
|
2836
|
+
process.exit(1);
|
|
2837
|
+
}
|
|
2838
|
+
console.log(`Removed mirror '${result.key}' (${result.path}).`);
|
|
2839
|
+
});
|
|
2840
|
+
remote
|
|
2841
|
+
.command('ls')
|
|
2842
|
+
.helpGroup(HEADLINE_GROUP)
|
|
2843
|
+
.description('List every registered hub mirror with its origin URL + transport. The origin URL is read from each mirror (the key encoding is lossy — it drops scheme/transport), so it is authoritative, not reconstructed from the key.')
|
|
2844
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
2845
|
+
.option('--json', 'output the raw list as JSON')
|
|
2846
|
+
.action((flags) => {
|
|
2847
|
+
const config = resolveGlobalConfig(loadConfig(flags.config), {});
|
|
2848
|
+
const mirrors = listMirrors({ workspacesDir: config.workspacesDir });
|
|
2849
|
+
if (flags.json) {
|
|
2850
|
+
console.log(JSON.stringify(mirrors, null, 2));
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2853
|
+
if (mirrors.length === 0) {
|
|
2854
|
+
console.log('No registered mirrors. Use `remote add <url>` or `remote find <folder>`.');
|
|
2855
|
+
return;
|
|
2856
|
+
}
|
|
2857
|
+
for (const m of mirrors) {
|
|
2858
|
+
console.log(`${m.key} ${m.transport} ${m.originUrl ?? '(no origin)'}`);
|
|
2859
|
+
}
|
|
2860
|
+
});
|
|
2861
|
+
remote
|
|
2862
|
+
.command('find <folder>')
|
|
2863
|
+
.helpGroup(HEADLINE_GROUP)
|
|
2864
|
+
.description('Discover work/-participating repos under <folder> (a populated work/backlog/), then toggle-add the chosen ones via `remote add`. Interactive multi-select by default; --yes adds ALL discovered repos non-interactively.')
|
|
2865
|
+
.option('-c, --config <path>', 'config file path', defaultConfigPath())
|
|
2866
|
+
.option('--yes', 'add all discovered participating repos (no prompt)')
|
|
2867
|
+
.action(async (folder, flags) => {
|
|
2868
|
+
const config = resolveGlobalConfig(loadConfig(flags.config), {});
|
|
2869
|
+
const repos = findParticipatingRepos(folder);
|
|
2870
|
+
if (repos.length === 0) {
|
|
2871
|
+
console.log(`No work/-participating repos found under ${folder}.`);
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
2874
|
+
const chosen = flags.yes ? repos : await promptMultiSelect(repos);
|
|
2875
|
+
if (chosen.length === 0) {
|
|
2876
|
+
console.log('Nothing selected; no mirrors added.');
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
for (const repoPath of chosen) {
|
|
2880
|
+
// Each discovered repo is registered as a LOCAL bare arbiter (it is a
|
|
2881
|
+
// working checkout on disk, not a remote URL) — the same path
|
|
2882
|
+
// `remote add --local` takes. The transport guard still applies.
|
|
2883
|
+
try {
|
|
2884
|
+
const result = remoteAdd({
|
|
2885
|
+
target: repoPath,
|
|
2886
|
+
local: true,
|
|
2887
|
+
workspacesDir: config.workspacesDir,
|
|
2888
|
+
arbitersDir: config.arbitersDir,
|
|
2889
|
+
arbiterRemote: DEFAULT_ARBITER_REMOTE,
|
|
2890
|
+
note: (message) => console.error(`>> ${message}`),
|
|
2891
|
+
});
|
|
2892
|
+
console.log(`${result.created ? 'Registered' : 'Already registered'} '${result.key}' (${repoPath}).`);
|
|
2893
|
+
}
|
|
2894
|
+
catch (err) {
|
|
2895
|
+
if (err instanceof RegistryError) {
|
|
2896
|
+
console.error(`skipped ${repoPath}: ${err.message}`);
|
|
2897
|
+
continue;
|
|
2898
|
+
}
|
|
2899
|
+
throw err;
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
});
|
|
2903
|
+
program
|
|
2904
|
+
.command('install-ci')
|
|
2905
|
+
.helpGroup(ADVANCED_GROUP)
|
|
2906
|
+
.description('Scaffold the CI auth/setup foundation (a one-time, human-run SCAFFOLDER): write the shared composite setup action (`dorfl-setup`) + provider auth (models.json default, or auth.json + GH_PAT + OAuth refresh) and set the provider secrets via the GitHub seam. Interactive wizard, or `--config <file>` for a non-interactive reproduction; `--export-config` round-trips the config; `--fake` writes to `.fake/` (never `.github/`) and sets NO real secret (a snapshot dry-run).')
|
|
2907
|
+
.option('--config <file>', 'non-interactive: load the CI config from this JSON file (skips the wizard)')
|
|
2908
|
+
.option('--export-config <file>', 'write the gathered config as JSON to this path instead of generating artifacts')
|
|
2909
|
+
.option('--include-secrets', '(with --export-config) also gather + include the secret values in the export')
|
|
2910
|
+
.option('--fake', 'snapshot mode: write artifacts to `.fake/` (NEVER `.github/`) and set NO real secret')
|
|
2911
|
+
.option('--repo <owner/repo>', 'the GitHub repo to set secrets on (else auto-detected via gh)')
|
|
2912
|
+
.option('--gh-bin <bin>', 'the gh CLI binary (default: gh on PATH)')
|
|
2913
|
+
.option('--cwd <dir>', 'the target repo working dir (default: cwd)')
|
|
2914
|
+
.option('--install-source <registry|workspace>', 'where the CI installs the CLI from: `registry` (npm install -g, the default) or `workspace` (build from the checked-out source + link onto PATH, for the self-hosting monorepo). Overrides auto-detection in both directions.')
|
|
2915
|
+
.option('--max-parallel <n>', 'cap on CONCURRENT advance-lifecycle matrix legs (the propose/merge `max-parallel`). Each leg is a full agent session, so a large fan-out can exhaust the model provider rate limit + thrash the CAS. Default 4.')
|
|
2916
|
+
.action(async (flags) => {
|
|
2917
|
+
const workDir = flags.cwd ?? process.cwd();
|
|
2918
|
+
if (flags.installSource !== undefined &&
|
|
2919
|
+
flags.installSource !== 'registry' &&
|
|
2920
|
+
flags.installSource !== 'workspace') {
|
|
2921
|
+
console.error(`install-ci: --install-source must be "registry" or "workspace" (got "${flags.installSource}")`);
|
|
2922
|
+
process.exitCode = 1;
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
let maxParallel;
|
|
2926
|
+
if (flags.maxParallel !== undefined) {
|
|
2927
|
+
const n = Number(flags.maxParallel);
|
|
2928
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
2929
|
+
console.error(`install-ci: --max-parallel must be a positive integer (got "${flags.maxParallel}")`);
|
|
2930
|
+
process.exitCode = 1;
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
maxParallel = n;
|
|
2934
|
+
}
|
|
2935
|
+
const ctx = new GitHubCIContext({
|
|
2936
|
+
workDir,
|
|
2937
|
+
repo: flags.repo,
|
|
2938
|
+
ghBin: flags.ghBin,
|
|
2939
|
+
});
|
|
2940
|
+
// Discover the registered capability emitters (the directory-of-modules
|
|
2941
|
+
// seam: each capability self-registers from its own file under
|
|
2942
|
+
// `install-ci-capabilities/`, picked up here WITHOUT a shared-list edit).
|
|
2943
|
+
// This core task ships only a no-op reference (emits []); the sibling
|
|
2944
|
+
// capability tasks add self-registering modules that flow through here
|
|
2945
|
+
// automatically once landed. A no-op emitter contributes nothing.
|
|
2946
|
+
const capabilities = await loadCapabilityRegistry();
|
|
2947
|
+
const prompts = flags.config ? undefined : readlinePrompts();
|
|
2948
|
+
await installCI({
|
|
2949
|
+
ctx,
|
|
2950
|
+
fake: flags.fake === true,
|
|
2951
|
+
configFile: flags.config,
|
|
2952
|
+
exportConfig: flags.exportConfig,
|
|
2953
|
+
includeSecrets: flags.includeSecrets === true,
|
|
2954
|
+
installSource: flags.installSource,
|
|
2955
|
+
maxParallel,
|
|
2956
|
+
prompts,
|
|
2957
|
+
capabilities,
|
|
2958
|
+
log: (line) => console.error(line),
|
|
2959
|
+
});
|
|
2960
|
+
});
|
|
2961
|
+
return program;
|
|
2962
|
+
}
|
|
2963
|
+
/**
|
|
2964
|
+
* A readline-backed {@link WizardPrompts} for the interactive `install-ci`
|
|
2965
|
+
* wizard. Prompts go to stderr (stdout is reserved for any machine output); a
|
|
2966
|
+
* non-TTY invocation falls back to defaults (or empty), so a piped run never
|
|
2967
|
+
* hangs — use `--config` for a fully non-interactive reproduction.
|
|
2968
|
+
*/
|
|
2969
|
+
function readlinePrompts() {
|
|
2970
|
+
const ask = (message, mask = false) => new Promise((resolvePrompt) => {
|
|
2971
|
+
if (!process.stdin.isTTY) {
|
|
2972
|
+
resolvePrompt('');
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
const rl = createInterface({
|
|
2976
|
+
input: process.stdin,
|
|
2977
|
+
output: process.stderr,
|
|
2978
|
+
});
|
|
2979
|
+
void mask; // readline has no native masking; secrets are typed visibly
|
|
2980
|
+
rl.question(`${message} `, (answer) => {
|
|
2981
|
+
rl.close();
|
|
2982
|
+
resolvePrompt(answer);
|
|
2983
|
+
});
|
|
2984
|
+
});
|
|
2985
|
+
return {
|
|
2986
|
+
async input(message, opts) {
|
|
2987
|
+
const hint = opts?.default ? ` [${opts.default}]` : '';
|
|
2988
|
+
const answer = (await ask(`${message}${hint}`)).trim();
|
|
2989
|
+
return answer === '' && opts?.default ? opts.default : answer;
|
|
2990
|
+
},
|
|
2991
|
+
async password(message) {
|
|
2992
|
+
return (await ask(message, true)).trim();
|
|
2993
|
+
},
|
|
2994
|
+
async confirm(message, opts) {
|
|
2995
|
+
const hint = opts.default ? ' [Y/n]' : ' [y/N]';
|
|
2996
|
+
const answer = (await ask(`${message}${hint}`)).trim().toLowerCase();
|
|
2997
|
+
if (answer === '')
|
|
2998
|
+
return opts.default;
|
|
2999
|
+
return answer === 'y' || answer === 'yes';
|
|
3000
|
+
},
|
|
3001
|
+
async select(message, choices) {
|
|
3002
|
+
process.stderr.write(`${message}\n`);
|
|
3003
|
+
choices.forEach((c, i) => {
|
|
3004
|
+
process.stderr.write(` [${i + 1}] ${c.name}\n`);
|
|
3005
|
+
});
|
|
3006
|
+
const answer = (await ask('Choose (number):')).trim();
|
|
3007
|
+
const n = Number(answer);
|
|
3008
|
+
if (Number.isInteger(n) && n >= 1 && n <= choices.length) {
|
|
3009
|
+
return choices[n - 1].value;
|
|
3010
|
+
}
|
|
3011
|
+
return choices[0].value; // default to the first choice
|
|
3012
|
+
},
|
|
3013
|
+
};
|
|
3014
|
+
}
|
|
3015
|
+
/**
|
|
3016
|
+
* A minimal interactive multi-select toggle for `remote find`: list the
|
|
3017
|
+
* discovered repos numbered, let the user type the numbers to add (space/comma
|
|
3018
|
+
* separated; `all` for everything; blank for none). A non-interactive (no TTY)
|
|
3019
|
+
* invocation selects nothing — use `--yes` to add all without a prompt.
|
|
3020
|
+
*/
|
|
3021
|
+
function promptMultiSelect(repos) {
|
|
3022
|
+
return new Promise((resolvePrompt) => {
|
|
3023
|
+
if (!process.stdin.isTTY) {
|
|
3024
|
+
resolvePrompt([]);
|
|
3025
|
+
return;
|
|
3026
|
+
}
|
|
3027
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
3028
|
+
process.stderr.write('Discovered work/-participating repos:\n');
|
|
3029
|
+
repos.forEach((repo, i) => {
|
|
3030
|
+
process.stderr.write(` [${i + 1}] ${repo}\n`);
|
|
3031
|
+
});
|
|
3032
|
+
rl.question('Add which? (numbers, `all`, or blank for none) ', (answer) => {
|
|
3033
|
+
rl.close();
|
|
3034
|
+
const trimmed = answer.trim().toLowerCase();
|
|
3035
|
+
if (trimmed === '') {
|
|
3036
|
+
resolvePrompt([]);
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
if (trimmed === 'all') {
|
|
3040
|
+
resolvePrompt([...repos]);
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
const picks = new Set();
|
|
3044
|
+
for (const token of trimmed.split(/[\s,]+/)) {
|
|
3045
|
+
const n = Number(token);
|
|
3046
|
+
if (Number.isInteger(n) && n >= 1 && n <= repos.length) {
|
|
3047
|
+
picks.add(repos[n - 1]);
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
resolvePrompt([...picks]);
|
|
3051
|
+
});
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
3054
|
+
/**
|
|
3055
|
+
* Run the CLI: build the program and parse argv. Split from {@link buildProgram}
|
|
3056
|
+
* so tests can build + introspect/parse the program WITHOUT triggering a real
|
|
3057
|
+
* argv parse + `process.exit` on import (the module-level bootstrap below only
|
|
3058
|
+
* fires when this file is the process entry point).
|
|
3059
|
+
*/
|
|
3060
|
+
export async function runCli(argv = process.argv) {
|
|
3061
|
+
const program = buildProgram();
|
|
3062
|
+
try {
|
|
3063
|
+
await program.parseAsync(argv);
|
|
3064
|
+
}
|
|
3065
|
+
catch (err) {
|
|
3066
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
3067
|
+
process.exit(1);
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
// Only bootstrap when invoked as the entry point (the installed `bin`), never on
|
|
3071
|
+
// import (so `buildProgram`/`runCli` are import-safe for tests).
|
|
3072
|
+
if (isCliEntryPoint()) {
|
|
3073
|
+
void runCli();
|
|
3074
|
+
}
|
|
3075
|
+
/**
|
|
3076
|
+
* True iff this module is the process entry point (the `dorfl` bin).
|
|
3077
|
+
* Resolves both sides through `realpathSync` so a bin SYMLINK (npm/pnpm install
|
|
3078
|
+
* a `node_modules/.bin/dorfl` link to `dist/cli.js`) still matches.
|
|
3079
|
+
*/
|
|
3080
|
+
function isCliEntryPoint() {
|
|
3081
|
+
const entry = process.argv[1];
|
|
3082
|
+
if (!entry) {
|
|
3083
|
+
return false;
|
|
3084
|
+
}
|
|
3085
|
+
try {
|
|
3086
|
+
const entryReal = realpathSync(entry);
|
|
3087
|
+
const selfReal = realpathSync(fileURLToPath(import.meta.url));
|
|
3088
|
+
return entryReal === selfReal;
|
|
3089
|
+
}
|
|
3090
|
+
catch {
|
|
3091
|
+
return false;
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
//# sourceMappingURL=cli.js.map
|