dorfl 0.0.0 → 0.1.1
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 +3100 -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 +208 -0
- package/dist/frontmatter.d.ts.map +1 -0
- package/dist/frontmatter.js +344 -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 +329 -0
- package/dist/prd-to-spec.d.ts.map +1 -0
- package/dist/prd-to-spec.js +706 -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 +363 -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 +951 -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 +4379 -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 +477 -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 +1059 -0
- package/src/prepare.ts +230 -0
- package/src/prompt.ts +763 -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 +1437 -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/intake.ts
ADDED
|
@@ -0,0 +1,2445 @@
|
|
|
1
|
+
import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'node:fs';
|
|
2
|
+
import {dirname, join} from 'node:path';
|
|
3
|
+
import {runAsync, type RunResult} from './git.js';
|
|
4
|
+
import {workFolderRel, workItemRel} from './work-layout.js';
|
|
5
|
+
import {paramCase} from './brand.js';
|
|
6
|
+
import {
|
|
7
|
+
performIntegration,
|
|
8
|
+
type IntegrationCoreResult,
|
|
9
|
+
} from './integration-core.js';
|
|
10
|
+
import type {IntegrateResult, ReviewProvider} from './integrator.js';
|
|
11
|
+
import {integrationFromFlags} from './complete.js';
|
|
12
|
+
import type {IntegrationMode, SpecsLandIn} from './config.js';
|
|
13
|
+
import type {OriginTrust} from './frontmatter.js';
|
|
14
|
+
import {
|
|
15
|
+
placementFolder,
|
|
16
|
+
resolvePlacement,
|
|
17
|
+
type PlacementSlots,
|
|
18
|
+
} from './placement.js';
|
|
19
|
+
import {
|
|
20
|
+
identityEnv,
|
|
21
|
+
assertTransportAllowed,
|
|
22
|
+
type Identity,
|
|
23
|
+
} from './identity.js';
|
|
24
|
+
import {NullHarness, type Harness} from './harness.js';
|
|
25
|
+
import {workBranchRef, type SlugNamespace} from './slug-namespace.js';
|
|
26
|
+
import {launchWithOptionalWatch} from './agent-launch.js';
|
|
27
|
+
import {
|
|
28
|
+
GitHubIssueProvider,
|
|
29
|
+
PROCESSING_LOCK_LABEL,
|
|
30
|
+
type Issue,
|
|
31
|
+
type IssueComment,
|
|
32
|
+
type IssueProvider,
|
|
33
|
+
} from './issue-provider.js';
|
|
34
|
+
import {extractJsonObjectSpan} from './verdict-json.js';
|
|
35
|
+
import {
|
|
36
|
+
parseReviewVerdict,
|
|
37
|
+
reviewDisciplinePrompt,
|
|
38
|
+
verdictContractPrompt,
|
|
39
|
+
type ReviewFinding,
|
|
40
|
+
type ReviewVerdict,
|
|
41
|
+
} from './review-verdict.js';
|
|
42
|
+
import {
|
|
43
|
+
stampIntakeMarker,
|
|
44
|
+
computeSeenDelta,
|
|
45
|
+
type IntakeMarkerKind,
|
|
46
|
+
} from './intake-marker.js';
|
|
47
|
+
import {triageIntake, type IntakeTriageDecision} from './intake-triage.js';
|
|
48
|
+
import {renderTaskBody, renderSpecBody} from './buildable-body.js';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* **`intake <N>`** (prd `issue-intake`, task `intake-tracer-slice-outcome`): the
|
|
52
|
+
* KEYSTONE of the issue front-door. A new, GATE-FREE command — explicit invocation
|
|
53
|
+
* IS the authorization (precedent: `explicit-do-prd-not-gated-by-autoslice`), so
|
|
54
|
+
* `autoTask`/`autoBuild` config does NOT apply — that reads a GitHub issue + its
|
|
55
|
+
* thread through the {@link IssueProvider} seam, runs the decision as a
|
|
56
|
+
* **prompt → VERDICT**, and DISPATCHES on the verdict.
|
|
57
|
+
*
|
|
58
|
+
* The engine shape MIRRORS the review gate (prompt → `approve|block` → dispatch):
|
|
59
|
+
* the decision prompt is an INLINE builder ({@link buildIntakeDecisionSpec}, like
|
|
60
|
+
* `buildTaskingPrd`); the **dispatcher is the testable seam** — a STUBBED verdict
|
|
61
|
+
* (injected, no model/network) drives it, exactly as `ReviewGate` is injected. The
|
|
62
|
+
* prompt's JUDGEMENT is NOT unit-tested (like the review prompt's is not); only the
|
|
63
|
+
* dispatch is.
|
|
64
|
+
*
|
|
65
|
+
* The dispatcher implements the FULL four-outcome decision table (prd
|
|
66
|
+
* `issue-intake` — the source of truth):
|
|
67
|
+
* - **ASK** (not clear enough to act on): `postIssueComment` the next clarifying
|
|
68
|
+
* question; emit NOTHING; STOP.
|
|
69
|
+
* - **TASK** (clear AND fits ONE tracer-bullet task): write
|
|
70
|
+
* `work/backlog/<slug>.md` (`covers: []`, NO `prd:`) carrying `issue: N` (the
|
|
71
|
+
* lone-task closure link, NOT `Fixes #N`), integrate via {@link
|
|
72
|
+
* performIntegration} (default `propose`).
|
|
73
|
+
* - **PRD** (clear AND coherent but >1 task — INCLUDING a coupled-but-SMALL pair,
|
|
74
|
+
* which is NEVER bounced): write the prd file (`work/prds/ready/<slug>.md`) with `issue: N` (+ the gate
|
|
75
|
+
* axes the verdict carried), integrate, STOP (tasking is the separate `do prd:`
|
|
76
|
+
* step).
|
|
77
|
+
* - **BOUNCE** (genuinely UNRELATED concerns — no shared vision): the bounce is
|
|
78
|
+
* TERMINAL (the asks are unrelated and must be re-filed), so intake CLOSES the
|
|
79
|
+
* issue ATOMICALLY — the "file separate issues" text as the closing comment +
|
|
80
|
+
* `reason: not planned` (the honest GitHub-native signal) in ONE `closeIssue`
|
|
81
|
+
* call; emit NOTHING. Intake closes on BOUNCE (as not planned); NEVER on
|
|
82
|
+
* task/prd (CI's close-job closes those via the `issue:` field) / ask.
|
|
83
|
+
*
|
|
84
|
+
* The per-outcome integration KNOBS, the processing LOCK, and event-classification
|
|
85
|
+
* are LATER tasks and are NOT built here (default `propose` is fine here).
|
|
86
|
+
*
|
|
87
|
+
* The AGENT only DRAFTS (returns the verdict object); the RUNNER (this dispatcher)
|
|
88
|
+
* owns every git/seam side-effect — the write + integrate (and, in later tasks,
|
|
89
|
+
* the comment + label ops). The agent is git-free AND seam-free: the in-band
|
|
90
|
+
* boundary (the SAME discipline the build/tasker agents follow).
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The four outcomes the decision prompt classifies an issue into (the decision
|
|
95
|
+
* table). EXPAND step (prd
|
|
96
|
+
* `prd-to-spec-vocabulary-cutover-and-migration-command`): the `spec` outcome
|
|
97
|
+
* names the "clear + coherent but >1 task" classification (a parent SPEC). HARD
|
|
98
|
+
* CUTOVER (contract step): the legacy `prd` outcome token is GONE — the prompt
|
|
99
|
+
* emits `spec` and the parser rejects any other token.
|
|
100
|
+
*/
|
|
101
|
+
export type IntakeOutcome = 'ask' | 'task' | 'spec' | 'bounce';
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The VERDICT the decision prompt returns — `{ask,task,spec,bounce}` + the drafted
|
|
105
|
+
* content for the chosen outcome. THIS code path consumes only the `task` branch's
|
|
106
|
+
* fields (`taskSlug` / `taskTitle` / `taskBody`); the `ask`/`spec`/`bounce`
|
|
107
|
+
* fields are carried on the shape (so the type is stable for the next task) but
|
|
108
|
+
* not dispatched here.
|
|
109
|
+
*/
|
|
110
|
+
export interface IntakeVerdict {
|
|
111
|
+
/** Which outcome the prompt chose for the issue. */
|
|
112
|
+
outcome: IntakeOutcome;
|
|
113
|
+
/**
|
|
114
|
+
* The drafted task's content-derived slug (`task` outcome). The dispatcher
|
|
115
|
+
* SANITISES it (a content-derived slug, never a counter) before writing
|
|
116
|
+
* `work/backlog/<slug>.md`. Falls back to a slug derived from {@link taskTitle}
|
|
117
|
+
* when absent/empty.
|
|
118
|
+
*/
|
|
119
|
+
taskSlug?: string;
|
|
120
|
+
/** The drafted task's `title:` (`task` outcome). */
|
|
121
|
+
taskTitle?: string;
|
|
122
|
+
/**
|
|
123
|
+
* The drafted task BODY (`task` outcome) — the markdown AFTER the frontmatter
|
|
124
|
+
* (the `## What to build` / `## Acceptance criteria` / `## Prompt` sections). The
|
|
125
|
+
* dispatcher writes the frontmatter (slug/title/`covers: []`, NO `prd:`) carrying
|
|
126
|
+
* the lone-task `issue: N` closure link itself; the agent never writes
|
|
127
|
+
* git-visible files.
|
|
128
|
+
*/
|
|
129
|
+
taskBody?: string;
|
|
130
|
+
/**
|
|
131
|
+
* The drafted clarifying question (`ask` outcome) — the dispatcher posts it via
|
|
132
|
+
* `postIssueComment`, emits nothing, and STOPS (a later run resumes from the
|
|
133
|
+
* updated thread).
|
|
134
|
+
*/
|
|
135
|
+
question?: string;
|
|
136
|
+
/**
|
|
137
|
+
* The drafted spec's content-derived slug (`spec` outcome). The dispatcher
|
|
138
|
+
* SANITISES it through `paramCase` (never a counter) before writing the spec
|
|
139
|
+
* file (`work/specs/ready/<slug>.md`). Falls back to a slug derived from {@link specTitle} when
|
|
140
|
+
* absent/empty.
|
|
141
|
+
*/
|
|
142
|
+
specSlug?: string;
|
|
143
|
+
/** The drafted spec's `title:` (`spec` outcome). */
|
|
144
|
+
specTitle?: string;
|
|
145
|
+
/**
|
|
146
|
+
* The drafted spec BODY (`spec` outcome) — the markdown AFTER the frontmatter
|
|
147
|
+
* (`## Problem Statement` / `## Solution` / `## User Stories` / …). The dispatcher
|
|
148
|
+
* writes the frontmatter (title/slug/`issue: N` + the gate axes) itself; the
|
|
149
|
+
* agent never writes git-visible files.
|
|
150
|
+
*/
|
|
151
|
+
specBody?: string;
|
|
152
|
+
/**
|
|
153
|
+
* The spec's gate axes (`spec` outcome) AS THE PROMPT JUDGED THEM — surfaced onto
|
|
154
|
+
* the emitted spec frontmatter (prd `issue-intake` US #8: "the emitted artifact
|
|
155
|
+
* carries … its own gate axes"). Both omitted (undeclared) by default; the prompt
|
|
156
|
+
* sets `specHumanOnly: true` when a human should drive the TASKING and/or
|
|
157
|
+
* `specNeedsAnswers: true` when open questions remain.
|
|
158
|
+
*/
|
|
159
|
+
specHumanOnly?: boolean;
|
|
160
|
+
specNeedsAnswers?: boolean;
|
|
161
|
+
/**
|
|
162
|
+
* The drafted bounce message (`bounce` outcome) — the dispatcher carries it as
|
|
163
|
+
* the CLOSING COMMENT on the atomic `closeIssue` ("please file separate issues")
|
|
164
|
+
* with `reason: not planned`, then emits nothing. A bounce is TERMINAL, so the
|
|
165
|
+
* issue is CLOSED (not left open).
|
|
166
|
+
*/
|
|
167
|
+
bounceMessage?: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The terminal status of one `intake <N>` run. */
|
|
171
|
+
export type IntakeRunOutcome =
|
|
172
|
+
| 'tasked' // a `task` verdict → backlog task written + integrated
|
|
173
|
+
| 'asked' // an `ask` verdict → clarifying question posted, nothing emitted
|
|
174
|
+
| 'spec-written' // a `spec` verdict → the spec file (`work/specs/ready/<slug>.md`) written + integrated
|
|
175
|
+
| 'bounced' // a `bounce` verdict → split-issues comment posted, nothing emitted
|
|
176
|
+
| 'no-new-input' // the TRIAGE saw intake had the last word + nothing unseen → SKIP (ran, deliberately did nothing)
|
|
177
|
+
| 'already-terminal' // the TRIAGE saw the issue was already transformed (a `bounced`/`created` marker) → SKIP
|
|
178
|
+
| 'locked' // the `processing` lock was already held → backed off (did nothing)
|
|
179
|
+
| 'lock-failed' // the lock could not be ACQUIRED on a label-supporting provider → fail (do NOT proceed lock-less)
|
|
180
|
+
| 'agent-failed' // the decision agent invocation itself errored
|
|
181
|
+
| 'stale' // the integrate rebase conflicted against an advanced main
|
|
182
|
+
| 'usage-error'; // usage / environment problem
|
|
183
|
+
|
|
184
|
+
export interface IntakeResult {
|
|
185
|
+
exitCode: 0 | 1 | 4;
|
|
186
|
+
outcome: IntakeRunOutcome;
|
|
187
|
+
/** The issue number acted on. */
|
|
188
|
+
issueNumber: number;
|
|
189
|
+
/** The slug of the emitted artifact (task OR prd outcome). */
|
|
190
|
+
emittedSlug?: string;
|
|
191
|
+
/** Repo-relative path of the emitted artifact (task OR prd outcome). */
|
|
192
|
+
emitted?: string;
|
|
193
|
+
/** True iff a comment was posted on the issue (ask / bounce outcomes). */
|
|
194
|
+
commented?: boolean;
|
|
195
|
+
/**
|
|
196
|
+
* True iff the ISSUE was closed (the BOUNCE outcome — a terminal bounce closes
|
|
197
|
+
* the issue atomically as `not planned`). Additive (mirrors {@link commented}),
|
|
198
|
+
* so CI / callers can observe the close. Never set on ask/task/prd.
|
|
199
|
+
*/
|
|
200
|
+
closed?: boolean;
|
|
201
|
+
/** Human-readable summary of the terminal condition. */
|
|
202
|
+
message: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The DECISION step: given the issue + thread, return a VERDICT. Tests inject a
|
|
207
|
+
* canned verdict (the STUBBED seam that drives the dispatcher, no model/network).
|
|
208
|
+
* Production wires the harness through {@link harnessIntakeDecision}.
|
|
209
|
+
*/
|
|
210
|
+
export type IntakeDecider = (input: {
|
|
211
|
+
cwd: string;
|
|
212
|
+
issue: Issue;
|
|
213
|
+
comments: IssueComment[];
|
|
214
|
+
prompt: string;
|
|
215
|
+
env?: NodeJS.ProcessEnv;
|
|
216
|
+
}) => Promise<IntakeVerdict>;
|
|
217
|
+
|
|
218
|
+
export interface PerformIntakeOptions {
|
|
219
|
+
/** The issue number to intake (`intake <N>`). */
|
|
220
|
+
issueNumber: number;
|
|
221
|
+
/** The working clone/checkout the intake runs in. */
|
|
222
|
+
cwd: string;
|
|
223
|
+
/** Name of the arbiter git remote. Defaults to `origin`. */
|
|
224
|
+
arbiter?: string;
|
|
225
|
+
/**
|
|
226
|
+
* The issue seam (read the issue + thread). Tests inject a STUB; production
|
|
227
|
+
* defaults to {@link GitHubIssueProvider} (the only place `gh` is shelled out).
|
|
228
|
+
*/
|
|
229
|
+
issueProvider?: IssueProvider;
|
|
230
|
+
/**
|
|
231
|
+
* The DECISION seam (prompt → verdict). Tests inject a CANNED verdict (no
|
|
232
|
+
* model/network) — this is the unit-test target. Production wires the harness.
|
|
233
|
+
*/
|
|
234
|
+
decide?: IntakeDecider;
|
|
235
|
+
/**
|
|
236
|
+
* The LONE-TASK bounded-review seam (prompt → review verdict). After a `task`
|
|
237
|
+
* verdict and BEFORE the write/integrate, {@link dispatchTask} runs a bounded
|
|
238
|
+
* (3-round, HARD-CAPPED) adversarial self-review on the SINGLE drafted task
|
|
239
|
+
* through this seam (observation
|
|
240
|
+
* `intake-lone-task-skips-adversarial-review-the-prd-path-gets`, rulings A/B/C).
|
|
241
|
+
* Tests inject a CANNED review verdict (no model/network) — the new testable
|
|
242
|
+
* seam; production wires the harness ({@link harnessLoneTaskReviewGate}). It
|
|
243
|
+
* mirrors {@link decide}'s injectable shape, NOT the tasker loop (which is a
|
|
244
|
+
* SET-level reviewer this never imports/calls).
|
|
245
|
+
*/
|
|
246
|
+
reviewTask?: LoneTaskReviewGate;
|
|
247
|
+
/** The harness seam used when {@link decide} is omitted; defaults to the null adapter. */
|
|
248
|
+
harness?: Harness;
|
|
249
|
+
/** The configured agent command the harness shells out to (null adapter). */
|
|
250
|
+
agentCmd?: string;
|
|
251
|
+
/** The model routing intent forwarded to the harness (ADR §13). */
|
|
252
|
+
model?: string;
|
|
253
|
+
/** The HOST-ONLY sessions root for the pi session file. */
|
|
254
|
+
sessionsDir?: string;
|
|
255
|
+
/**
|
|
256
|
+
* The PER-OUTCOME integration modes (prd `issue-intake` US #9) the emitted artifact integrates
|
|
257
|
+
* THROUGH the shared core with. Because `intake` decides the artifact TYPE at
|
|
258
|
+
* RUNTIME, the mode is keyed per type: an emitted task integrates with
|
|
259
|
+
* `integration.task`, an emitted prd with `integration.prd` (`propose` =
|
|
260
|
+
* push the `work/<slug>` branch + open a PR, NO `main` touch; `merge` = land on
|
|
261
|
+
* `main`). The CLI resolves this from the granular + aggregate flags via
|
|
262
|
+
* {@link resolveIntakeIntegrationModes}; ask/bounce emit nothing, so the modes
|
|
263
|
+
* are no-ops for them. Unset ⇒ propose for both.
|
|
264
|
+
*/
|
|
265
|
+
integration?: IntakeIntegrationModes;
|
|
266
|
+
/**
|
|
267
|
+
* **The ORIGIN-TRUST verdict, passed IN** (task
|
|
268
|
+
* `untrusted-origin-forces-build-propose`; the `--origin-trust <trusted|untrusted>`
|
|
269
|
+
* CLI flag). `intake` STAMPS `origin: issue` + this `originTrust` onto every prd/
|
|
270
|
+
* task it emits, so the author-trust signal SURVIVES the prd/task merge
|
|
271
|
+
* boundary (a landed-on-main artifact otherwise erases how it was born, the
|
|
272
|
+
* laundering gap). `intake` does NOT resolve trust itself: the verdict is CI's
|
|
273
|
+
* POLICY, computed in the `intake.yml` shell from the SAME `author_association`
|
|
274
|
+
* case as the integration flags and threaded IN here (preserving the ~L296
|
|
275
|
+
* boundary). UNSET means the artifact is emitted UNSTAMPED, read as `human`/trusted:
|
|
276
|
+
* a LOCAL `dorfl intake <N>` (no CI shell, no `--origin-trust`) is the
|
|
277
|
+
* human-IS-the-checkpoint path, gate-free exactly as `do`.
|
|
278
|
+
*/
|
|
279
|
+
originTrust?: OriginTrust;
|
|
280
|
+
/**
|
|
281
|
+
* **The PR-INTENT axis** (config `noPR`, ADR §6): when `true`, intake's propose
|
|
282
|
+
* emissions push the branch but skip the PR (the explicit suppress-PR intent).
|
|
283
|
+
* NOT a provider choice — the provider is purely arbiter-derived. Unset/false ⇒
|
|
284
|
+
* the PR opens normally.
|
|
285
|
+
*/
|
|
286
|
+
noPR?: boolean;
|
|
287
|
+
/**
|
|
288
|
+
* **The per-repo PRD-PLACEMENT default, passed IN** (prd
|
|
289
|
+
* `staging-pool-position-gate-and-trust-model` US #2/#5, task
|
|
290
|
+
* `pre-prd-staging-pool-split-and-untrusted-prd-placement`). The resolved
|
|
291
|
+
* per-repo default landing for `intake`-authored prds (`pre-proposed` =
|
|
292
|
+
* staging; `ready` = the auto-tasking pool), fed as the CONFIGURED-DEFAULT rung
|
|
293
|
+
* into the shared placement resolver (`src/placement.ts`). The resolver
|
|
294
|
+
* overlays an EXPLICIT operator flag ({@link explicitSpecsLandIn}, top) and
|
|
295
|
+
* the UNTRUSTED-ORIGIN force (`originTrust: untrusted` ⇒ staging) on top.
|
|
296
|
+
* Unset ⇒ the resolver's built-in floor applies (`staging` = `prds/proposed/`,
|
|
297
|
+
* the conservative landing). The PRD TWIN of `tasksLandIn` on the tasker
|
|
298
|
+
* path — one resolver, two lifecycles.
|
|
299
|
+
*/
|
|
300
|
+
specsLandIn?: SpecsLandIn;
|
|
301
|
+
/**
|
|
302
|
+
* **The OPERATOR's EXPLICIT spec-placement override** (the TOP precedence
|
|
303
|
+
* rung). When set, the runner-deterministic resolver lands the spec HERE
|
|
304
|
+
* regardless of `originTrust` or {@link specsLandIn} — the positional
|
|
305
|
+
* analogue of `explicitMerge` overriding the untrusted-origin
|
|
306
|
+
* build-propose rule ("the operator is present; CLI always wins, no
|
|
307
|
+
* special force-key"). Set ONLY when the operator typed
|
|
308
|
+
* `--specs-land-in <where>`; never when the value came from config.
|
|
309
|
+
*/
|
|
310
|
+
explicitSpecsLandIn?: SpecsLandIn;
|
|
311
|
+
/**
|
|
312
|
+
* Optional FULLY-FORMED review provider INSTANCE used VERBATIM (the SAME seam
|
|
313
|
+
* `run`/`do` expose; forwarded to `performIntegration` as `providerInstance`).
|
|
314
|
+
* Tests/embeddings inject a stubbed `GitHubProvider` (a custom `gh` path) to
|
|
315
|
+
* drive intake's propose pipeline OFFLINE. The resolved provider OBJECT, NOT a
|
|
316
|
+
* config override. Unset ⇒ the core selects from the arbiter URL.
|
|
317
|
+
*/
|
|
318
|
+
providerInstance?: ReviewProvider;
|
|
319
|
+
/**
|
|
320
|
+
* The optional runner IDENTITY (a bot), threaded from host-only
|
|
321
|
+
* `config.identity`. It scopes intake's GIT + provider operations — the `gh`
|
|
322
|
+
* issue ops (read/label/comment/close), the push, and the PR — via process-
|
|
323
|
+
* scoped env overrides. It is NEVER applied to the intake AGENT launches (the
|
|
324
|
+
* decision agent + the lone-task review agent), which stay ambient: an agent
|
|
325
|
+
* must not act as the bot. Absent ⇒ ambient (today's behaviour).
|
|
326
|
+
*/
|
|
327
|
+
identity?: Identity;
|
|
328
|
+
/** Environment for child git/agent processes (the AGENT-launch ambient env). */
|
|
329
|
+
env?: NodeJS.ProcessEnv;
|
|
330
|
+
/** Sink for human-readable progress notes. */
|
|
331
|
+
note?: (message: string) => void;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const DEFAULT_ARBITER = 'origin';
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* **The STAGED-prds dir** (prd `staging-pool-position-gate-and-trust-model`,
|
|
338
|
+
* task `pre-prd-staging-pool-split-and-untrusted-prd-placement`, governing
|
|
339
|
+
* ADR `placement-is-runner-deterministic-humanonly-is-agent-judgement`). When
|
|
340
|
+
* the runner-deterministic placement resolver picks the staging side for an
|
|
341
|
+
* `intake`-authored prd, the runner writes the prd file HERE instead of in
|
|
342
|
+
* `work/prds/ready/`. An item born in `prds/proposed/` is durable + readable but NOT in
|
|
343
|
+
* the tasking candidate POOL (`work/prds/ready/` is the pool). A runner/human-owned promotion
|
|
344
|
+
* ({@link promoteFromPreSpec} in `needs-attention.ts`) moves an approved prd
|
|
345
|
+
* `prds/proposed/ → prds/ready/` to make it taskable.
|
|
346
|
+
*/
|
|
347
|
+
export const STAGED_SPECS_DIR = workFolderRel('specs-proposed');
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* The POOL folder specs land in when the runner-deterministic placement
|
|
351
|
+
* resolver chooses the pool side (`specsLandIn: 'ready'` + a trusted origin, or
|
|
352
|
+
* an `--specs-land-in ready` operator override). This is `work/specs/ready/`,
|
|
353
|
+
* the tasking candidate pool.
|
|
354
|
+
*/
|
|
355
|
+
const POOL_SPECS_DIR = workFolderRel('specs-ready');
|
|
356
|
+
|
|
357
|
+
/** The placement slots for the spec lifecycle (folder names). */
|
|
358
|
+
const SPEC_PLACEMENT_SLOTS: PlacementSlots = {
|
|
359
|
+
staging: STAGED_SPECS_DIR,
|
|
360
|
+
pool: POOL_SPECS_DIR,
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Map the `specsLandIn` value spelling (`pre-proposed` | `ready`) onto the
|
|
365
|
+
* resolver's lifecycle-generic side enum (`staging` | `pool`). Returns
|
|
366
|
+
* `undefined` when no value is set, so the resolver's next precedence rung
|
|
367
|
+
* applies (the built-in floor). The spec twin of `landingToSide` on the
|
|
368
|
+
* tasker path — same shape, different slots.
|
|
369
|
+
*/
|
|
370
|
+
function specLandingToSide(
|
|
371
|
+
landing: SpecsLandIn | undefined,
|
|
372
|
+
): 'staging' | 'pool' | undefined {
|
|
373
|
+
if (landing === 'pre-proposed') return 'staging';
|
|
374
|
+
if (landing === 'ready') return 'pool';
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* The emitted artifact TYPE `intake` decides at RUNTIME — a `task` verdict emits
|
|
380
|
+
* `work/backlog/<slug>.md`, a `prd` verdict emits the prd file (`work/prds/ready/<slug>.md`). The two
|
|
381
|
+
* granular flag axes (`--merge-task`/`--propose-task` vs `--merge-spec`/
|
|
382
|
+
* `--propose-spec`) are keyed on this. (ask/bounce emit NOTHING, so the modes are
|
|
383
|
+
* no-ops for them.)
|
|
384
|
+
*/
|
|
385
|
+
export type IntakeArtifactType = 'task' | 'spec';
|
|
386
|
+
// prd → spec cutover (MIGRATE batch): `'spec'` is the CANONICAL artifact type;
|
|
387
|
+
// `'prd'` stays as an accepted ALIAS until the contract task removes it.
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* The PER-OUTCOME integration mode FLAG SET (prd `issue-intake` US #9). Because
|
|
391
|
+
* `intake` decides the artifact TYPE at runtime, a single `--merge`/`--propose`
|
|
392
|
+
* cannot express a type-conditional policy ("merge a prd but propose a task") —
|
|
393
|
+
* hence the four GRANULAR per-type flags layered over the two AGGREGATES:
|
|
394
|
+
*
|
|
395
|
+
* - **granular:** `--merge-spec`/`--propose-spec` apply iff the outcome is a spec;
|
|
396
|
+
* `--merge-task`/`--propose-task` apply iff it is a task.
|
|
397
|
+
* - **aggregates:** `--merge` = merge BOTH types; `--propose` = propose BOTH.
|
|
398
|
+
*
|
|
399
|
+
* `intake` owns only these KNOBS; WHICH knobs CI sets (from gate state +
|
|
400
|
+
* author-trust) is CI's POLICY, authored in `runner-in-ci` — NOT here.
|
|
401
|
+
*/
|
|
402
|
+
export interface IntakeIntegrationFlags {
|
|
403
|
+
/** Aggregate: merge BOTH a task and a prd (the broad knob, overridden per type). */
|
|
404
|
+
merge?: boolean;
|
|
405
|
+
/** Aggregate: propose BOTH a task and a prd. */
|
|
406
|
+
propose?: boolean;
|
|
407
|
+
/** Granular: merge a spec (overrides the aggregate for the spec outcome). */
|
|
408
|
+
mergeSpec?: boolean;
|
|
409
|
+
/** Granular: propose a spec (overrides the aggregate for the spec outcome). */
|
|
410
|
+
proposeSpec?: boolean;
|
|
411
|
+
/** Granular: merge a task (overrides the aggregate for the task outcome). */
|
|
412
|
+
mergeTask?: boolean;
|
|
413
|
+
/** Granular: propose a task (overrides the aggregate for the task outcome). */
|
|
414
|
+
proposeTask?: boolean;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Both per-type integration modes, resolved from the flag set in ONE eager pass. */
|
|
418
|
+
export interface IntakeIntegrationModes {
|
|
419
|
+
/** The mode an EMITTED task integrates with. */
|
|
420
|
+
task: IntegrationMode;
|
|
421
|
+
/**
|
|
422
|
+
* The mode an EMITTED spec integrates with. `spec` is the CANONICAL key (prd →
|
|
423
|
+
* spec cutover); the user-facing `--merge-spec`/`--propose-spec` flags that FEED
|
|
424
|
+
* it carry the same `spec` spelling (the cli-flag rename landed in batch 4f).
|
|
425
|
+
*/
|
|
426
|
+
spec: IntegrationMode;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Default per-outcome integration mode when no flag selects one — propose (matches `do`). */
|
|
430
|
+
const DEFAULT_INTEGRATION: IntegrationMode = 'propose';
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Resolve the GRANULAR per-type axis (`--merge-<t>` / `--propose-<t>`) for ONE
|
|
434
|
+
* artifact type, REUSING {@link integrationFromFlags} for its mutual-exclusion +
|
|
435
|
+
* "mutually exclusive" error message (the same-type-both usage error) — so the
|
|
436
|
+
* granular axis is NOT a forked second resolver, just `integrationFromFlags`
|
|
437
|
+
* applied to the per-type pair. Returns the granular mode, or `undefined` when
|
|
438
|
+
* neither granular flag for this type was given (the aggregate/default then
|
|
439
|
+
* decides). The error message is reworded to name the granular flag pair.
|
|
440
|
+
*/
|
|
441
|
+
function granularFromFlags(
|
|
442
|
+
type: IntakeArtifactType,
|
|
443
|
+
merge: boolean | undefined,
|
|
444
|
+
propose: boolean | undefined,
|
|
445
|
+
): IntegrationMode | undefined {
|
|
446
|
+
try {
|
|
447
|
+
return integrationFromFlags({merge, propose});
|
|
448
|
+
} catch {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`--merge-${type} and --propose-${type} are mutually exclusive; pass at most one.`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* The PURE per-outcome integration mode resolution (prd `issue-intake` US #9 —
|
|
457
|
+
* the canonical table). Given ONLY the flag set, resolve BOTH per-type modes in
|
|
458
|
+
* one eager pass (so a usage error is caught before the runtime verdict is even
|
|
459
|
+
* known). The rules, all decided in the prd:
|
|
460
|
+
*
|
|
461
|
+
* - **unset ⇒ propose for BOTH** (conservative default; matches `do`).
|
|
462
|
+
* - **aggregates:** `--merge` ⇒ merge both; `--propose` ⇒ propose both (this axis
|
|
463
|
+
* COMPOSES the existing {@link integrationFromFlags}, reusing its mutual
|
|
464
|
+
* exclusion + error message).
|
|
465
|
+
* - **granular routes per type:** `--merge-spec` merges a spec (and leaves a task at
|
|
466
|
+
* the aggregate/default), etc.
|
|
467
|
+
* - **GRANULAR OVERRIDES AGGREGATE:** `--merge --propose-task` ⇒ merge a spec,
|
|
468
|
+
* propose a task.
|
|
469
|
+
* - **same type + both modes is a usage ERROR:** `--merge-spec --propose-spec` (and
|
|
470
|
+
* `--merge-task --propose-task`), and the aggregate `--merge --propose`.
|
|
471
|
+
*
|
|
472
|
+
* Throws (a usage error) on any mutually-exclusive pair. The dispatcher picks the
|
|
473
|
+
* field matching the runtime verdict's type; ask/bounce never integrate, so the
|
|
474
|
+
* modes are no-ops for them.
|
|
475
|
+
*
|
|
476
|
+
* `defaultMode` is the FALLBACK when NEITHER a granular nor the aggregate flag
|
|
477
|
+
* selects a mode for a type — it defaults to `propose` (so the pure table reads
|
|
478
|
+
* "unset ⇒ propose for both"), but the CLI passes the per-repo/global
|
|
479
|
+
* config-resolved mode so the established precedence chain (flag > per-repo >
|
|
480
|
+
* global > default) is preserved, exactly as `do`/`complete` resolve it.
|
|
481
|
+
*/
|
|
482
|
+
export function resolveIntakeIntegrationModes(
|
|
483
|
+
flags: IntakeIntegrationFlags,
|
|
484
|
+
defaultMode: IntegrationMode = DEFAULT_INTEGRATION,
|
|
485
|
+
): IntakeIntegrationModes {
|
|
486
|
+
// AGGREGATE axis — reuse the existing resolver (its mutual exclusion + the
|
|
487
|
+
// "--merge and --propose are mutually exclusive" message). `undefined` ⇒ unset.
|
|
488
|
+
const aggregate = integrationFromFlags({
|
|
489
|
+
merge: flags.merge,
|
|
490
|
+
propose: flags.propose,
|
|
491
|
+
});
|
|
492
|
+
// GRANULAR axes — `integrationFromFlags` per type (the same-type-both error).
|
|
493
|
+
const specGranular = granularFromFlags(
|
|
494
|
+
'spec',
|
|
495
|
+
flags.mergeSpec,
|
|
496
|
+
flags.proposeSpec,
|
|
497
|
+
);
|
|
498
|
+
const taskGranular = granularFromFlags(
|
|
499
|
+
'task',
|
|
500
|
+
flags.mergeTask,
|
|
501
|
+
flags.proposeTask,
|
|
502
|
+
);
|
|
503
|
+
// GRANULAR OVERRIDES AGGREGATE; aggregate over the (config/propose) default.
|
|
504
|
+
// The result KEYS carry the `spec` vocabulary (canonical); the per-type flag axes
|
|
505
|
+
// (`--merge-spec`/`--merge-task`) carry the same `spec` spelling (the cli-flag
|
|
506
|
+
// rename landed in batch 4f).
|
|
507
|
+
return {
|
|
508
|
+
spec: specGranular ?? aggregate ?? defaultMode,
|
|
509
|
+
task: taskGranular ?? aggregate ?? defaultMode,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Run `intake <N>` end-to-end (the LOCAL one-shot). Never throws for the expected
|
|
515
|
+
* agent-failed / stale / usage cases — those are returned with the corresponding
|
|
516
|
+
* exit code and outcome. The runner owns all git/seam side-effects; the agent only
|
|
517
|
+
* DRAFTS the verdict.
|
|
518
|
+
*/
|
|
519
|
+
export async function performIntake(
|
|
520
|
+
options: PerformIntakeOptions,
|
|
521
|
+
): Promise<IntakeResult> {
|
|
522
|
+
const note = options.note ?? (() => {});
|
|
523
|
+
const arbiter = options.arbiter ?? DEFAULT_ARBITER;
|
|
524
|
+
const cwd = options.cwd;
|
|
525
|
+
// `env` is intake's GIT + provider env, scoped to the configured identity (the
|
|
526
|
+
// `gh` issue ops, the push, the PR). The intake AGENT launches (decision agent
|
|
527
|
+
// + lone-task review agent) read `options.env` directly — they stay AMBIENT
|
|
528
|
+
// (an agent must not act as the bot). Absent identity ⇒ `options.env` unchanged.
|
|
529
|
+
// A configured identity that cannot be resolved (e.g. `tokenEnv` names an unset
|
|
530
|
+
// env var) is a clean usage error, never a crash or a silent ambient fallback.
|
|
531
|
+
const issueNumber = options.issueNumber;
|
|
532
|
+
let env: NodeJS.ProcessEnv;
|
|
533
|
+
try {
|
|
534
|
+
env = identityEnv(options.identity, options.env ?? process.env);
|
|
535
|
+
} catch (err) {
|
|
536
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
537
|
+
note(message);
|
|
538
|
+
return {exitCode: 1, outcome: 'usage-error', issueNumber, message};
|
|
539
|
+
}
|
|
540
|
+
const issueProvider = options.issueProvider ?? new GitHubIssueProvider();
|
|
541
|
+
|
|
542
|
+
// Push-time transport-coherence guard (identity): if a configured identity
|
|
543
|
+
// forbids the arbiter's transport, fail with a clear message rather than
|
|
544
|
+
// silently pushing under an ambient credential. Resolve the arbiter URL softly
|
|
545
|
+
// (a non-zero/unknown URL is skipped — the guard is a no-op without an identity
|
|
546
|
+
// or a resolvable URL).
|
|
547
|
+
if (options.identity !== undefined) {
|
|
548
|
+
const urlRes = await runAsync('git', ['remote', 'get-url', arbiter], cwd, {
|
|
549
|
+
env,
|
|
550
|
+
});
|
|
551
|
+
if (urlRes.status === 0) {
|
|
552
|
+
try {
|
|
553
|
+
assertTransportAllowed(options.identity, urlRes.stdout.trim());
|
|
554
|
+
} catch (err) {
|
|
555
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
556
|
+
note(message);
|
|
557
|
+
return {exitCode: 1, outcome: 'usage-error', issueNumber, message};
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// 1. READ the issue + thread via the seam (the core never imports `gh`; only the
|
|
563
|
+
// adapter shells out). A read failure surfaces as a usage error — `intake`
|
|
564
|
+
// cannot decide without the issue.
|
|
565
|
+
let issue: Issue;
|
|
566
|
+
let comments: IssueComment[];
|
|
567
|
+
try {
|
|
568
|
+
issue = await issueProvider.getIssue({cwd, issueNumber, env});
|
|
569
|
+
comments = await issueProvider.listComments({cwd, issueNumber, env});
|
|
570
|
+
} catch (err) {
|
|
571
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
572
|
+
const message = `Could not read issue #${issueNumber}: ${detail}`;
|
|
573
|
+
note(message);
|
|
574
|
+
return {exitCode: 1, outcome: 'usage-error', issueNumber, message};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// 2. ACQUIRE the `processing` LOCK (prd `issue-intake` US #10): a TRANSIENT concurrency mutex
|
|
578
|
+
// that serialises two concurrent runs on the SAME issue. Read the labels; if
|
|
579
|
+
// the lock is ALREADY present, BACK OFF (do nothing — another run owns it). The
|
|
580
|
+
// winner ADDS the label and proceeds; the label is REMOVED on finish (success
|
|
581
|
+
// OR handled failure, in the `finally` below). It is NOT a `work/` CAS and NOT a
|
|
582
|
+
// label state-machine (ADR §12) — ONE transient lock label.
|
|
583
|
+
//
|
|
584
|
+
// Fail-vs-degrade (maintainer decision): a lock that is MEANINGFUL but cannot
|
|
585
|
+
// be taken must NOT silently proceed lock-less. Only a genuinely-UNSUPPORTED
|
|
586
|
+
// provider (no label concept at all) legitimately degrades to best-effort (the
|
|
587
|
+
// spec's provider-pluggability; CI's per-issue concurrency group is then the
|
|
588
|
+
// only serialiser — out of scope here). A real FAILURE on a label-supporting
|
|
589
|
+
// provider (e.g. `gh` unauthenticated) FAILS the run with the REAL cause
|
|
590
|
+
// surfaced, rather than misattributing it or proceeding without serialisation.
|
|
591
|
+
const labels = await issueProvider.getLabels({cwd, issueNumber, env});
|
|
592
|
+
if (labels.outcome === 'failed') {
|
|
593
|
+
// The provider HAS labels but we could not READ the lock state — we cannot tell
|
|
594
|
+
// whether another run holds it, so guessing "free" could let two runs proceed.
|
|
595
|
+
// FAIL with the real cause (the actual `gh` stderr), not a hard-coded guess.
|
|
596
|
+
const message =
|
|
597
|
+
`Intake of issue #${issueNumber} could not acquire the ` +
|
|
598
|
+
`\`${PROCESSING_LOCK_LABEL}\` lock: ${labels.instruction}`;
|
|
599
|
+
note(message);
|
|
600
|
+
return {exitCode: 1, outcome: 'lock-failed', issueNumber, message};
|
|
601
|
+
}
|
|
602
|
+
if (
|
|
603
|
+
labels.outcome === 'ok' &&
|
|
604
|
+
labels.labels.includes(PROCESSING_LOCK_LABEL)
|
|
605
|
+
) {
|
|
606
|
+
const message =
|
|
607
|
+
`Intake of issue #${issueNumber} backed off: the \`${PROCESSING_LOCK_LABEL}\` ` +
|
|
608
|
+
`lock is already held by a concurrent run; doing nothing.`;
|
|
609
|
+
note(message);
|
|
610
|
+
return {exitCode: 0, outcome: 'locked', issueNumber, message};
|
|
611
|
+
}
|
|
612
|
+
let locked = false;
|
|
613
|
+
if (labels.outcome === 'ok') {
|
|
614
|
+
const acquired = await issueProvider.addLabel({
|
|
615
|
+
cwd,
|
|
616
|
+
issueNumber,
|
|
617
|
+
label: PROCESSING_LOCK_LABEL,
|
|
618
|
+
env,
|
|
619
|
+
});
|
|
620
|
+
if (acquired.outcome === 'failed') {
|
|
621
|
+
// The provider HAS labels but the ACQUIRE failed for a real reason (e.g. `gh`
|
|
622
|
+
// lost auth, or the label could not be created on a fresh repo). The lock is
|
|
623
|
+
// meaningful but unacquirable → FAIL with the real cause, do NOT proceed
|
|
624
|
+
// lock-less (which would let a concurrent run race us).
|
|
625
|
+
const message =
|
|
626
|
+
`Intake of issue #${issueNumber} could not acquire the ` +
|
|
627
|
+
`\`${PROCESSING_LOCK_LABEL}\` lock: ${acquired.instruction}`;
|
|
628
|
+
note(message);
|
|
629
|
+
return {exitCode: 1, outcome: 'lock-failed', issueNumber, message};
|
|
630
|
+
}
|
|
631
|
+
locked = acquired.applied;
|
|
632
|
+
} else {
|
|
633
|
+
// Non-label provider (genuinely UNSUPPORTED) → the ONLY legitimate degrade:
|
|
634
|
+
// proceed without the lock, surfaced honestly.
|
|
635
|
+
note(`Processing lock degraded: ${labels.instruction}`);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// INTERRUPTION-SAFETY (maintainer point 3): the `finally` below releases on every
|
|
639
|
+
// EXCEPTION path, but a SIGINT/SIGTERM (Ctrl-C, kill) unwinds the process WITHOUT
|
|
640
|
+
// running `finally` — which would LEAK the lock label and block all future intake
|
|
641
|
+
// runs on this issue. While the lock is held we install signal handlers that
|
|
642
|
+
// release it best-effort before the process exits. A leaked lock must ALSO be
|
|
643
|
+
// recoverable by hand and that recovery must be DISCOVERABLE, so we surface the
|
|
644
|
+
// exact manual command (`gh issue edit <N> --remove-label <label>`) whenever the
|
|
645
|
+
// best-effort release does not confirm.
|
|
646
|
+
const manualRecovery =
|
|
647
|
+
`If the \`${PROCESSING_LOCK_LABEL}\` lock is left behind, release it with: ` +
|
|
648
|
+
`gh issue edit ${issueNumber} --remove-label '${PROCESSING_LOCK_LABEL}'`;
|
|
649
|
+
const releaseLock = createLockReleaser({
|
|
650
|
+
locked,
|
|
651
|
+
issueProvider,
|
|
652
|
+
cwd,
|
|
653
|
+
issueNumber,
|
|
654
|
+
env,
|
|
655
|
+
note,
|
|
656
|
+
manualRecovery,
|
|
657
|
+
});
|
|
658
|
+
const onSignal = (signal: NodeJS.Signals) => {
|
|
659
|
+
// Synchronous best-effort release on interruption, then re-raise the default
|
|
660
|
+
// disposition so the process still exits with the conventional signal code.
|
|
661
|
+
if (locked) {
|
|
662
|
+
note(
|
|
663
|
+
`Received ${signal}; releasing the \`${PROCESSING_LOCK_LABEL}\` lock on issue #${issueNumber} before exit.`,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
releaseLock.releaseSync();
|
|
667
|
+
process.removeListener('SIGINT', onSignal);
|
|
668
|
+
process.removeListener('SIGTERM', onSignal);
|
|
669
|
+
process.kill(process.pid, signal);
|
|
670
|
+
};
|
|
671
|
+
if (locked) {
|
|
672
|
+
process.once('SIGINT', onSignal);
|
|
673
|
+
process.once('SIGTERM', onSignal);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
try {
|
|
677
|
+
return await decideAndDispatch(options, cwd, issue, comments, {
|
|
678
|
+
arbiter,
|
|
679
|
+
issueProvider,
|
|
680
|
+
note,
|
|
681
|
+
// The identity-scoped GIT/provider env (the `gh` ops, push, PR). The AGENT
|
|
682
|
+
// launches inside dispatch read `options.env` (ambient) — not this.
|
|
683
|
+
gitEnv: env,
|
|
684
|
+
});
|
|
685
|
+
} finally {
|
|
686
|
+
// RELEASE the lock on FINISH (success OR handled failure). Only the winner that
|
|
687
|
+
// actually acquired it releases it — a degraded/best-effort run holds nothing.
|
|
688
|
+
process.removeListener('SIGINT', onSignal);
|
|
689
|
+
process.removeListener('SIGTERM', onSignal);
|
|
690
|
+
await releaseLock.release();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Build the lock RELEASER for {@link performIntake}: one `release()` (the normal
|
|
696
|
+
* async finish path) and one `releaseSync()` (the signal-handler path — a
|
|
697
|
+
* best-effort synchronous release that must run inside a signal handler). Both are
|
|
698
|
+
* no-ops when the run never held the lock (a degraded/unsupported run holds
|
|
699
|
+
* nothing). When a release does not CONFIRM, the manual-recovery hint is surfaced
|
|
700
|
+
* so a leaked lock stays recoverable AND discoverable (maintainer point 3).
|
|
701
|
+
*/
|
|
702
|
+
function createLockReleaser(params: {
|
|
703
|
+
locked: boolean;
|
|
704
|
+
issueProvider: IssueProvider;
|
|
705
|
+
cwd: string;
|
|
706
|
+
issueNumber: number;
|
|
707
|
+
env: NodeJS.ProcessEnv | undefined;
|
|
708
|
+
note: (message: string) => void;
|
|
709
|
+
manualRecovery: string;
|
|
710
|
+
}): {release: () => Promise<void>; releaseSync: () => void} {
|
|
711
|
+
const {locked, issueProvider, cwd, issueNumber, env, note, manualRecovery} =
|
|
712
|
+
params;
|
|
713
|
+
let released = false;
|
|
714
|
+
const surfaceFailure = (instruction: string) => {
|
|
715
|
+
note(`Processing lock release degraded: ${instruction}`);
|
|
716
|
+
note(manualRecovery);
|
|
717
|
+
};
|
|
718
|
+
return {
|
|
719
|
+
async release() {
|
|
720
|
+
if (!locked || released) {
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
released = true;
|
|
724
|
+
const result = await issueProvider.removeLabel({
|
|
725
|
+
cwd,
|
|
726
|
+
issueNumber,
|
|
727
|
+
label: PROCESSING_LOCK_LABEL,
|
|
728
|
+
env,
|
|
729
|
+
});
|
|
730
|
+
if (!result.applied) {
|
|
731
|
+
surfaceFailure(result.instruction);
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
releaseSync() {
|
|
735
|
+
if (!locked || released) {
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
released = true;
|
|
739
|
+
// A signal handler cannot await. The GitHub adapter's `removeLabel` shells out
|
|
740
|
+
// SYNCHRONOUSLY (spawnSync) inside its async wrapper, so firing it here still
|
|
741
|
+
// runs the `gh` call before the process exits — but we cannot READ the result
|
|
742
|
+
// synchronously through the async seam, so we ALWAYS surface the manual-recovery
|
|
743
|
+
// hint too. That keeps a leaked lock both recoverable AND discoverable even if
|
|
744
|
+
// the in-handler release did not complete (maintainer point 3).
|
|
745
|
+
void issueProvider.removeLabel({
|
|
746
|
+
cwd,
|
|
747
|
+
issueNumber,
|
|
748
|
+
label: PROCESSING_LOCK_LABEL,
|
|
749
|
+
env,
|
|
750
|
+
});
|
|
751
|
+
note(manualRecovery);
|
|
752
|
+
},
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* The DECIDE (prompt → verdict) + DISPATCH (the four-outcome table) band, run
|
|
758
|
+
* INSIDE the `processing` lock {@link performIntake} acquires/releases around it.
|
|
759
|
+
* Split out so the lock release is a clean `try`/`finally` in the caller (the lock
|
|
760
|
+
* MUST release on every terminal path — success or handled failure). The agent
|
|
761
|
+
* DRAFTS only; the runner owns every git/seam side-effect here.
|
|
762
|
+
*/
|
|
763
|
+
async function decideAndDispatch(
|
|
764
|
+
options: PerformIntakeOptions,
|
|
765
|
+
cwd: string,
|
|
766
|
+
issue: Issue,
|
|
767
|
+
comments: IssueComment[],
|
|
768
|
+
ctx: {
|
|
769
|
+
arbiter: string;
|
|
770
|
+
issueProvider: IssueProvider;
|
|
771
|
+
note: (message: string) => void;
|
|
772
|
+
/** The identity-scoped GIT/provider env (the `gh` ops, push, PR). */
|
|
773
|
+
gitEnv: NodeJS.ProcessEnv | undefined;
|
|
774
|
+
},
|
|
775
|
+
): Promise<IntakeResult> {
|
|
776
|
+
const {arbiter, issueProvider, note} = ctx;
|
|
777
|
+
const issueNumber = issue.number;
|
|
778
|
+
// `env` here is the identity-scoped GIT/provider env (the runner's `gh`/git
|
|
779
|
+
// ops). The AGENT launches (decision agent, lone-task review) use the AMBIENT
|
|
780
|
+
// `options.env` — an agent must not act as the bot.
|
|
781
|
+
const env = ctx.gitEnv;
|
|
782
|
+
|
|
783
|
+
// TRIAGE (deterministic, under the lock, BEFORE the prompt): decide whether to run
|
|
784
|
+
// the decision at all, built ENTIRELY on intake's own MARKER on the thread (no
|
|
785
|
+
// sidecar/cursor/bot-identity). It SKIPS when intake has the last word
|
|
786
|
+
// (`no-new-input`) or the issue is already terminal (`already-terminal`), and runs
|
|
787
|
+
// the prompt ONLY on genuine new human input. This is also the COMPLETE fix for the
|
|
788
|
+
// self-trigger hazard: intake's own freshly-posted comment carries a marker, so it
|
|
789
|
+
// is excluded from the human-comment check by construction.
|
|
790
|
+
const triage = triageIntake(comments);
|
|
791
|
+
if (triage.action === 'skip') {
|
|
792
|
+
const message =
|
|
793
|
+
triage.outcome === 'no-new-input'
|
|
794
|
+
? `Intake of issue #${issueNumber} found nothing new: it has the last word ` +
|
|
795
|
+
`on the thread and has already seen every human comment up to it; doing ` +
|
|
796
|
+
`nothing (the decision prompt did not run).`
|
|
797
|
+
: `Intake of issue #${issueNumber} skipped: the issue was already ` +
|
|
798
|
+
`transformed (a terminal intake marker is on the thread); a later human ` +
|
|
799
|
+
`comment does not re-open it (the decision prompt did not run).`;
|
|
800
|
+
note(message);
|
|
801
|
+
return {exitCode: 0, outcome: triage.outcome, issueNumber, message};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// DECIDE: prompt → VERDICT. The agent DRAFTS only (no git, no seam ops). Tests
|
|
805
|
+
// inject a canned verdict (the dispatcher's testable seam); production wires the
|
|
806
|
+
// harness. The prompt's judgement is not unit-tested — only the dispatch.
|
|
807
|
+
const prompt = buildIntakeDecisionSpec(issue, comments, triage);
|
|
808
|
+
let verdict: IntakeVerdict;
|
|
809
|
+
try {
|
|
810
|
+
verdict = await runDecision(options, cwd, issue, comments, prompt);
|
|
811
|
+
} catch (err) {
|
|
812
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
813
|
+
const message = `Intake decision failed for issue #${issueNumber}: ${detail}`;
|
|
814
|
+
note(message);
|
|
815
|
+
return {exitCode: 1, outcome: 'agent-failed', issueNumber, message};
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// DISPATCH on the verdict — the FULL four-outcome decision table (prd
|
|
819
|
+
// `issue-intake`). The agent only DRAFTED the verdict; the runner owns every
|
|
820
|
+
// git/seam side-effect below (the in-band boundary): the write + integrate
|
|
821
|
+
// (task/prd) and the `postIssueComment` (ask/bounce).
|
|
822
|
+
//
|
|
823
|
+
// PER-OUTCOME integration (prd `issue-intake` US #9): the resolved mode is keyed on the runtime
|
|
824
|
+
// artifact TYPE — a `task` verdict integrates with the task mode, a `spec`
|
|
825
|
+
// verdict with the spec mode. Unset ⇒ propose for both. ask/bounce never
|
|
826
|
+
// integrate, so the modes are no-ops for them.
|
|
827
|
+
const modes = options.integration ?? {task: 'propose', spec: 'propose'};
|
|
828
|
+
// The per-run `seen=` DELTA (the HUMAN comment ids intake READ this run, excluding
|
|
829
|
+
// its own marker-comments + already-seen ids) the marker records on every comment
|
|
830
|
+
// intake posts — the chain-model primitive the TRIAGE unions into `seenSet`.
|
|
831
|
+
const seenDelta = computeSeenDelta(comments);
|
|
832
|
+
switch (verdict.outcome) {
|
|
833
|
+
case 'task':
|
|
834
|
+
return dispatchTask({
|
|
835
|
+
verdict,
|
|
836
|
+
issueNumber,
|
|
837
|
+
cwd,
|
|
838
|
+
arbiter,
|
|
839
|
+
integration: modes.task,
|
|
840
|
+
// The origin-trust STAMP, passed IN (not resolved here): the emitted task
|
|
841
|
+
// carries `origin: issue` + this verdict so the becomes-code checkpoint is
|
|
842
|
+
// not laundered. Unset ⇒ unstamped (a local intake ⇒ human/trusted).
|
|
843
|
+
originTrust: options.originTrust,
|
|
844
|
+
noPR: options.noPR,
|
|
845
|
+
providerInstance: options.providerInstance,
|
|
846
|
+
issueProvider,
|
|
847
|
+
// The bounded lone-task review seam (tests inject a canned verdict;
|
|
848
|
+
// production wires the harness via the default below).
|
|
849
|
+
reviewTask: resolveLoneTaskReviewGate(options),
|
|
850
|
+
seen: seenDelta,
|
|
851
|
+
env,
|
|
852
|
+
// The lone-task review AGENT launches AMBIENT (an agent must not act as
|
|
853
|
+
// the bot); `env` above is the identity-scoped git/provider env.
|
|
854
|
+
agentEnv: options.env,
|
|
855
|
+
note,
|
|
856
|
+
});
|
|
857
|
+
// The `spec` outcome is the parent-spec verdict; it dispatches through
|
|
858
|
+
// `modes.spec`. HARD CUTOVER: the legacy `prd` outcome case is GONE.
|
|
859
|
+
case 'spec':
|
|
860
|
+
return dispatchSpec({
|
|
861
|
+
verdict,
|
|
862
|
+
issueNumber,
|
|
863
|
+
cwd,
|
|
864
|
+
arbiter,
|
|
865
|
+
integration: modes.spec,
|
|
866
|
+
// Same origin-trust stamp on the prd outcome (propagated onto its tasks
|
|
867
|
+
// later by the tasker). Passed IN; not resolved here.
|
|
868
|
+
originTrust: options.originTrust,
|
|
869
|
+
noPR: options.noPR,
|
|
870
|
+
// RUNNER-DETERMINISTIC PLACEMENT (task
|
|
871
|
+
// `pre-prd-staging-pool-split-and-untrusted-prd-placement`): the
|
|
872
|
+
// configured-default + explicit-flag rungs, fed into the SHARED placement
|
|
873
|
+
// resolver alongside the `originTrust` stamp above. The resolver decides
|
|
874
|
+
// `prds/proposed/` (staging) vs `prds/ready/` (the tasking pool); `intake` never
|
|
875
|
+
// places itself.
|
|
876
|
+
specsLandIn: options.specsLandIn,
|
|
877
|
+
explicitSpecsLandIn: options.explicitSpecsLandIn,
|
|
878
|
+
providerInstance: options.providerInstance,
|
|
879
|
+
issueProvider,
|
|
880
|
+
seen: seenDelta,
|
|
881
|
+
env,
|
|
882
|
+
note,
|
|
883
|
+
});
|
|
884
|
+
case 'ask':
|
|
885
|
+
return dispatchComment({
|
|
886
|
+
outcome: 'asked',
|
|
887
|
+
cwd,
|
|
888
|
+
issueNumber,
|
|
889
|
+
issueProvider,
|
|
890
|
+
// The drafted clarifying question; a thin fallback keeps the comment
|
|
891
|
+
// non-empty if the agent left it blank.
|
|
892
|
+
body:
|
|
893
|
+
verdict.question && verdict.question.trim() !== ''
|
|
894
|
+
? verdict.question
|
|
895
|
+
: `Could you clarify issue #${issueNumber} so it can be acted on?`,
|
|
896
|
+
// STAMP the MARKER recording `kind=ask` (non-terminal — the TRIAGE owns
|
|
897
|
+
// that) + the `seen=` delta, so a re-run recognises this as intake's own
|
|
898
|
+
// turn and resumes only on genuine new human input.
|
|
899
|
+
markerKind: 'ask',
|
|
900
|
+
seen: seenDelta,
|
|
901
|
+
env,
|
|
902
|
+
note,
|
|
903
|
+
});
|
|
904
|
+
case 'bounce':
|
|
905
|
+
return dispatchComment({
|
|
906
|
+
outcome: 'bounced',
|
|
907
|
+
cwd,
|
|
908
|
+
issueNumber,
|
|
909
|
+
issueProvider,
|
|
910
|
+
// The drafted bounce message; a thin fallback restates the "file separate
|
|
911
|
+
// issues" ask. A bounce is TERMINAL: the issue is CLOSED atomically (this
|
|
912
|
+
// text as the closing comment + reason not planned).
|
|
913
|
+
body:
|
|
914
|
+
verdict.bounceMessage && verdict.bounceMessage.trim() !== ''
|
|
915
|
+
? verdict.bounceMessage
|
|
916
|
+
: `This issue looks like multiple unrelated concerns — please file ` +
|
|
917
|
+
`separate issues so each can be intaken on its own.`,
|
|
918
|
+
// STAMP `kind=bounced` (TERMINAL — the TRIAGE then SKIPS `already-terminal`
|
|
919
|
+
// on a later human comment) + the `seen=` delta.
|
|
920
|
+
markerKind: 'bounced',
|
|
921
|
+
seen: seenDelta,
|
|
922
|
+
env,
|
|
923
|
+
note,
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* DISPATCH the `ask` / `bounce` outcomes — the SHARED comment band, which now
|
|
930
|
+
* BRANCHES on the outcome:
|
|
931
|
+
*
|
|
932
|
+
* - **ask** (non-terminal): `postIssueComment` the drafted question, emit NOTHING,
|
|
933
|
+
* and LEAVE THE ISSUE OPEN — it waits for the thread to be answered (a later run
|
|
934
|
+
* resumes from it). The task/prd path also never closes (CI's close-job does,
|
|
935
|
+
* via the `issue:` field). Intake closes ONLY on BOUNCE.
|
|
936
|
+
* - **bounce** (TERMINAL): the asks are unrelated and must be re-filed, so an OPEN
|
|
937
|
+
* issue is a dishonest "still in play" signal. Intake CLOSES the issue
|
|
938
|
+
* ATOMICALLY via a single `closeIssue` carrying the bounce text as the closing
|
|
939
|
+
* comment + `reason: not planned` (one call — no post-then-close partial-failure
|
|
940
|
+
* window). The result's `closed` reflects it.
|
|
941
|
+
*
|
|
942
|
+
* Both the comment poster and the atomic close are advisory and DEGRADE (a
|
|
943
|
+
* missing/unauthenticated `gh` never throws — the text/real cause is surfaced via
|
|
944
|
+
* `ghFailureReason`, never a hard-coded guess), so the terminal outcome is
|
|
945
|
+
* unchanged (`asked`/`bounced`, exit 0) and the run still terminates cleanly.
|
|
946
|
+
*/
|
|
947
|
+
async function dispatchComment(params: {
|
|
948
|
+
outcome: 'asked' | 'bounced';
|
|
949
|
+
cwd: string;
|
|
950
|
+
issueNumber: number;
|
|
951
|
+
issueProvider: IssueProvider;
|
|
952
|
+
body: string;
|
|
953
|
+
/** The neutral `kind` the MARKER records (`ask` for an ask, `bounced` for a bounce). */
|
|
954
|
+
markerKind: IntakeMarkerKind;
|
|
955
|
+
/** The per-run `seen=` delta of HUMAN comment ids intake read this run. */
|
|
956
|
+
seen: string[];
|
|
957
|
+
env: NodeJS.ProcessEnv | undefined;
|
|
958
|
+
note: (message: string) => void;
|
|
959
|
+
}): Promise<IntakeResult> {
|
|
960
|
+
const {
|
|
961
|
+
outcome,
|
|
962
|
+
cwd,
|
|
963
|
+
issueNumber,
|
|
964
|
+
issueProvider,
|
|
965
|
+
body,
|
|
966
|
+
markerKind,
|
|
967
|
+
seen,
|
|
968
|
+
env,
|
|
969
|
+
note,
|
|
970
|
+
} = params;
|
|
971
|
+
// STAMP the intake MARKER onto the body so a re-run recognises this as intake's
|
|
972
|
+
// own comment (the SOLE self-recognition signal — no author identity). Hidden HTML
|
|
973
|
+
// comment; renders as nothing, present in the raw markdown the TRIAGE parses.
|
|
974
|
+
const stamped = stampIntakeMarker(body, {kind: markerKind, seen});
|
|
975
|
+
|
|
976
|
+
if (outcome === 'bounced') {
|
|
977
|
+
// BOUNCE is TERMINAL: CLOSE the issue ATOMICALLY (bounce text as the closing
|
|
978
|
+
// comment + reason not planned) in ONE call — no separate postIssueComment, no
|
|
979
|
+
// post-then-close window. The close DEGRADES (never throws) on a missing/
|
|
980
|
+
// unauthenticated `gh`, surfacing the REAL cause; the terminal outcome stays
|
|
981
|
+
// `bounced`/exit 0 regardless.
|
|
982
|
+
const close = await issueProvider.closeIssue({
|
|
983
|
+
cwd,
|
|
984
|
+
issueNumber,
|
|
985
|
+
comment: stamped,
|
|
986
|
+
reason: 'not planned',
|
|
987
|
+
env,
|
|
988
|
+
});
|
|
989
|
+
const tail = close.closed
|
|
990
|
+
? 'the issue was closed (as not planned) with the bounce comment'
|
|
991
|
+
: `the issue could NOT be closed (${close.instruction})`;
|
|
992
|
+
const message =
|
|
993
|
+
`Intake bounced issue #${issueNumber}; emitted no artifact and closed the ` +
|
|
994
|
+
`issue as not planned — ${tail}.`;
|
|
995
|
+
note(message);
|
|
996
|
+
return {
|
|
997
|
+
exitCode: 0,
|
|
998
|
+
outcome,
|
|
999
|
+
issueNumber,
|
|
1000
|
+
commented: close.closed,
|
|
1001
|
+
closed: close.closed,
|
|
1002
|
+
message,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// ASK (non-terminal): post the clarifying question and LEAVE THE ISSUE OPEN.
|
|
1007
|
+
const posted = await issueProvider.postIssueComment({
|
|
1008
|
+
cwd,
|
|
1009
|
+
issueNumber,
|
|
1010
|
+
body: stamped,
|
|
1011
|
+
env,
|
|
1012
|
+
});
|
|
1013
|
+
const tail = posted.posted
|
|
1014
|
+
? 'the comment was posted'
|
|
1015
|
+
: `the comment could NOT be posted (${posted.instruction})`;
|
|
1016
|
+
const message =
|
|
1017
|
+
`Intake asked a clarifying question on issue #${issueNumber}; emitted no ` +
|
|
1018
|
+
`artifact and left the issue open — ${tail}.`;
|
|
1019
|
+
note(message);
|
|
1020
|
+
return {
|
|
1021
|
+
exitCode: 0,
|
|
1022
|
+
outcome,
|
|
1023
|
+
issueNumber,
|
|
1024
|
+
commented: posted.posted,
|
|
1025
|
+
message,
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* DISPATCH the `task` outcome: derive a content-derived slug, write
|
|
1031
|
+
* `work/backlog/<slug>.md` (`covers: []`, NO `prd:`) carrying `issue: N` (the
|
|
1032
|
+
* lone-task closure link, NOT `Fixes #N`), and integrate via {@link
|
|
1033
|
+
* performIntegration}. The runner owns the git: it onboards a
|
|
1034
|
+
* `work/<slug>` branch off fresh `<arbiter>/main`, then the lifecycle `stage`
|
|
1035
|
+
* writes + stages the task and the band commits + rebases + integrates it. The
|
|
1036
|
+
* agent did NO git/seam ops.
|
|
1037
|
+
*/
|
|
1038
|
+
async function dispatchTask(params: {
|
|
1039
|
+
verdict: IntakeVerdict;
|
|
1040
|
+
issueNumber: number;
|
|
1041
|
+
cwd: string;
|
|
1042
|
+
arbiter: string;
|
|
1043
|
+
integration: IntegrationMode;
|
|
1044
|
+
/** The origin-trust stamp passed IN (unset ⇒ emit unstamped ⇒ human/trusted). */
|
|
1045
|
+
originTrust: OriginTrust | undefined;
|
|
1046
|
+
noPR: boolean | undefined;
|
|
1047
|
+
providerInstance: ReviewProvider | undefined;
|
|
1048
|
+
/** The issue seam the completion comment is posted back through (runner-owned). */
|
|
1049
|
+
issueProvider: IssueProvider;
|
|
1050
|
+
/** The bounded lone-task review seam (tests inject a canned verdict; prod: harness). */
|
|
1051
|
+
reviewTask: LoneTaskReviewGate;
|
|
1052
|
+
/** The per-run `seen=` delta of HUMAN comment ids the completion marker records. */
|
|
1053
|
+
seen: string[];
|
|
1054
|
+
/** The identity-scoped GIT/provider env (push, PR, completion comment). */
|
|
1055
|
+
env: NodeJS.ProcessEnv | undefined;
|
|
1056
|
+
/** The AMBIENT env for the lone-task review AGENT launch (never the identity). */
|
|
1057
|
+
agentEnv: NodeJS.ProcessEnv | undefined;
|
|
1058
|
+
note: (message: string) => void;
|
|
1059
|
+
}): Promise<IntakeResult> {
|
|
1060
|
+
const {
|
|
1061
|
+
verdict,
|
|
1062
|
+
issueNumber,
|
|
1063
|
+
cwd,
|
|
1064
|
+
arbiter,
|
|
1065
|
+
integration,
|
|
1066
|
+
originTrust,
|
|
1067
|
+
noPR,
|
|
1068
|
+
providerInstance,
|
|
1069
|
+
issueProvider,
|
|
1070
|
+
reviewTask,
|
|
1071
|
+
seen,
|
|
1072
|
+
env,
|
|
1073
|
+
agentEnv,
|
|
1074
|
+
note,
|
|
1075
|
+
} = params;
|
|
1076
|
+
|
|
1077
|
+
// A content-derived slug — NEVER a counter (prd `issue-intake` US #8). Prefer the drafted
|
|
1078
|
+
// `taskSlug`, else derive from the drafted title; sanitise either through
|
|
1079
|
+
// `paramCase` so the filename + frontmatter slug are well-formed.
|
|
1080
|
+
const slug = resolveSlug(verdict);
|
|
1081
|
+
if (slug === '') {
|
|
1082
|
+
const message =
|
|
1083
|
+
`Intake produced a 'task' verdict for issue #${issueNumber} with no usable ` +
|
|
1084
|
+
`slug/title to derive a content-derived slug from (never a counter).`;
|
|
1085
|
+
note(message);
|
|
1086
|
+
return {exitCode: 1, outcome: 'usage-error', issueNumber, message};
|
|
1087
|
+
}
|
|
1088
|
+
const relPath = workItemRel('tasks-ready', `${slug}.md`);
|
|
1089
|
+
|
|
1090
|
+
// BOUNDED INTERNAL REVIEW (observation
|
|
1091
|
+
// `intake-lone-task-skips-adversarial-review-the-prd-path-gets`, rulings A/B/C):
|
|
1092
|
+
// the `do prd:` path gets `runTaskReviewLoop`; the lone-TASK path got NOTHING.
|
|
1093
|
+
// AFTER the `task` verdict and BEFORE the write/integrate, run a bounded (3-round,
|
|
1094
|
+
// HARD-CAPPED) adversarial self-review on the SINGLE drafted task. It mutates the
|
|
1095
|
+
// candidate body IN MEMORY (no `work/backlog/` write pre-convergence). A launch/
|
|
1096
|
+
// parse failure THROWS — `decideAndDispatch`'s try/catch maps it onto `agent-failed`
|
|
1097
|
+
// (never a silent emit of the un-reviewed task).
|
|
1098
|
+
let review: LoneTaskReviewResult;
|
|
1099
|
+
try {
|
|
1100
|
+
review = await runLoneTaskReview({
|
|
1101
|
+
slug,
|
|
1102
|
+
issueNumber,
|
|
1103
|
+
draftTitle: verdict.taskTitle ?? slug,
|
|
1104
|
+
draftBody: verdict.taskBody,
|
|
1105
|
+
gate: reviewTask,
|
|
1106
|
+
cwd,
|
|
1107
|
+
// The review AGENT launches AMBIENT (never the identity-scoped env).
|
|
1108
|
+
env: agentEnv,
|
|
1109
|
+
note,
|
|
1110
|
+
});
|
|
1111
|
+
} catch (err) {
|
|
1112
|
+
// A review-agent launch/parse FAILURE DEGRADES honestly onto the EXISTING
|
|
1113
|
+
// `agent-failed` outcome (exit 1) — NEVER a silent emit of the un-reviewed
|
|
1114
|
+
// task. The SAME try/catch discipline the decision step uses.
|
|
1115
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1116
|
+
const message = `Intake lone-task review failed for issue #${issueNumber}: ${detail}`;
|
|
1117
|
+
note(message);
|
|
1118
|
+
return {exitCode: 1, outcome: 'agent-failed', issueNumber, message};
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
if (review.outcome === 'non-converge') {
|
|
1122
|
+
// NON-CONVERGE (ruling C): FLIP the verdict TASK→ASK, reusing the EXISTING
|
|
1123
|
+
// `asked` outcome + `kind=ask` marker. The ASK comment carries BOTH the proposed
|
|
1124
|
+
// task DRAFT and the open question(s) in its BODY (NOT a new marker kind) — the
|
|
1125
|
+
// human reacts to a concrete draft, strictly richer than a blank-question ask.
|
|
1126
|
+
// NEVER write `work/backlog/<slug>.md`; NEVER silently emit the under-refined
|
|
1127
|
+
// task. The next intake run resumes via the already-built triage gate.
|
|
1128
|
+
note(
|
|
1129
|
+
`Intake's lone-task review did not converge for issue #${issueNumber} ` +
|
|
1130
|
+
`(${review.passes} round(s)); flipping TASK→ASK with the draft + open ` +
|
|
1131
|
+
`question(s) in the comment body.`,
|
|
1132
|
+
);
|
|
1133
|
+
return dispatchComment({
|
|
1134
|
+
outcome: 'asked',
|
|
1135
|
+
cwd,
|
|
1136
|
+
issueNumber,
|
|
1137
|
+
issueProvider,
|
|
1138
|
+
body: composeLoneTaskAskComment({
|
|
1139
|
+
issueNumber,
|
|
1140
|
+
slug,
|
|
1141
|
+
draftTitle: review.title,
|
|
1142
|
+
draftBody: review.body,
|
|
1143
|
+
questions: review.questions,
|
|
1144
|
+
}),
|
|
1145
|
+
markerKind: 'ask',
|
|
1146
|
+
seen,
|
|
1147
|
+
env,
|
|
1148
|
+
note,
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// CONVERGED: the (possibly edited) task is emitted via the EXISTING write/integrate
|
|
1153
|
+
// path below + the existing `task created` completion comment. The refined body
|
|
1154
|
+
// replaces the agent's first draft.
|
|
1155
|
+
const reviewedBody = review.body;
|
|
1156
|
+
|
|
1157
|
+
// ONBOARD the task write onto a `work/intake-task-<slug>` branch cut from the
|
|
1158
|
+
// freshly-fetched `<arbiter>/main` (the SAME runner-owns-git discipline the
|
|
1159
|
+
// tasking path uses): the lifecycle `stage` writes the file ON THIS BRANCH and
|
|
1160
|
+
// the shared integrate core (`--propose` PR / `--merge` main) lands it. The
|
|
1161
|
+
// intake- producer prefix keeps it distinct from a later `do task:<slug>`
|
|
1162
|
+
// build branch for the same slug. The agent ran no git.
|
|
1163
|
+
await switchToWorkBranch(cwd, arbiter, 'task', slug, env);
|
|
1164
|
+
|
|
1165
|
+
const taskContent = renderBacklogTask({
|
|
1166
|
+
slug,
|
|
1167
|
+
title: review.title,
|
|
1168
|
+
body: reviewedBody,
|
|
1169
|
+
issueNumber,
|
|
1170
|
+
originTrust,
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
const core = await performIntegration({
|
|
1174
|
+
cwd,
|
|
1175
|
+
arbiter,
|
|
1176
|
+
slug,
|
|
1177
|
+
// `source`/`recovering` are task-shaped and IGNORED when `lifecycle` is set.
|
|
1178
|
+
source: 'in-progress',
|
|
1179
|
+
recovering: false,
|
|
1180
|
+
// An intake-emitted task has no `verify` floor of its own (it is a new
|
|
1181
|
+
// backlog item, not a build); skip the acceptance gate, exactly as the
|
|
1182
|
+
// tasking transition does.
|
|
1183
|
+
skipVerify: true,
|
|
1184
|
+
// Default `propose` (the per-outcome KNOBS are a later task). The
|
|
1185
|
+
// EXPLICITLY-chosen mode proceeds as-is: a future `--merge-task` lands on main
|
|
1186
|
+
// (`merge` IS the auto-land mode, never downgraded).
|
|
1187
|
+
mode: integration,
|
|
1188
|
+
noPR,
|
|
1189
|
+
providerInstance,
|
|
1190
|
+
type: 'feat',
|
|
1191
|
+
lifecycle: {
|
|
1192
|
+
// The emitted task IS the title source. Pass the DRAFTED title EXPLICITLY
|
|
1193
|
+
// (not a read-from-path): `stage()` WRITES `work/backlog/<slug>.md` AFTER the
|
|
1194
|
+
// core reads the title, so a `titlePath` read would race the write and degrade
|
|
1195
|
+
// the commit subject / PR title to the generic fallback. `titlePath` stays set
|
|
1196
|
+
// (the lifecycle contract requires it) but is IGNORED while `title` is present.
|
|
1197
|
+
titlePath: join(cwd, relPath),
|
|
1198
|
+
title: review.title,
|
|
1199
|
+
commitTag: 'intake',
|
|
1200
|
+
stage: () =>
|
|
1201
|
+
stageIntakeContent({cwd, relPath, content: taskContent, env}),
|
|
1202
|
+
},
|
|
1203
|
+
env,
|
|
1204
|
+
note,
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
return integrationToIntakeResult(core, {
|
|
1208
|
+
issueNumber,
|
|
1209
|
+
slug,
|
|
1210
|
+
relPath,
|
|
1211
|
+
cwd,
|
|
1212
|
+
issueProvider,
|
|
1213
|
+
seen,
|
|
1214
|
+
env,
|
|
1215
|
+
note,
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/**
|
|
1220
|
+
* DISPATCH the `spec` outcome (canonical; the legacy `prd` outcome routes here too
|
|
1221
|
+
* through the cutover): derive a content-derived slug, write the spec file
|
|
1222
|
+
* (`work/specs/ready/<slug>.md`) carrying `issue: N` (the loop-closure linkage the
|
|
1223
|
+
* close JOB reaches via `task.spec: → spec issue:`; on a fanned spec the number
|
|
1224
|
+
* lives ONLY on the spec — a fanned task uses `spec:`, NOT its own `issue:`, which
|
|
1225
|
+
* is the lone-task outcome's link) + the gate axes the prompt JUDGED, integrate it
|
|
1226
|
+
* via {@link performIntegration}, then STOP. Tasking the emitted spec is the
|
|
1227
|
+
* SEPARATE `do spec:` step (NOT done here). A coupled-but-SMALL pair lands here too
|
|
1228
|
+
* (the spec vs BOUNCE line is SHARED VISION, not size — the over-bounce guard). The
|
|
1229
|
+
* runner owns the git exactly as the task branch does; the agent did NO git/seam ops.
|
|
1230
|
+
*/
|
|
1231
|
+
async function dispatchSpec(params: {
|
|
1232
|
+
verdict: IntakeVerdict;
|
|
1233
|
+
issueNumber: number;
|
|
1234
|
+
cwd: string;
|
|
1235
|
+
arbiter: string;
|
|
1236
|
+
integration: IntegrationMode;
|
|
1237
|
+
/** The origin-trust stamp passed IN (unset ⇒ emit unstamped ⇒ human/trusted). */
|
|
1238
|
+
originTrust: OriginTrust | undefined;
|
|
1239
|
+
noPR: boolean | undefined;
|
|
1240
|
+
/** The per-repo SPEC-PLACEMENT default (configured-default rung of the placement chain). */
|
|
1241
|
+
specsLandIn: SpecsLandIn | undefined;
|
|
1242
|
+
/** The OPERATOR's EXPLICIT spec-placement override (the TOP rung). */
|
|
1243
|
+
explicitSpecsLandIn: SpecsLandIn | undefined;
|
|
1244
|
+
providerInstance: ReviewProvider | undefined;
|
|
1245
|
+
/** The issue seam the completion comment is posted back through (runner-owned). */
|
|
1246
|
+
issueProvider: IssueProvider;
|
|
1247
|
+
/** The per-run `seen=` delta of HUMAN comment ids the completion marker records. */
|
|
1248
|
+
seen: string[];
|
|
1249
|
+
env: NodeJS.ProcessEnv | undefined;
|
|
1250
|
+
note: (message: string) => void;
|
|
1251
|
+
}): Promise<IntakeResult> {
|
|
1252
|
+
const {
|
|
1253
|
+
verdict,
|
|
1254
|
+
issueNumber,
|
|
1255
|
+
cwd,
|
|
1256
|
+
arbiter,
|
|
1257
|
+
integration,
|
|
1258
|
+
originTrust,
|
|
1259
|
+
noPR,
|
|
1260
|
+
specsLandIn,
|
|
1261
|
+
explicitSpecsLandIn,
|
|
1262
|
+
providerInstance,
|
|
1263
|
+
issueProvider,
|
|
1264
|
+
seen,
|
|
1265
|
+
env,
|
|
1266
|
+
note,
|
|
1267
|
+
} = params;
|
|
1268
|
+
|
|
1269
|
+
// A content-derived slug — NEVER a counter (prd `issue-intake` US #8). Prefer the drafted
|
|
1270
|
+
// `specSlug`, else derive from the drafted title.
|
|
1271
|
+
const slug = resolveSpecSlug(verdict);
|
|
1272
|
+
if (slug === '') {
|
|
1273
|
+
const message =
|
|
1274
|
+
`Intake produced a 'spec' verdict for issue #${issueNumber} with no usable ` +
|
|
1275
|
+
`slug/title to derive a content-derived slug from (never a counter).`;
|
|
1276
|
+
note(message);
|
|
1277
|
+
return {exitCode: 1, outcome: 'usage-error', issueNumber, message};
|
|
1278
|
+
}
|
|
1279
|
+
// RUNNER-DETERMINISTIC PLACEMENT (task
|
|
1280
|
+
// `pre-prd-staging-pool-split-and-untrusted-prd-placement`, governing ADR
|
|
1281
|
+
// `placement-is-runner-deterministic-humanonly-is-agent-judgement`). Resolve
|
|
1282
|
+
// which folder the runner writes the intake-authored prd into BEFORE handing
|
|
1283
|
+
// it to the shared integrate band: the SAME precedence chain the tasker uses
|
|
1284
|
+
// (`explicit > untrusted-origin ⇒ staging > specsLandIn > built-in (staging)`),
|
|
1285
|
+
// the SAME shared resolver — only the lifecycle SLOTS differ. The agent
|
|
1286
|
+
// (the intake decider) never influences placement; it returns the verdict and
|
|
1287
|
+
// the runner computes the destination from unforgeable inputs.
|
|
1288
|
+
const placementDecision = resolvePlacement({
|
|
1289
|
+
explicit: specLandingToSide(explicitSpecsLandIn),
|
|
1290
|
+
originTrust,
|
|
1291
|
+
configuredDefault: specLandingToSide(specsLandIn),
|
|
1292
|
+
});
|
|
1293
|
+
const placementDir = placementFolder(
|
|
1294
|
+
SPEC_PLACEMENT_SLOTS,
|
|
1295
|
+
placementDecision.choice,
|
|
1296
|
+
);
|
|
1297
|
+
const relPath = `${placementDir}/${slug}.md`;
|
|
1298
|
+
|
|
1299
|
+
// ONBOARD onto a `work/intake-spec-<slug>` branch off fresh `<arbiter>/main` —
|
|
1300
|
+
// the SAME runner-owns-git discipline the task branch uses; the intake-
|
|
1301
|
+
// producer prefix keeps it distinct from a `do spec:<slug>` tasking branch.
|
|
1302
|
+
await switchToWorkBranch(cwd, arbiter, 'spec', slug, env);
|
|
1303
|
+
|
|
1304
|
+
const specContent = renderSpec({
|
|
1305
|
+
slug,
|
|
1306
|
+
title: verdict.specTitle ?? slug,
|
|
1307
|
+
body: verdict.specBody,
|
|
1308
|
+
issueNumber,
|
|
1309
|
+
humanOnly: verdict.specHumanOnly,
|
|
1310
|
+
needsAnswers: verdict.specNeedsAnswers,
|
|
1311
|
+
originTrust,
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
const core = await performIntegration({
|
|
1315
|
+
cwd,
|
|
1316
|
+
arbiter,
|
|
1317
|
+
slug,
|
|
1318
|
+
source: 'in-progress',
|
|
1319
|
+
recovering: false,
|
|
1320
|
+
// An intake-emitted prd has no `verify` floor of its own (it is a new spec,
|
|
1321
|
+
// not a build), exactly as the task branch + the tasking transition skip it.
|
|
1322
|
+
skipVerify: true,
|
|
1323
|
+
mode: integration,
|
|
1324
|
+
noPR,
|
|
1325
|
+
providerInstance,
|
|
1326
|
+
type: 'feat',
|
|
1327
|
+
lifecycle: {
|
|
1328
|
+
// The emitted prd IS the title source. Pass the DRAFTED title EXPLICITLY (same
|
|
1329
|
+
// race as the task path: `stage()` writes the prd file AFTER the title
|
|
1330
|
+
// read). `titlePath` stays set but is IGNORED while `title` is present.
|
|
1331
|
+
titlePath: join(cwd, relPath),
|
|
1332
|
+
title: verdict.specTitle ?? slug,
|
|
1333
|
+
commitTag: 'intake',
|
|
1334
|
+
stage: () =>
|
|
1335
|
+
stageIntakeContent({cwd, relPath, content: specContent, env}),
|
|
1336
|
+
},
|
|
1337
|
+
env,
|
|
1338
|
+
note,
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
return integrationToIntakeResult(core, {
|
|
1342
|
+
issueNumber,
|
|
1343
|
+
slug,
|
|
1344
|
+
relPath,
|
|
1345
|
+
kind: 'spec',
|
|
1346
|
+
cwd,
|
|
1347
|
+
issueProvider,
|
|
1348
|
+
seen,
|
|
1349
|
+
env,
|
|
1350
|
+
note,
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Map the shared integrate band's {@link IntegrationCoreResult} onto the intake
|
|
1356
|
+
* {@link IntakeResult}. On `completed` the artifact was written + integrated; a
|
|
1357
|
+
* `rebase-conflict` against an advanced `main` maps to `stale` (the analogue of
|
|
1358
|
+
* "the backlog moved under us"); everything else maps defensively to a usage error
|
|
1359
|
+
* (the intake task path passes `skipVerify` + has no review gate, so neither
|
|
1360
|
+
* `gate-failed` nor `review-blocked` can occur).
|
|
1361
|
+
*/
|
|
1362
|
+
async function integrationToIntakeResult(
|
|
1363
|
+
core: IntegrationCoreResult,
|
|
1364
|
+
ctx: {
|
|
1365
|
+
issueNumber: number;
|
|
1366
|
+
slug: string;
|
|
1367
|
+
relPath: string;
|
|
1368
|
+
kind?: 'task' | 'spec';
|
|
1369
|
+
/** The working checkout the issue seam shells `gh` in. */
|
|
1370
|
+
cwd: string;
|
|
1371
|
+
/** The issue seam the completion comment is posted back through. */
|
|
1372
|
+
issueProvider: IssueProvider;
|
|
1373
|
+
/** The per-run `seen=` delta the completion marker records (chain model). */
|
|
1374
|
+
seen: string[];
|
|
1375
|
+
env: NodeJS.ProcessEnv | undefined;
|
|
1376
|
+
note: (message: string) => void;
|
|
1377
|
+
},
|
|
1378
|
+
): Promise<IntakeResult> {
|
|
1379
|
+
const {issueNumber, slug, relPath, cwd, issueProvider, seen, env, note} = ctx;
|
|
1380
|
+
const kind = ctx.kind ?? 'task';
|
|
1381
|
+
const artifact = kind === 'spec' ? 'spec' : 'task';
|
|
1382
|
+
if (core.outcome === 'completed') {
|
|
1383
|
+
const landed =
|
|
1384
|
+
core.integration?.mode === 'merge'
|
|
1385
|
+
? 'landed it on the arbiter main'
|
|
1386
|
+
: 'opened a PR carrying it (main untouched)';
|
|
1387
|
+
// Both a lone task and a prd carry `issue: N` as their closure link (the task
|
|
1388
|
+
// closes its own issue; a prd is reached via `task.prd: → prd issue:`). On the
|
|
1389
|
+
// task/prd path `intake` never closes the issue (CI's close-job does, via the
|
|
1390
|
+
// `issue:` field; intake closes ONLY on BOUNCE) and emits no `Fixes #N` (a
|
|
1391
|
+
// deferred GitHub-only optimisation).
|
|
1392
|
+
const link = `issue: ${issueNumber}`;
|
|
1393
|
+
const message =
|
|
1394
|
+
`Intake of issue #${issueNumber} → wrote ${relPath} (${link}); ` +
|
|
1395
|
+
`the runner integrated it through the shared core and ${landed}.`;
|
|
1396
|
+
// CLOSE THE LOOP (this task): post ONE INFORMATIONAL completion comment back on
|
|
1397
|
+
// the issue for the SUCCESSFUL outcome — the confirmation the ASK/BOUNCE comments
|
|
1398
|
+
// already give the author. It reports `task created` / `spec created` (NEVER
|
|
1399
|
+
// "issue resolved"; intake never closes on task/spec — CI's close-job does, via
|
|
1400
|
+
// the `issue:` field) and links the artifact by integration mode: the PR `url` in
|
|
1401
|
+
// propose, the landed `commit` in merge. The marker carries `kind=created` (the
|
|
1402
|
+
// TRIAGE treats it as TERMINAL → `already-terminal`), so the comment cannot
|
|
1403
|
+
// re-trigger intake. ADVISORY — it DEGRADES (a missing/unauthenticated `gh` never
|
|
1404
|
+
// throws), so a degrade leaves the run's success outcome unchanged.
|
|
1405
|
+
const posted = await postCompletionComment({
|
|
1406
|
+
issueProvider,
|
|
1407
|
+
issueNumber,
|
|
1408
|
+
kind,
|
|
1409
|
+
slug,
|
|
1410
|
+
integration: core.integration,
|
|
1411
|
+
seen,
|
|
1412
|
+
cwd,
|
|
1413
|
+
env,
|
|
1414
|
+
note,
|
|
1415
|
+
});
|
|
1416
|
+
return {
|
|
1417
|
+
exitCode: 0,
|
|
1418
|
+
outcome: kind === 'spec' ? 'spec-written' : 'tasked',
|
|
1419
|
+
issueNumber,
|
|
1420
|
+
emittedSlug: slug,
|
|
1421
|
+
emitted: relPath,
|
|
1422
|
+
commented: posted,
|
|
1423
|
+
message,
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
if (core.outcome === 'rebase-conflict') {
|
|
1427
|
+
return {
|
|
1428
|
+
exitCode: 4,
|
|
1429
|
+
outcome: 'stale',
|
|
1430
|
+
issueNumber,
|
|
1431
|
+
message:
|
|
1432
|
+
core.reason ??
|
|
1433
|
+
`Integrating the intake ${artifact} for issue #${issueNumber} conflicted ` +
|
|
1434
|
+
`against the latest main; re-run intake.`,
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
return {
|
|
1438
|
+
exitCode: 1,
|
|
1439
|
+
outcome: 'usage-error',
|
|
1440
|
+
issueNumber,
|
|
1441
|
+
message:
|
|
1442
|
+
core.reason ??
|
|
1443
|
+
`Integrating the intake ${artifact} for issue #${issueNumber} failed unexpectedly.`,
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* Build the INFORMATIONAL completion-comment BODY (with its FULL `created` marker)
|
|
1449
|
+
* for a SUCCESSFUL `task` / `spec` outcome — the PURE, seam-free core of
|
|
1450
|
+
* {@link postCompletionComment}, exported so both link variants are unit-testable
|
|
1451
|
+
* without a live seam. The comment:
|
|
1452
|
+
*
|
|
1453
|
+
* - reports `task created` / `spec created` framed as CREATED — NEVER "issue
|
|
1454
|
+
* resolved/closed" (intake never closes on the task/spec path).
|
|
1455
|
+
* - LINKS the artifact by INTEGRATION MODE: the PR `url` in propose, the landed
|
|
1456
|
+
* `commit` (the additive {@link IntegrateResult.commit}) in merge. A degraded
|
|
1457
|
+
* propose (no `url`) or a failed merge-tip read (no `commit`) simply OMITS the
|
|
1458
|
+
* link — the comment still confirms what was created (the artifact is safe on the
|
|
1459
|
+
* branch/main regardless). No prd link beyond the slug.
|
|
1460
|
+
* - carries the FULL intake MARKER via the SHARED {@link stampIntakeMarker} helper
|
|
1461
|
+
* (`kind=created slug=<slug> seen=<id>,…`) so the triage's `already-terminal`
|
|
1462
|
+
* branch consumes it — the comment cannot re-trigger intake.
|
|
1463
|
+
*/
|
|
1464
|
+
export function composeIntakeCompletionComment(params: {
|
|
1465
|
+
kind: 'task' | 'spec';
|
|
1466
|
+
slug: string;
|
|
1467
|
+
integration: IntegrateResult | undefined;
|
|
1468
|
+
seen: string[];
|
|
1469
|
+
}): string {
|
|
1470
|
+
const {kind, slug, integration, seen} = params;
|
|
1471
|
+
const artifact = kind === 'spec' ? 'spec' : 'task';
|
|
1472
|
+
const link =
|
|
1473
|
+
integration?.mode === 'merge'
|
|
1474
|
+
? integration.commit !== undefined
|
|
1475
|
+
? `\n\nIt landed on \`main\` in commit ${integration.commit}.`
|
|
1476
|
+
: ''
|
|
1477
|
+
: integration?.url !== undefined
|
|
1478
|
+
? `\n\nIt is carried by the PR: ${integration.url}`
|
|
1479
|
+
: '';
|
|
1480
|
+
const body =
|
|
1481
|
+
`Created ${artifact} \`${slug}\` from this issue.${link}\n\n` +
|
|
1482
|
+
`This is an informational update — the issue stays open (it remains in play ` +
|
|
1483
|
+
`until the ${artifact} lands; intake does not change the issue's state).`;
|
|
1484
|
+
// STAMP the FULL marker (incl. `seen=`) via the SHARED helper, so the triage's
|
|
1485
|
+
// `already-terminal` branch recognises this terminal `created` comment.
|
|
1486
|
+
return stampIntakeMarker(body, {kind: 'created', seen, slug});
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
/**
|
|
1490
|
+
* Post the INFORMATIONAL completion comment for a SUCCESSFUL `task` / `spec`
|
|
1491
|
+
* outcome (this task). It closes the loop the ASK/BOUNCE comments already close
|
|
1492
|
+
* for the other outcomes: the issue author gets a confirmation when intake did the
|
|
1493
|
+
* useful thing. The comment:
|
|
1494
|
+
*
|
|
1495
|
+
* - reports `task created` / `spec created` — NEVER "issue resolved/closed".
|
|
1496
|
+
* Intake never closes the issue on the task/spec path (CI's future close-job
|
|
1497
|
+
* does, via the `issue:` field); this comment changes NO issue state.
|
|
1498
|
+
* - LINKS the artifact by INTEGRATION MODE: the PR `url` in propose, the landed
|
|
1499
|
+
* `commit` (the additive {@link IntegrateResult.commit} this task surfaces) in
|
|
1500
|
+
* merge. No prd link beyond the slug.
|
|
1501
|
+
* - carries the FULL intake MARKER via the SHARED {@link stampIntakeMarker} helper
|
|
1502
|
+
* (`kind=created slug=<slug> seen=<id>,…`). `kind=created` is TERMINAL, so the
|
|
1503
|
+
* triage's `already-terminal` branch then treats the issue as already-transformed
|
|
1504
|
+
* — the completion comment cannot re-trigger intake.
|
|
1505
|
+
*
|
|
1506
|
+
* ADVISORY — it DEGRADES (a missing/unauthenticated `gh` surfaces the text, never
|
|
1507
|
+
* throws), so a degrade does NOT change the run's success outcome. Returns whether
|
|
1508
|
+
* a comment was actually posted (for {@link IntakeResult.commented}).
|
|
1509
|
+
*/
|
|
1510
|
+
async function postCompletionComment(params: {
|
|
1511
|
+
issueProvider: IssueProvider;
|
|
1512
|
+
issueNumber: number;
|
|
1513
|
+
kind: 'task' | 'spec';
|
|
1514
|
+
slug: string;
|
|
1515
|
+
/** The integrate result — carries the propose `url` / the merge `commit` link. */
|
|
1516
|
+
integration: IntegrateResult | undefined;
|
|
1517
|
+
/** The per-run `seen=` delta the marker records (the chain-model primitive). */
|
|
1518
|
+
seen: string[];
|
|
1519
|
+
/** The working checkout the issue seam shells `gh` in. */
|
|
1520
|
+
cwd: string;
|
|
1521
|
+
env: NodeJS.ProcessEnv | undefined;
|
|
1522
|
+
note: (message: string) => void;
|
|
1523
|
+
}): Promise<boolean> {
|
|
1524
|
+
const {
|
|
1525
|
+
issueProvider,
|
|
1526
|
+
issueNumber,
|
|
1527
|
+
kind,
|
|
1528
|
+
slug,
|
|
1529
|
+
integration,
|
|
1530
|
+
seen,
|
|
1531
|
+
cwd,
|
|
1532
|
+
env,
|
|
1533
|
+
note,
|
|
1534
|
+
} = params;
|
|
1535
|
+
const artifact = kind === 'spec' ? 'spec' : 'task';
|
|
1536
|
+
// Build the full stamped body (CREATED wording + mode-keyed link + the FULL
|
|
1537
|
+
// `created` marker) via the exported pure builder — unit-tested directly for both
|
|
1538
|
+
// link variants (propose `url` / merge `commit`).
|
|
1539
|
+
const stamped = composeIntakeCompletionComment({
|
|
1540
|
+
kind,
|
|
1541
|
+
slug,
|
|
1542
|
+
integration,
|
|
1543
|
+
seen,
|
|
1544
|
+
});
|
|
1545
|
+
const posted = await issueProvider.postIssueComment({
|
|
1546
|
+
cwd,
|
|
1547
|
+
issueNumber,
|
|
1548
|
+
body: stamped,
|
|
1549
|
+
env,
|
|
1550
|
+
});
|
|
1551
|
+
note(
|
|
1552
|
+
posted.posted
|
|
1553
|
+
? `Posted a '${artifact} created' completion comment on issue #${issueNumber}.`
|
|
1554
|
+
: `Could not post the completion comment on issue #${issueNumber} ` +
|
|
1555
|
+
`(${posted.instruction}); the ${artifact} was still created.`,
|
|
1556
|
+
);
|
|
1557
|
+
return posted.posted;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/**
|
|
1561
|
+
* Resolve a content-derived slug from the verdict — NEVER a counter (prd `issue-intake` US #8).
|
|
1562
|
+
* Prefer the drafted `taskSlug`, else derive from the drafted title; both go
|
|
1563
|
+
* through `paramCase` (the brand case-transform) so the result is a clean
|
|
1564
|
+
* lowercase-`-`-joined slug. An empty result (no slug AND no title) signals the
|
|
1565
|
+
* caller to refuse (a counter fallback is forbidden).
|
|
1566
|
+
*/
|
|
1567
|
+
function resolveSlug(verdict: IntakeVerdict): string {
|
|
1568
|
+
const candidate =
|
|
1569
|
+
verdict.taskSlug && verdict.taskSlug.trim() !== ''
|
|
1570
|
+
? verdict.taskSlug
|
|
1571
|
+
: (verdict.taskTitle ?? '');
|
|
1572
|
+
return paramCase(candidate);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
/**
|
|
1576
|
+
* Resolve a content-derived slug for the spec outcome — NEVER a counter (prd `issue-intake` US #8).
|
|
1577
|
+
* Prefer the drafted `specSlug`, else derive from the drafted spec title; both go
|
|
1578
|
+
* through `paramCase`. An empty result signals the caller to refuse.
|
|
1579
|
+
*/
|
|
1580
|
+
function resolveSpecSlug(verdict: IntakeVerdict): string {
|
|
1581
|
+
const candidate =
|
|
1582
|
+
verdict.specSlug && verdict.specSlug.trim() !== ''
|
|
1583
|
+
? verdict.specSlug
|
|
1584
|
+
: (verdict.specTitle ?? '');
|
|
1585
|
+
return paramCase(candidate);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
/**
|
|
1589
|
+
* Render the backlog task file: the frontmatter (`title`/`slug`/`covers: []`, NO
|
|
1590
|
+
* `prd:` — its own source of truth, prd `issue-intake` decision table) carrying the lone-task
|
|
1591
|
+
* `issue: N` closure link + the drafted body. The task closes its source issue
|
|
1592
|
+
* via its `issue:` field (the provider-agnostic link a FUTURE CI close-job reads
|
|
1593
|
+
* from folder + field state); it carries NO `Fixes #N` (a deferred GitHub-only
|
|
1594
|
+
* optimisation, structurally unplaceable on the `--merge` path). The number is
|
|
1595
|
+
* the task's own closure path — `issue:` XOR `prd:`; a lone task never carries a
|
|
1596
|
+
* `prd:` (prd `issue-intake` decision table). When the agent drafted no body, a thin default
|
|
1597
|
+
* scaffold keeps the file a valid task.
|
|
1598
|
+
*/
|
|
1599
|
+
export function renderBacklogTask(params: {
|
|
1600
|
+
slug: string;
|
|
1601
|
+
title: string;
|
|
1602
|
+
body: string | undefined;
|
|
1603
|
+
issueNumber: number;
|
|
1604
|
+
/**
|
|
1605
|
+
* The origin-trust STAMP (task `untrusted-origin-forces-build-propose`).
|
|
1606
|
+
* Present ⇒ emit `origin: issue` + `originTrust: <value>` so the becomes-code
|
|
1607
|
+
* checkpoint survives the merge boundary. UNSET (a local intake, no CI shell)
|
|
1608
|
+
* ⇒ NO stamp (the human running intake IS the checkpoint ⇒ human/trusted).
|
|
1609
|
+
*/
|
|
1610
|
+
originTrust?: OriginTrust;
|
|
1611
|
+
}): string {
|
|
1612
|
+
const {slug, title, body, issueNumber, originTrust} = params;
|
|
1613
|
+
const lines = [
|
|
1614
|
+
'---',
|
|
1615
|
+
`title: ${title}`,
|
|
1616
|
+
`slug: ${slug}`,
|
|
1617
|
+
`issue: ${issueNumber}`,
|
|
1618
|
+
];
|
|
1619
|
+
if (originTrust !== undefined) {
|
|
1620
|
+
lines.push('origin: issue', `originTrust: ${originTrust}`);
|
|
1621
|
+
}
|
|
1622
|
+
lines.push('covers: []', 'blockedBy: []', '---');
|
|
1623
|
+
const frontmatter = lines.join('\n');
|
|
1624
|
+
// The drafted body (agent-authored, headings and all) is wrapped VERBATIM.
|
|
1625
|
+
// Only the empty-body DEFAULT SCAFFOLD is sourced from the shared section
|
|
1626
|
+
// skeleton owner (`renderTaskBody`, prd
|
|
1627
|
+
// `centralize-buildable-task-renderer-shared-by-intake-and-promotion` US #2),
|
|
1628
|
+
// so intake's fallback and promotion's body cannot drift on section
|
|
1629
|
+
// names/order. The shared renderer ends its body with a trailing newline (its
|
|
1630
|
+
// last line is a blank); intake owns the single trailing `\n` in the join
|
|
1631
|
+
// below, so we `trimEnd()` the renderer output to stay byte-for-byte identical
|
|
1632
|
+
// to the pre-rewire literal.
|
|
1633
|
+
const drafted =
|
|
1634
|
+
body && body.trim() !== ''
|
|
1635
|
+
? body.trim()
|
|
1636
|
+
: renderTaskBody({
|
|
1637
|
+
whatToBuild: title,
|
|
1638
|
+
acceptanceCriteria: '- [ ] the issue is resolved',
|
|
1639
|
+
prompt: `Resolve issue #${issueNumber}: ${title}`,
|
|
1640
|
+
}).trimEnd();
|
|
1641
|
+
return `${frontmatter}\n\n${drafted}\n`;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/**
|
|
1645
|
+
* Render the emitted prd file: the frontmatter (`title`/`slug` + the loop-closure
|
|
1646
|
+
* `issue: N` + the gate axes the prompt JUDGED) followed by the drafted prd body.
|
|
1647
|
+
* For a FANNED prd the `issue: N` lives ONLY on the prd — never duplicated across
|
|
1648
|
+
* the N fanned tasks, which reach it via `task.prd: → prd issue:` (a fanned
|
|
1649
|
+
* task carries `prd:`, NOT its own `issue:`; the lone-task outcome is the only
|
|
1650
|
+
* one that puts `issue:` on a task). The close JOB reaches the prd's number via
|
|
1651
|
+
* `task.prd: → prd issue:`. The gate axes (`humanOnly`/`needsAnswers`) are emitted ONLY when the
|
|
1652
|
+
* verdict declared them `true` — an omitted axis is `undefined` (undeclared), the
|
|
1653
|
+
* same convention `frontmatter.ts` parses. When the agent drafted no body, a thin
|
|
1654
|
+
* default scaffold keeps the file a valid prd that `do prd:` can later task.
|
|
1655
|
+
*/
|
|
1656
|
+
export function renderSpec(params: {
|
|
1657
|
+
slug: string;
|
|
1658
|
+
title: string;
|
|
1659
|
+
body: string | undefined;
|
|
1660
|
+
issueNumber: number;
|
|
1661
|
+
humanOnly: boolean | undefined;
|
|
1662
|
+
needsAnswers: boolean | undefined;
|
|
1663
|
+
/**
|
|
1664
|
+
* The origin-trust STAMP (task `untrusted-origin-forces-build-propose`).
|
|
1665
|
+
* Present ⇒ emit `origin: issue` + `originTrust: <value>`, PROPAGATED onto every
|
|
1666
|
+
* emitted task by the tasker so the build transition can read it. UNSET (a
|
|
1667
|
+
* local intake) ⇒ NO stamp (human/trusted).
|
|
1668
|
+
*/
|
|
1669
|
+
originTrust?: OriginTrust;
|
|
1670
|
+
}): string {
|
|
1671
|
+
const {slug, title, body, issueNumber, humanOnly, needsAnswers, originTrust} =
|
|
1672
|
+
params;
|
|
1673
|
+
const lines = [
|
|
1674
|
+
'---',
|
|
1675
|
+
`title: ${title}`,
|
|
1676
|
+
`slug: ${slug}`,
|
|
1677
|
+
`issue: ${issueNumber}`,
|
|
1678
|
+
];
|
|
1679
|
+
// The origin-trust stamp (only when passed IN from the CI shell): the
|
|
1680
|
+
// becomes-code checkpoint that survives the merge boundary. A local intake
|
|
1681
|
+
// leaves it unset ⇒ no stamp ⇒ human/trusted.
|
|
1682
|
+
if (originTrust !== undefined) {
|
|
1683
|
+
lines.push('origin: issue', `originTrust: ${originTrust}`);
|
|
1684
|
+
}
|
|
1685
|
+
// Surface the gate axes AS THE PROMPT JUDGED THEM (prd `issue-intake` US #8). Only emit a `true`
|
|
1686
|
+
// axis — an undeclared axis stays absent (parsed as `undefined`).
|
|
1687
|
+
if (humanOnly === true) {
|
|
1688
|
+
lines.push('humanOnly: true');
|
|
1689
|
+
}
|
|
1690
|
+
if (needsAnswers === true) {
|
|
1691
|
+
lines.push('needsAnswers: true');
|
|
1692
|
+
}
|
|
1693
|
+
lines.push('---');
|
|
1694
|
+
const frontmatter = lines.join('\n');
|
|
1695
|
+
// As with `renderBacklogTask`: the drafted PRD body is wrapped VERBATIM; only
|
|
1696
|
+
// the empty-body DEFAULT SCAFFOLD is sourced from the shared section skeleton
|
|
1697
|
+
// owner (`renderSpecBody` with `solution` + `userStories`, prd
|
|
1698
|
+
// `centralize-buildable-task-renderer-shared-by-intake-and-promotion` US #2),
|
|
1699
|
+
// so intake's PRD fallback and promotion's PRD body cannot drift. `trimEnd()`
|
|
1700
|
+
// drops the renderer's trailing blank line so intake's single trailing `\n`
|
|
1701
|
+
// (owned by the join below) keeps the bytes identical to the pre-rewire literal.
|
|
1702
|
+
const drafted =
|
|
1703
|
+
body && body.trim() !== ''
|
|
1704
|
+
? body.trim()
|
|
1705
|
+
: renderSpecBody({
|
|
1706
|
+
problemStatement: `Transformed from issue #${issueNumber}: ${title}`,
|
|
1707
|
+
solution: '(to be detailed; this prd needs tasking via `do prd:`).',
|
|
1708
|
+
userStories: `1. As a user, I want issue #${issueNumber} addressed.`,
|
|
1709
|
+
}).trimEnd();
|
|
1710
|
+
return `${frontmatter}\n\n${drafted}\n`;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/**
|
|
1714
|
+
* STAGE the intake artifact content into the index on the `work/<slug>` branch (the
|
|
1715
|
+
* {@link performIntegration} lifecycle seam): write the `work/backlog/<slug>.md`
|
|
1716
|
+
* file (runner-owned; the agent never writes git-visible files) and `git add` it.
|
|
1717
|
+
* The band's subsequent `git add -A` + atomic commit folds it into ONE runner-owned
|
|
1718
|
+
* commit.
|
|
1719
|
+
*/
|
|
1720
|
+
async function stageIntakeContent(params: {
|
|
1721
|
+
cwd: string;
|
|
1722
|
+
relPath: string;
|
|
1723
|
+
content: string;
|
|
1724
|
+
env: NodeJS.ProcessEnv | undefined;
|
|
1725
|
+
}): Promise<void> {
|
|
1726
|
+
const {cwd, relPath, content, env} = params;
|
|
1727
|
+
const abs = join(cwd, relPath);
|
|
1728
|
+
mkdirSync(dirname(abs), {recursive: true});
|
|
1729
|
+
writeFileSync(abs, content);
|
|
1730
|
+
await gitHard(['add', '--', relPath], cwd, env);
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
/**
|
|
1734
|
+
* ONBOARD the intake write onto a NAMESPACED, INTAKE-PRODUCED branch
|
|
1735
|
+
* (`work/intake-task-<slug>` / `work/intake-prd-<slug>`) cut from the freshly-
|
|
1736
|
+
* fetched `<arbiter>/main` (the SAME discipline `tasking.ts` uses). The
|
|
1737
|
+
* `intake-` PRODUCER prefix keeps this short-lived "create the item" branch
|
|
1738
|
+
* DISTINCT from the later build branch (`work/task-<slug>`) for the same slug
|
|
1739
|
+
* — the firing `intake` × `do task:` collision the observation traced. The
|
|
1740
|
+
* task-emit path passes `'task'`, the prd-emit path `'prd'`. A pre-existing
|
|
1741
|
+
* local branch (a re-run) is force-recreated off fresh main.
|
|
1742
|
+
*/
|
|
1743
|
+
async function switchToWorkBranch(
|
|
1744
|
+
cwd: string,
|
|
1745
|
+
arbiter: string,
|
|
1746
|
+
type: SlugNamespace,
|
|
1747
|
+
slug: string,
|
|
1748
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
1749
|
+
): Promise<void> {
|
|
1750
|
+
const branch = workBranchRef(type, slug, {producer: 'intake'});
|
|
1751
|
+
await gitHard(['fetch', '--quiet', arbiter], cwd, env);
|
|
1752
|
+
await gitHard(
|
|
1753
|
+
['switch', '--quiet', '-C', branch, `${arbiter}/main`],
|
|
1754
|
+
cwd,
|
|
1755
|
+
env,
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
/** Run git; throw on non-zero (genuinely unexpected plumbing failures). */
|
|
1760
|
+
async function gitHard(
|
|
1761
|
+
args: string[],
|
|
1762
|
+
cwd: string,
|
|
1763
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
1764
|
+
): Promise<RunResult> {
|
|
1765
|
+
const result = await runAsync('git', args, cwd, {env});
|
|
1766
|
+
if (result.status !== 0) {
|
|
1767
|
+
throw new Error(
|
|
1768
|
+
`git ${args.join(' ')} failed (exit ${result.status}): ${result.stderr.trim()}`,
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
return result;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
/** Run the decision step. Prefers the injected decider; else the harness seam. */
|
|
1775
|
+
async function runDecision(
|
|
1776
|
+
options: PerformIntakeOptions,
|
|
1777
|
+
cwd: string,
|
|
1778
|
+
issue: Issue,
|
|
1779
|
+
comments: IssueComment[],
|
|
1780
|
+
prompt: string,
|
|
1781
|
+
): Promise<IntakeVerdict> {
|
|
1782
|
+
if (options.decide) {
|
|
1783
|
+
return options.decide({cwd, issue, comments, prompt, env: options.env});
|
|
1784
|
+
}
|
|
1785
|
+
// PRODUCTION: launch the harness with the decision prd, then PARSE the verdict
|
|
1786
|
+
// the agent emitted out of its ANSWER channel (`launched.output`) — the SAME wire
|
|
1787
|
+
// the review gate runs (launch → `parseReviewVerdict(readOutput(launched.output))`;
|
|
1788
|
+
// `harnessReviewGate`). The agent emits a single fenced ```json block (the OUTPUT
|
|
1789
|
+
// CONTRACT {@link buildIntakeDecisionSpec} appends); {@link parseIntakeVerdict}
|
|
1790
|
+
// extracts + validates it. The model's JUDGEMENT is not unit-tested — only the
|
|
1791
|
+
// parse + dispatch — exactly as the review prompt's judgement is not.
|
|
1792
|
+
const harness = options.harness ?? new NullHarness();
|
|
1793
|
+
const launched = await launchWithOptionalWatch({
|
|
1794
|
+
harness,
|
|
1795
|
+
dir: cwd,
|
|
1796
|
+
slug: `intake-${issue.number}`,
|
|
1797
|
+
command: options.agentCmd ?? '',
|
|
1798
|
+
prompt,
|
|
1799
|
+
model: options.model,
|
|
1800
|
+
sessionId: `intake-${issue.number}`,
|
|
1801
|
+
sessionsDir: options.sessionsDir,
|
|
1802
|
+
env: options.env,
|
|
1803
|
+
});
|
|
1804
|
+
if (!launched.ok) {
|
|
1805
|
+
throw new Error(launched.detail ?? 'the intake decision agent failed.');
|
|
1806
|
+
}
|
|
1807
|
+
// Read the verdict from the agent's ANSWER channel (`output`), NOT `detail` (the
|
|
1808
|
+
// failure channel, empty on success) — the SAME `output ?? ''` normalisation the
|
|
1809
|
+
// review gate's `readOutput` default applies. A malformed/absent verdict throws,
|
|
1810
|
+
// which `decideAndDispatch`'s try/catch already maps onto `agent-failed` (exit 1).
|
|
1811
|
+
return parseIntakeVerdict(launched.output ?? '');
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
/**
|
|
1815
|
+
* Parse the decision agent's emitted VERDICT out of its (possibly prose-wrapped /
|
|
1816
|
+
* fenced) textual output into an {@link IntakeVerdict} — the PRODUCTION wire
|
|
1817
|
+
* between the launched agent and the already-built dispatcher, modeled 1:1 on the
|
|
1818
|
+
* review gate's `parseReviewVerdict` twin (`review-gate.ts`). It pulls the first
|
|
1819
|
+
* JSON object carrying an `"outcome"` field via the SHARED
|
|
1820
|
+
* {@link extractJsonObjectSpan} (NOT a forked second "first JSON object in agent
|
|
1821
|
+
* prose" extractor — the review gates anchor on `"verdict"`, intake on
|
|
1822
|
+
* `"outcome"`; same need, one implementation — coherence), `JSON.parse`s it, and
|
|
1823
|
+
* validates the shape: `outcome ∈ {ask,task,spec,bounce}`.
|
|
1824
|
+
*
|
|
1825
|
+
* The per-outcome fields map 1:1 onto {@link IntakeVerdict} (`task` →
|
|
1826
|
+
* taskSlug?/taskTitle/taskBody, `spec` →
|
|
1827
|
+
* specSlug?/specTitle/specBody/specHumanOnly?/specNeedsAnswers?, `ask` → question,
|
|
1828
|
+
* `bounce` → bounceMessage). Missing OPTIONALS are tolerated — the dispatcher
|
|
1829
|
+
* already has fallbacks (slug-from-title, the thin comment/scaffold defaults).
|
|
1830
|
+
*
|
|
1831
|
+
* THROWS a clear error on: no JSON object present, invalid JSON, or an `outcome`
|
|
1832
|
+
* not in the set. The caller (`decideAndDispatch`) maps any throw onto the
|
|
1833
|
+
* `agent-failed` outcome (exit 1) — a malformed verdict degrades honestly, never
|
|
1834
|
+
* a crash and never a silent dispatch.
|
|
1835
|
+
*/
|
|
1836
|
+
export function parseIntakeVerdict(output: string): IntakeVerdict {
|
|
1837
|
+
const span = extractJsonObjectSpan(output, 'outcome');
|
|
1838
|
+
if (span === undefined) {
|
|
1839
|
+
throw new Error(
|
|
1840
|
+
'intake decision agent produced no parseable {outcome, …} verdict.',
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
let parsed: unknown;
|
|
1844
|
+
try {
|
|
1845
|
+
parsed = JSON.parse(output.slice(span.start, span.end));
|
|
1846
|
+
} catch (err) {
|
|
1847
|
+
throw new Error(
|
|
1848
|
+
`intake verdict was not valid JSON: ${(err as Error).message}`,
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
1852
|
+
throw new Error('intake verdict was not an object.');
|
|
1853
|
+
}
|
|
1854
|
+
const obj = parsed as Record<string, unknown>;
|
|
1855
|
+
const outcome = obj.outcome;
|
|
1856
|
+
if (
|
|
1857
|
+
outcome !== 'ask' &&
|
|
1858
|
+
outcome !== 'task' &&
|
|
1859
|
+
outcome !== 'spec' &&
|
|
1860
|
+
outcome !== 'bounce'
|
|
1861
|
+
) {
|
|
1862
|
+
// The prompt teaches the LLM to emit `spec`, so the accepted outcome set is
|
|
1863
|
+
// `ask|task|spec|bounce`. HARD CUTOVER: the legacy `prd` outcome token is
|
|
1864
|
+
// fully gone (rejected here + removed from the `IntakeOutcome` type + the
|
|
1865
|
+
// dispatch `case`).
|
|
1866
|
+
throw new Error(
|
|
1867
|
+
`intake verdict 'outcome' was not one of ask|task|spec|bounce (got ` +
|
|
1868
|
+
`${JSON.stringify(outcome)}).`,
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
// Map the per-outcome fields onto the verdict shape, keeping ONLY the strings/
|
|
1872
|
+
// booleans the dispatcher consumes (a missing optional stays absent — the
|
|
1873
|
+
// dispatcher's fallbacks cover it). Every field is optional on the type, so the
|
|
1874
|
+
// `task`/`spec` content + the `ask`/`bounce` text are carried verbatim when present.
|
|
1875
|
+
const str = (v: unknown): string | undefined =>
|
|
1876
|
+
typeof v === 'string' ? v : undefined;
|
|
1877
|
+
const bool = (v: unknown): boolean | undefined =>
|
|
1878
|
+
typeof v === 'boolean' ? v : undefined;
|
|
1879
|
+
return {
|
|
1880
|
+
outcome,
|
|
1881
|
+
...(str(obj.taskSlug) !== undefined ? {taskSlug: str(obj.taskSlug)} : {}),
|
|
1882
|
+
...(str(obj.taskTitle) !== undefined
|
|
1883
|
+
? {taskTitle: str(obj.taskTitle)}
|
|
1884
|
+
: {}),
|
|
1885
|
+
...(str(obj.taskBody) !== undefined ? {taskBody: str(obj.taskBody)} : {}),
|
|
1886
|
+
...(str(obj.question) !== undefined ? {question: str(obj.question)} : {}),
|
|
1887
|
+
...(str(obj.specSlug) !== undefined ? {specSlug: str(obj.specSlug)} : {}),
|
|
1888
|
+
...(str(obj.specTitle) !== undefined
|
|
1889
|
+
? {specTitle: str(obj.specTitle)}
|
|
1890
|
+
: {}),
|
|
1891
|
+
...(str(obj.specBody) !== undefined ? {specBody: str(obj.specBody)} : {}),
|
|
1892
|
+
...(bool(obj.specHumanOnly) !== undefined
|
|
1893
|
+
? {specHumanOnly: bool(obj.specHumanOnly)}
|
|
1894
|
+
: {}),
|
|
1895
|
+
...(bool(obj.specNeedsAnswers) !== undefined
|
|
1896
|
+
? {specNeedsAnswers: bool(obj.specNeedsAnswers)}
|
|
1897
|
+
: {}),
|
|
1898
|
+
...(str(obj.bounceMessage) !== undefined
|
|
1899
|
+
? {bounceMessage: str(obj.bounceMessage)}
|
|
1900
|
+
: {}),
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// ---------------------------------------------------------------------------
|
|
1905
|
+
// The LONE-TASK bounded internal review (observation
|
|
1906
|
+
// `intake-lone-task-skips-adversarial-review-the-prd-path-gets`, rulings A/B/C).
|
|
1907
|
+
//
|
|
1908
|
+
// Give intake's lone-TASK outcome the adversarial refinement the `do prd:` path
|
|
1909
|
+
// already gets — but as a small intake-NATIVE bounded review, NOT by integrating
|
|
1910
|
+
// the tasker loop. This is a NEW prompt + a small loop + an injectable gate seam,
|
|
1911
|
+
// MIRRORING the tasker loop's verdict/output CONVENTIONS (fenced JSON
|
|
1912
|
+
// `{verdict, findings, edit}` parsed via the shared `extractJsonObjectSpan`) WITHOUT
|
|
1913
|
+
// importing or calling `runTaskReviewLoop`. The differences are load-bearing: this
|
|
1914
|
+
// reviews ONE drafted task (N=1 — the SET/graph/overlap lenses are OFF), it never
|
|
1915
|
+
// touches disk pre-convergence (the task has not been emitted yet), and its only
|
|
1916
|
+
// non-converge sink is the EXISTING `asked` outcome (verdict flips TASK→ASK with
|
|
1917
|
+
// the draft + question(s) in the comment body — ruling C).
|
|
1918
|
+
// ---------------------------------------------------------------------------
|
|
1919
|
+
|
|
1920
|
+
/** The HARD-CODED round cap for the lone-task review (ruling A — a literal, no config/flag). */
|
|
1921
|
+
const LONE_TASK_REVIEW_MAX_ROUNDS = 3;
|
|
1922
|
+
|
|
1923
|
+
/**
|
|
1924
|
+
* Backwards-compatible alias for {@link ReviewFinding} (task
|
|
1925
|
+
* `review-protocol-doc-and-shared-machinery`). Existing imports keep compiling;
|
|
1926
|
+
* new code should reach for `ReviewFinding` from `review-verdict.ts`.
|
|
1927
|
+
*/
|
|
1928
|
+
export type LoneTaskReviewFinding = ReviewFinding;
|
|
1929
|
+
|
|
1930
|
+
/**
|
|
1931
|
+
* The lone-task review verdict shape is now the UNIFIED {@link ReviewVerdict}.
|
|
1932
|
+
* The lone-task caller consumes the `edit` / `questions` channels of the wide
|
|
1933
|
+
* type; other review callers consume different channels. The SET/graph/overlap
|
|
1934
|
+
* lenses are still N=1-OFF in the PROMPT (intake never spawns the tasker loop's
|
|
1935
|
+
* set-level sinks); the type itself is shared.
|
|
1936
|
+
*/
|
|
1937
|
+
export type LoneTaskReviewVerdict = ReviewVerdict;
|
|
1938
|
+
|
|
1939
|
+
/** What the lone-task review gate needs to launch / answer ONE review round. */
|
|
1940
|
+
export interface LoneTaskReviewGateInput {
|
|
1941
|
+
/** The drafted task's slug (the candidate under review). */
|
|
1942
|
+
slug: string;
|
|
1943
|
+
/** The source issue number (the destination check's target behaviour). */
|
|
1944
|
+
issueNumber: number;
|
|
1945
|
+
/** The drafted task's title (the candidate under review). */
|
|
1946
|
+
title: string;
|
|
1947
|
+
/** The drafted task BODY as it stands THIS round (after any prior in-memory edits). */
|
|
1948
|
+
body: string;
|
|
1949
|
+
/** Which review ROUND this is (1-based, 1..{@link LONE_TASK_REVIEW_MAX_ROUNDS}). */
|
|
1950
|
+
round: number;
|
|
1951
|
+
/** The working clone/checkout the review runs in. */
|
|
1952
|
+
cwd: string;
|
|
1953
|
+
/** Environment for the review-agent launch. */
|
|
1954
|
+
env?: NodeJS.ProcessEnv;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
/**
|
|
1958
|
+
* The lone-task review SEAM: run ONE adversarial review round on the SINGLE
|
|
1959
|
+
* drafted task and return a parsed verdict (incl. an optional in-memory edit).
|
|
1960
|
+
* Tests inject a canned verdict (no model/network) — the new testable seam, mirroring
|
|
1961
|
+
* {@link IntakeDecider}. Production uses {@link harnessLoneTaskReviewGate}.
|
|
1962
|
+
*/
|
|
1963
|
+
export type LoneTaskReviewGate = (
|
|
1964
|
+
input: LoneTaskReviewGateInput,
|
|
1965
|
+
) => Promise<LoneTaskReviewVerdict>;
|
|
1966
|
+
|
|
1967
|
+
/** The terminal disposition of the bounded lone-task review. */
|
|
1968
|
+
interface LoneTaskReviewResult {
|
|
1969
|
+
/** `converge` → emit the (edited) task; `non-converge` → flip TASK→ASK. */
|
|
1970
|
+
outcome: 'converge' | 'non-converge';
|
|
1971
|
+
/** The task TITLE after the review (unchanged today; carried for symmetry). */
|
|
1972
|
+
title: string;
|
|
1973
|
+
/** The task BODY after all applied in-memory edits (the body to emit / carry). */
|
|
1974
|
+
body: string | undefined;
|
|
1975
|
+
/** On `non-converge`: the open question(s) to carry into the ASK comment body. */
|
|
1976
|
+
questions: string[];
|
|
1977
|
+
/** How many review rounds ran. */
|
|
1978
|
+
passes: number;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
/**
|
|
1982
|
+
* Run the BOUNDED lone-task adversarial self-review over the SINGLE drafted task.
|
|
1983
|
+
* Each round runs the gate (the `review` skill's per-task + destination lenses on
|
|
1984
|
+
* the ONE task); a round may propose an EDIT (full replacement body) applied IN
|
|
1985
|
+
* MEMORY and re-reviewed. The cap is the HARD-CODED literal
|
|
1986
|
+
* {@link LONE_TASK_REVIEW_MAX_ROUNDS} = 3 (ruling A — no config/flag).
|
|
1987
|
+
*
|
|
1988
|
+
* - CONVERGE — a round returns `approve` with no NEW blocking issue → emit the
|
|
1989
|
+
* improved task (the caller's existing write/integrate + completion comment).
|
|
1990
|
+
* - NON-CONVERGE — a round `block`s with an open question (no clear thread answer)
|
|
1991
|
+
* OR the cap is hit with an unresolved blocker → flip TASK→ASK carrying the
|
|
1992
|
+
* draft + the open question(s) (ruling C).
|
|
1993
|
+
*
|
|
1994
|
+
* A gate launch/parse failure THROWS (the caller's try/catch maps it onto
|
|
1995
|
+
* `agent-failed`) — never a silent emit of the un-reviewed task.
|
|
1996
|
+
*/
|
|
1997
|
+
async function runLoneTaskReview(params: {
|
|
1998
|
+
slug: string;
|
|
1999
|
+
issueNumber: number;
|
|
2000
|
+
draftTitle: string;
|
|
2001
|
+
draftBody: string | undefined;
|
|
2002
|
+
gate: LoneTaskReviewGate;
|
|
2003
|
+
cwd: string;
|
|
2004
|
+
env: NodeJS.ProcessEnv | undefined;
|
|
2005
|
+
note: (message: string) => void;
|
|
2006
|
+
}): Promise<LoneTaskReviewResult> {
|
|
2007
|
+
const {slug, issueNumber, draftTitle, draftBody, gate, cwd, env, note} =
|
|
2008
|
+
params;
|
|
2009
|
+
let body = draftBody;
|
|
2010
|
+
let lastVerdict: LoneTaskReviewVerdict = {verdict: 'block', findings: []};
|
|
2011
|
+
let passes = 0;
|
|
2012
|
+
for (let round = 1; round <= LONE_TASK_REVIEW_MAX_ROUNDS; round++) {
|
|
2013
|
+
const verdict = await gate({
|
|
2014
|
+
slug,
|
|
2015
|
+
issueNumber,
|
|
2016
|
+
title: draftTitle,
|
|
2017
|
+
// The body the reviewer sees this round (after any prior in-memory edit);
|
|
2018
|
+
// fall back to the rendered scaffold-input the emit path also tolerates.
|
|
2019
|
+
body: body ?? '',
|
|
2020
|
+
round,
|
|
2021
|
+
cwd,
|
|
2022
|
+
env,
|
|
2023
|
+
});
|
|
2024
|
+
passes = round;
|
|
2025
|
+
lastVerdict = verdict;
|
|
2026
|
+
// APPLY the proposed EDIT IN MEMORY (no `work/backlog/` write pre-convergence).
|
|
2027
|
+
if (verdict.edit !== undefined && verdict.edit.trim() !== '') {
|
|
2028
|
+
body = verdict.edit;
|
|
2029
|
+
}
|
|
2030
|
+
if (verdict.verdict === 'approve') {
|
|
2031
|
+
return {
|
|
2032
|
+
outcome: 'converge',
|
|
2033
|
+
title: draftTitle,
|
|
2034
|
+
body,
|
|
2035
|
+
questions: [],
|
|
2036
|
+
passes,
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
// EARLY FLIP → ASK (the non-converge trigger the source observation names
|
|
2040
|
+
// FIRST: "a blocking question with NO clear answer in the issue thread"). When a
|
|
2041
|
+
// round BLOCKS, carries open `questions`, and proposes NO `edit`, the agent is
|
|
2042
|
+
// saying "this needs the HUMAN, I have nothing left to tighten" — so flip to ASK
|
|
2043
|
+
// NOW rather than burning the remaining rounds (which cannot resolve a question
|
|
2044
|
+
// only the human can answer). Symmetric with the early CONVERGE return above; it
|
|
2045
|
+
// retires the "flips only at the cap" behaviour (PR #62 review nit #1). A `block`
|
|
2046
|
+
// that DID propose an `edit` is still iterated — the edit may converge it; only a
|
|
2047
|
+
// no-edit blocking question short-circuits.
|
|
2048
|
+
const hasEdit = verdict.edit !== undefined && verdict.edit.trim() !== '';
|
|
2049
|
+
const earlyQuestions = loneTaskBlockingQuestions(verdict);
|
|
2050
|
+
if (!hasEdit && earlyQuestions.length > 0) {
|
|
2051
|
+
note(
|
|
2052
|
+
`Intake lone-task review round ${round}/${LONE_TASK_REVIEW_MAX_ROUNDS} ` +
|
|
2053
|
+
`surfaced a blocking question with no edit to apply; flipping TASK→ASK ` +
|
|
2054
|
+
`early (no clear thread answer — the human must decide).`,
|
|
2055
|
+
);
|
|
2056
|
+
return {
|
|
2057
|
+
outcome: 'non-converge',
|
|
2058
|
+
title: draftTitle,
|
|
2059
|
+
body,
|
|
2060
|
+
questions: earlyQuestions,
|
|
2061
|
+
passes,
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
note(
|
|
2065
|
+
`Intake lone-task review round ${round}/${LONE_TASK_REVIEW_MAX_ROUNDS} ` +
|
|
2066
|
+
`found ${loneTaskBlockingCount(verdict)} blocking issue(s)` +
|
|
2067
|
+
`${hasEdit ? ' (an edit was applied)' : ''}.`,
|
|
2068
|
+
);
|
|
2069
|
+
}
|
|
2070
|
+
// The cap was hit with a still-`block` verdict → NON-CONVERGE (flip TASK→ASK).
|
|
2071
|
+
return {
|
|
2072
|
+
outcome: 'non-converge',
|
|
2073
|
+
title: draftTitle,
|
|
2074
|
+
body,
|
|
2075
|
+
questions: loneTaskBlockingQuestions(lastVerdict),
|
|
2076
|
+
passes,
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
/** Count blocking findings in a lone-task review verdict. */
|
|
2081
|
+
function loneTaskBlockingCount(verdict: LoneTaskReviewVerdict): number {
|
|
2082
|
+
return verdict.findings.filter((f) => f.severity === 'blocking').length;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
/**
|
|
2086
|
+
* The open question(s) for the ASK comment body on a non-converge: prefer the
|
|
2087
|
+
* verdict's explicit `questions`, else fall back to the blocking findings'
|
|
2088
|
+
* questions (so the human always gets a concrete question, never a blank ask).
|
|
2089
|
+
*/
|
|
2090
|
+
function loneTaskBlockingQuestions(verdict: LoneTaskReviewVerdict): string[] {
|
|
2091
|
+
if (verdict.questions && verdict.questions.length > 0) {
|
|
2092
|
+
return verdict.questions;
|
|
2093
|
+
}
|
|
2094
|
+
const blocking = verdict.findings.filter((f) => f.severity === 'blocking');
|
|
2095
|
+
const source = blocking.length > 0 ? blocking : verdict.findings;
|
|
2096
|
+
return source.map((f) =>
|
|
2097
|
+
f.context ? `${f.question} (${f.context})` : f.question,
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
/**
|
|
2102
|
+
* Compose the NON-CONVERGE ASK comment BODY (ruling C): it carries BOTH the proposed
|
|
2103
|
+
* task DRAFT and the open question(s) that arose, so the human reacts to a concrete
|
|
2104
|
+
* draft ("yes, yes, but…"), strictly richer than a blank-question ask. The draft
|
|
2105
|
+
* rides in the comment BODY — NOT a new marker kind; {@link dispatchComment} stamps
|
|
2106
|
+
* the EXISTING `kind=ask` marker around it.
|
|
2107
|
+
*/
|
|
2108
|
+
function composeLoneTaskAskComment(params: {
|
|
2109
|
+
issueNumber: number;
|
|
2110
|
+
slug: string;
|
|
2111
|
+
draftTitle: string;
|
|
2112
|
+
draftBody: string | undefined;
|
|
2113
|
+
questions: string[];
|
|
2114
|
+
}): string {
|
|
2115
|
+
const {issueNumber, slug, draftTitle, draftBody, questions} = params;
|
|
2116
|
+
const draft = renderBacklogTask({
|
|
2117
|
+
slug,
|
|
2118
|
+
title: draftTitle,
|
|
2119
|
+
body: draftBody,
|
|
2120
|
+
issueNumber,
|
|
2121
|
+
});
|
|
2122
|
+
const questionLines =
|
|
2123
|
+
questions.length > 0
|
|
2124
|
+
? questions.map((q) => `- ${q}`).join('\n')
|
|
2125
|
+
: '- (the draft below needs a clarification before it can be built)';
|
|
2126
|
+
return [
|
|
2127
|
+
`I drafted a task for issue #${issueNumber} but the internal review surfaced`,
|
|
2128
|
+
`open question(s) it could not resolve from the thread. Please weigh in on the`,
|
|
2129
|
+
`draft below — once the question(s) are answered, a later run can emit it.`,
|
|
2130
|
+
'',
|
|
2131
|
+
'## Open question(s)',
|
|
2132
|
+
'',
|
|
2133
|
+
questionLines,
|
|
2134
|
+
'',
|
|
2135
|
+
'## Proposed task draft',
|
|
2136
|
+
'',
|
|
2137
|
+
'```markdown',
|
|
2138
|
+
draft.trimEnd(),
|
|
2139
|
+
'```',
|
|
2140
|
+
].join('\n');
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
/**
|
|
2144
|
+
* Resolve the lone-task review GATE from the intake options: the injected
|
|
2145
|
+
* {@link PerformIntakeOptions.reviewTask} (tests' canned seam) when present, else
|
|
2146
|
+
* the production harness-backed gate ({@link harnessLoneTaskReviewGate}) wired to
|
|
2147
|
+
* the same harness/agent the decision step uses. Mirrors how {@link runDecision}
|
|
2148
|
+
* prefers the injected `decide`.
|
|
2149
|
+
*/
|
|
2150
|
+
function resolveLoneTaskReviewGate(
|
|
2151
|
+
options: PerformIntakeOptions,
|
|
2152
|
+
): LoneTaskReviewGate {
|
|
2153
|
+
if (options.reviewTask) {
|
|
2154
|
+
return options.reviewTask;
|
|
2155
|
+
}
|
|
2156
|
+
return harnessLoneTaskReviewGate({
|
|
2157
|
+
harness: options.harness,
|
|
2158
|
+
agentCmd: options.agentCmd,
|
|
2159
|
+
model: options.model,
|
|
2160
|
+
sessionsDir: options.sessionsDir,
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
/** Options for the production harness-backed lone-task review gate. */
|
|
2165
|
+
export interface HarnessLoneTaskReviewGateOptions {
|
|
2166
|
+
/** The harness seam used to launch the fresh-context review agent. */
|
|
2167
|
+
harness?: Harness;
|
|
2168
|
+
/** The configured agent command the harness shells out to. */
|
|
2169
|
+
agentCmd?: string;
|
|
2170
|
+
/** The model routing intent forwarded to the harness (ADR §13). */
|
|
2171
|
+
model?: string;
|
|
2172
|
+
/** The HOST-ONLY sessions root for the review session file. */
|
|
2173
|
+
sessionsDir?: string;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
/**
|
|
2177
|
+
* The PRODUCTION lone-task review gate: launch the `review` SKILL as an agent
|
|
2178
|
+
* through the EXISTING harness seam (the SAME wire {@link runDecision} uses), then
|
|
2179
|
+
* PARSE the emitted `{verdict, findings, edit, questions}` via
|
|
2180
|
+
* {@link parseLoneTaskReviewVerdict}. The agent makes the review JUDGEMENT (the
|
|
2181
|
+
* per-task + destination lenses on the ONE task); this gate launches it and parses
|
|
2182
|
+
* its verdict. A launch failure THROWS (the dispatcher's try/catch maps it onto
|
|
2183
|
+
* `agent-failed`). MIRRORS {@link harnessTaskReviewGate} WITHOUT importing it.
|
|
2184
|
+
*/
|
|
2185
|
+
export function harnessLoneTaskReviewGate(
|
|
2186
|
+
options: HarnessLoneTaskReviewGateOptions = {},
|
|
2187
|
+
): LoneTaskReviewGate {
|
|
2188
|
+
const harness = options.harness ?? new NullHarness();
|
|
2189
|
+
return async (
|
|
2190
|
+
input: LoneTaskReviewGateInput,
|
|
2191
|
+
): Promise<LoneTaskReviewVerdict> => {
|
|
2192
|
+
const launched = await launchWithOptionalWatch({
|
|
2193
|
+
harness,
|
|
2194
|
+
dir: input.cwd,
|
|
2195
|
+
slug: `intake-task-review-${input.slug}`,
|
|
2196
|
+
command: options.agentCmd ?? '',
|
|
2197
|
+
prompt: buildLoneTaskReviewPrompt(input),
|
|
2198
|
+
model: options.model,
|
|
2199
|
+
// A DISTINCT session id per round so launches never collide.
|
|
2200
|
+
sessionId: `intake-task-review-${input.slug}-r${input.round}`,
|
|
2201
|
+
sessionsDir: options.sessionsDir,
|
|
2202
|
+
env: input.env,
|
|
2203
|
+
});
|
|
2204
|
+
if (!launched.ok) {
|
|
2205
|
+
throw new Error(
|
|
2206
|
+
`intake lone-task review agent launch failed${
|
|
2207
|
+
launched.detail ? `: ${launched.detail}` : ''
|
|
2208
|
+
}`,
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
return parseReviewVerdict(launched.output ?? '');
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
/**
|
|
2216
|
+
* Build the LONE-TASK review PROMPT: instruct a fresh-context agent to apply
|
|
2217
|
+
* the **review discipline** (`work/protocol/REVIEW-PROTOCOL.md`) to the SINGLE
|
|
2218
|
+
* drafted task — per-task well-formedness + the destination check ("if this
|
|
2219
|
+
* task is built exactly as written, do we end up with the behaviour issue #N
|
|
2220
|
+
* asks for?"). The SET / graph / overlap lenses are N=1 and EXPLICITLY OFF.
|
|
2221
|
+
* A round may propose an `edit` (the FULL replacement task body) the runner
|
|
2222
|
+
* applies IN MEMORY and re-reviews; converge when a round finds NO new blocking
|
|
2223
|
+
* issue, else carry the open `questions` into the ASK comment for the human.
|
|
2224
|
+
*
|
|
2225
|
+
* The discipline body and the JSON-emitted-shape contract are SHARED helpers
|
|
2226
|
+
* (task `review-protocol-doc-and-shared-machinery`); this builder owns ONLY
|
|
2227
|
+
* the lone-task-specific framing.
|
|
2228
|
+
*/
|
|
2229
|
+
export function buildLoneTaskReviewPrompt(
|
|
2230
|
+
input: LoneTaskReviewGateInput,
|
|
2231
|
+
): string {
|
|
2232
|
+
return [
|
|
2233
|
+
`You are a FRESH-CONTEXT reviewer in intake's BOUNDED lone-task review. A`,
|
|
2234
|
+
`single task has just been drafted from GitHub issue #${input.issueNumber}.`,
|
|
2235
|
+
`Review THIS ONE task adversarially (round ${input.round} of at most ${LONE_TASK_REVIEW_MAX_ROUNDS}).`,
|
|
2236
|
+
'',
|
|
2237
|
+
reviewDisciplinePrompt(),
|
|
2238
|
+
'',
|
|
2239
|
+
`Drafted task slug: ${input.slug}`,
|
|
2240
|
+
`Drafted task title: ${input.title}`,
|
|
2241
|
+
'',
|
|
2242
|
+
'Drafted task body (the markdown AFTER the frontmatter):',
|
|
2243
|
+
'```markdown',
|
|
2244
|
+
input.body.trim() === ''
|
|
2245
|
+
? '(empty — only a scaffold was drafted)'
|
|
2246
|
+
: input.body,
|
|
2247
|
+
'```',
|
|
2248
|
+
'',
|
|
2249
|
+
'## Which lenses apply (N=1 — this is ONE task, not a SET)',
|
|
2250
|
+
'',
|
|
2251
|
+
'Apply ONLY the per-task lenses, ENDING in the destination check:',
|
|
2252
|
+
'- **Per-task well-formedness** — is it a single tracer-bullet vertical task',
|
|
2253
|
+
' (one thin end-to-end path)? Are the `## What to build`, `## Acceptance',
|
|
2254
|
+
' criteria`, and `## Prompt` present, concrete, and self-contained (an AFK',
|
|
2255
|
+
' agent could start from the file alone)? Are claims/paths/“reuse X” real?',
|
|
2256
|
+
'- **The DESTINATION check** — if this task is built EXACTLY as written, do we',
|
|
2257
|
+
` end up with the behaviour issue #${input.issueNumber} asks for? A hole here is`,
|
|
2258
|
+
' the highest-value thing to flag.',
|
|
2259
|
+
'',
|
|
2260
|
+
'The SET / graph / overlap / goal-COMPOSITION lenses are OFF: there is only ONE',
|
|
2261
|
+
'task (N=1), so there is no dependency graph, no set-level gap, and no',
|
|
2262
|
+
'duplicate/overlap to assess. Do NOT invent a decomposition.',
|
|
2263
|
+
'',
|
|
2264
|
+
'## How to iterate',
|
|
2265
|
+
'',
|
|
2266
|
+
'You do NOT edit files or run git — you EMIT a verdict and the runner applies it',
|
|
2267
|
+
'in memory, then re-reviews. If a finding can be FIXED by tightening the draft,',
|
|
2268
|
+
'propose an `edit` (the FULL replacement task body — the markdown AFTER the',
|
|
2269
|
+
'frontmatter; the runner writes the frontmatter + the issue link). CONVERGE',
|
|
2270
|
+
'(`approve`, no blocking findings) when a round finds NO new blocking issue.',
|
|
2271
|
+
'When a BLOCKING question has NO clear answer in the issue thread — it needs the',
|
|
2272
|
+
'human, not another edit — `block` and put it in `questions`: the runner asks the',
|
|
2273
|
+
'human, carrying this draft. Flag, do not guess.',
|
|
2274
|
+
'',
|
|
2275
|
+
verdictContractPrompt(),
|
|
2276
|
+
'',
|
|
2277
|
+
'Fill the channels appropriate to THIS caller (the lone-task review):',
|
|
2278
|
+
' - `edit` — a single full-replacement task body (NOT a path; the task is',
|
|
2279
|
+
' not yet emitted) when tightening the draft fixes the finding.',
|
|
2280
|
+
' - `questions` — the open question(s) for the human when a blocking issue',
|
|
2281
|
+
' has no clear thread answer.',
|
|
2282
|
+
'Do NOT fill `review` / `edits` / `uncertainTasks` / `decompositionUnclear`',
|
|
2283
|
+
"— those are other callers' channels.",
|
|
2284
|
+
].join('\n');
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
/**
|
|
2288
|
+
* Backwards-compatible alias for the unified {@link parseReviewVerdict}
|
|
2289
|
+
* (task `review-protocol-doc-and-shared-machinery`). The lone-task review
|
|
2290
|
+
* verdict is now the unified {@link ReviewVerdict}; the alias keeps existing
|
|
2291
|
+
* tests/callers compiling.
|
|
2292
|
+
*/
|
|
2293
|
+
export const parseLoneTaskReviewVerdict = parseReviewVerdict;
|
|
2294
|
+
|
|
2295
|
+
/**
|
|
2296
|
+
* Build the intake decision PRD (an inline prompt builder, like `buildTaskingPrd`
|
|
2297
|
+
* in `tasking.ts` / the reviewer prompts in `review-gate.ts` — NOT a standalone
|
|
2298
|
+
* asset/`.md` file; no such convention exists in this package). It encodes the FULL
|
|
2299
|
+
* four-outcome decision table (prd `issue-intake` — the source of truth) and the
|
|
2300
|
+
* three DECISION AIDS stated once there:
|
|
2301
|
+
*
|
|
2302
|
+
* 1. the **"clear?" bar** = `to-task`/`needsAnswers`' "would I build the wrong
|
|
2303
|
+
* thing if I guessed?" — if a material requirement/scope/acceptance question is
|
|
2304
|
+
* unanswered, ASK (never guess a spec from a vague issue);
|
|
2305
|
+
* 2. the **"one task?" bar** = `to-task`' tracer-bullet test (one thin end-to-end
|
|
2306
|
+
* path, demoable on its own) — fits → TASK, needs splitting → PRD;
|
|
2307
|
+
* 3. **PRD vs BOUNCE** turns on a **SHARED VISION**: coupled (even if small) → PRD;
|
|
2308
|
+
* genuinely unrelated → BOUNCE. Size NEVER forces a bounce — only unrelatedness
|
|
2309
|
+
* (the over-bounce guard: a coupled-but-small pair gets a light PRD, never a
|
|
2310
|
+
* bounce).
|
|
2311
|
+
*
|
|
2312
|
+
* The prompt anchors to `to-task`/`to-spec` for the task/spec SHAPES it drafts. Its
|
|
2313
|
+
* JUDGEMENT is NOT unit-tested (exactly as the review prompt's is not) — only the
|
|
2314
|
+
* dispatch is. The agent only DRAFTS the verdict + its content; it does NO git/seam
|
|
2315
|
+
* ops (the runner owns every postComment / write / integrate — the in-band boundary).
|
|
2316
|
+
*/
|
|
2317
|
+
export function buildIntakeDecisionSpec(
|
|
2318
|
+
issue: Issue,
|
|
2319
|
+
comments: IssueComment[],
|
|
2320
|
+
triage?: IntakeTriageDecision,
|
|
2321
|
+
): string {
|
|
2322
|
+
const thread =
|
|
2323
|
+
comments.length === 0
|
|
2324
|
+
? '(no comments yet)'
|
|
2325
|
+
: comments
|
|
2326
|
+
.map(
|
|
2327
|
+
(c, i) =>
|
|
2328
|
+
`#${i + 1} ${c.author ? `@${c.author}` : '(unknown)'}: ${c.body}`,
|
|
2329
|
+
)
|
|
2330
|
+
.join('\n\n');
|
|
2331
|
+
// TRIAGE ENRICHMENT (prd `issue-intake`): on the raced PROCEED path the prompt is
|
|
2332
|
+
// told which comment(s) PRE-DATE intake's last turn (context for a prior state,
|
|
2333
|
+
// not necessarily a fresh answer) and — only then — how many previously-SEEN
|
|
2334
|
+
// comments were DELETED (a flag + count; the bodies are gone, so do not name them).
|
|
2335
|
+
const triageNotes: string[] = [];
|
|
2336
|
+
if (triage?.action === 'proceed' && triage.predatingIds.length > 0) {
|
|
2337
|
+
triageNotes.push(
|
|
2338
|
+
'',
|
|
2339
|
+
'## Triage note — raced comment(s) that PRE-DATE intake’s last turn',
|
|
2340
|
+
'',
|
|
2341
|
+
`${triage.predatingIds.length} comment(s) landed AFTER intake last read the`,
|
|
2342
|
+
'thread but BEFORE it posted its last turn, so they pre-date that turn',
|
|
2343
|
+
'(possibly concurrent). Treat them as possibly-already-addressed context for a',
|
|
2344
|
+
'PRIOR state — NOT necessarily a direct answer to intake’s latest question.',
|
|
2345
|
+
);
|
|
2346
|
+
if (triage.deletedSeenCount > 0) {
|
|
2347
|
+
triageNotes.push(
|
|
2348
|
+
'',
|
|
2349
|
+
`ALSO: ${triage.deletedSeenCount} previously-seen comment(s) were DELETED since`,
|
|
2350
|
+
'intake last read the thread. Their content is gone and not recoverable; do',
|
|
2351
|
+
'NOT assume your prior reasoning’s premises still hold — reassess from the',
|
|
2352
|
+
'current thread.',
|
|
2353
|
+
);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
return [
|
|
2357
|
+
`You are the dorfl INTAKE agent. Decide what to do with GitHub issue`,
|
|
2358
|
+
`#${issue.number}: "${issue.title}". You read the issue + its full comment`,
|
|
2359
|
+
`thread and return ONE verdict (the runner DISPATCHES on it deterministically).`,
|
|
2360
|
+
'',
|
|
2361
|
+
'Issue body:',
|
|
2362
|
+
issue.body.trim() === '' ? '(empty)' : issue.body,
|
|
2363
|
+
'',
|
|
2364
|
+
'Comment thread (oldest first):',
|
|
2365
|
+
thread,
|
|
2366
|
+
...triageNotes,
|
|
2367
|
+
'',
|
|
2368
|
+
'## The decision — classify the issue into exactly ONE of four verdicts',
|
|
2369
|
+
'',
|
|
2370
|
+
'- **ASK** — the issue is NOT clear enough to act on: a material requirement,',
|
|
2371
|
+
' scope, or acceptance question is unanswered. Use the same bar `to-task`',
|
|
2372
|
+
' uses for `needsAnswers`: "would I build the WRONG thing if I guessed now?"',
|
|
2373
|
+
' If yes → ASK. Draft the SINGLE next clarifying question (do NOT guess a spec',
|
|
2374
|
+
' from a vague issue). The runner posts it and stops; a later run resumes from',
|
|
2375
|
+
' the updated thread.',
|
|
2376
|
+
'',
|
|
2377
|
+
'- **TASK** — the issue is CLEAR *and* it fits ONE tracer-bullet vertical task',
|
|
2378
|
+
' (a single thin end-to-end path, demoable on its own — `to-task`’ criterion).',
|
|
2379
|
+
' Draft that ONE task in the `to-task` shape (a `## What to build`,',
|
|
2380
|
+
' `## Acceptance criteria`, and `## Prompt`). The runner writes',
|
|
2381
|
+
' `work/backlog/<slug>.md` (`covers: []`, NO `prd:`) carrying `issue: N` (the',
|
|
2382
|
+
' lone-task closure link, NOT `Fixes #N`) and integrates it.',
|
|
2383
|
+
'',
|
|
2384
|
+
'- **PRD** — the issue is CLEAR *and* coherent but needs MORE THAN ONE task (it',
|
|
2385
|
+
' cannot be one tracer-bullet path — it splits for scope/architecture). >1 task',
|
|
2386
|
+
' ⟺ a shared vision worth recording ⟺ a spec. Draft a spec in the `to-spec` shape',
|
|
2387
|
+
' (`## Problem Statement`, `## Solution`, `## User Stories`, `## Out of Scope`).',
|
|
2388
|
+
' The runner writes the prd file (`work/prds/ready/<slug>.md`) with `issue: N` and integrates it;',
|
|
2389
|
+
' TASKING the prd is a SEPARATE later step (`do prd:`) — do not task it here.',
|
|
2390
|
+
' **INCLUDES a coupled-but-SMALL pair: if two asks share a vision they get a',
|
|
2391
|
+
' (light) prd — they are NEVER bounced.**',
|
|
2392
|
+
'',
|
|
2393
|
+
'- **BOUNCE** — the issue is really MULTIPLE UNRELATED concerns wearing one issue:',
|
|
2394
|
+
' you cannot articulate a SINGLE shared vision tying them together. Draft a short',
|
|
2395
|
+
' message asking the author to file separate issues. A bounce is TERMINAL, so the',
|
|
2396
|
+
' runner CLOSES the issue ATOMICALLY — your message as the closing comment +',
|
|
2397
|
+
' reason "not planned" (the honest signal that the asks must be re-filed). Intake',
|
|
2398
|
+
' closes on BOUNCE only; never on task/prd (CI’s close-job) / ask.',
|
|
2399
|
+
'',
|
|
2400
|
+
'## The three decision aids (apply them in order)',
|
|
2401
|
+
'',
|
|
2402
|
+
'1. **"clear?"** (ASK vs the rest): the `needsAnswers` bar — would acting now risk',
|
|
2403
|
+
' building the wrong thing? If yes → ASK. Otherwise it is clear; continue.',
|
|
2404
|
+
'2. **"one task?"** (TASK vs PRD): the `to-task` tracer-bullet test — one thin',
|
|
2405
|
+
' end-to-end path, demoable alone? Fits → TASK; needs splitting → PRD.',
|
|
2406
|
+
'3. **"shared vision?"** (PRD vs BOUNCE): coupled (even if small) → PRD; genuinely',
|
|
2407
|
+
' unrelated → BOUNCE. SIZE NEVER forces a bounce — only UNRELATEDNESS does. Do',
|
|
2408
|
+
' not over-bounce a small coupled pair: it is a light prd.',
|
|
2409
|
+
'',
|
|
2410
|
+
'## Boundary',
|
|
2411
|
+
'',
|
|
2412
|
+
'You only DRAFT the verdict + its content (the task/prd body, or the comment',
|
|
2413
|
+
'text). You do NOT perform ANY git operation and you do NOT post any comment — the',
|
|
2414
|
+
'runner owns every git/seam side-effect (write, integrate, postComment). For a prd',
|
|
2415
|
+
'verdict, also judge its gate axes (humanOnly / needsAnswers) so the runner can',
|
|
2416
|
+
'surface them on the emitted prd.',
|
|
2417
|
+
'',
|
|
2418
|
+
'## Output — hand the verdict back as ONE fenced JSON block',
|
|
2419
|
+
'',
|
|
2420
|
+
'Emit your verdict as a SINGLE fenced ```json block (and nothing else that looks',
|
|
2421
|
+
'like JSON). Its keys map 1:1 onto the verdict the runner dispatches on — always an',
|
|
2422
|
+
'`"outcome"` plus ONLY the fields for that outcome:',
|
|
2423
|
+
'',
|
|
2424
|
+
'```json',
|
|
2425
|
+
'{"outcome": "task", "taskSlug": "<content-derived-slug>", "taskTitle": "<title>", "taskBody": "<the markdown AFTER the frontmatter>"}',
|
|
2426
|
+
'```',
|
|
2427
|
+
'',
|
|
2428
|
+
'- **task** → `taskTitle` + `taskBody` (the `## What to build` / `## Acceptance',
|
|
2429
|
+
' criteria` / `## Prompt` markdown — NOT the frontmatter; the runner writes the',
|
|
2430
|
+
' frontmatter + the `issue: N` link) and an optional `taskSlug` (the runner',
|
|
2431
|
+
' derives one from the title if you omit it — never a counter).',
|
|
2432
|
+
'- **spec** → `specTitle` + `specBody` (the `## Problem Statement` / `## Solution` / …',
|
|
2433
|
+
' markdown AFTER the frontmatter; the runner writes the frontmatter + `issue: N`),',
|
|
2434
|
+
' an optional `specSlug`, and the gate axes `specHumanOnly` / `specNeedsAnswers`',
|
|
2435
|
+
' (booleans — set `true` when a human should drive the TASKING and/or open',
|
|
2436
|
+
' questions remain; omit otherwise).',
|
|
2437
|
+
'- **ask** → `question` (the single next clarifying question).',
|
|
2438
|
+
'- **bounce** → `bounceMessage` (the “file separate issues” message).',
|
|
2439
|
+
'',
|
|
2440
|
+
'`outcome` MUST be exactly one of `ask` | `task` | `spec` | `bounce`. Strings are',
|
|
2441
|
+
'plain text inside the JSON (escape newlines as \\n). Do not wrap the JSON in any',
|
|
2442
|
+
'other structure — the runner pulls the first `{"outcome": …}` object out and',
|
|
2443
|
+
'dispatches on it.',
|
|
2444
|
+
].join('\n');
|
|
2445
|
+
}
|