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/src/advance.ts
ADDED
|
@@ -0,0 +1,1564 @@
|
|
|
1
|
+
import {existsSync, readFileSync} from 'node:fs';
|
|
2
|
+
import {join} from 'node:path';
|
|
3
|
+
import {type WorkFolderKey, workItemPath, workItemRel} from './work-layout.js';
|
|
4
|
+
import {
|
|
5
|
+
resolveAdvanceArg,
|
|
6
|
+
SlugResolutionError,
|
|
7
|
+
type SlugNamespace,
|
|
8
|
+
} from './slug-namespace.js';
|
|
9
|
+
import {ledgerRead, type LedgerReadStrategy} from './ledger-read.js';
|
|
10
|
+
import {parseFrontmatter} from './frontmatter.js';
|
|
11
|
+
import {
|
|
12
|
+
parseSidecar,
|
|
13
|
+
sidecarPathFor,
|
|
14
|
+
type SidecarModel,
|
|
15
|
+
type SidecarType,
|
|
16
|
+
} from './sidecar.js';
|
|
17
|
+
import {classifyTick, type TickClassification} from './advance-classify.js';
|
|
18
|
+
import {
|
|
19
|
+
acquireAdvancingLock,
|
|
20
|
+
releaseAdvancingLock,
|
|
21
|
+
type AcquireAdvancingLockResult,
|
|
22
|
+
type ReleaseAdvancingLockResult,
|
|
23
|
+
} from './advancing-lock.js';
|
|
24
|
+
import {performDo, type DoOptions, type DoResult} from './do.js';
|
|
25
|
+
import {
|
|
26
|
+
harnessSurfaceGate,
|
|
27
|
+
toNewQuestions,
|
|
28
|
+
type SurfaceGate,
|
|
29
|
+
} from './surface-gate.js';
|
|
30
|
+
import {harnessTriageGate, type TriageGate} from './triage-gate.js';
|
|
31
|
+
import type {ObservationTriage} from './config.js';
|
|
32
|
+
import {
|
|
33
|
+
autoDispositionObservation,
|
|
34
|
+
promoteObservation,
|
|
35
|
+
type AutoDispositionOptions,
|
|
36
|
+
type AutoDispositionResult,
|
|
37
|
+
type PromoteObservationOptions,
|
|
38
|
+
type PromoteObservationResult,
|
|
39
|
+
} from './triage-persist.js';
|
|
40
|
+
import {mintAdr, type MintAdrOptions, type MintAdrResult} from './mint-adr.js';
|
|
41
|
+
import {LIFECYCLE_CAS_CONTENTION} from './advancing-lock.js';
|
|
42
|
+
import {
|
|
43
|
+
decide,
|
|
44
|
+
DisallowedOutcomeError,
|
|
45
|
+
type DecisionVerdict,
|
|
46
|
+
} from './decision-engine.js';
|
|
47
|
+
import {
|
|
48
|
+
APPLY_ALLOWED_OUTCOMES,
|
|
49
|
+
buildApplyDecisionInput,
|
|
50
|
+
harnessApplyDecider,
|
|
51
|
+
type ApplyDecider,
|
|
52
|
+
} from './apply-decide.js';
|
|
53
|
+
import {
|
|
54
|
+
persistSurfacedQuestions,
|
|
55
|
+
type SurfacePersistOptions,
|
|
56
|
+
type SurfacePersistResult,
|
|
57
|
+
} from './surface-persist.js';
|
|
58
|
+
import {
|
|
59
|
+
applyAnsweredQuestions,
|
|
60
|
+
type ApplyAnsweredQuestionsOptions,
|
|
61
|
+
type ApplyAnsweredQuestionsResult,
|
|
62
|
+
} from './apply-persist.js';
|
|
63
|
+
import {
|
|
64
|
+
detectAnsweredMergeAction,
|
|
65
|
+
performMergeAction,
|
|
66
|
+
type MergeActionHandler,
|
|
67
|
+
type MergeActionResult,
|
|
68
|
+
} from './apply-merge-action.js';
|
|
69
|
+
import type {VerifyConfig} from './verify.js';
|
|
70
|
+
import type {NewQuestion} from './sidecar.js';
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The **`advance` verb SKELETON** (prd `advance-loop`, task
|
|
74
|
+
* `advance-verb-resolver`, US #1/5/6/18). `advance` is the SIBLING top-level verb
|
|
75
|
+
* (NOT a `do` subcommand — `do` subcommands are REJECTED in the prd) that drives
|
|
76
|
+
* a `work/` item ONE lifecycle rung toward "ready/built", reusing the SAME shared
|
|
77
|
+
* `prefix:arg` resolver `do` uses (extended with the `obs:` namespace, see
|
|
78
|
+
* {@link resolveAdvanceArg}).
|
|
79
|
+
*
|
|
80
|
+
* This module delivers the **classify → lock → execute SKELETON** — the contract
|
|
81
|
+
* both drivers (the later one-shot/loop tasks) wrap:
|
|
82
|
+
*
|
|
83
|
+
* 1. **classify** — read-only, NO model, NO lock: read the item's two signals
|
|
84
|
+
* (`needsAnswers` + the sidecar's answered-state) and call the pure
|
|
85
|
+
* {@link classifyTick} to get the rung kind. A CAS loser will have spent ONLY
|
|
86
|
+
* this free classification.
|
|
87
|
+
* 2. **lock** — take the `advancing` CAS borrow ({@link acquireAdvancingLock})
|
|
88
|
+
* for the classified rung, keyed on the item's `<type>-<slug>` identity. The
|
|
89
|
+
* expensive (agent/model) phase is ALWAYS post-lock, so a loser backs off
|
|
90
|
+
* having done ~nothing (a TOCTOU between classify and CAS is harmless — only
|
|
91
|
+
* the free classification is wasted, and the loser never starts model work).
|
|
92
|
+
* 3. **execute** — WINNER ONLY: dispatch the classified rung to the
|
|
93
|
+
* {@link RungExecutor} seam, then release the borrow.
|
|
94
|
+
*
|
|
95
|
+
* The **rung BODIES** are now ALL filled (their own tasks): `surface`
|
|
96
|
+
* (`advance-rung-surface`), `apply` (`advance-rung-apply`), and
|
|
97
|
+
* `triage-observation` (`advance-rung-triage`) dispatch through the clearly-named
|
|
98
|
+
* executor SEAM ({@link RungExecutor}); the build/task rungs ORCHESTRATE
|
|
99
|
+
* `do`/`do prd:`. What this verb does NOT do (LATER tasks):
|
|
100
|
+
* - The two **DRIVERS** (one-shot sequential / loop) + `-n` + the gate-FAMILY
|
|
101
|
+
* WIRING that resolves `autoBuild`/`autoTask`/`observationTriage` and threads
|
|
102
|
+
* them into the build/task gate composition — task `advance-drivers-and-gates`.
|
|
103
|
+
* (This verb already RESPECTS `observationTriage` in the triage rung — the gate's
|
|
104
|
+
* resolution chain + the build/task gate composition is the drivers task.)
|
|
105
|
+
* - The bare `advance` (eligible-SET) form — it needs the pool scan / driver, so
|
|
106
|
+
* the verb here is a SINGLE named-item tick; the bare form errors clearly
|
|
107
|
+
* ("needs the driver task"). See the `## Decisions` block in the task.
|
|
108
|
+
*
|
|
109
|
+
* The build-task / task-spec rungs ORCHESTRATE the existing `do` / `do spec:`
|
|
110
|
+
* machinery ({@link performDo}) — `advance` is a driver layered ON TOP, NEVER a
|
|
111
|
+
* peer that duplicates the build/task path (ONE build path, ONE task path —
|
|
112
|
+
* US #6).
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
const DEFAULT_ARBITER = 'origin';
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The terminal condition of one `advance` tick (mirrors `DoOutcome`'s shape).
|
|
119
|
+
*
|
|
120
|
+
* `vanished` (task `observation-identity-is-its-filename-not-a-foreign-slug`):
|
|
121
|
+
* a BENIGN SKIP when the item's file was enumerated into the lifecycle pool but
|
|
122
|
+
* has since been moved/triaged/deleted by a sibling leg (the cross-tick window
|
|
123
|
+
* under parallel CI). It is `exitCode: 0` so the matrix tolerates it, but it is
|
|
124
|
+
* DISTINGUISHABLE from `no-op` (which is a calm classify result, e.g. a pending
|
|
125
|
+
* sidecar) so reviewers can grep these out. NOT used for a human-typed bare slug
|
|
126
|
+
* that names nothing — that is a malformed invocation; today the two are not
|
|
127
|
+
* distinguished at this seam (a human typo also skips benignly) and the matrix
|
|
128
|
+
* scale of the calm condition justifies the trade.
|
|
129
|
+
*/
|
|
130
|
+
export type AdvanceOutcome =
|
|
131
|
+
| 'advanced'
|
|
132
|
+
| 'no-op'
|
|
133
|
+
| 'vanished'
|
|
134
|
+
| 'usage-error'
|
|
135
|
+
| 'lost'
|
|
136
|
+
| 'contended'
|
|
137
|
+
| 'not-implemented'
|
|
138
|
+
| 'invariant-violation';
|
|
139
|
+
|
|
140
|
+
/** Maps onto the claim-CAS exit codes (identical semantics). */
|
|
141
|
+
export type AdvanceExitCode = 0 | 1 | 2 | 3;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The injectable rung-executor SEAM — WHAT happens once the tick has classified a
|
|
145
|
+
* rung AND won the `advancing` lock. It is the boundary between the skeleton (this
|
|
146
|
+
* task) and the rung bodies (later tasks): the surface/apply/triage rungs are
|
|
147
|
+
* filled by their own tasks; the build/task rungs ORCHESTRATE `do`/`do prd:`.
|
|
148
|
+
*
|
|
149
|
+
* Production wires {@link defaultRungExecutor}; tests inject a spy to assert the
|
|
150
|
+
* classify→lock→dispatch ORDER (and that a CAS loser never reaches the executor).
|
|
151
|
+
*/
|
|
152
|
+
export interface RungExecutor {
|
|
153
|
+
/** A ready task → build it by ORCHESTRATING `do <slug>` (NOT a re-implementation). */
|
|
154
|
+
buildTask(input: RungExecInput): Promise<RungExecResult>;
|
|
155
|
+
/** A ready spec → task it by ORCHESTRATING `do spec:<slug>` (NOT a re-implementation). */
|
|
156
|
+
taskSpec(input: RungExecInput): Promise<RungExecResult>;
|
|
157
|
+
/** An untriaged observation → triage it (LATER task fills this body). */
|
|
158
|
+
triageObservation(input: RungExecInput): Promise<RungExecResult>;
|
|
159
|
+
/** `needsAnswers` but no sidecar → surface the questions (LATER task fills this). */
|
|
160
|
+
surface(input: RungExecInput): Promise<RungExecResult>;
|
|
161
|
+
/** Every entry answered → apply the answers + advance (LATER task fills this). */
|
|
162
|
+
apply(input: RungExecInput): Promise<RungExecResult>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** What a rung executor is handed: the resolved identity + the run context. */
|
|
166
|
+
export interface RungExecInput {
|
|
167
|
+
/** The canonical namespaced identity (`task:<slug>` / `prd:<slug>` / `observation:<slug>`). */
|
|
168
|
+
item: string;
|
|
169
|
+
/** The resolved namespace (`task` / `prd` / `observation`). */
|
|
170
|
+
namespace: SlugNamespace;
|
|
171
|
+
/** The bare slug. */
|
|
172
|
+
slug: string;
|
|
173
|
+
/** The classification that selected this rung (the two signals are visible). */
|
|
174
|
+
classification: TickClassification;
|
|
175
|
+
/** The tick's run context (cwd, arbiter, …) — threaded to `do`/`do prd:`. */
|
|
176
|
+
context: AdvanceContext;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** A rung executor's result (the outcome the tick reports). */
|
|
180
|
+
export interface RungExecResult {
|
|
181
|
+
exitCode: AdvanceExitCode;
|
|
182
|
+
outcome: AdvanceOutcome;
|
|
183
|
+
message: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The run context threaded from the CLI into the tick + the rung executor. */
|
|
187
|
+
export interface AdvanceContext {
|
|
188
|
+
/** The working clone/checkout to run in-place in. */
|
|
189
|
+
cwd: string;
|
|
190
|
+
/** Name of the arbiter git remote. Defaults to `origin`. */
|
|
191
|
+
arbiter?: string;
|
|
192
|
+
/** The base `do` options the build/task rungs orchestrate `performDo` with. */
|
|
193
|
+
doOptions?: Omit<DoOptions, 'arg'>;
|
|
194
|
+
/**
|
|
195
|
+
* The build/task ORCHESTRATION DRIVER seam (task
|
|
196
|
+
* `advance-loop-driver-registry-set-job-worktrees`). The build-task / task-spec
|
|
197
|
+
* rungs ORCHESTRATE `do` by handing the resolved arg + the threaded
|
|
198
|
+
* {@link doOptions} to THIS driver. `undefined` ⇒ {@link performDo} (the IN-PLACE
|
|
199
|
+
* substrate — the human-local one-shot `advance` command + today's
|
|
200
|
+
* single-mirror `run --advance`, which build in the cwd checkout). The
|
|
201
|
+
* registry-set advance driver injects a PER-MIRROR JOB-WORKTREE driver
|
|
202
|
+
* ({@link jobWorktreeDoDriver}) so the daemon/CI path builds isolated off each
|
|
203
|
+
* mirror's arbiter (the SAME isolation `run`'s build tick gives `runOneItem`),
|
|
204
|
+
* NOT in `process.cwd()`. This is the parameterised isolation strategy the
|
|
205
|
+
* task's `## Decisions` records: in-place and worktree COEXIST behind one seam,
|
|
206
|
+
* reusing the EXISTING `selectIsolationStrategy`/`jobWorktreeStrategy` (no second
|
|
207
|
+
* isolation mechanism). The DEFAULT keeps in-place behaviour byte-for-byte.
|
|
208
|
+
*/
|
|
209
|
+
doDriver?: (options: DoOptions) => Promise<DoResult>;
|
|
210
|
+
/**
|
|
211
|
+
* The SURFACE gate seam — the fresh-context `surface-questions` spawn the
|
|
212
|
+
* surface rung uses (task `advance-rung-surface`). The skill JUDGES (emits
|
|
213
|
+
* questions); the engine PERSISTS. Production wires {@link harnessSurfaceGate};
|
|
214
|
+
* tests inject a stub emit. `undefined` ⇒ the surface rung defaults to
|
|
215
|
+
* {@link harnessSurfaceGate} (a NullHarness, no real model) so the seam is never
|
|
216
|
+
* a crash — but the CLI threads the real harness-backed gate.
|
|
217
|
+
*/
|
|
218
|
+
surfaceGate?: SurfaceGate;
|
|
219
|
+
/**
|
|
220
|
+
* The model the SURFACE agent runs on (de-correlated from the builder, like
|
|
221
|
+
* `reviewModel`). Flows to the gate's launch through `LaunchInput.model`.
|
|
222
|
+
*/
|
|
223
|
+
surfaceModel?: string;
|
|
224
|
+
/**
|
|
225
|
+
* Persist the surfaced questions ATOMICALLY (append-or-create the sidecar + set
|
|
226
|
+
* `needsAnswers:true` in ONE commit). Tests inject a spy; production uses
|
|
227
|
+
* {@link persistSurfacedQuestions}. The ENGINE owns ALL persistence — the skill
|
|
228
|
+
* writes nothing.
|
|
229
|
+
*/
|
|
230
|
+
surfacePersist?: (options: SurfacePersistOptions) => SurfacePersistResult;
|
|
231
|
+
/**
|
|
232
|
+
* Apply the HUMAN's answered sidecar ATOMICALLY (item body + sidecar in ONE
|
|
233
|
+
* commit, via the sidecar contract's atomic-apply), then resolve / re-pause /
|
|
234
|
+
* disposition to a terminal. Tests inject a spy; production uses
|
|
235
|
+
* {@link applyAnsweredQuestions}. The engine applies ONLY human-authored answers
|
|
236
|
+
* — it NEVER invents one.
|
|
237
|
+
*/
|
|
238
|
+
applyPersist?: (
|
|
239
|
+
options: ApplyAnsweredQuestionsOptions,
|
|
240
|
+
) => ApplyAnsweredQuestionsResult;
|
|
241
|
+
/**
|
|
242
|
+
* Supply the NEW follow-up questions an apply discovered (so it APPENDS them and
|
|
243
|
+
* re-pauses rather than resolving). `undefined`/empty ⇒ the apply resolves (or
|
|
244
|
+
* dispositions) the item. The follow-up GENERATION is the surface skill's job;
|
|
245
|
+
* this seam lets the apply rung append already-formulated follow-ups (and lets
|
|
246
|
+
* tests drive the append-re-pause path) WITHOUT inventing an ANSWER.
|
|
247
|
+
*/
|
|
248
|
+
applyFollowups?: NewQuestion[];
|
|
249
|
+
/**
|
|
250
|
+
* The AGENTIC apply DECISION seam (task
|
|
251
|
+
* `agentic-apply-retire-disposition-vocabulary`): the fresh-context decision
|
|
252
|
+
* agent the apply rung runs on a fully-answered OBSERVATION to choose what to DO
|
|
253
|
+
* with the signal (`mint-task | mint-prd | mint-adr | delete-source |
|
|
254
|
+
* ask-follow-up`),
|
|
255
|
+
* grounded in the source's full context. It is the injected
|
|
256
|
+
* {@link ApplyDecider} the shared `decide(input, allowedOutcomes)` engine runs;
|
|
257
|
+
* tests inject a CANNED verdict (no model). `undefined` ⇒ the apply rung defaults
|
|
258
|
+
* to {@link harnessApplyDecider} (a NullHarness, no real model) so the seam is
|
|
259
|
+
* never a crash — but the CLI threads the real harness-backed decider. The
|
|
260
|
+
* verdict's type SELECTION (task vs prd vs adr) replaces the retired `promote-*`
|
|
261
|
+
* disposition token; `adr` is now WIRED (task `agentic-apply-mint-adr-route`).
|
|
262
|
+
*/
|
|
263
|
+
applyDecide?: ApplyDecider;
|
|
264
|
+
/** The model the apply-DECISION agent runs on (de-correlated, like `surfaceModel`). */
|
|
265
|
+
applyModel?: string;
|
|
266
|
+
/**
|
|
267
|
+
* The 3-state `observationTriage` policy (ADR `ci-config-policy-and-gate-
|
|
268
|
+
* family` §2) read at the triage rung. It governs the rung-internal
|
|
269
|
+
* ask-vs-auto distinction (the SELECTION-layer `off` gate is applied EARLIER, in
|
|
270
|
+
* the driver, by dropping the observation pool — so a rung that runs was either
|
|
271
|
+
* `ask`/`auto`-selected OR explicitly named, which BYPASSES the selection gate):
|
|
272
|
+
* - `'auto'` ⇒ the conservative auto-disposition EXCEPTION is live (ask the
|
|
273
|
+
* {@link TriageGate}; auto-dispose ONLY the no-question cases);
|
|
274
|
+
* - `'ask'` / `'off'` / `undefined` ⇒ surface the promote/keep/delete question
|
|
275
|
+
* and WAIT (the question-gated path). Under `off` + an EXPLICIT `obs:<slug>`
|
|
276
|
+
* (which bypasses the selection gate) the rung runs in `ask`-mode — the
|
|
277
|
+
* conservative, question-surfacing default (task `## Decisions`). SURFACE +
|
|
278
|
+
* APPLY stay ALWAYS allowed; this gate ONLY governs the auto-disposition
|
|
279
|
+
* exception, never the always-allowed question loop.
|
|
280
|
+
*/
|
|
281
|
+
observationTriage?: ObservationTriage;
|
|
282
|
+
/**
|
|
283
|
+
* The TRIAGE auto-disposition gate seam — the fresh-context spawn the triage
|
|
284
|
+
* rung asks (ONLY when `observationTriage` is `'auto'`) whether an observation
|
|
285
|
+
* is a no-question case. The skill JUDGES; the engine ACTS. Production wires
|
|
286
|
+
* {@link harnessTriageGate}; tests inject a stub decision. `undefined` ⇒ the
|
|
287
|
+
* triage rung defaults to {@link harnessTriageGate} (a NullHarness, no real
|
|
288
|
+
* model) so the seam is never a crash.
|
|
289
|
+
*/
|
|
290
|
+
triageGate?: TriageGate;
|
|
291
|
+
/** The model the TRIAGE agent runs on (de-correlated, like `surfaceModel`). */
|
|
292
|
+
triageModel?: string;
|
|
293
|
+
/**
|
|
294
|
+
* Execute the conservative auto-disposition ATOMICALLY (record + marker, one
|
|
295
|
+
* commit). Tests inject a spy; production uses {@link autoDispositionObservation}.
|
|
296
|
+
*/
|
|
297
|
+
autoDisposition?: (options: AutoDispositionOptions) => AutoDispositionResult;
|
|
298
|
+
/**
|
|
299
|
+
* Promote an answered observation: CAS-create a new backlog stub keyed on the
|
|
300
|
+
* NEW item's identity, then record + resolve the observation. Tests inject a
|
|
301
|
+
* spy; production uses {@link promoteObservation}.
|
|
302
|
+
*/
|
|
303
|
+
promote?: (
|
|
304
|
+
options: PromoteObservationOptions,
|
|
305
|
+
) => Promise<PromoteObservationResult>;
|
|
306
|
+
/**
|
|
307
|
+
* Mint an ADR from an answered observation (the agentic `mint-adr` verdict, task
|
|
308
|
+
* `agentic-apply-mint-adr-route`): CAS-create `docs/adr/<slug>.md` keyed on the
|
|
309
|
+
* NEW ADR's identity, with the source + sidecar deleted in the SAME commit. The
|
|
310
|
+
* SIBLING of {@link promote} for the `docs/adr/` target (an ADR lands OUTSIDE the
|
|
311
|
+
* work board, so it is a distinct route, not a `promoteObservation` artifact
|
|
312
|
+
* type). Tests inject a spy; production uses {@link mintAdr}.
|
|
313
|
+
*/
|
|
314
|
+
mintAdr?: (options: MintAdrOptions) => Promise<MintAdrResult>;
|
|
315
|
+
/**
|
|
316
|
+
* The NEW backlog slug an answered promote drafts. `undefined` ⇒ the promote
|
|
317
|
+
* defaults to the observation's own slug. Lets a test (or a future driver) steer
|
|
318
|
+
* the promoted item's identity WITHOUT inventing the answer.
|
|
319
|
+
*/
|
|
320
|
+
promoteSlug?: string;
|
|
321
|
+
/**
|
|
322
|
+
* The execution working area (`workspacesDir`, default `~/.dorfl`) the
|
|
323
|
+
* answered-merge LAND uses to cut a per-job worktree from the hub mirror
|
|
324
|
+
* (via `workspace.ts` `createJob`). Unset ⇒ no `workspacesDir` is available
|
|
325
|
+
* to the dispatcher, so an answered `kind: merge` entry is REFUSED (clean
|
|
326
|
+
* surfacing); the answer stays for a follow-up. The registry-set advance
|
|
327
|
+
* driver threads the resolved `workspacesDir` here.
|
|
328
|
+
* (prd `land-time-reverify-and-parallel-merge-ceiling`, task
|
|
329
|
+
* `apply-rung-merge-disposition`)
|
|
330
|
+
*/
|
|
331
|
+
workspacesDir?: string;
|
|
332
|
+
/**
|
|
333
|
+
* The arbiter URL the answered-merge LAND mirrors from. Optional: when
|
|
334
|
+
* unset the dispatcher resolves it via `git remote get-url <arbiter>` in the
|
|
335
|
+
* apply rung's `cwd` (the in-place / one-shot caller). The registry-set
|
|
336
|
+
* advance driver threads the per-mirror origin URL here directly so the
|
|
337
|
+
* per-mirror tree-less clone (whose `origin` points at the LOCAL mirror
|
|
338
|
+
* path, not the real arbiter URL) is bypassed.
|
|
339
|
+
*/
|
|
340
|
+
arbiterUrl?: string;
|
|
341
|
+
/**
|
|
342
|
+
* Per-repo env-prep config (`prepare`) the answered-merge LAND threads into
|
|
343
|
+
* `performIntegration` so the fresh-worktree gate runs `prepare` then
|
|
344
|
+
* `verify` on the rebased tip — the SAME config the build path uses.
|
|
345
|
+
*/
|
|
346
|
+
prepare?: VerifyConfig;
|
|
347
|
+
/**
|
|
348
|
+
* Per-repo acceptance gate (`verify`) the answered-merge LAND threads into
|
|
349
|
+
* `performIntegration` so the fresh-worktree gate re-verifies the rebased
|
|
350
|
+
* tip before integrating.
|
|
351
|
+
*/
|
|
352
|
+
verify?: VerifyConfig;
|
|
353
|
+
/**
|
|
354
|
+
* Resolved `strictMergeApproval` boolean (sibling task
|
|
355
|
+
* `strict-merge-approval-gate`). Default OFF ⇒ honour the prior approval +
|
|
356
|
+
* land on a green re-verify (cheap, the PRD-applied OQ6 default). ON ⇒
|
|
357
|
+
* re-surface the merge-question when the merge-base moved between the
|
|
358
|
+
* surfacer's question and this apply (the host-agnostic analogue of
|
|
359
|
+
* GitHub's "dismiss stale approvals when the base changes").
|
|
360
|
+
*/
|
|
361
|
+
strictMergeApproval?: boolean;
|
|
362
|
+
/**
|
|
363
|
+
* The merge-action dispatch SEAM (task `apply-rung-merge-disposition`):
|
|
364
|
+
* the deterministic answer-driven runner-action handler the apply rung
|
|
365
|
+
* invokes BEFORE the agentic decider when an answered `kind: merge` entry
|
|
366
|
+
* is detected. Production wires {@link performMergeAction} (checks out via
|
|
367
|
+
* `createJob` + lands via `performIntegration` with `committedRecovery:
|
|
368
|
+
* true` + `freshWorktreeGate: true`); tests inject a stub so they assert on
|
|
369
|
+
* the apply-rung's `landed | refused | restale | hold | drop` routing
|
|
370
|
+
* WITHOUT spinning up a hub mirror or running real verify.
|
|
371
|
+
*/
|
|
372
|
+
mergeAction?: MergeActionHandler;
|
|
373
|
+
/** Sink for human-readable progress notes. */
|
|
374
|
+
note?: (message: string) => void;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** The options one `advance` tick consumes. */
|
|
378
|
+
export interface AdvanceOptions extends AdvanceContext {
|
|
379
|
+
/**
|
|
380
|
+
* The raw CLI slug argument: bare (= task), `task:<slug>`, `prd:<slug>`, or
|
|
381
|
+
* `obs:<slug>` / `observation:<slug>`. Omit/empty ⇒ the bare eligible-SET form,
|
|
382
|
+
* which needs the driver task (a clear error here — see `## Decisions`).
|
|
383
|
+
*/
|
|
384
|
+
arg?: string;
|
|
385
|
+
/** The repo working-tree root whose `work/` to read (defaults to `cwd`). */
|
|
386
|
+
repoPath?: string;
|
|
387
|
+
/** The read seam for the resolver's cross-namespace existence check. */
|
|
388
|
+
read?: LedgerReadStrategy;
|
|
389
|
+
/** The rung executor seam (defaults to {@link defaultRungExecutor}). */
|
|
390
|
+
executor?: RungExecutor;
|
|
391
|
+
/** Inject the classify signals (tests); production reads them from disk. */
|
|
392
|
+
readSignals?: (input: ReadSignalsInput) => ItemSignals;
|
|
393
|
+
/** Inject the lock acquire (tests); production uses {@link acquireAdvancingLock}. */
|
|
394
|
+
acquireLock?: (item: string) => Promise<AcquireAdvancingLockResult>;
|
|
395
|
+
/** Inject the lock release (tests); production uses {@link releaseAdvancingLock}. */
|
|
396
|
+
releaseLock?: (item: string) => Promise<ReleaseAdvancingLockResult>;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** The tick's terminal result. */
|
|
400
|
+
export interface AdvanceResult {
|
|
401
|
+
exitCode: AdvanceExitCode;
|
|
402
|
+
outcome: AdvanceOutcome;
|
|
403
|
+
/** The classified rung kind, when the tick got far enough to classify. */
|
|
404
|
+
rung?: TickClassification['kind'];
|
|
405
|
+
/** The resolved bare slug acted on, when one was resolved. */
|
|
406
|
+
slug?: string;
|
|
407
|
+
/** Human-readable summary of the terminal condition. */
|
|
408
|
+
message: string;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** The two classify SIGNALS read off the item (the only state the classifier needs). */
|
|
412
|
+
export interface ItemSignals {
|
|
413
|
+
/** The item-body `needsAnswers` flag. */
|
|
414
|
+
needsAnswers: boolean | undefined;
|
|
415
|
+
/** The parsed ACTIVE sidecar, or `undefined` when none exists. */
|
|
416
|
+
sidecar: SidecarModel | undefined;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** What {@link readItemSignals} needs to read an item's two signals off disk. */
|
|
420
|
+
export interface ReadSignalsInput {
|
|
421
|
+
/** The repo working-tree root. */
|
|
422
|
+
repoPath: string;
|
|
423
|
+
/** The item type (task / prd / observation). */
|
|
424
|
+
type: SidecarType;
|
|
425
|
+
/** The bare slug. */
|
|
426
|
+
slug: string;
|
|
427
|
+
/** The canonical namespaced identity (`<namespace>:<slug>`). */
|
|
428
|
+
item: string;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Map the resolver's namespace onto the sidecar type. The `spec` namespace maps
|
|
433
|
+
* to the `spec` sidecar type, so `advance spec:<slug>` resolves its
|
|
434
|
+
* signals/folders (`FOLDERS_FOR_TYPE['spec']`) and orchestrates `do spec:<slug>`.
|
|
435
|
+
*/
|
|
436
|
+
function sidecarTypeFor(namespace: SlugNamespace): SidecarType {
|
|
437
|
+
return namespace === 'observation'
|
|
438
|
+
? 'observation'
|
|
439
|
+
: namespace === 'spec'
|
|
440
|
+
? 'spec'
|
|
441
|
+
: 'task';
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Read an item's two CLASSIFY signals off disk (read-only, NO model, NO lock):
|
|
446
|
+
* - `needsAnswers` from the item-body frontmatter (searching the lifecycle
|
|
447
|
+
* folders the type may rest in), and
|
|
448
|
+
* - the ACTIVE sidecar (`work/questions/<type>-<slug>.md`) parsed when present.
|
|
449
|
+
*
|
|
450
|
+
* Identity-keyed: the sidecar path derives PURELY from `<type>-<slug>` (it
|
|
451
|
+
* survives the item's `git mv`s with no lock-step move). Returns
|
|
452
|
+
* `needsAnswers:undefined` when no item file is found (the classifier treats it
|
|
453
|
+
* as "not gated", an ANALYSE rung) — the verb's job here is to wire the two
|
|
454
|
+
* signals, not to assert the item exists (that is the rung's concern).
|
|
455
|
+
*/
|
|
456
|
+
export function readItemSignals(input: ReadSignalsInput): ItemSignals {
|
|
457
|
+
const {repoPath, type, slug, item} = input;
|
|
458
|
+
const needsAnswers = readNeedsAnswers(repoPath, type, slug);
|
|
459
|
+
const sidecarRel = sidecarPathFor(item);
|
|
460
|
+
const sidecarAbs = join(repoPath, sidecarRel);
|
|
461
|
+
let sidecar: SidecarModel | undefined;
|
|
462
|
+
if (existsSync(sidecarAbs)) {
|
|
463
|
+
sidecar = parseSidecar(readFileSync(sidecarAbs, 'utf8'));
|
|
464
|
+
}
|
|
465
|
+
return {needsAnswers, sidecar};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* The lifecycle folders each item type may rest in (frontmatter source). After
|
|
470
|
+
* the capstone cut-over (task
|
|
471
|
+
* `cutover-retire-slicing-advancing-markers-and-trim-folder-sets`) the transient
|
|
472
|
+
* `tasking/` folder is GONE — a prd rests in `prds/ready` (source) or
|
|
473
|
+
* `prds/tasked` (tasked); while it is being tasked the body STAYS in
|
|
474
|
+
* `prds/ready` (the lock no longer moves it), so `tasking/` is never a
|
|
475
|
+
* frontmatter source.
|
|
476
|
+
*
|
|
477
|
+
* STAGING IS INCLUDED (`tasks-backlog` / `prds-proposed`): with `surfaceStaging`
|
|
478
|
+
* on (the user-visible default) the lifecycle surface pool enumerates
|
|
479
|
+
* `needsAnswers` items resting in STAGING as `task:`/`prd:` legs
|
|
480
|
+
* (`lifecycle-gather.ts`). The rung CLASSIFIER's signal read MUST see those
|
|
481
|
+
* staged bodies, or `needsAnswers` reads back `undefined`, the classifier
|
|
482
|
+
* mis-routes the item to the BUILD rung, and claim dies with "not found on
|
|
483
|
+
* origin/main" (observation
|
|
484
|
+
* `advance-task-folder-set-omits-tasks-backlog-staged-surface-items-misroute-to-build`).
|
|
485
|
+
* This is the staging-inclusive set its sibling `apply-persist.ts`
|
|
486
|
+
* (`APPLY_LIFECYCLE_FOLDERS`) already uses — kept in step here. BUILD/claim
|
|
487
|
+
* eligibility is UNCHANGED (still pool-only; staging items stay non-claimable):
|
|
488
|
+
* only the rung-classifier's frontmatter-source folders widen.
|
|
489
|
+
*/
|
|
490
|
+
const FOLDERS_FOR_TYPE: Record<SidecarType, readonly WorkFolderKey[]> = {
|
|
491
|
+
task: ['tasks-backlog', 'tasks-ready', 'in-progress', 'done'],
|
|
492
|
+
// `spec` rests in the parent-spec regime folders (a `spec:<slug>` legs
|
|
493
|
+
// frontmatter-source read resolves against these).
|
|
494
|
+
spec: ['specs-proposed', 'specs-ready', 'specs-tasked'],
|
|
495
|
+
observation: ['observations'],
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
/** Read `needsAnswers` off the FIRST `work/<folder>/<slug>.md` that exists. */
|
|
499
|
+
function readNeedsAnswers(
|
|
500
|
+
repoPath: string,
|
|
501
|
+
type: SidecarType,
|
|
502
|
+
slug: string,
|
|
503
|
+
): boolean | undefined {
|
|
504
|
+
for (const folder of FOLDERS_FOR_TYPE[type]) {
|
|
505
|
+
const abs = workItemPath(repoPath, folder, slug);
|
|
506
|
+
if (existsSync(abs)) {
|
|
507
|
+
return parseFrontmatter(readFileSync(abs, 'utf8')).needsAnswers;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return undefined;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* The PRODUCTION rung executor: build/task rungs ORCHESTRATE the existing
|
|
515
|
+
* `do`/`do prd:` machinery ({@link performDo}); the `surface`/`apply`/
|
|
516
|
+
* `triage-observation` rung bodies are filled by their own tasks
|
|
517
|
+
* ({@link surfaceRung} / {@link applyRung} / {@link triageRung}). It NEVER
|
|
518
|
+
* re-implements the build/task path — it hands the resolved arg to `performDo`,
|
|
519
|
+
* which spans both namespaces (the task path is the `do prd:` rung the prd's
|
|
520
|
+
* 2026-06-09 UPDATE confirms routes through `performIntegration`).
|
|
521
|
+
*/
|
|
522
|
+
export const defaultRungExecutor: RungExecutor = {
|
|
523
|
+
async buildTask(input) {
|
|
524
|
+
return orchestrateDo(input);
|
|
525
|
+
},
|
|
526
|
+
async taskSpec(input) {
|
|
527
|
+
return orchestrateDo(input);
|
|
528
|
+
},
|
|
529
|
+
async triageObservation(input) {
|
|
530
|
+
return triageRung(input);
|
|
531
|
+
},
|
|
532
|
+
async surface(input) {
|
|
533
|
+
return surfaceRung(input);
|
|
534
|
+
},
|
|
535
|
+
async apply(input) {
|
|
536
|
+
return applyRung(input);
|
|
537
|
+
},
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* ORCHESTRATE `do`/`do spec:` for the build-task / task-spec rungs: hand the
|
|
542
|
+
* resolved namespaced identity to {@link performDo} (the ONE build path / ONE
|
|
543
|
+
* task path). `advance` is a driver ON TOP — it does NOT duplicate `do`. The
|
|
544
|
+
* `do` outcome is mapped back onto the tick's outcome surface.
|
|
545
|
+
*/
|
|
546
|
+
async function orchestrateDo(input: RungExecInput): Promise<RungExecResult> {
|
|
547
|
+
const {item, context} = input;
|
|
548
|
+
const base = context.doOptions;
|
|
549
|
+
if (base === undefined) {
|
|
550
|
+
// The skeleton can classify + lock + DISPATCH without `do` options wired
|
|
551
|
+
// (the driver task threads them). Report it honestly rather than crash —
|
|
552
|
+
// the orchestration TARGET is `performDo`, named here, not re-implemented.
|
|
553
|
+
return {
|
|
554
|
+
exitCode: 1,
|
|
555
|
+
outcome: 'usage-error',
|
|
556
|
+
message:
|
|
557
|
+
`advance would ORCHESTRATE \`do ${item}\` for this rung, but no \`do\` ` +
|
|
558
|
+
`options were threaded into the tick (the driver task wires them).`,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
// The ORCHESTRATION TARGET is `performDo` by DEFAULT (in-place, the cwd checkout
|
|
562
|
+
// IS the isolation), or the injected {@link AdvanceContext.doDriver} — the
|
|
563
|
+
// registry-set advance driver threads a PER-MIRROR JOB-WORKTREE driver so the
|
|
564
|
+
// daemon/CI build runs isolated off the mirror's arbiter. Either way `advance`
|
|
565
|
+
// ORCHESTRATES `do` (the ONE build path / ONE task path) — it does NOT duplicate it.
|
|
566
|
+
const driver = context.doDriver ?? performDo;
|
|
567
|
+
const result: DoResult = await driver({...base, arg: item});
|
|
568
|
+
return {
|
|
569
|
+
exitCode: result.exitCode,
|
|
570
|
+
outcome: result.exitCode === 0 ? 'advanced' : mapDoOutcome(result),
|
|
571
|
+
message: result.message,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** Map a non-zero `do` result onto the tick's outcome vocabulary. */
|
|
576
|
+
function mapDoOutcome(result: DoResult): AdvanceOutcome {
|
|
577
|
+
switch (result.outcome) {
|
|
578
|
+
case 'lost':
|
|
579
|
+
return 'lost';
|
|
580
|
+
case 'contended':
|
|
581
|
+
return 'contended';
|
|
582
|
+
default:
|
|
583
|
+
return 'usage-error';
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* The SURFACE rung BODY (task `advance-rung-surface`, US #32/33): the FIRST rung
|
|
589
|
+
* filling the executor seam, establishing the spawn→emit→persist pattern the
|
|
590
|
+
* other rung bodies reuse. Under the `advancing` CAS lock (held by
|
|
591
|
+
* {@link performAdvance} BEFORE this runs — so the expensive spawn is POST-lock,
|
|
592
|
+
* winner-only), it:
|
|
593
|
+
*
|
|
594
|
+
* 1. spawns a FRESH-CONTEXT agent with `surface-questions` loaded (the
|
|
595
|
+
* {@link SurfaceGate} seam, mirroring the review gate's `review` spawn) and
|
|
596
|
+
* collects the EMITTED questions — the skill JUDGES, writes nothing; and
|
|
597
|
+
* 2. has the ENGINE ITSELF write/append them to the sidecar CAS-atomically AND
|
|
598
|
+
* set `needsAnswers:true` in the SAME commit
|
|
599
|
+
* ({@link persistSurfacedQuestions}) — the engine PERSISTS.
|
|
600
|
+
*
|
|
601
|
+
* Append-never-overwrite: a re-surface ADDS `qN+1` and flips a previously-all-
|
|
602
|
+
* answered sidecar back to not-all-answered (the persist owns that). An EMPTY
|
|
603
|
+
* emit (the skill's honest "no open judgement") writes nothing and reports it.
|
|
604
|
+
*/
|
|
605
|
+
async function surfaceRung(input: RungExecInput): Promise<RungExecResult> {
|
|
606
|
+
const {item, context} = input;
|
|
607
|
+
const note = context.note ?? (() => {});
|
|
608
|
+
const cwd = context.cwd;
|
|
609
|
+
|
|
610
|
+
// Locate the item file (the only thing the persist needs beyond the questions:
|
|
611
|
+
// the file to set `needsAnswers:true` on). The sidecar path is identity-derived,
|
|
612
|
+
// not folder-derived, so only the ITEM file's folder must be found.
|
|
613
|
+
const itemPath = findItemPath(cwd, input.namespace, input.slug);
|
|
614
|
+
if (itemPath === undefined) {
|
|
615
|
+
return vanishedSkip({rung: 'surface', item});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// 1. SPAWN the fresh-context `surface-questions` agent (the skill JUDGES). The
|
|
619
|
+
// expensive model work is POST-lock (the lock is held by `performAdvance`).
|
|
620
|
+
const gate = context.surfaceGate ?? harnessSurfaceGate();
|
|
621
|
+
let emit;
|
|
622
|
+
try {
|
|
623
|
+
emit = await gate({
|
|
624
|
+
item,
|
|
625
|
+
cwd,
|
|
626
|
+
surfaceModel: context.surfaceModel,
|
|
627
|
+
});
|
|
628
|
+
} catch (err) {
|
|
629
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
630
|
+
return {
|
|
631
|
+
exitCode: 1,
|
|
632
|
+
outcome: 'usage-error',
|
|
633
|
+
message: `surface ${item}: the surface-questions agent produced no usable emit (${detail}).`,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// 2. The ENGINE persists (the skill wrote nothing): append-or-create the sidecar
|
|
638
|
+
// + set `needsAnswers:true` in ONE commit (CAS-atomic under the held lock).
|
|
639
|
+
const persist = context.surfacePersist ?? persistSurfacedQuestions;
|
|
640
|
+
const result = persist({
|
|
641
|
+
cwd,
|
|
642
|
+
item,
|
|
643
|
+
itemPath,
|
|
644
|
+
questions: toNewQuestions(emit),
|
|
645
|
+
note,
|
|
646
|
+
});
|
|
647
|
+
if (result.outcome === 'nothing') {
|
|
648
|
+
// LIMBO DETECTION (task `advance-surface-limbo-observation-loudly-instead-of-
|
|
649
|
+
// silent-no-op`): an untriaged observation with NO sidecar whose surfacer had
|
|
650
|
+
// nothing to ask is a TRAP — the human's triage answer may have been recorded
|
|
651
|
+
// in an in-BODY "Applied answers" block, but the engine reads triage-vs-
|
|
652
|
+
// settled only from the `triaged:` frontmatter marker (`ledger-read.ts`) and
|
|
653
|
+
// the promote path only from an answered `disposition: promote` SIDECAR
|
|
654
|
+
// (`triage-persist.ts`). Without either, the item is untriaged (re-enumerated
|
|
655
|
+
// every tick), un-surfaceable (nothing to ask), un-promotable (no sidecar) —
|
|
656
|
+
// a silent exit-0 no-op forever. Surface it LOUDLY instead.
|
|
657
|
+
const limbo = detectObservationLimbo(input);
|
|
658
|
+
if (limbo !== undefined) {
|
|
659
|
+
note(limbo);
|
|
660
|
+
return {exitCode: 1, outcome: 'usage-error', message: limbo};
|
|
661
|
+
}
|
|
662
|
+
return {
|
|
663
|
+
exitCode: 0,
|
|
664
|
+
outcome: 'no-op',
|
|
665
|
+
message: `surface ${item}: no open judgement — nothing surfaced.`,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
return {
|
|
669
|
+
exitCode: 0,
|
|
670
|
+
outcome: 'advanced',
|
|
671
|
+
message:
|
|
672
|
+
`surfaced ${result.entryCount} question(s) for ${item} → ${result.sidecarPath} ` +
|
|
673
|
+
`(needsAnswers:true, CAS-atomic).`,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Detect the OBSERVATION LIMBO shape (task `advance-surface-limbo-observation-
|
|
679
|
+
* loudly-instead-of-silent-no-op`): an observation whose triage answer was
|
|
680
|
+
* (mis-)authored in an in-body "Applied answers" block instead of the sidecar/
|
|
681
|
+
* frontmatter channels the engine reads. All four conditions must hold:
|
|
682
|
+
*
|
|
683
|
+
* 1. the item is an OBSERVATION;
|
|
684
|
+
* 2. its frontmatter has NO `triaged:` marker (untriaged per `ledger-read.ts`);
|
|
685
|
+
* 3. there is NO active question sidecar at `work/questions/observation-<slug>.md`;
|
|
686
|
+
* 4. the surfacer just returned empty (the caller — {@link surfaceRung} — only
|
|
687
|
+
* calls this on the persist's `nothing` branch).
|
|
688
|
+
*
|
|
689
|
+
* Returns the loud diagnostic to emit (naming BOTH valid channels), or `undefined`
|
|
690
|
+
* when the shape does not match (the surfacer's empty is then the normal calm no-op).
|
|
691
|
+
* The engine does NOT (and will not) honour in-body disposition prose — one channel
|
|
692
|
+
* (the sidecar + `triaged:` frontmatter) keeps the loop honest.
|
|
693
|
+
*/
|
|
694
|
+
function detectObservationLimbo(input: RungExecInput): string | undefined {
|
|
695
|
+
if (input.namespace !== 'observation') return undefined;
|
|
696
|
+
const {item, slug, context} = input;
|
|
697
|
+
const cwd = context.cwd;
|
|
698
|
+
const itemRel = findItemPath(cwd, input.namespace, slug);
|
|
699
|
+
if (itemRel === undefined) return undefined;
|
|
700
|
+
const fm = parseFrontmatter(readFileSync(join(cwd, itemRel), 'utf8'));
|
|
701
|
+
if (fm.triaged !== undefined && fm.triaged !== '') return undefined;
|
|
702
|
+
const sidecarRel = sidecarPathFor(item);
|
|
703
|
+
if (existsSync(join(cwd, sidecarRel))) return undefined;
|
|
704
|
+
return (
|
|
705
|
+
`observation \`${slug}\` is in a limbo: no \`triaged:\` frontmatter marker ` +
|
|
706
|
+
`AND no answered question sidecar at \`${sidecarRel}\`, but the surfacer has ` +
|
|
707
|
+
`nothing to ask. If a human triage decision (promote-slice / keep / duplicate) ` +
|
|
708
|
+
`has been recorded in the observation BODY, that channel is INVISIBLE to the ` +
|
|
709
|
+
`runner — author the sidecar, or set \`triaged:\` in frontmatter. The engine ` +
|
|
710
|
+
`does not (and will not) honour in-body disposition prose.`
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* The observation TRIAGE rung BODY (task `advance-rung-triage`, US #16/17/23):
|
|
716
|
+
* the rung the classifier picks for an UNTRIAGED observation (`needsAnswers` not
|
|
717
|
+
* set, no sidecar). It is QUESTION-GATED BY DEFAULT: it surfaces a promote/keep/
|
|
718
|
+
* delete question and WAITS — so "is this worth building?" is NEVER decided
|
|
719
|
+
* autonomously. A CONSERVATIVE `observationTriage: 'auto'`-gated EXCEPTION (US #17,
|
|
720
|
+
* high bar) may auto-disposition ONLY the no-question cases:
|
|
721
|
+
*
|
|
722
|
+
* - **default (question-gated):** delegate to {@link surfaceRung} — spawn the
|
|
723
|
+
* `surface-questions` agent (it emits a PLAIN "what becomes of this signal?"
|
|
724
|
+
* question — NO disposition token any more, task
|
|
725
|
+
* `agentic-apply-retire-disposition-vocabulary`) and the ENGINE persists the
|
|
726
|
+
* sidecar + `needsAnswers`. When the human answers, the AGENTIC apply decision
|
|
727
|
+
* (not a stamped token) reads the answer + source and chooses what to DO.
|
|
728
|
+
* Surface stays ALWAYS allowed (US #23) — this path runs under `ask`/`off`
|
|
729
|
+
* (and `off` + an explicit `obs:` runs in this `ask`-mode).
|
|
730
|
+
* - **`auto` exception:** ONLY under `observationTriage: 'auto'`, ask the
|
|
731
|
+
* {@link TriageGate} whether the observation is a no-question case. If it emits
|
|
732
|
+
* `auto: true` (`duplicate` → DELETE the redundant note; `map` → unambiguous
|
|
733
|
+
* map onto an existing item), the engine auto-dispositions it WITHOUT a
|
|
734
|
+
* question ({@link autoDispositionObservation}). It NEVER auto-deletes a
|
|
735
|
+
* NON-duplicate (a `duplicate` discharges by deletion because it is a
|
|
736
|
+
* redundant copy of an already-captured signal — nothing is lost) and NEVER
|
|
737
|
+
* auto-promotes a judgement call (`auto: false` ⇒ fall back to the surface
|
|
738
|
+
* question). Promotion is ALWAYS a human answer (the apply path).
|
|
739
|
+
*
|
|
740
|
+
* Under the `advancing` CAS lock (held by {@link performAdvance} BEFORE this runs),
|
|
741
|
+
* so the expensive spawn is POST-lock, winner-only.
|
|
742
|
+
*/
|
|
743
|
+
async function triageRung(input: RungExecInput): Promise<RungExecResult> {
|
|
744
|
+
const {item, context} = input;
|
|
745
|
+
const note = context.note ?? (() => {});
|
|
746
|
+
const cwd = context.cwd;
|
|
747
|
+
|
|
748
|
+
// The CONSERVATIVE auto-disposition EXCEPTION — ONLY under `observationTriage:
|
|
749
|
+
// 'auto'`. Under `'ask'`/`'off'`/unset (including `off` + an EXPLICIT obs:<slug>
|
|
750
|
+
// that bypassed the selection gate), EVERY untriaged observation surfaces the
|
|
751
|
+
// question (the always-allowed path), so "worth building?" is never decided
|
|
752
|
+
// autonomously — `off` + explicit runs in the conservative `ask`-mode.
|
|
753
|
+
if (context.observationTriage === 'auto') {
|
|
754
|
+
const itemPath = findItemPath(cwd, input.namespace, input.slug);
|
|
755
|
+
if (itemPath === undefined) {
|
|
756
|
+
return vanishedSkip({rung: 'triage', item});
|
|
757
|
+
}
|
|
758
|
+
const gate = context.triageGate ?? harnessTriageGate();
|
|
759
|
+
let decision;
|
|
760
|
+
try {
|
|
761
|
+
decision = await gate({item, cwd, triageModel: context.triageModel});
|
|
762
|
+
} catch (err) {
|
|
763
|
+
// A gate failure is NOT a reason to auto-dispose — fall back to the SAFE
|
|
764
|
+
// question-gated path (surface the question), never the reverse.
|
|
765
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
766
|
+
note(
|
|
767
|
+
`triage ${item}: the auto-triage gate produced no usable emit (${detail}); ` +
|
|
768
|
+
'falling back to the question-gated surface path.',
|
|
769
|
+
);
|
|
770
|
+
decision = {auto: false as const};
|
|
771
|
+
}
|
|
772
|
+
if (decision.auto === true) {
|
|
773
|
+
// A no-question case (duplicate / map) — auto-disposition WITHOUT a
|
|
774
|
+
// question. BOTH discharge the redundant note BY DELETION (a duplicate is a
|
|
775
|
+
// redundant copy; a map is already covered by the item it maps onto). There
|
|
776
|
+
// is no resting `triaged:keep` state any more. NEVER auto-deletes a
|
|
777
|
+
// NON-redundant signal; NEVER auto-promotes.
|
|
778
|
+
const dispose = context.autoDisposition ?? autoDispositionObservation;
|
|
779
|
+
const result = dispose({
|
|
780
|
+
cwd,
|
|
781
|
+
item,
|
|
782
|
+
itemPath,
|
|
783
|
+
kind: decision.kind,
|
|
784
|
+
existing: decision.existing,
|
|
785
|
+
reason: decision.reason,
|
|
786
|
+
note,
|
|
787
|
+
});
|
|
788
|
+
return {exitCode: 0, outcome: 'advanced', message: result.message};
|
|
789
|
+
}
|
|
790
|
+
// `auto: false` ⇒ a judgement call. Fall through to the surface question.
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// DEFAULT (question-gated): surface a PLAIN "what becomes of this signal?"
|
|
794
|
+
// question + WAIT. This REUSES the surface rung verbatim (the `surface-questions`
|
|
795
|
+
// skill emits the triage question — NO disposition token any more, task
|
|
796
|
+
// `agentic-apply-retire-disposition-vocabulary`); the AGENTIC apply decision
|
|
797
|
+
// reads the human's answer + source and decides what to DO when it is answered.
|
|
798
|
+
return surfaceRung(input);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* The APPLY rung BODY (task `advance-rung-apply`; AGENTIC apply, task
|
|
803
|
+
* `agentic-apply-retire-disposition-vocabulary`): when the classifier says `apply`
|
|
804
|
+
* (ALL sidecar entries answered), apply the HUMAN's answers.
|
|
805
|
+
*
|
|
806
|
+
* For a fully-answered OBSERVATION (and no caller-supplied follow-up batch), the
|
|
807
|
+
* apply rung is now AGENT-DRIVEN: it runs the shared `decide(input, allowedOutcomes)`
|
|
808
|
+
* engine ({@link decide}) over `(the answered question(s) + the SOURCE item + its
|
|
809
|
+
* type/context)` via the injected {@link ApplyDecider}, allowing the set
|
|
810
|
+
* `{task | prd | adr | delete | ask}` (= `{mint-task | mint-prd | mint-adr |
|
|
811
|
+
* delete-source | ask-follow-up}`; `adr` is now WIRED by task
|
|
812
|
+
* `agentic-apply-mint-adr-route`, which added the {@link mintAdr} route). The
|
|
813
|
+
* verdict ROUTES:
|
|
814
|
+
* - `ask` → the EXISTING append/re-pause loop ({@link applyAnsweredQuestions}
|
|
815
|
+
* with the follow-up appended; `needsAnswers:true` stays, re-pause in one
|
|
816
|
+
* commit);
|
|
817
|
+
* - `adr` → {@link mintAdr} (mint a SELF-CONTAINED ADR into `docs/adr/` + `git
|
|
818
|
+
* rm` the source + sidecar in the SAME atomic commit; the SIBLING route for the
|
|
819
|
+
* off-board target);
|
|
820
|
+
* - `task` / `prd` → {@link promoteObservation} (mint a SELF-CONTAINED artifact +
|
|
821
|
+
* `git rm` the source + sidecar in the SAME atomic commit); the artifact type
|
|
822
|
+
* comes from the agent's VERDICT, NOT a human `promote-*` field;
|
|
823
|
+
* - `delete` → {@link applyAnsweredQuestions} discharge-by-deletion (`git rm`
|
|
824
|
+
* source + sidecar in one revertible commit, the reason in the commit message).
|
|
825
|
+
*
|
|
826
|
+
* For a TASK/PRD (answering its OWN open questions) or a caller-supplied follow-up
|
|
827
|
+
* batch, it delegates straight to {@link applyAnsweredQuestions} (resolve fully /
|
|
828
|
+
* re-pause) — the lifecycle path is untouched (a task/prd is dropped by its own
|
|
829
|
+
* lifecycle, not by a question answer).
|
|
830
|
+
*
|
|
831
|
+
* Under the `advancing` CAS lock (held by {@link performAdvance} BEFORE this runs
|
|
832
|
+
* — so the work is POST-lock, winner-only). ALWAYS allowed (no gate). NEVER
|
|
833
|
+
* invents an answer — it applies ONLY the human-authored `answer:` text; a
|
|
834
|
+
* subset-answered sidecar is not even classified `apply` (the classifier NO-OPs),
|
|
835
|
+
* asserted in the persist.
|
|
836
|
+
*/
|
|
837
|
+
async function applyRung(input: RungExecInput): Promise<RungExecResult> {
|
|
838
|
+
const {item, context} = input;
|
|
839
|
+
const note = context.note ?? (() => {});
|
|
840
|
+
const cwd = context.cwd;
|
|
841
|
+
|
|
842
|
+
const itemPath = findItemPath(cwd, input.namespace, input.slug);
|
|
843
|
+
if (itemPath === undefined) {
|
|
844
|
+
return vanishedSkip({rung: 'apply', item});
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// RUNNER-ACTION KIND-CHECK (task `apply-rung-merge-disposition`, prd
|
|
848
|
+
// `land-time-reverify-and-parallel-merge-ceiling`): an answered `kind: merge`
|
|
849
|
+
// sidecar entry is a DETERMINISTIC land action, NOT a content decision. It
|
|
850
|
+
// dispatches HERE (a sibling of the agentic `decide()`), keyed off the
|
|
851
|
+
// question kind + the human's plain `merge | hold | drop` answer, BEFORE the
|
|
852
|
+
// agentic decider runs. The dispatcher invokes the EXISTING land primitive
|
|
853
|
+
// (`performIntegration` with `committedRecovery: true` + `freshWorktreeGate:
|
|
854
|
+
// true`) through the EXISTING per-job worktree seam (`workspace.ts`
|
|
855
|
+
// `createJob` off the hub mirror) — it does NOT re-implement rebase / verify
|
|
856
|
+
// / advance and does NOT improvise a worktree or clone.
|
|
857
|
+
const mergeRoute = await maybeRunMergeAction(input, itemPath);
|
|
858
|
+
if (mergeRoute !== undefined) {
|
|
859
|
+
return mergeRoute;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// AGENTIC APPLY for an answered OBSERVATION (the subsumed triage rung): run the
|
|
863
|
+
// shared decision engine over the answer(s) + source, route the verdict. A
|
|
864
|
+
// caller-supplied follow-up batch (`applyFollowups`) bypasses the decision and
|
|
865
|
+
// re-pauses directly (a test/driver hook). TASK/PRD items resolve in place
|
|
866
|
+
// (their own lifecycle), so they skip the decision and fall through to the
|
|
867
|
+
// persist below.
|
|
868
|
+
const runAgenticDecision =
|
|
869
|
+
input.namespace === 'observation' &&
|
|
870
|
+
!(context.applyFollowups && context.applyFollowups.length > 0);
|
|
871
|
+
if (runAgenticDecision) {
|
|
872
|
+
return applyAgenticDecision(input, itemPath);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const apply = context.applyPersist ?? applyAnsweredQuestions;
|
|
876
|
+
try {
|
|
877
|
+
const result = apply({
|
|
878
|
+
cwd,
|
|
879
|
+
item,
|
|
880
|
+
itemPath,
|
|
881
|
+
appendQuestions: context.applyFollowups,
|
|
882
|
+
note,
|
|
883
|
+
});
|
|
884
|
+
// Map the apply persist's outcome to the rung's outcome. `vanished` is the
|
|
885
|
+
// F3a clean-exit case (item was gone between capture and write, e.g. a
|
|
886
|
+
// concurrent promote); benign skip, exitCode 0, distinct from `no-op`.
|
|
887
|
+
const mapped: AdvanceOutcome =
|
|
888
|
+
result.outcome === 'repaused'
|
|
889
|
+
? 'no-op'
|
|
890
|
+
: result.outcome === 'vanished'
|
|
891
|
+
? 'vanished'
|
|
892
|
+
: 'advanced';
|
|
893
|
+
return {
|
|
894
|
+
exitCode: 0,
|
|
895
|
+
outcome: mapped,
|
|
896
|
+
message: result.message,
|
|
897
|
+
};
|
|
898
|
+
} catch (err) {
|
|
899
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
900
|
+
return {
|
|
901
|
+
exitCode: 1,
|
|
902
|
+
outcome: 'usage-error',
|
|
903
|
+
message: `apply ${item}: ${detail}`,
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Dispatch an answered MERGE-QUESTION (a sidecar entry stamped `kind: merge` by
|
|
910
|
+
* the merge-question surfacer) through the EXISTING land primitive
|
|
911
|
+
* (`performIntegration` with `committedRecovery: true` + `freshWorktreeGate:
|
|
912
|
+
* true`) via the EXISTING per-job worktree seam (`workspace.ts` `createJob`).
|
|
913
|
+
* The deterministic SIBLING of the agentic `decide()` content-decision (PRD
|
|
914
|
+
* `land-time-reverify-and-parallel-merge-ceiling`, task
|
|
915
|
+
* `apply-rung-merge-disposition`; Stories #15, #16): a merge-acceptance has no
|
|
916
|
+
* judgement content (the human's plain `merge | hold | drop` answer IS the
|
|
917
|
+
* decision; the apply-time re-verify on the rebased tip is the real correctness
|
|
918
|
+
* gate), so the apply rung KIND-CHECKS the sidecar BEFORE the agentic decider.
|
|
919
|
+
*
|
|
920
|
+
* Returns `undefined` when there is no answered `kind: merge` entry to dispatch
|
|
921
|
+
* (the apply rung then proceeds to the existing path — agentic for
|
|
922
|
+
* observations, normal `applyAnsweredQuestions` for task/prd content questions).
|
|
923
|
+
* Returns a {@link RungExecResult} when the dispatcher handled the rung:
|
|
924
|
+
*
|
|
925
|
+
* - `landed` / `already-integrated` ⇒ the kept commit landed on `main` (or
|
|
926
|
+
* was already there); the dispatcher FALLS THROUGH to the normal apply path
|
|
927
|
+
* so the answer is recorded in the item body + the sidecar is resolved.
|
|
928
|
+
* - `refused` ⇒ the LAND was refused (RED re-verify on the rebased tip,
|
|
929
|
+
* rebase conflict, or pre-checkout failure); `performIntegration` routed
|
|
930
|
+
* the item to needs-attention through its own shared seam, so `main` never
|
|
931
|
+
* received a failing tree. The apply rung SHORT-CIRCUITS — the sidecar is
|
|
932
|
+
* LEFT IN PLACE so the open answer stays surfaced for a human follow-up.
|
|
933
|
+
* - `restale` ⇒ `strictMergeApproval` was ON and the merge-base moved
|
|
934
|
+
* between the surfacer's question and this apply; the apply rung appends
|
|
935
|
+
* a follow-up question + re-pauses (the human re-confirms against the new
|
|
936
|
+
* base).
|
|
937
|
+
* - `hold` / `drop` ⇒ no land; fall through to the normal apply path so the
|
|
938
|
+
* answer is recorded in body. The branch stays unmerged; a future
|
|
939
|
+
* surfacer pass may re-emit a merge-question.
|
|
940
|
+
*/
|
|
941
|
+
async function maybeRunMergeAction(
|
|
942
|
+
input: RungExecInput,
|
|
943
|
+
itemPath: string,
|
|
944
|
+
): Promise<RungExecResult | undefined> {
|
|
945
|
+
const {item, context} = input;
|
|
946
|
+
const cwd = context.cwd;
|
|
947
|
+
const note = context.note ?? (() => {});
|
|
948
|
+
|
|
949
|
+
const detected = detectAnsweredMergeAction(cwd, item);
|
|
950
|
+
if (detected === undefined) return undefined;
|
|
951
|
+
|
|
952
|
+
const handler = context.mergeAction ?? performMergeAction;
|
|
953
|
+
|
|
954
|
+
// The dispatcher needs a `workspacesDir` to cut a per-job worktree (the
|
|
955
|
+
// `createJob` seam). When unset (a caller that has not threaded it) we
|
|
956
|
+
// REFUSE cleanly rather than guess: a land without isolation is not the
|
|
957
|
+
// shape this dispatcher promises.
|
|
958
|
+
const workspacesDir = context.workspacesDir;
|
|
959
|
+
if (workspacesDir === undefined && context.mergeAction === undefined) {
|
|
960
|
+
const message =
|
|
961
|
+
`apply ${item}: answered merge-question detected (kind=merge, answer=` +
|
|
962
|
+
`${detected.verb}) but no \`workspacesDir\` is threaded into the apply ` +
|
|
963
|
+
`rung — the dispatcher needs one to cut the per-job worktree via ` +
|
|
964
|
+
`\`workspace.ts\` \`createJob\`. NOT landing; the answer stays surfaced.`;
|
|
965
|
+
note(message);
|
|
966
|
+
return {exitCode: 1, outcome: 'usage-error', message};
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
let result: MergeActionResult;
|
|
970
|
+
try {
|
|
971
|
+
result = await handler({
|
|
972
|
+
action: detected,
|
|
973
|
+
item,
|
|
974
|
+
slug: input.slug,
|
|
975
|
+
cwd,
|
|
976
|
+
arbiter: context.arbiter ?? DEFAULT_ARBITER,
|
|
977
|
+
arbiterUrl: context.arbiterUrl,
|
|
978
|
+
workspacesDir: workspacesDir ?? '',
|
|
979
|
+
prepare: context.prepare,
|
|
980
|
+
verify: context.verify,
|
|
981
|
+
strictMergeApproval: context.strictMergeApproval,
|
|
982
|
+
note,
|
|
983
|
+
});
|
|
984
|
+
} catch (err) {
|
|
985
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
986
|
+
const message =
|
|
987
|
+
`apply ${item}: answered merge-question dispatch raised (${detail}); NOT ` +
|
|
988
|
+
`landing; the answer stays surfaced.`;
|
|
989
|
+
note(message);
|
|
990
|
+
return {exitCode: 1, outcome: 'usage-error', message};
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (result.outcome === 'refused') {
|
|
994
|
+
// `main` never received a failing tree (performIntegration routed the
|
|
995
|
+
// bounce to needs-attention through its own shared seam). SHORT-CIRCUIT:
|
|
996
|
+
// leave the sidecar so the open answer stays surfaced — the apply rung
|
|
997
|
+
// MUST NOT also resolve it (the next surfacer / human will follow up).
|
|
998
|
+
note(result.message);
|
|
999
|
+
return {exitCode: 1, outcome: 'usage-error', message: result.message};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (result.outcome === 'restale') {
|
|
1003
|
+
// `strictMergeApproval` re-surface: append a follow-up question and
|
|
1004
|
+
// re-pause. The previous answer stays recorded in the entry; the human
|
|
1005
|
+
// re-confirms against the new merge-base in the appended question.
|
|
1006
|
+
const apply = context.applyPersist ?? applyAnsweredQuestions;
|
|
1007
|
+
try {
|
|
1008
|
+
const applied = apply({
|
|
1009
|
+
cwd,
|
|
1010
|
+
item,
|
|
1011
|
+
itemPath,
|
|
1012
|
+
appendQuestions: [
|
|
1013
|
+
{
|
|
1014
|
+
question:
|
|
1015
|
+
`Merge-base for \`work/${input.slug}\` moved since your last ` +
|
|
1016
|
+
`answer (strictMergeApproval is ON). Re-confirm: still land?`,
|
|
1017
|
+
context: result.message,
|
|
1018
|
+
default: 'merge | hold | drop',
|
|
1019
|
+
kind: 'merge',
|
|
1020
|
+
},
|
|
1021
|
+
],
|
|
1022
|
+
note,
|
|
1023
|
+
});
|
|
1024
|
+
return {
|
|
1025
|
+
exitCode: 0,
|
|
1026
|
+
outcome: 'no-op',
|
|
1027
|
+
message: applied.message,
|
|
1028
|
+
};
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1031
|
+
return {
|
|
1032
|
+
exitCode: 1,
|
|
1033
|
+
outcome: 'usage-error',
|
|
1034
|
+
message: `apply ${item}: re-surfacing the merge-question failed (${detail}).`,
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// landed | already-integrated | hold | drop: the dispatcher's action is
|
|
1040
|
+
// done; FALL THROUGH to the normal apply path so the answer is recorded in
|
|
1041
|
+
// the item body + the sidecar is resolved.
|
|
1042
|
+
note(result.message);
|
|
1043
|
+
const apply = context.applyPersist ?? applyAnsweredQuestions;
|
|
1044
|
+
try {
|
|
1045
|
+
const applied = apply({cwd, item, itemPath, note});
|
|
1046
|
+
const mapped: AdvanceOutcome =
|
|
1047
|
+
applied.outcome === 'repaused'
|
|
1048
|
+
? 'no-op'
|
|
1049
|
+
: applied.outcome === 'vanished'
|
|
1050
|
+
? 'vanished'
|
|
1051
|
+
: 'advanced';
|
|
1052
|
+
return {
|
|
1053
|
+
exitCode: 0,
|
|
1054
|
+
outcome: mapped,
|
|
1055
|
+
message: `${result.message} ${applied.message}`,
|
|
1056
|
+
};
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1059
|
+
return {
|
|
1060
|
+
exitCode: 1,
|
|
1061
|
+
outcome: 'usage-error',
|
|
1062
|
+
message: `apply ${item}: ${detail}`,
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* The cross-tick-window BENIGN SKIP shared by all three rungs that need to
|
|
1069
|
+
* resolve an item file (surface / triage / apply). The lifecycle pool enumerated
|
|
1070
|
+
* the item at scan-time, but by the time this leg ran a sibling parallel leg had
|
|
1071
|
+
* already triaged/settled/deleted it. At the ~33-way CI matrix scale this is a
|
|
1072
|
+
* CALM, EXPECTED condition — making it an exit-1 "a human must reconcile" turned
|
|
1073
|
+
* the matrix into a wall of red. It is now a `vanished` outcome (`exitCode: 0`,
|
|
1074
|
+
* distinguishable from `no-op`) carrying a clear message naming the rung + item.
|
|
1075
|
+
* (See task `observation-identity-is-its-filename-not-a-foreign-slug`.)
|
|
1076
|
+
*/
|
|
1077
|
+
function vanishedSkip(input: {
|
|
1078
|
+
rung: 'surface' | 'triage' | 'apply';
|
|
1079
|
+
item: string;
|
|
1080
|
+
}): RungExecResult {
|
|
1081
|
+
return {
|
|
1082
|
+
exitCode: 0,
|
|
1083
|
+
outcome: 'vanished',
|
|
1084
|
+
message:
|
|
1085
|
+
`advance classified the '${input.rung}' rung for ${input.item} but its ` +
|
|
1086
|
+
`item file was gone from work/ by the time the leg ran (a sibling leg ` +
|
|
1087
|
+
`likely triaged/settled/deleted it between enumerate and run) — benign ` +
|
|
1088
|
+
`skip.`,
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Find the item file `work/<folder>/<slug>.md` for a type, across the lifecycle
|
|
1094
|
+
* folders it may rest in (the SAME folder set {@link readNeedsAnswers} searches).
|
|
1095
|
+
* Returns the path RELATIVE to `cwd`, or `undefined` when no file exists.
|
|
1096
|
+
*/
|
|
1097
|
+
function findItemPath(
|
|
1098
|
+
cwd: string,
|
|
1099
|
+
namespace: SlugNamespace,
|
|
1100
|
+
slug: string,
|
|
1101
|
+
): string | undefined {
|
|
1102
|
+
const type = sidecarTypeFor(namespace);
|
|
1103
|
+
for (const folder of FOLDERS_FOR_TYPE[type]) {
|
|
1104
|
+
const rel = workItemRel(folder, `${slug}.md`);
|
|
1105
|
+
if (existsSync(join(cwd, rel))) {
|
|
1106
|
+
return rel;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return undefined;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* The AGENTIC apply DECISION for a fully-answered OBSERVATION (task
|
|
1114
|
+
* `agentic-apply-retire-disposition-vocabulary`): run the shared
|
|
1115
|
+
* `decide(input, allowedOutcomes)` engine over the answer(s) + source, then ROUTE
|
|
1116
|
+
* the verdict. The artifact-type selection (task vs prd) comes from the agent's
|
|
1117
|
+
* VERDICT, NOT a human `promote-*` field (which is retired). Replaces the old
|
|
1118
|
+
* `answeredPromoteArtifact` + disposition picker.
|
|
1119
|
+
*
|
|
1120
|
+
* - `ask` → append the follow-up question(s) + re-pause (the EXISTING loop, via
|
|
1121
|
+
* {@link applyAnsweredQuestions}'s `appendQuestions`);
|
|
1122
|
+
* - `task` / `prd` → {@link promoteObservation} (mint self-contained + delete
|
|
1123
|
+
* source in the same atomic commit);
|
|
1124
|
+
* - `adr` → {@link mintAdr} (mint a self-contained ADR into `docs/adr/` + delete
|
|
1125
|
+
* source in the same atomic commit; the SIBLING route for the off-board target,
|
|
1126
|
+
* task `agentic-apply-mint-adr-route`);
|
|
1127
|
+
* - `delete` → {@link applyAnsweredQuestions}'s discharge-by-deletion (`git rm`
|
|
1128
|
+
* source + sidecar in one revertible commit, the reason in the message).
|
|
1129
|
+
*
|
|
1130
|
+
* The allowed set is `{task | prd | adr | delete | ask}`; a verdict outside it is
|
|
1131
|
+
* rejected by the engine's allowed-outcome guard ({@link DisallowedOutcomeError})
|
|
1132
|
+
* and mapped onto a usage-error — never dispatched.
|
|
1133
|
+
*/
|
|
1134
|
+
async function applyAgenticDecision(
|
|
1135
|
+
input: RungExecInput,
|
|
1136
|
+
itemPath: string,
|
|
1137
|
+
): Promise<RungExecResult> {
|
|
1138
|
+
const {item, context} = input;
|
|
1139
|
+
const note = context.note ?? (() => {});
|
|
1140
|
+
const cwd = context.cwd;
|
|
1141
|
+
|
|
1142
|
+
const decisionInput = buildApplyDecisionInput({
|
|
1143
|
+
item,
|
|
1144
|
+
type: sidecarTypeFor(input.namespace),
|
|
1145
|
+
itemPath,
|
|
1146
|
+
cwd,
|
|
1147
|
+
model: context.applyModel,
|
|
1148
|
+
});
|
|
1149
|
+
if (decisionInput === undefined) {
|
|
1150
|
+
// No sidecar / item to decide over (a sibling leg removed it between classify
|
|
1151
|
+
// and run) — the same benign clean-exit the persist's vanished branch gives.
|
|
1152
|
+
return vanishedSkip({rung: 'apply', item});
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const decider = context.applyDecide ?? harnessApplyDecider();
|
|
1156
|
+
let verdict: DecisionVerdict;
|
|
1157
|
+
try {
|
|
1158
|
+
verdict = await decide(decisionInput, decider, APPLY_ALLOWED_OUTCOMES);
|
|
1159
|
+
} catch (err) {
|
|
1160
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1161
|
+
// A DisallowedOutcomeError (e.g. a stubbed `adr` verdict) and an agent-failed
|
|
1162
|
+
// parse both degrade HONESTLY onto a usage-error — never a silent dispatch.
|
|
1163
|
+
const label =
|
|
1164
|
+
err instanceof DisallowedOutcomeError
|
|
1165
|
+
? `apply ${item}: the decision verdict is not an allowed outcome (${detail})`
|
|
1166
|
+
: `apply ${item}: the decision agent produced no usable verdict (${detail})`;
|
|
1167
|
+
return {exitCode: 1, outcome: 'usage-error', message: label};
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (verdict.outcome === 'ask') {
|
|
1171
|
+
// ask-follow-up → the EXISTING append/re-pause loop. One BATCH of follow-ups.
|
|
1172
|
+
const apply = context.applyPersist ?? applyAnsweredQuestions;
|
|
1173
|
+
const question = (verdict.question ?? '').trim();
|
|
1174
|
+
if (question === '') {
|
|
1175
|
+
return {
|
|
1176
|
+
exitCode: 1,
|
|
1177
|
+
outcome: 'usage-error',
|
|
1178
|
+
message: `apply ${item}: the decision agent chose 'ask' but emitted no follow-up question.`,
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
try {
|
|
1182
|
+
const result = apply({
|
|
1183
|
+
cwd,
|
|
1184
|
+
item,
|
|
1185
|
+
itemPath,
|
|
1186
|
+
appendQuestions: [{question}],
|
|
1187
|
+
note,
|
|
1188
|
+
});
|
|
1189
|
+
return {
|
|
1190
|
+
exitCode: 0,
|
|
1191
|
+
outcome: result.outcome === 'vanished' ? 'vanished' : 'no-op',
|
|
1192
|
+
message: result.message,
|
|
1193
|
+
};
|
|
1194
|
+
} catch (err) {
|
|
1195
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1196
|
+
return {
|
|
1197
|
+
exitCode: 1,
|
|
1198
|
+
outcome: 'usage-error',
|
|
1199
|
+
message: `apply ${item}: ${detail}`,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if (verdict.outcome === 'task' || verdict.outcome === 'spec') {
|
|
1205
|
+
// mint-task / mint-spec → CAS-create a SELF-CONTAINED artifact + delete the
|
|
1206
|
+
// source + sidecar in the SAME commit (delete-on-promote, preserved). The
|
|
1207
|
+
// verdict's drafted body (when present) seeds the new item; else the writer
|
|
1208
|
+
// builds a self-contained body FROM the observation (carrying the answers +
|
|
1209
|
+
// open-question scoping). The artifact type is the agent's VERDICT.
|
|
1210
|
+
const promote = context.promote ?? promoteObservation;
|
|
1211
|
+
const draftedBody =
|
|
1212
|
+
verdict.outcome === 'task' ? verdict.taskBody : verdict.specBody;
|
|
1213
|
+
const draftedSlug =
|
|
1214
|
+
verdict.outcome === 'task' ? verdict.taskSlug : verdict.specSlug;
|
|
1215
|
+
try {
|
|
1216
|
+
const result = await promote({
|
|
1217
|
+
cwd,
|
|
1218
|
+
item,
|
|
1219
|
+
itemPath,
|
|
1220
|
+
artifact: verdict.outcome,
|
|
1221
|
+
newSlug: context.promoteSlug ?? draftedSlug,
|
|
1222
|
+
...(draftedBody !== undefined && draftedBody.trim() !== ''
|
|
1223
|
+
? {stubContent: draftedBody}
|
|
1224
|
+
: {}),
|
|
1225
|
+
arbiter: context.arbiter,
|
|
1226
|
+
// Lifecycle FAN-OUT: widen the CAS contention budget + jitter the retries
|
|
1227
|
+
// so N parallel promote legs desync (task
|
|
1228
|
+
// `jitter-and-widen-cas-contention-retry-for-lifecycle-fanout`).
|
|
1229
|
+
contention: LIFECYCLE_CAS_CONTENTION,
|
|
1230
|
+
note,
|
|
1231
|
+
});
|
|
1232
|
+
return {
|
|
1233
|
+
exitCode: result.exitCode,
|
|
1234
|
+
outcome:
|
|
1235
|
+
result.outcome === 'promoted'
|
|
1236
|
+
? 'advanced'
|
|
1237
|
+
: result.outcome === 'lost'
|
|
1238
|
+
? 'lost'
|
|
1239
|
+
: result.outcome === 'contended'
|
|
1240
|
+
? 'contended'
|
|
1241
|
+
: 'usage-error',
|
|
1242
|
+
message: result.message,
|
|
1243
|
+
};
|
|
1244
|
+
} catch (err) {
|
|
1245
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1246
|
+
return {
|
|
1247
|
+
exitCode: 1,
|
|
1248
|
+
outcome: 'usage-error',
|
|
1249
|
+
message: `apply ${item}: ${detail}`,
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
if (verdict.outcome === 'adr') {
|
|
1255
|
+
// mint-adr → CAS-create a SELF-CONTAINED ADR into `docs/adr/` + delete the
|
|
1256
|
+
// source + sidecar in the SAME commit (delete-on-promote, preserved via the
|
|
1257
|
+
// shared create-CAS). An ADR lives OUTSIDE the work board, so this is the
|
|
1258
|
+
// SIBLING route (NOT a `promoteObservation` artifact type). The verdict's
|
|
1259
|
+
// drafted body (when present) seeds the ADR; else `mintAdr` builds a
|
|
1260
|
+
// self-contained body FROM the observation + the answered question(s).
|
|
1261
|
+
const mint = context.mintAdr ?? mintAdr;
|
|
1262
|
+
const answers = decisionInput.sidecar.entries.map((e) => ({
|
|
1263
|
+
question: e.question,
|
|
1264
|
+
answer: e.answer,
|
|
1265
|
+
}));
|
|
1266
|
+
try {
|
|
1267
|
+
const result = await mint({
|
|
1268
|
+
cwd,
|
|
1269
|
+
item,
|
|
1270
|
+
itemPath,
|
|
1271
|
+
adrSlug: context.promoteSlug ?? verdict.adrSlug,
|
|
1272
|
+
...(verdict.adrTitle !== undefined ? {adrTitle: verdict.adrTitle} : {}),
|
|
1273
|
+
...(verdict.adrBody !== undefined && verdict.adrBody.trim() !== ''
|
|
1274
|
+
? {adrBody: verdict.adrBody}
|
|
1275
|
+
: {}),
|
|
1276
|
+
answers,
|
|
1277
|
+
arbiter: context.arbiter,
|
|
1278
|
+
// Lifecycle FAN-OUT: widen the CAS contention budget + jitter the retries
|
|
1279
|
+
// so N parallel mint-adr legs desync (task
|
|
1280
|
+
// `jitter-and-widen-cas-contention-retry-for-lifecycle-fanout`).
|
|
1281
|
+
contention: LIFECYCLE_CAS_CONTENTION,
|
|
1282
|
+
note,
|
|
1283
|
+
});
|
|
1284
|
+
return {
|
|
1285
|
+
exitCode: result.exitCode,
|
|
1286
|
+
outcome:
|
|
1287
|
+
result.outcome === 'minted'
|
|
1288
|
+
? 'advanced'
|
|
1289
|
+
: result.outcome === 'lost'
|
|
1290
|
+
? 'lost'
|
|
1291
|
+
: result.outcome === 'contended'
|
|
1292
|
+
? 'contended'
|
|
1293
|
+
: 'usage-error',
|
|
1294
|
+
message: result.message,
|
|
1295
|
+
};
|
|
1296
|
+
} catch (err) {
|
|
1297
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1298
|
+
return {
|
|
1299
|
+
exitCode: 1,
|
|
1300
|
+
outcome: 'usage-error',
|
|
1301
|
+
message: `apply ${item}: ${detail}`,
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// delete-source → discharge by deletion (DIRECT, no confirm — decision 12). The
|
|
1307
|
+
// human's answer is the source of truth; the deletion is a single revertible
|
|
1308
|
+
// commit with the reason in the message.
|
|
1309
|
+
const apply = context.applyPersist ?? applyAnsweredQuestions;
|
|
1310
|
+
try {
|
|
1311
|
+
const result = apply({
|
|
1312
|
+
cwd,
|
|
1313
|
+
item,
|
|
1314
|
+
itemPath,
|
|
1315
|
+
discharge: {reason: verdict.deleteReason ?? ''},
|
|
1316
|
+
note,
|
|
1317
|
+
});
|
|
1318
|
+
return {
|
|
1319
|
+
exitCode: 0,
|
|
1320
|
+
outcome: result.outcome === 'vanished' ? 'vanished' : 'advanced',
|
|
1321
|
+
message: result.message,
|
|
1322
|
+
};
|
|
1323
|
+
} catch (err) {
|
|
1324
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1325
|
+
return {
|
|
1326
|
+
exitCode: 1,
|
|
1327
|
+
outcome: 'usage-error',
|
|
1328
|
+
message: `apply ${item}: ${detail}`,
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* Run ONE `advance` tick over a SINGLE named item: classify → lock → dispatch →
|
|
1335
|
+
* release. The pure tick the drivers (later tasks) wrap. The expensive phase is
|
|
1336
|
+
* ALWAYS post-lock — a CAS loser backs off having done ONLY the free
|
|
1337
|
+
* classification (it never reaches the executor).
|
|
1338
|
+
*/
|
|
1339
|
+
export async function performAdvance(
|
|
1340
|
+
options: AdvanceOptions,
|
|
1341
|
+
): Promise<AdvanceResult> {
|
|
1342
|
+
const note = options.note ?? (() => {});
|
|
1343
|
+
const cwd = options.cwd;
|
|
1344
|
+
const repoPath = options.repoPath ?? cwd;
|
|
1345
|
+
const arbiter = options.arbiter ?? DEFAULT_ARBITER;
|
|
1346
|
+
|
|
1347
|
+
// {@link performAdvance} is the SINGLE-item tick. The bare `advance`
|
|
1348
|
+
// (eligible-SET) form is the DRIVER's job ({@link performAdvanceAuto} in
|
|
1349
|
+
// `advance-drivers.ts`, which selects over the pool + runs THIS tick per item) —
|
|
1350
|
+
// the tick itself REQUIRES a named item, so an empty arg here is a usage error
|
|
1351
|
+
// (the CLI dispatches the bare form to the driver before reaching here).
|
|
1352
|
+
if (options.arg === undefined || options.arg.trim() === '') {
|
|
1353
|
+
const message =
|
|
1354
|
+
'`advance` with no item is the eligible-SET form (the one-shot driver). ' +
|
|
1355
|
+
'The single-item tick needs a named item: ' +
|
|
1356
|
+
'`advance <slug>` / `advance prd:<slug>` / `advance obs:<slug>`.';
|
|
1357
|
+
note(message);
|
|
1358
|
+
return {exitCode: 1, outcome: 'usage-error', message};
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// 1. RESOLVE the arg via the SHARED resolver (extended with `obs:`). `advance`
|
|
1362
|
+
// spans task / prd / observation; a collision / bad arg is a loud usage error.
|
|
1363
|
+
let resolved;
|
|
1364
|
+
try {
|
|
1365
|
+
resolved = resolveAdvanceArg({
|
|
1366
|
+
arg: options.arg,
|
|
1367
|
+
repoPath,
|
|
1368
|
+
read: options.read ?? ledgerRead,
|
|
1369
|
+
});
|
|
1370
|
+
} catch (err) {
|
|
1371
|
+
if (err instanceof SlugResolutionError) {
|
|
1372
|
+
return {exitCode: 1, outcome: 'usage-error', message: err.message};
|
|
1373
|
+
}
|
|
1374
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1375
|
+
return {exitCode: 1, outcome: 'usage-error', message};
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
const type = sidecarTypeFor(resolved.namespace);
|
|
1379
|
+
const item = `${resolved.namespace}:${resolved.slug}`;
|
|
1380
|
+
|
|
1381
|
+
// 2. CLASSIFY — read-only, NO model, NO lock. Read the two signals + run the
|
|
1382
|
+
// pure classifier. This is the ONLY work a CAS loser will have spent.
|
|
1383
|
+
const readSignals = options.readSignals ?? readItemSignals;
|
|
1384
|
+
const signals = readSignals({repoPath, type, slug: resolved.slug, item});
|
|
1385
|
+
const classification = classifyTick({
|
|
1386
|
+
type,
|
|
1387
|
+
needsAnswers: signals.needsAnswers,
|
|
1388
|
+
sidecar: signals.sidecar,
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
// A NO-OP (pending sidecar / nothing eligible) or an invariant violation never
|
|
1392
|
+
// takes the lock — there is nothing to execute, so do NOT pay the CAS.
|
|
1393
|
+
if (classification.kind === 'no-op') {
|
|
1394
|
+
const message = `no-op for ${item} (${classification.reason ?? 'nothing to advance'}).`;
|
|
1395
|
+
note(message);
|
|
1396
|
+
return {
|
|
1397
|
+
exitCode: 0,
|
|
1398
|
+
outcome: 'no-op',
|
|
1399
|
+
rung: 'no-op',
|
|
1400
|
+
slug: resolved.slug,
|
|
1401
|
+
message,
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
if (classification.kind === 'invariant-violation') {
|
|
1405
|
+
const message =
|
|
1406
|
+
`refusing to advance ${item}: the \`needsAnswers\` flag and the sidecar ` +
|
|
1407
|
+
`disagree (${classification.reason ?? 'invariant violation'}). ` +
|
|
1408
|
+
`A human must reconcile them.`;
|
|
1409
|
+
note(message);
|
|
1410
|
+
return {
|
|
1411
|
+
exitCode: 1,
|
|
1412
|
+
outcome: 'invariant-violation',
|
|
1413
|
+
rung: 'invariant-violation',
|
|
1414
|
+
slug: resolved.slug,
|
|
1415
|
+
message,
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// 3. LOCK — take the `advancing` CAS borrow for the classified rung, keyed on
|
|
1420
|
+
// the item's `<type>-<slug>` identity. The expensive phase is POST-lock.
|
|
1421
|
+
//
|
|
1422
|
+
// UNIFIED PER-ITEM LOCK, TREE-LESS RUNGS ONLY (prd
|
|
1423
|
+
// `ledger-status-per-item-lock-refs` US #1/#3/#18; ADR
|
|
1424
|
+
// `ledger-status-on-per-item-lock-refs`). The rung kind is KNOWN here
|
|
1425
|
+
// (`classification.kind`, classified pre-lock), so the tree-less-only policy
|
|
1426
|
+
// lives HERE — where the rung is known — and `advancing-lock.ts` stays
|
|
1427
|
+
// rung-agnostic (it only learns "unified or not" via `acquireUnified`). For a
|
|
1428
|
+
// TREE-LESS rung (`surface`/`apply`/`triage-observation`) the advancing acquire
|
|
1429
|
+
// ALSO takes the item's unified lock (`action: advance`) — these rungs have NO
|
|
1430
|
+
// inner `do`, so the unified hold is what realises advance∥claim / advance∥task
|
|
1431
|
+
// exclusion. For a BUILD-TASK / TASK-PRD rung we do NOT take the unified lock
|
|
1432
|
+
// at the advance layer: `performAdvance` ORCHESTRATES an inner `performDo` that
|
|
1433
|
+
// ITSELF acquires the SAME `task-<slug>`/`prd-<slug>` ref (the create-only CAS
|
|
1434
|
+
// with NO re-entrancy/auto-steal, per the ADR), so taking it here too would
|
|
1435
|
+
// DEADLOCK the tick against itself. The inner `do`'s claim/task lock IS the
|
|
1436
|
+
// single exclusion point for those rungs. The `work/advancing/<entry>.md` marker
|
|
1437
|
+
// CAS is KEPT for ALL rungs (its removal is the capstone task #9).
|
|
1438
|
+
const unifiedForRung = isTreeLessRung(classification.kind);
|
|
1439
|
+
const acquire =
|
|
1440
|
+
options.acquireLock ??
|
|
1441
|
+
((lockItem: string) =>
|
|
1442
|
+
acquireAdvancingLock({
|
|
1443
|
+
item: lockItem,
|
|
1444
|
+
cwd,
|
|
1445
|
+
arbiter,
|
|
1446
|
+
acquireUnified: unifiedForRung,
|
|
1447
|
+
note,
|
|
1448
|
+
}));
|
|
1449
|
+
const lock = await acquire(item);
|
|
1450
|
+
if (lock.exitCode !== 0) {
|
|
1451
|
+
// A CAS LOSER (exit 2) or contended (exit 3) backs off having spent ONLY the
|
|
1452
|
+
// free classification above — it never reaches the executor.
|
|
1453
|
+
return {
|
|
1454
|
+
exitCode: lock.exitCode,
|
|
1455
|
+
outcome: lock.outcome === 'lost' ? 'lost' : 'contended',
|
|
1456
|
+
rung: classification.kind,
|
|
1457
|
+
slug: resolved.slug,
|
|
1458
|
+
message: lock.message,
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// 4. EXECUTE — WINNER ONLY: dispatch the classified rung to the executor seam,
|
|
1463
|
+
// then ALWAYS release the borrow (the item never moved; release is clean).
|
|
1464
|
+
const executor = options.executor ?? defaultRungExecutor;
|
|
1465
|
+
const release =
|
|
1466
|
+
options.releaseLock ??
|
|
1467
|
+
((lockItem: string) =>
|
|
1468
|
+
releaseAdvancingLock({
|
|
1469
|
+
item: lockItem,
|
|
1470
|
+
cwd,
|
|
1471
|
+
arbiter,
|
|
1472
|
+
releaseUnified: unifiedForRung,
|
|
1473
|
+
note,
|
|
1474
|
+
}));
|
|
1475
|
+
try {
|
|
1476
|
+
const exec = await dispatchRung(executor, {
|
|
1477
|
+
item,
|
|
1478
|
+
namespace: resolved.namespace,
|
|
1479
|
+
slug: resolved.slug,
|
|
1480
|
+
classification,
|
|
1481
|
+
context: {
|
|
1482
|
+
cwd,
|
|
1483
|
+
arbiter,
|
|
1484
|
+
doOptions: options.doOptions,
|
|
1485
|
+
doDriver: options.doDriver,
|
|
1486
|
+
surfaceGate: options.surfaceGate,
|
|
1487
|
+
surfaceModel: options.surfaceModel,
|
|
1488
|
+
surfacePersist: options.surfacePersist,
|
|
1489
|
+
applyPersist: options.applyPersist,
|
|
1490
|
+
applyFollowups: options.applyFollowups,
|
|
1491
|
+
applyDecide: options.applyDecide,
|
|
1492
|
+
applyModel: options.applyModel,
|
|
1493
|
+
observationTriage: options.observationTriage,
|
|
1494
|
+
triageGate: options.triageGate,
|
|
1495
|
+
triageModel: options.triageModel,
|
|
1496
|
+
autoDisposition: options.autoDisposition,
|
|
1497
|
+
promote: options.promote,
|
|
1498
|
+
mintAdr: options.mintAdr,
|
|
1499
|
+
promoteSlug: options.promoteSlug,
|
|
1500
|
+
// The answered-merge LAND dispatch context (task
|
|
1501
|
+
// `apply-rung-merge-disposition`): threaded into the apply rung's
|
|
1502
|
+
// kind-check for a `kind: merge` runner-action.
|
|
1503
|
+
workspacesDir: options.workspacesDir,
|
|
1504
|
+
arbiterUrl: options.arbiterUrl,
|
|
1505
|
+
prepare: options.prepare,
|
|
1506
|
+
verify: options.verify,
|
|
1507
|
+
strictMergeApproval: options.strictMergeApproval,
|
|
1508
|
+
mergeAction: options.mergeAction,
|
|
1509
|
+
note,
|
|
1510
|
+
},
|
|
1511
|
+
});
|
|
1512
|
+
return {
|
|
1513
|
+
exitCode: exec.exitCode,
|
|
1514
|
+
outcome: exec.outcome,
|
|
1515
|
+
rung: classification.kind,
|
|
1516
|
+
slug: resolved.slug,
|
|
1517
|
+
message: exec.message,
|
|
1518
|
+
};
|
|
1519
|
+
} finally {
|
|
1520
|
+
await release(item);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* Is this a TREE-LESS rung (`surface`/`apply`/`triage-observation`) — the rungs
|
|
1526
|
+
* that have NO inner `performDo`, so the advancing acquire must ALSO take the
|
|
1527
|
+
* unified per-item lock (`action: advance`) to realise advance∥claim / advance∥task
|
|
1528
|
+
* exclusion? The build/task rungs (`build-task`/`task-spec`) are the inverse:
|
|
1529
|
+
* their inner `do` holds the SAME unified ref, so the advance layer must NOT take
|
|
1530
|
+
* it (it would deadlock the tick against itself). `no-op`/`invariant-violation`
|
|
1531
|
+
* never reach the lock step. This is the single place the tree-less-only policy
|
|
1532
|
+
* is expressed (the rung is known here); `advancing-lock.ts` stays rung-agnostic.
|
|
1533
|
+
*/
|
|
1534
|
+
function isTreeLessRung(kind: TickClassification['kind']): boolean {
|
|
1535
|
+
return (
|
|
1536
|
+
kind === 'surface' || kind === 'apply' || kind === 'triage-observation'
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
/** Dispatch the classified rung to the executor seam (winner-only). */
|
|
1541
|
+
function dispatchRung(
|
|
1542
|
+
executor: RungExecutor,
|
|
1543
|
+
input: RungExecInput,
|
|
1544
|
+
): Promise<RungExecResult> {
|
|
1545
|
+
switch (input.classification.kind) {
|
|
1546
|
+
case 'build-task':
|
|
1547
|
+
return executor.buildTask(input);
|
|
1548
|
+
case 'task-spec':
|
|
1549
|
+
return executor.taskSpec(input);
|
|
1550
|
+
case 'triage-observation':
|
|
1551
|
+
return executor.triageObservation(input);
|
|
1552
|
+
case 'surface':
|
|
1553
|
+
return executor.surface(input);
|
|
1554
|
+
case 'apply':
|
|
1555
|
+
return executor.apply(input);
|
|
1556
|
+
default:
|
|
1557
|
+
// `no-op` / `invariant-violation` never reach here (handled pre-lock).
|
|
1558
|
+
return Promise.resolve({
|
|
1559
|
+
exitCode: 1,
|
|
1560
|
+
outcome: 'usage-error',
|
|
1561
|
+
message: `unexpected rung kind '${input.classification.kind}' at dispatch.`,
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
}
|